diff --git a/.gitignore b/.gitignore
index 7237b34a..e54692b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,9 @@ package-lock.json
functions/*
.temp/**
+# claude code local settings
+.claude/settings.local.json
+
# umi
.umi
.umi-production
diff --git a/README.ar-DZ.md b/README.ar-DZ.md
deleted file mode 100644
index 10976d68..00000000
--- a/README.ar-DZ.md
+++ /dev/null
@@ -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)
-
-
Ant Design Pro
-
-
-
-حل واجهة مستخدم جاهز لتطبيقات المؤسسات مبني على 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/)
-
-
-
-
-
-
-- المعاينة: 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
-```
-
-## المتصفحات المدعومة
-
-المتصفحات الحديثة.
-
-| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)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) لتحسين الكود الخاص بنا.
diff --git a/README.es-ES.md b/README.es-ES.md
deleted file mode 100644
index 0ec0206d..00000000
--- a/README.es-ES.md
+++ /dev/null
@@ -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)
-
-Ant Design Pro
-
-
-
-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/)
-
-
-
-
-
-
-- 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.
-
-| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)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.
diff --git a/README.fr-FR.md b/README.fr-FR.md
deleted file mode 100644
index 76c445a6..00000000
--- a/README.fr-FR.md
+++ /dev/null
@@ -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)
-
-Ant Design Pro
-
-
-
-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/)
-
-
-
-
-
-
-- 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.
-
-| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)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.
diff --git a/README.ja-JP.md b/README.ja-JP.md
deleted file mode 100644
index 208d19ea..00000000
--- a/README.ja-JP.md
+++ /dev/null
@@ -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)
-
-Ant Design Pro
-
-
-
-エンタープライズアプリケーション向けの、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/)
-
-
-
-
-
-
-- プレビュー: 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
-```
-
-## 対応ブラウザ
-
-モダンブラウザ対応。
-
-| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)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)でコード改善を提案する
diff --git a/README.md b/README.md
index 2f9c625c..0c47e18a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Language: 🇺🇸 | [🇨🇳](./README.zh-CN.md) | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇵🇹](./README.pt-BR.md) | [🇸🇦](./README.ar-DZ.md) | [🇪🇸](./README.es-ES.md)
+Language: 🇺🇸 | [🇨🇳](./README.zh-CN.md)
Ant Design Pro
@@ -74,36 +74,48 @@ An out-of-box UI solution for enterprise applications as a React boilerplate.
## Usage
-### Use bash
+### Get Started
-We provide pro-cli to quickly initialize scaffolding.
+Clone or download this repository to your local machine:
```bash
-# use npm
-npm i @ant-design/pro-cli -g
-pro create myapp
+git clone --depth=1 https://github.com/ant-design/ant-design-pro.git myapp
+cd myapp
```
-Choose the pro template. Simple is the basic template, which only provides the basic content of the framework operation. Complete contains all blocks, which is not suitable for secondary development as a basic template.
+### Installation
-```shell
-? 🚀 Full or a simple scaffold? (Use arrow keys)
-❯ simple
- complete
+```bash
+npm install
+```
+
+### Development
+
+```bash
+# Start development server (full version by default)
+npm start
```
-Initialized Git repository:
+### Simplify to Simple Version
-```shell
-$ git init myapp
+This project includes all blocks by default. If you need a minimal version, run:
+
+```bash
+npm run simple
```
-Install dependencies:
+This will:
+- Remove extra page directories (dashboard, form, list/*, profile, result, exception, account, etc.)
+- Remove extra mock files
+- Replace routes with simple version
+- Remove extra dependencies from package.json
+
+**Note**: This operation is irreversible and will permanently delete files.
-```shell
-$ cd myapp && tyarn
-// or
-$ cd myapp && npm install
+### Build
+
+```bash
+npm run build
```
## Browsers support
diff --git a/README.pt-BR.md b/README.pt-BR.md
deleted file mode 100644
index 2041969a..00000000
--- a/README.pt-BR.md
+++ /dev/null
@@ -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)
-
-Ant Design Pro
-
-
-
-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/)
-
-
-
-
-
-
-- 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.
-
-| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)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.
diff --git a/README.ru-RU.md b/README.ru-RU.md
deleted file mode 100644
index fbca4948..00000000
--- a/README.ru-RU.md
+++ /dev/null
@@ -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)
-
-Ant Design Pro
-
-
-
-Готовое решение 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/)
-
-
-
-
-
-
-- Превью: 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
-```
-
-## Поддержка браузеров
-
-Современные браузеры.
-
-| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)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) для улучшения кода.
diff --git a/README.tr-TR.md b/README.tr-TR.md
deleted file mode 100644
index 2cb2191e..00000000
--- a/README.tr-TR.md
+++ /dev/null
@@ -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)
-
-Ant Design Pro
-
-
-
-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/)
-
-
-
-
-
-
-- Ö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.
-
-| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)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.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 9cfd93e4..8cc58499 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -1,4 +1,4 @@
-Language : [🇺🇸](./README.md) | 🇨🇳 | [🇷🇺](./README.ru-RU.md) | [🇹🇷](./README.tr-TR.md) | [🇯🇵](./README.ja-JP.md) | [🇫🇷](./README.fr-FR.md) | [🇵🇹](./README.pt-BR.md) | [🇸🇦](./README.ar-DZ.md)
+Language : [🇺🇸](./README.md) | 🇨🇳
Ant Design Pro
@@ -77,28 +77,48 @@ Language : [🇺🇸](./README.md) | 🇨🇳 | [🇷🇺](./README.ru-RU.md) |
## 使用
-我们提供了 pro-cli 来快速的初始化脚手架。
+### 开始使用
+
+克隆或下载本项目到本地:
+
+```bash
+git clone --depth=1 https://github.com/ant-design/ant-design-pro.git myapp
+cd myapp
+```
+
+### 安装依赖
+
+```bash
+npm install
+```
+
+### 开发
```bash
-# 使用 npm
-npm i @ant-design/pro-cli -g
-pro create myapp
+# 启动开发服务器(默认为完整版)
+npm start
```
-选择 pro 的模板,simple 是基础模板,只提供了框架运行的基本内容,complete 包含所有区块,不太适合当基础模板来进行二次开发。
+### 精简为简单版本
-```shell
-? 🚀 要全量的还是一个简单的脚手架? (Use arrow keys)
-❯ simple
- complete
+本项目默认包含所有区块。如果你需要一个最小化的版本,运行:
+
+```bash
+npm run simple
```
-安装依赖:
+这将会:
+- 删除多余的页面目录(dashboard、form、list/*、profile、result、exception、account 等)
+- 删除多余的 mock 文件
+- 替换路由为简单版本
+- 从 package.json 中移除多余的依赖
-```shell
-$ cd myapp && tyarn
-// 或
-$ cd myapp && npm install
+**注意**:此操作不可逆,将永久删除文件。
+
+### 构建
+
+```bash
+npm run build
```
更多信息请参考 [使用文档](http://pro.ant.design/docs/getting-started)。
diff --git a/biome.json b/biome.json
index 3acf0db6..b7f7df06 100644
--- a/biome.json
+++ b/biome.json
@@ -27,7 +27,8 @@
"rules": {
"recommended": true,
"suspicious": {
- "noExplicitAny": "off"
+ "noExplicitAny": "off",
+ "noUnknownAtRules": "off"
},
"correctness": {
"useUniqueElementIds": "off",
diff --git a/config/config.ts b/config/config.ts
index f288bcfb..2ba8a846 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -143,6 +143,7 @@ export default defineConfig({
// 解决首次加载时白屏的问题
{ src: join(PUBLIC_PATH, 'scripts/loading.js'), async: true },
],
+
//================ pro 插件配置 =================
presets: ['umi-presets-pro'],
/**
@@ -165,6 +166,7 @@ export default defineConfig({
projectName: 'swagger',
},
],
+
mock: {
include: ['mock/**/*', 'src/pages/**/_mock.ts'],
},
@@ -174,4 +176,5 @@ export default defineConfig({
define: {
'process.env.CI': process.env.CI,
},
+ tailwindcss: {},
});
diff --git a/config/routes.simple.ts b/config/routes.simple.ts
new file mode 100644
index 00000000..4ced012e
--- /dev/null
+++ b/config/routes.simple.ts
@@ -0,0 +1,55 @@
+/**
+ * @name 简单版路由配置
+ * @description 此配置用于 npm run simple 命令执行后使用
+ */
+export default [
+ {
+ path: '/user',
+ layout: false,
+ routes: [
+ {
+ name: 'login',
+ path: '/user/login',
+ component: './user/login',
+ },
+ ],
+ },
+ {
+ path: '/welcome',
+ name: 'welcome',
+ icon: 'smile',
+ component: './Welcome',
+ },
+ {
+ path: '/admin',
+ name: 'admin',
+ icon: 'crown',
+ access: 'canAdmin',
+ routes: [
+ {
+ path: '/admin',
+ redirect: '/admin/sub-page',
+ },
+ {
+ path: '/admin/sub-page',
+ name: 'sub-page',
+ component: './Admin',
+ },
+ ],
+ },
+ {
+ name: 'list.table-list',
+ icon: 'table',
+ path: '/list',
+ component: './table-list',
+ },
+ {
+ path: '/',
+ redirect: '/welcome',
+ },
+ {
+ component: '404',
+ layout: false,
+ path: './*',
+ },
+];
diff --git a/config/routes.ts b/config/routes.ts
index 27ff5087..e0d1d2ae 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -1,4 +1,4 @@
-/**
+/**
* @name umi 的路由配置
* @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置
* @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。
@@ -16,10 +16,31 @@ export default [
layout: false,
routes: [
{
- name: 'login',
path: '/user/login',
+ layout: false,
+ name: 'login',
component: './user/login',
},
+ {
+ path: '/user',
+ redirect: '/user/login',
+ },
+ {
+ name: 'register-result',
+ icon: 'smile',
+ path: '/user/register-result',
+ component: './user/register-result',
+ },
+ {
+ name: 'register',
+ icon: 'smile',
+ path: '/user/register',
+ component: './user/register',
+ },
+ {
+ component: '404',
+ path: '/user/*',
+ },
],
},
{
@@ -46,18 +67,225 @@ export default [
],
},
{
- name: 'list.table-list',
- icon: 'table',
+ path: '/dashboard',
+ name: 'dashboard',
+ icon: 'dashboard',
+ routes: [
+ {
+ path: '/dashboard',
+ redirect: '/dashboard/analysis',
+ },
+ {
+ name: 'analysis',
+ icon: 'smile',
+ path: '/dashboard/analysis',
+ component: './dashboard/analysis',
+ },
+ {
+ name: 'monitor',
+ icon: 'smile',
+ path: '/dashboard/monitor',
+ component: './dashboard/monitor',
+ },
+ {
+ name: 'workplace',
+ icon: 'smile',
+ path: '/dashboard/workplace',
+ component: './dashboard/workplace',
+ },
+ ],
+ },
+ {
+ path: '/form',
+ icon: 'form',
+ name: 'form',
+ routes: [
+ {
+ path: '/form',
+ redirect: '/form/basic-form',
+ },
+ {
+ name: 'basic-form',
+ icon: 'smile',
+ path: '/form/basic-form',
+ component: './form/basic-form',
+ },
+ {
+ name: 'step-form',
+ icon: 'smile',
+ path: '/form/step-form',
+ component: './form/step-form',
+ },
+ {
+ name: 'advanced-form',
+ icon: 'smile',
+ path: '/form/advanced-form',
+ component: './form/advanced-form',
+ },
+ ],
+ },
+ {
path: '/list',
- component: './table-list',
+ icon: 'table',
+ name: 'list',
+ routes: [
+ {
+ path: '/list/search',
+ name: 'search-list',
+ component: './list/search',
+ routes: [
+ {
+ path: '/list/search',
+ redirect: '/list/search/articles',
+ },
+ {
+ name: 'articles',
+ icon: 'smile',
+ path: '/list/search/articles',
+ component: './list/search/articles',
+ },
+ {
+ name: 'projects',
+ icon: 'smile',
+ path: '/list/search/projects',
+ component: './list/search/projects',
+ },
+ {
+ name: 'applications',
+ icon: 'smile',
+ path: '/list/search/applications',
+ component: './list/search/applications',
+ },
+ ],
+ },
+ {
+ path: '/list',
+ redirect: '/list/table-list',
+ },
+ {
+ name: 'table-list',
+ icon: 'smile',
+ path: '/list/table-list',
+ component: './table-list',
+ },
+ {
+ name: 'basic-list',
+ icon: 'smile',
+ path: '/list/basic-list',
+ component: './list/basic-list',
+ },
+ {
+ name: 'card-list',
+ icon: 'smile',
+ path: '/list/card-list',
+ component: './list/card-list',
+ },
+ ],
+ },
+ {
+ path: '/profile',
+ name: 'profile',
+ icon: 'profile',
+ routes: [
+ {
+ path: '/profile',
+ redirect: '/profile/basic',
+ },
+ {
+ name: 'basic',
+ icon: 'smile',
+ path: '/profile/basic',
+ component: './profile/basic',
+ },
+ {
+ name: 'advanced',
+ icon: 'smile',
+ path: '/profile/advanced',
+ component: './profile/advanced',
+ },
+ ],
+ },
+ {
+ name: 'result',
+ icon: 'CheckCircleOutlined',
+ path: '/result',
+ routes: [
+ {
+ path: '/result',
+ redirect: '/result/success',
+ },
+ {
+ name: 'success',
+ icon: 'smile',
+ path: '/result/success',
+ component: './result/success',
+ },
+ {
+ name: 'fail',
+ icon: 'smile',
+ path: '/result/fail',
+ component: './result/fail',
+ },
+ ],
+ },
+ {
+ name: 'exception',
+ icon: 'warning',
+ path: '/exception',
+ routes: [
+ {
+ path: '/exception',
+ redirect: '/exception/403',
+ },
+ {
+ name: '403',
+ icon: 'smile',
+ path: '/exception/403',
+ component: './exception/403',
+ },
+ {
+ name: '404',
+ icon: 'smile',
+ path: '/exception/404',
+ component: './exception/404',
+ },
+ {
+ name: '500',
+ icon: 'smile',
+ path: '/exception/500',
+ component: './exception/500',
+ },
+ ],
+ },
+ {
+ name: 'account',
+ icon: 'user',
+ path: '/account',
+ routes: [
+ {
+ path: '/account',
+ redirect: '/account/center',
+ },
+ {
+ name: 'center',
+ icon: 'smile',
+ path: '/account/center',
+ component: './account/center',
+ },
+ {
+ name: 'settings',
+ icon: 'smile',
+ path: '/account/settings',
+ component: './account/settings',
+ },
+ ],
},
{
path: '/',
- redirect: '/welcome',
+ redirect: '/dashboard/analysis',
},
{
component: '404',
- layout: false,
path: './*',
},
];
diff --git a/jest.config.ts b/jest.config.ts
index 99051c41..230bef7d 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -1,4 +1,4 @@
-import { configUmiAlias, createConfig } from '@umijs/max/test';
+import { configUmiAlias, createConfig } from '@umijs/max/test.js';
export default async (): Promise => {
const config = await configUmiAlias({
diff --git a/mock/analysis.mock.ts b/mock/analysis.mock.ts
new file mode 100644
index 00000000..b09b8b07
--- /dev/null
+++ b/mock/analysis.mock.ts
@@ -0,0 +1,210 @@
+import dayjs from 'dayjs';
+import type { Request, Response } from 'express';
+import type { AnalysisData, DataItem, RadarData } from '../src/pages/dashboard/analysis/data';
+
+// mock data
+const visitData: DataItem[] = [];
+const beginDay = new Date().getTime();
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+for (let i = 0; i < fakeY.length; i += 1) {
+ visitData.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY[i],
+ });
+}
+
+const visitData2 = [];
+const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
+for (let i = 0; i < fakeY2.length; i += 1) {
+ visitData2.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY2[i],
+ });
+}
+
+const salesData = [];
+for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+}
+const searchData = [];
+for (let i = 0; i < 50; i += 1) {
+ searchData.push({
+ index: i + 1,
+ keyword: `搜索关键词-${i}`,
+ count: Math.floor(Math.random() * 1000),
+ range: Math.floor(Math.random() * 100),
+ status: Math.floor((Math.random() * 10) % 2),
+ });
+}
+const salesTypeData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+];
+
+const salesTypeDataOnline = [
+ {
+ x: '家用电器',
+ y: 244,
+ },
+ {
+ x: '食用酒水',
+ y: 321,
+ },
+ {
+ x: '个护健康',
+ y: 311,
+ },
+ {
+ x: '服饰箱包',
+ y: 41,
+ },
+ {
+ x: '母婴产品',
+ y: 121,
+ },
+ {
+ x: '其他',
+ y: 111,
+ },
+];
+
+const salesTypeDataOffline = [
+ {
+ x: '家用电器',
+ y: 99,
+ },
+ {
+ x: '食用酒水',
+ y: 188,
+ },
+ {
+ x: '个护健康',
+ y: 344,
+ },
+ {
+ x: '服饰箱包',
+ y: 255,
+ },
+ {
+ x: '其他',
+ y: 65,
+ },
+];
+
+const offlineData = [];
+for (let i = 0; i < 10; i += 1) {
+ offlineData.push({
+ name: `Stores ${i}`,
+ cvr: Math.ceil(Math.random() * 9) / 10,
+ });
+}
+const offlineChartData = [];
+for (let i = 0; i < 20; i += 1) {
+ const date = dayjs(new Date().getTime() + 1000 * 60 * 30 * i).format('HH:mm');
+ offlineChartData.push({
+ date,
+ type: '客流量',
+ value: Math.floor(Math.random() * 100) + 10,
+ });
+ offlineChartData.push({
+ date,
+ type: '支付笔数',
+ value: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+const radarOriginData = [
+ {
+ name: '个人',
+ ref: 10,
+ koubei: 8,
+ output: 4,
+ contribute: 5,
+ hot: 7,
+ },
+ {
+ name: '团队',
+ ref: 3,
+ koubei: 9,
+ output: 6,
+ contribute: 3,
+ hot: 1,
+ },
+ {
+ name: '部门',
+ ref: 4,
+ koubei: 1,
+ output: 6,
+ contribute: 5,
+ hot: 7,
+ },
+];
+
+const radarData: RadarData[] = [];
+const radarTitleMap = {
+ ref: '引用',
+ koubei: '口碑',
+ output: '产量',
+ contribute: '贡献',
+ hot: '热度',
+};
+radarOriginData.forEach((item) => {
+ Object.keys(item).forEach((key) => {
+ if (key !== 'name') {
+ radarData.push({
+ name: item.name,
+ label: radarTitleMap[key as 'ref'],
+ value: item[key as 'ref'],
+ });
+ }
+ });
+});
+
+const getFakeChartData: AnalysisData = {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ radarData,
+};
+
+const fakeChartData = (_: Request, res: Response) => {
+ return res.json({
+ data: getFakeChartData,
+ });
+};
+
+export default {
+ 'GET /api/fake_analysis_chart_data': fakeChartData,
+};
diff --git a/mock/listTableList.ts b/mock/listTableList.ts
index dd88bf91..ddb857c2 100644
--- a/mock/listTableList.ts
+++ b/mock/listTableList.ts
@@ -34,10 +34,7 @@ let tableListDataSource = genList(1, 100);
function getRule(req: Request, res: Response, u: string) {
let realUrl = u;
- if (
- !realUrl ||
- Object.prototype.toString.call(realUrl) !== '[object String]'
- ) {
+ if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
@@ -81,25 +78,21 @@ function getRule(req: Request, res: Response, u: string) {
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
- return (Object.keys(filter) as Array).some(
- (key) => {
- if (!filter[key]) {
- return true;
- }
- if (filter[key].includes(`${item[key]}`)) {
- return true;
- }
- return false;
- },
- );
+ return (Object.keys(filter) as Array).some((key) => {
+ if (!filter[key]) {
+ return true;
+ }
+ if (filter[key].includes(`${item[key]}`)) {
+ return true;
+ }
+ return false;
+ });
});
}
}
if (params.name) {
- dataSource = dataSource.filter((data) =>
- data?.name?.includes(params.name || ''),
- );
+ dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
}
const result = {
data: dataSource,
@@ -114,10 +107,7 @@ function getRule(req: Request, res: Response, u: string) {
function postRule(req: Request, res: Response, u: string, b: Request) {
let realUrl = u;
- if (
- !realUrl ||
- Object.prototype.toString.call(realUrl) !== '[object String]'
- ) {
+ if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
@@ -126,9 +116,7 @@ function postRule(req: Request, res: Response, u: string, b: Request) {
switch (method) {
case 'delete':
- tableListDataSource = tableListDataSource.filter(
- (item) => key.indexOf(item.key) === -1,
- );
+ tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {
diff --git a/mock/notices.ts b/mock/notices.ts
index d8963113..a8a28d9b 100644
--- a/mock/notices.ts
+++ b/mock/notices.ts
@@ -85,8 +85,7 @@ const getNotices = (_req: Request, res: Response) => {
{
id: '000000010',
title: '第三方紧急代码变更',
- description:
- '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+ description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: 'event',
@@ -102,8 +101,7 @@ const getNotices = (_req: Request, res: Response) => {
{
id: '000000012',
title: 'ABCD 版本发布',
- description:
- '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+ description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: 'event',
diff --git a/mock/requestRecord.mock.js b/mock/requestRecord.mock.js
index 7c8f0de5..6c59e198 100644
--- a/mock/requestRecord.mock.js
+++ b/mock/requestRecord.mock.js
@@ -2,8 +2,7 @@ module.exports = {
'GET /api/currentUser': {
data: {
name: 'Serati Ma',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
@@ -34,8 +33,7 @@ module.exports = {
key: 99,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 99',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -49,8 +47,7 @@ module.exports = {
key: 98,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 98',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -64,8 +61,7 @@ module.exports = {
key: 97,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 97',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -79,8 +75,7 @@ module.exports = {
key: 96,
disabled: true,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 96',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -94,8 +89,7 @@ module.exports = {
key: 95,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 95',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -109,8 +103,7 @@ module.exports = {
key: 94,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 94',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -124,8 +117,7 @@ module.exports = {
key: 93,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 93',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -139,8 +131,7 @@ module.exports = {
key: 92,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 92',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -154,8 +145,7 @@ module.exports = {
key: 91,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 91',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -169,8 +159,7 @@ module.exports = {
key: 90,
disabled: true,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 90',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -184,8 +173,7 @@ module.exports = {
key: 89,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 89',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -199,8 +187,7 @@ module.exports = {
key: 88,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 88',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -214,8 +201,7 @@ module.exports = {
key: 87,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 87',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -229,8 +215,7 @@ module.exports = {
key: 86,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 86',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -244,8 +229,7 @@ module.exports = {
key: 85,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 85',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -259,8 +243,7 @@ module.exports = {
key: 84,
disabled: true,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 84',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -274,8 +257,7 @@ module.exports = {
key: 83,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 83',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -289,8 +271,7 @@ module.exports = {
key: 82,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 82',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -304,8 +285,7 @@ module.exports = {
key: 81,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 81',
owner: '曲丽丽',
desc: '这是一段描述',
@@ -319,8 +299,7 @@ module.exports = {
key: 80,
disabled: false,
href: 'https://ant.design',
- avatar:
- 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 80',
owner: '曲丽丽',
desc: '这是一段描述',
diff --git a/mock/user.ts b/mock/user.ts
index 06125bf5..2c78c5bb 100644
--- a/mock/user.ts
+++ b/mock/user.ts
@@ -20,8 +20,7 @@ const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
* current user access, if is '', user need login
* 如果是 pro 的预览,默认是有权限的
*/
-let access =
- ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
+let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
const getAccess = () => {
return access;
@@ -46,8 +45,7 @@ export default {
success: true,
data: {
name: 'Serati Ma',
- avatar:
- 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+ avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
diff --git a/mock/workplace.mock.ts b/mock/workplace.mock.ts
new file mode 100644
index 00000000..8640d56d
--- /dev/null
+++ b/mock/workplace.mock.ts
@@ -0,0 +1,418 @@
+import dayjs from 'dayjs';
+import type { Request, Response } from 'express';
+import type { DataItem, OfflineDataType } from '../src/pages/dashboard/workplace/data.d';
+
+export type SearchDataType = {
+ index: number;
+ keyword: string;
+ count: number;
+ range: number;
+ status: number;
+};
+
+// mock data
+const visitData: DataItem[] = [];
+const beginDay = new Date().getTime();
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+for (let i = 0; i < fakeY.length; i += 1) {
+ visitData.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY[i],
+ });
+}
+
+const visitData2: DataItem[] = [];
+const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
+for (let i = 0; i < fakeY2.length; i += 1) {
+ visitData2.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY2[i],
+ });
+}
+
+const salesData: DataItem[] = [];
+for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+}
+const searchData: SearchDataType[] = [];
+for (let i = 0; i < 50; i += 1) {
+ searchData.push({
+ index: i + 1,
+ keyword: `搜索关键词-${i}`,
+ count: Math.floor(Math.random() * 1000),
+ range: Math.floor(Math.random() * 100),
+ status: Math.floor((Math.random() * 10) % 2),
+ });
+}
+const salesTypeData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+];
+
+const salesTypeDataOnline = [
+ {
+ x: '家用电器',
+ y: 244,
+ },
+ {
+ x: '食用酒水',
+ y: 321,
+ },
+ {
+ x: '个护健康',
+ y: 311,
+ },
+ {
+ x: '服饰箱包',
+ y: 41,
+ },
+ {
+ x: '母婴产品',
+ y: 121,
+ },
+ {
+ x: '其他',
+ y: 111,
+ },
+];
+
+const salesTypeDataOffline = [
+ {
+ x: '家用电器',
+ y: 99,
+ },
+ {
+ x: '食用酒水',
+ y: 188,
+ },
+ {
+ x: '个护健康',
+ y: 344,
+ },
+ {
+ x: '服饰箱包',
+ y: 255,
+ },
+ {
+ x: '其他',
+ y: 65,
+ },
+];
+
+const offlineData: OfflineDataType[] = [];
+for (let i = 0; i < 10; i += 1) {
+ offlineData.push({
+ name: `Stores ${i}`,
+ cvr: Math.ceil(Math.random() * 9) / 10,
+ });
+}
+const offlineChartData: DataItem[] = [];
+for (let i = 0; i < 20; i += 1) {
+ offlineChartData.push({
+ x: new Date().getTime() + 1000 * 60 * 30 * i,
+ y1: Math.floor(Math.random() * 100) + 10,
+ y2: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+const titles = [
+ 'Alipay',
+ 'Angular',
+ 'Ant Design',
+ 'Ant Design Pro',
+ 'Bootstrap',
+ 'React',
+ 'Vue',
+ 'Webpack',
+];
+const avatars = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
+ 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
+ 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
+ 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
+ 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
+ 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
+ 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
+ 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
+];
+
+const avatars2 = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
+];
+
+const getNotice = (_: Request, res: Response) => {
+ res.json({
+ data: [
+ {
+ id: 'xxx1',
+ title: titles[0],
+ logo: avatars[0],
+ description: '那是一种内在的东西,他们到达不了,也无法触及的',
+ updatedAt: new Date(),
+ member: '科学搬砖组',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx2',
+ title: titles[1],
+ logo: avatars[1],
+ description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ updatedAt: new Date('2017-07-24'),
+ member: '全组都是吴彦祖',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx3',
+ title: titles[2],
+ logo: avatars[2],
+ description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ updatedAt: new Date(),
+ member: '中二少女团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx4',
+ title: titles[3],
+ logo: avatars[3],
+ description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+ updatedAt: new Date('2017-07-23'),
+ member: '程序员日常',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx5',
+ title: titles[4],
+ logo: avatars[4],
+ description: '凛冬将至',
+ updatedAt: new Date('2017-07-23'),
+ member: '高逼格设计天团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx6',
+ title: titles[5],
+ logo: avatars[5],
+ description: '生命就像一盒巧克力,结果往往出人意料',
+ updatedAt: new Date('2017-07-23'),
+ member: '骗你来学计算机',
+ href: '',
+ memberLink: '',
+ },
+ ],
+ });
+};
+
+const getActivities = (_: Request, res: Response) => {
+ res.json({
+ data: [
+ {
+ id: 'trend-1',
+ updatedAt: new Date(),
+ user: {
+ name: '曲丽丽',
+ avatar: avatars2[0],
+ },
+ group: {
+ name: '高逼格设计天团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-2',
+ updatedAt: new Date(),
+ user: {
+ name: '付小小',
+ avatar: avatars2[1],
+ },
+ group: {
+ name: '高逼格设计天团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-3',
+ updatedAt: new Date(),
+ user: {
+ name: '林东东',
+ avatar: avatars2[2],
+ },
+ group: {
+ name: '中二少女团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-4',
+ updatedAt: new Date(),
+ user: {
+ name: '周星星',
+ avatar: avatars2[4],
+ },
+ project: {
+ name: '5 月日常迭代',
+ link: 'http://github.com/',
+ },
+ template: '将 @{project} 更新至已发布状态',
+ },
+ {
+ id: 'trend-5',
+ updatedAt: new Date(),
+ user: {
+ name: '朱偏右',
+ avatar: avatars2[3],
+ },
+ project: {
+ name: '工程效能',
+ link: 'http://github.com/',
+ },
+ comment: {
+ name: '留言',
+ link: 'http://github.com/',
+ },
+ template: '在 @{project} 发布了 @{comment}',
+ },
+ {
+ id: 'trend-6',
+ updatedAt: new Date(),
+ user: {
+ name: '乐哥',
+ avatar: avatars2[5],
+ },
+ group: {
+ name: '程序员日常',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '品牌迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ ],
+ });
+};
+
+const radarOriginData = [
+ {
+ name: '个人',
+ ref: 10,
+ koubei: 8,
+ output: 4,
+ contribute: 5,
+ hot: 7,
+ },
+ {
+ name: '团队',
+ ref: 3,
+ koubei: 9,
+ output: 6,
+ contribute: 3,
+ hot: 1,
+ },
+ {
+ name: '部门',
+ ref: 4,
+ koubei: 1,
+ output: 6,
+ contribute: 5,
+ hot: 7,
+ },
+];
+
+const radarData: any[] = [];
+const radarTitleMap = {
+ ref: '引用',
+ koubei: '口碑',
+ output: '产量',
+ contribute: '贡献',
+ hot: '热度',
+};
+radarOriginData.forEach((item) => {
+ Object.keys(item).forEach((key) => {
+ if (key !== 'name') {
+ radarData.push({
+ name: item.name,
+ label: radarTitleMap[key as 'ref'],
+ value: item[key as 'ref'],
+ });
+ }
+ });
+});
+
+const getChartData = (_: Request, res: Response) => {
+ res.json({
+ data: {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ radarData,
+ },
+ });
+};
+
+export default {
+ 'GET /api/project/notice': getNotice,
+ 'GET /api/activities': getActivities,
+ 'GET /api/fake_workplace_chart_data': getChartData,
+};
diff --git a/package.json b/package.json
index ff2d830c..ab9e9de4 100644
--- a/package.json
+++ b/package.json
@@ -10,17 +10,19 @@
"deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev",
"gh-pages": "gh-pages -d dist",
- "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
+ "i18n-remove": "max i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
"jest": "jest",
"lint": "npm run biome:lint && npm run tsc",
"lint-staged": "lint-staged",
"biome:lint": "npx @biomejs/biome lint",
+ "biome": "biome check --write",
"openapi": "max openapi",
"prepare": "husky",
"preview": "npm run build && max preview --port 8000",
"record": "cross-env NODE_ENV=development UMI_ENV=test max record --scene=login",
"serve": "umi-serve",
+ "simple": "node scripts/simple.js",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env UMI_ENV=dev MOCK=none max dev",
"start:no-mock": "cross-env MOCK=none max dev",
@@ -36,22 +38,29 @@
],
"dependencies": {
"@ant-design/icons": "^6.1.0",
+ "@ant-design/plots": "^2.6.0",
"@ant-design/pro-components": "3.1.2-0",
+ "@antv/l7": "^2.22.7",
+ "@antv/l7-react": "^2.4.3",
+ "@rc-component/util": "^1.9.0",
"antd": "^6.2.2",
"antd-style": "^4.1.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
+ "numeral": "^2.0.6",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
- "@ant-design/pro-cli": "^3.3.0",
+ "@biomejs/biome": "^2.1.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
+ "@types/node": "^25.0.3",
+ "@types/numeral": "^2.0.5",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@types/react-helmet": "^6.1.11",
@@ -65,12 +74,11 @@
"jest-environment-jsdom": "^30.0.5",
"lint-staged": "^16.1.2",
"mockjs": "^1.1.0",
+ "tailwindcss": "^3",
"ts-node": "^10.9.2",
"typescript": "^5.6.3",
"umi-presets-pro": "^2.0.3",
- "umi-serve": "^1.9.11",
- "@biomejs/biome": "^2.1.1",
- "@types/node": "^25.0.3"
+ "umi-serve": "^1.9.11"
},
"engines": {
"node": ">=20.0.0"
diff --git a/public/scripts/loading.js b/public/scripts/loading.js
index a4fdb3d5..16e3ff4a 100644
--- a/public/scripts/loading.js
+++ b/public/scripts/loading.js
@@ -2,7 +2,7 @@
* loading 占位
* 解决首次加载时白屏的问题
*/
-(function () {
+(() => {
const _root = document.querySelector('#root');
if (_root && _root.innerHTML === '') {
_root.innerHTML = `
diff --git a/scripts/simple.js b/scripts/simple.js
new file mode 100644
index 00000000..cefc6c10
--- /dev/null
+++ b/scripts/simple.js
@@ -0,0 +1,165 @@
+/**
+ * 精简脚本 - 将完整版精简为简单版
+ * 执行 npm run simple 运行此脚本
+ *
+ * 此操作不可逆,会删除以下内容:
+ * - 页面目录:dashboard, form, list/basic-list, list/card-list, list/search, profile, result, exception, account, user/register, user/register-result
+ * - Mock 文件:analysis.mock.ts, workplace.mock.ts
+ * - 替换路由配置为简单版
+ */
+
+const fs = require('node:fs');
+const path = require('node:path');
+
+// 需要删除的页面目录
+const pageDirsToDelete = [
+ 'src/pages/dashboard',
+ 'src/pages/form',
+ 'src/pages/list/basic-list',
+ 'src/pages/list/card-list',
+ 'src/pages/list/search',
+ 'src/pages/profile',
+ 'src/pages/result',
+ 'src/pages/exception',
+ 'src/pages/account',
+ 'src/pages/user/register',
+ 'src/pages/user/register-result',
+];
+
+// 需要删除的 mock 文件
+const mockFilesToDelete = ['mock/analysis.mock.ts', 'mock/workplace.mock.ts'];
+
+// 需要从 package.json 移除的依赖
+const depsToRemove = [
+ '@ant-design/plots',
+ '@antv/l7-react',
+ '@antv/l7',
+ 'numeral',
+];
+
+const devDepsToRemove = ['@types/numeral'];
+
+// 递归删除目录
+function deleteDir(dirPath) {
+ if (fs.existsSync(dirPath)) {
+ fs.rmSync(dirPath, { recursive: true, force: true });
+ console.log(`✓ 已删除目录: ${dirPath}`);
+ } else {
+ console.log(`- 目录不存在,跳过: ${dirPath}`);
+ }
+}
+
+// 删除文件
+function deleteFile(filePath) {
+ if (fs.existsSync(filePath)) {
+ fs.unlinkSync(filePath);
+ console.log(`✓ 已删除文件: ${filePath}`);
+ } else {
+ console.log(`- 文件不存在,跳过: ${filePath}`);
+ }
+}
+
+// 替换路由配置
+function replaceRoutes() {
+ const simpleRoutesPath = 'config/routes.simple.ts';
+ const routesPath = 'config/routes.ts';
+
+ if (fs.existsSync(simpleRoutesPath)) {
+ // 读取简单版路由
+ const simpleRoutes = fs.readFileSync(simpleRoutesPath, 'utf-8');
+ // 写入到 routes.ts
+ fs.writeFileSync(routesPath, simpleRoutes);
+ console.log(`✓ 已替换路由配置: ${routesPath}`);
+ // 删除简单版路由备份文件
+ fs.unlinkSync(simpleRoutesPath);
+ console.log(`✓ 已删除备份文件: ${simpleRoutesPath}`);
+ } else {
+ console.log(`- 简单版路由配置不存在,跳过: ${simpleRoutesPath}`);
+ }
+}
+
+// 更新 package.json
+function updatePackageJson() {
+ const pkgPath = 'package.json';
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
+
+ let modified = false;
+
+ // 移除 dependencies
+ if (pkg.dependencies) {
+ for (const dep of depsToRemove) {
+ if (pkg.dependencies[dep]) {
+ delete pkg.dependencies[dep];
+ console.log(`✓ 已移除依赖: ${dep}`);
+ modified = true;
+ }
+ }
+ }
+
+ // 移除 devDependencies
+ if (pkg.devDependencies) {
+ for (const dep of devDepsToRemove) {
+ if (pkg.devDependencies[dep]) {
+ delete pkg.devDependencies[dep];
+ console.log(`✓ 已移除开发依赖: ${dep}`);
+ modified = true;
+ }
+ }
+ }
+
+ // 移除 simple 脚本
+ if (pkg.scripts?.simple) {
+ delete pkg.scripts.simple;
+ console.log('✓ 已移除 simple 脚本');
+ modified = true;
+ }
+
+ if (modified) {
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
+ console.log('✓ 已更新 package.json');
+ } else {
+ console.log('- package.json 无需更新');
+ }
+}
+
+// 主函数
+function main() {
+ console.log('========================================');
+ console.log(' 开始执行精简脚本');
+ console.log('========================================\n');
+
+ console.log('>>> 删除页面目录...');
+ for (const dir of pageDirsToDelete) {
+ deleteDir(dir);
+ }
+
+ console.log('\n>>> 删除 mock 文件...');
+ for (const file of mockFilesToDelete) {
+ deleteFile(file);
+ }
+
+ console.log('\n>>> 替换路由配置...');
+ replaceRoutes();
+
+ console.log('\n>>> 更新 package.json...');
+ updatePackageJson();
+
+ // 删除自身
+ console.log('\n>>> 清理精简脚本...');
+ fs.unlinkSync(__filename);
+ console.log('✓ 已删除 scripts/simple.js');
+
+ // 尝试删除 scripts 目录(如果为空)
+ const scriptsDir = path.dirname(__filename);
+ if (fs.readdirSync(scriptsDir).length === 0) {
+ fs.rmdirSync(scriptsDir);
+ console.log('✓ 已删除空的 scripts 目录');
+ }
+
+ console.log('\n========================================');
+ console.log(' 精简完成!');
+ console.log(' 请运行 npm install 更新依赖');
+ console.log('========================================');
+}
+
+main();
diff --git a/src/locales/bn-BD.ts b/src/locales/bn-BD.ts
index 39207ef5..618cc783 100644
--- a/src/locales/bn-BD.ts
+++ b/src/locales/bn-BD.ts
@@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'গোপনীয়তা',
'layout.user.link.terms': 'শর্তাদি',
'app.preview.down.block': 'আপনার স্থানীয় প্রকল্পে এই পৃষ্ঠাটি ডাউনলোড করুন',
- 'app.welcome.link.fetch-blocks': 'সমস্ত ব্লক পান',
- 'app.welcome.link.block-list':
- '`block` ডেভেলপমেন্ট এর উপর ভিত্তি করে দ্রুত স্ট্যান্ডার্ড, পৃষ্ঠাসমূহ তৈরি করুন।',
...globalHeader,
...menu,
...settingDrawer,
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 93276ed7..09f0ef0e 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'app.preview.down.block': 'Download this page to your local project',
- 'app.welcome.link.fetch-blocks': 'Get all block',
- 'app.welcome.link.block-list':
- 'Quickly build standard, pages based on `block` development',
...globalHeader,
...menu,
...settingDrawer,
diff --git a/src/locales/fa-IR.ts b/src/locales/fa-IR.ts
index 123ba655..f743db30 100644
--- a/src/locales/fa-IR.ts
+++ b/src/locales/fa-IR.ts
@@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'حریم خصوصی',
'layout.user.link.terms': 'مقررات',
'app.preview.down.block': 'این صفحه را در پروژه محلی خود بارگیری کنید',
- 'app.welcome.link.fetch-blocks': 'دریافت تمام بلوک',
- 'app.welcome.link.block-list':
- 'به سرعت صفحات استاندارد مبتنی بر توسعه "بلوک" را بسازید',
...globalHeader,
...menu,
...settingDrawer,
diff --git a/src/locales/id-ID.ts b/src/locales/id-ID.ts
index aecd2bdc..ae9a39f3 100644
--- a/src/locales/id-ID.ts
+++ b/src/locales/id-ID.ts
@@ -12,9 +12,6 @@ export default {
'layout.user.link.privacy': 'Privasi',
'layout.user.link.terms': 'Ketentuan',
'app.preview.down.block': 'Unduh halaman ini dalam projek lokal anda',
- 'app.welcome.link.fetch-blocks': 'Dapatkan semua blok',
- 'app.welcome.link.block-list':
- 'Buat standar dengan cepat, halaman-halaman berdasarkan pengembangan `block`',
...globalHeader,
...menu,
...settingDrawer,
diff --git a/src/locales/ja-JP.ts b/src/locales/ja-JP.ts
index b2b8a1d4..f0d7eb79 100644
--- a/src/locales/ja-JP.ts
+++ b/src/locales/ja-JP.ts
@@ -13,8 +13,6 @@ export default {
'layout.user.link.terms': '利用規約',
'app.preview.down.block':
'このページをローカルプロジェクトにダウンロードしてください',
- 'app.welcome.link.fetch-blocks': '',
- 'app.welcome.link.block-list': '',
...globalHeader,
...menu,
...settingDrawer,
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 28c5d410..ce2ccd18 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -12,8 +12,6 @@ export default {
'layout.user.link.privacy': '隐私',
'layout.user.link.terms': '条款',
'app.preview.down.block': '下载此页面到本地项目',
- 'app.welcome.link.fetch-blocks': '获取全部区块',
- 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
...pages,
...globalHeader,
...menu,
diff --git a/src/pages/account/center/Center.style.ts b/src/pages/account/center/Center.style.ts
new file mode 100644
index 00000000..e8d91b75
--- /dev/null
+++ b/src/pages/account/center/Center.style.ts
@@ -0,0 +1,69 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ avatarHolder: {
+ marginBottom: '24px',
+ textAlign: 'center',
+ '& > img': { width: '104px', height: '104px', marginBottom: '20px' },
+ },
+ name: {
+ marginBottom: '4px',
+ color: token.colorTextHeading,
+ fontWeight: '500',
+ fontSize: '20px',
+ lineHeight: '28px',
+ },
+ detail: {
+ p: {
+ position: 'relative',
+ marginBottom: '8px',
+ paddingLeft: '26px',
+ '&:last-child': {
+ marginBottom: '0',
+ },
+ },
+ i: {
+ position: 'absolute',
+ top: '4px',
+ left: '0',
+ width: '14px',
+ height: '14px',
+ },
+ },
+ tagsTitle: {
+ marginBottom: '12px',
+ color: token.colorTextHeading,
+ fontWeight: '500',
+ },
+ teamTitle: {
+ marginBottom: '12px',
+ color: token.colorTextHeading,
+ fontWeight: '500',
+ },
+ tags: {
+ '.ant-tag': { marginBottom: '8px' },
+ },
+ team: {
+ '.ant-avatar': { marginRight: '12px' },
+ a: {
+ display: 'block',
+ marginBottom: '24px',
+ overflow: 'hidden',
+ color: token.colorText,
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ transition: 'color 0.3s',
+ '&:hover': {
+ color: token.colorPrimary,
+ },
+ },
+ },
+ tabsCard: {
+ '.ant-card-head': { padding: '0 16px' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/account/center/_mock.ts b/src/pages/account/center/_mock.ts
new file mode 100644
index 00000000..635dea0a
--- /dev/null
+++ b/src/pages/account/center/_mock.ts
@@ -0,0 +1,249 @@
+import type { Request, Response } from 'express';
+import type { ListItemDataType } from './data.d';
+
+const titles = [
+ 'Alipay',
+ 'Angular',
+ 'Ant Design',
+ 'Ant Design Pro',
+ 'Bootstrap',
+ 'React',
+ 'Vue',
+ 'Webpack',
+];
+const avatars = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
+ 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
+ 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
+ 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
+ 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
+ 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
+ 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
+ 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
+];
+
+const covers = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
+];
+const desc = [
+ '那是一种内在的东西, 他们到达不了,也无法触及的',
+ '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ '生命就像一盒巧克力,结果往往出人意料',
+ '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ '那时候我只会想自己想要什么,从不想自己拥有什么',
+];
+
+const user = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+];
+
+// 当前用户信息
+const currentUseDetail = {
+ name: 'Serati Ma',
+ avatar:
+ 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+ userid: '00000001',
+ email: 'antdesign@alipay.com',
+ signature: '海纳百川,有容乃大',
+ title: '交互专家',
+ group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+ tags: [
+ {
+ key: '0',
+ label: '很有想法的',
+ },
+ {
+ key: '1',
+ label: '专注设计',
+ },
+ {
+ key: '2',
+ label: '辣~',
+ },
+ {
+ key: '3',
+ label: '大长腿',
+ },
+ {
+ key: '4',
+ label: '川妹子',
+ },
+ {
+ key: '5',
+ label: '海纳百川',
+ },
+ ],
+ notice: [
+ {
+ id: 'xxx1',
+ title: titles[0],
+ logo: avatars[0],
+ description: '那是一种内在的东西,他们到达不了,也无法触及的',
+ updatedAt: new Date(),
+ member: '科学搬砖组',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx2',
+ title: titles[1],
+ logo: avatars[1],
+ description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ updatedAt: new Date('2017-07-24'),
+ member: '全组都是吴彦祖',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx3',
+ title: titles[2],
+ logo: avatars[2],
+ description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ updatedAt: new Date(),
+ member: '中二少女团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx4',
+ title: titles[3],
+ logo: avatars[3],
+ description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+ updatedAt: new Date('2017-07-23'),
+ member: '程序员日常',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx5',
+ title: titles[4],
+ logo: avatars[4],
+ description: '凛冬将至',
+ updatedAt: new Date('2017-07-23'),
+ member: '高逼格设计天团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx6',
+ title: titles[5],
+ logo: avatars[5],
+ description: '生命就像一盒巧克力,结果往往出人意料',
+ updatedAt: new Date('2017-07-23'),
+ member: '骗你来学计算机',
+ href: '',
+ memberLink: '',
+ },
+ ],
+ notifyCount: 12,
+ unreadCount: 11,
+ country: 'China',
+ geographic: {
+ province: {
+ label: '浙江省',
+ key: '330000',
+ },
+ city: {
+ label: '杭州市',
+ key: '330100',
+ },
+ },
+ address: '西湖区工专路 77 号',
+ phone: '0752-268888888',
+};
+
+function fakeList(count: number): ListItemDataType[] {
+ const list = [];
+ for (let i = 0; i < count; i += 1) {
+ list.push({
+ id: `fake-list-${i}`,
+ owner: user[i % 10],
+ title: titles[i % 8],
+ avatar: avatars[i % 8],
+ cover:
+ parseInt(`${i / 4}`, 10) % 2 === 0
+ ? covers[i % 4]
+ : covers[3 - (i % 4)],
+ status: ['active', 'exception', 'normal'][i % 3] as
+ | 'normal'
+ | 'exception'
+ | 'active'
+ | 'success',
+ percent: Math.ceil(Math.random() * 50) + 50,
+ logo: avatars[i % 8],
+ href: 'https://ant.design',
+ updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(),
+ createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2 * i).getTime(),
+ subDescription: desc[i % 5],
+ description:
+ '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
+ activeUser: Math.ceil(Math.random() * 100000) + 100000,
+ newUser: Math.ceil(Math.random() * 1000) + 1000,
+ star: Math.ceil(Math.random() * 100) + 100,
+ like: Math.ceil(Math.random() * 100) + 100,
+ message: Math.ceil(Math.random() * 10) + 10,
+ content:
+ '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
+ members: [
+ {
+ avatar:
+ 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
+ name: '曲丽丽',
+ id: 'member1',
+ },
+ {
+ avatar:
+ 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
+ name: '王昭君',
+ id: 'member2',
+ },
+ {
+ avatar:
+ 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
+ name: '董娜娜',
+ id: 'member3',
+ },
+ ],
+ });
+ }
+
+ return list;
+}
+
+function getFakeList(req: Request, res: Response) {
+ const params = req.query as any;
+
+ const count = Number(params.count) * 1 || 5;
+
+ const result = fakeList(count);
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+// 获取用户信息
+function getCurrentUser(_req: Request, res: Response) {
+ return res.json({
+ data: currentUseDetail,
+ });
+}
+
+export default {
+ 'GET /api/fake_list_Detail': getFakeList,
+ // 支持值为 Object 和 Array
+ 'GET /api/currentUserDetail': getCurrentUser,
+};
diff --git a/src/pages/account/center/components/Applications/index.style.ts b/src/pages/account/center/components/Applications/index.style.ts
new file mode 100644
index 00000000..aa149a27
--- /dev/null
+++ b/src/pages/account/center/components/Applications/index.style.ts
@@ -0,0 +1,43 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ filterCardList: {
+ marginBottom: '-24px',
+ '.ant-card-meta-content': { marginTop: '0' },
+ '.ant-card-meta-avatar': { fontSize: '0' },
+ '.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
+ },
+ cardInfo: {
+ marginTop: '16px',
+ marginLeft: '40px',
+ zoom: '1',
+ '&::before, &::after': { display: 'table', content: "' '" },
+ '&::after': {
+ clear: 'both',
+ height: '0',
+ fontSize: '0',
+ visibility: 'hidden',
+ },
+ '& > div': {
+ position: 'relative',
+ float: 'left',
+ width: '50%',
+ textAlign: 'left',
+ p: {
+ margin: '0',
+ fontSize: '24px',
+ lineHeight: '32px',
+ },
+ 'p:first-child': {
+ marginBottom: '4px',
+ color: token.colorTextSecondary,
+ fontSize: '12px',
+ lineHeight: '20px',
+ },
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/account/center/components/Applications/index.tsx b/src/pages/account/center/components/Applications/index.tsx
new file mode 100644
index 00000000..2cf4d832
--- /dev/null
+++ b/src/pages/account/center/components/Applications/index.tsx
@@ -0,0 +1,136 @@
+import {
+ DownloadOutlined,
+ EditOutlined,
+ EllipsisOutlined,
+ ShareAltOutlined,
+} from '@ant-design/icons';
+import { useRequest } from '@umijs/max';
+import { Avatar, Card, Dropdown, List, Tooltip } from 'antd';
+import numeral from 'numeral';
+import React from 'react';
+import type { ListItemDataType } from '../../data.d';
+import { queryFakeList } from '../../service';
+import useStyles from './index.style';
+
+export function formatWan(val: number) {
+ const v = val * 1;
+ if (!v || Number.isNaN(v)) return '';
+ let result: React.ReactNode = val;
+ if (val > 10000) {
+ result = (
+
+ {Math.floor(val / 10000)}
+
+ 万
+
+
+ );
+ }
+ return result;
+}
+
+const CardInfo: React.FC<{
+ activeUser: React.ReactNode;
+ newUser: React.ReactNode;
+}> = ({ activeUser, newUser }) => {
+ const { styles: stylesApplications } = useStyles();
+ return (
+
+ );
+};
+
+const Applications: React.FC = () => {
+ const { styles: stylesApplications } = useStyles();
+ // 获取tab列表数据
+ const { data: listData } = useRequest(() => {
+ return queryFakeList({
+ count: 30,
+ });
+ });
+
+ return (
+
+ 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) => (
+
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+ ]}
+ >
+ }
+ title={item.title}
+ />
+
+
+
+
+
+ )}
+ />
+ );
+};
+export default Applications;
diff --git a/src/pages/account/center/components/ArticleListContent/index.style.ts b/src/pages/account/center/components/ArticleListContent/index.style.ts
new file mode 100644
index 00000000..aea3ddbf
--- /dev/null
+++ b/src/pages/account/center/components/ArticleListContent/index.style.ts
@@ -0,0 +1,31 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ description: {
+ maxWidth: '720px',
+ lineHeight: '22px',
+ },
+ extra: {
+ marginTop: '16px',
+ color: token.colorTextSecondary,
+ lineHeight: '22px',
+ display: 'flex',
+ gap: '8px',
+ alignItems: 'center',
+ '& > em': {
+ color: token.colorTextDisabled,
+ fontStyle: 'normal',
+ },
+ [`@media screen and (max-width: ${token.screenXS}px)`]: {
+ '& > em': {
+ display: 'block',
+ marginTop: '8px',
+ marginLeft: '0',
+ },
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/account/center/components/ArticleListContent/index.tsx b/src/pages/account/center/components/ArticleListContent/index.tsx
new file mode 100644
index 00000000..24f81ad0
--- /dev/null
+++ b/src/pages/account/center/components/ArticleListContent/index.tsx
@@ -0,0 +1,29 @@
+import { Avatar } from 'antd';
+import dayjs from 'dayjs';
+import React from 'react';
+import useStyles from './index.style';
+export type ApplicationsProps = {
+ data: {
+ content?: string;
+ updatedAt?: any;
+ avatar?: string;
+ owner?: string;
+ href?: string;
+ };
+};
+const ArticleListContent: React.FC = ({
+ data: { content, updatedAt, avatar, owner, href },
+}) => {
+ const { styles } = useStyles();
+ return (
+
+
{content}
+
+
+
{owner} 发布在
{href}
+
{dayjs(updatedAt).format('YYYY-MM-DD HH:mm')}
+
+
+ );
+};
+export default ArticleListContent;
diff --git a/src/pages/account/center/components/Articles/index.style.ts b/src/pages/account/center/components/Articles/index.style.ts
new file mode 100644
index 00000000..5a9d385f
--- /dev/null
+++ b/src/pages/account/center/components/Articles/index.style.ts
@@ -0,0 +1,14 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ articleList: {
+ '.ant-list-item:first-child': { paddingTop: '0' },
+ },
+ listItemMetaTitle: {
+ color: token.colorTextHeading,
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/account/center/components/Articles/index.tsx b/src/pages/account/center/components/Articles/index.tsx
new file mode 100644
index 00000000..896ba3c2
--- /dev/null
+++ b/src/pages/account/center/components/Articles/index.tsx
@@ -0,0 +1,71 @@
+import { LikeOutlined, MessageFilled, StarTwoTone } from '@ant-design/icons';
+import { useRequest } from '@umijs/max';
+import { List, Tag } from 'antd';
+import React from 'react';
+import type { ListItemDataType } from '../../data.d';
+import { queryFakeList } from '../../service';
+import ArticleListContent from '../ArticleListContent';
+import useStyles from './index.style';
+
+const IconText: React.FC<{
+ icon: React.ReactNode;
+ text: React.ReactNode;
+}> = ({ icon, text }) => (
+
+ {icon} {text}
+
+);
+
+const Articles: React.FC = () => {
+ const { styles } = useStyles();
+
+ // 获取tab列表数据
+ const { data: listData } = useRequest(() => {
+ return queryFakeList({
+ count: 30,
+ });
+ });
+ return (
+
+ size="large"
+ className={styles.articleList}
+ rowKey="id"
+ itemLayout="vertical"
+ dataSource={listData?.list || []}
+ style={{
+ margin: '0 -24px',
+ }}
+ renderItem={(item) => (
+ } text={item.star} />,
+ } text={item.like} />,
+ }
+ text={item.message}
+ />,
+ ]}
+ >
+
+ {item.title}
+
+ }
+ description={
+
+ Ant Design
+ 设计语言
+ 蚂蚁金服
+
+ }
+ />
+
+
+ )}
+ />
+ );
+};
+export default Articles;
diff --git a/src/pages/account/center/components/AvatarList/index.style.ts b/src/pages/account/center/components/AvatarList/index.style.ts
new file mode 100644
index 00000000..d601a1d8
--- /dev/null
+++ b/src/pages/account/center/components/AvatarList/index.style.ts
@@ -0,0 +1,41 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ avatarList: {
+ display: 'inline-block',
+ ul: { display: 'inline-block', marginLeft: '8px', fontSize: '0' },
+ },
+ avatarItem: {
+ display: 'inline-block',
+ width: token.controlHeight,
+ height: token.controlHeight,
+ marginLeft: '-8px',
+ fontSize: token.fontSize,
+ '.ant-avatar': { border: `1px solid ${token.colorBorder}` },
+ },
+ avatarItemLarge: {
+ width: token.controlHeightLG,
+ height: token.controlHeightLG,
+ },
+ avatarItemSmall: {
+ width: token.controlHeightSM,
+ height: token.controlHeightSM,
+ },
+ avatarItemMini: {
+ width: '20px',
+ height: '20px',
+ '.ant-avatar': {
+ width: '20px',
+ height: '20px',
+ lineHeight: '20px',
+ '.ant-avatar-string': {
+ fontSize: '12px',
+ lineHeight: '18px',
+ },
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/account/center/components/AvatarList/index.tsx b/src/pages/account/center/components/AvatarList/index.tsx
new file mode 100644
index 00000000..67894e6e
--- /dev/null
+++ b/src/pages/account/center/components/AvatarList/index.tsx
@@ -0,0 +1,89 @@
+import { Avatar, Tooltip } from 'antd';
+import classNames from 'classnames';
+import React from 'react';
+import useStyles from './index.style';
+export declare type SizeType = number | 'small' | 'default' | 'large';
+export type AvatarItemProps = {
+ tips: React.ReactNode;
+ src: string;
+ size?: SizeType;
+ style?: React.CSSProperties;
+ onClick?: () => void;
+};
+export type AvatarListProps = {
+ Item?: React.ReactElement;
+ size?: SizeType;
+ maxLength?: number;
+ excessItemsStyle?: React.CSSProperties;
+ style?: React.CSSProperties;
+ children:
+ | React.ReactElement
+ | React.ReactElement[];
+};
+
+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 = ({
+ src,
+ size,
+ tips,
+ onClick = () => {},
+}) => {
+ const { styles } = useStyles();
+ const cls = avatarSizeToClassName(styles, size);
+ return (
+
+ {tips ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
+const AvatarList: React.FC & {
+ 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[];
+ const childrenWithProps = childrenArray.slice(0, numToShow).map((child) =>
+ React.cloneElement(child, {
+ size,
+ }),
+ );
+ if (numToShow < numOfChildren) {
+ const cls = avatarSizeToClassName(styles, size);
+ childrenWithProps.push(
+
+ {`+${numOfChildren - maxLength}`}
+ ,
+ );
+ }
+ return (
+
+ );
+};
+AvatarList.Item = Item;
+export default AvatarList;
diff --git a/src/pages/account/center/components/Projects/index.style.ts b/src/pages/account/center/components/Projects/index.style.ts
new file mode 100644
index 00000000..ca34a15b
--- /dev/null
+++ b/src/pages/account/center/components/Projects/index.style.ts
@@ -0,0 +1,49 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ card: {
+ '.ant-card-meta-title': {
+ marginBottom: '4px',
+ '& > a': {
+ display: 'inline-block',
+ maxWidth: '100%',
+ color: token.colorTextHeading,
+ },
+ },
+ '.ant-card-meta-description': {
+ height: '44px',
+ overflow: 'hidden',
+ lineHeight: '22px',
+ },
+ '&:hover': {
+ '.ant-card-meta-title > a': {
+ color: token.colorPrimary,
+ },
+ },
+ },
+ cardItemContent: {
+ display: 'flex',
+ height: '20px',
+ marginTop: '16px',
+ marginBottom: '-4px',
+ lineHeight: '20px',
+ '& > span': {
+ flex: '1',
+ color: token.colorTextSecondary,
+ fontSize: '12px',
+ },
+ },
+ avatarList: {
+ flex: '0 1 auto',
+ },
+ cardList: {
+ marginTop: '24px',
+ },
+ coverCardList: {
+ '.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/account/center/components/Projects/index.tsx b/src/pages/account/center/components/Projects/index.tsx
new file mode 100644
index 00000000..85b566c7
--- /dev/null
+++ b/src/pages/account/center/components/Projects/index.tsx
@@ -0,0 +1,65 @@
+import { useRequest } from '@umijs/max';
+import { Card, List } from 'antd';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import React from 'react';
+import type { ListItemDataType } from '../../data.d';
+import { queryFakeList } from '../../service';
+import AvatarList from '../AvatarList';
+import useStyles from './index.style';
+
+dayjs.extend(relativeTime);
+const Projects: React.FC = () => {
+ const { styles } = useStyles();
+ // 获取tab列表数据
+ const { data: listData } = useRequest(() => {
+ return queryFakeList({
+ count: 30,
+ });
+ });
+ return (
+
+ 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) => (
+
+ }
+ >
+ {item.title}}
+ description={item.subDescription}
+ />
+
+
{dayjs(item.updatedAt).fromNow()}
+
+
+ {item.members.map((member) => (
+
+ ))}
+
+
+
+
+
+ )}
+ />
+ );
+};
+export default Projects;
diff --git a/src/pages/account/center/data.d.ts b/src/pages/account/center/data.d.ts
new file mode 100644
index 00000000..6085c1e5
--- /dev/null
+++ b/src/pages/account/center/data.d.ts
@@ -0,0 +1,75 @@
+export type tabKeyType = 'articles' | 'applications' | 'projects';
+export interface TagType {
+ key: string;
+ label: string;
+}
+
+export type GeographicType = {
+ province: {
+ label: string;
+ key: string;
+ };
+ city: {
+ label: string;
+ key: string;
+ };
+};
+
+export type NoticeType = {
+ id: string;
+ title: string;
+ logo: string;
+ description: string;
+ updatedAt: string;
+ member: string;
+ href: string;
+ memberLink: string;
+};
+
+export type CurrentUser = {
+ name: string;
+ avatar: string;
+ userid: string;
+ notice: NoticeType[];
+ email: string;
+ signature: string;
+ title: string;
+ group: string;
+ tags: TagType[];
+ notifyCount: number;
+ unreadCount: number;
+ country: string;
+ geographic: GeographicType;
+ address: string;
+ phone: string;
+};
+
+export type Member = {
+ avatar: string;
+ name: string;
+ id: string;
+};
+
+export type ListItemDataType = {
+ id: string;
+ owner: string;
+ title: string;
+ avatar: string;
+ cover: string;
+ status: 'normal' | 'exception' | 'active' | 'success';
+ percent: number;
+ logo: string;
+ href: string;
+ body?: any;
+ updatedAt: number;
+ createdAt: number;
+ subDescription: string;
+ description: string;
+ activeUser: number;
+ newUser: number;
+ star: number;
+ like: number;
+ message: number;
+ content: string;
+ members: Member[];
+};
diff --git a/src/pages/account/center/index.tsx b/src/pages/account/center/index.tsx
new file mode 100644
index 00000000..88881321
--- /dev/null
+++ b/src/pages/account/center/index.tsx
@@ -0,0 +1,278 @@
+import {
+ ClusterOutlined,
+ ContactsOutlined,
+ HomeOutlined,
+ PlusOutlined,
+} from '@ant-design/icons';
+import { GridContent } from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import {
+ Avatar,
+ Card,
+ Col,
+ Divider,
+ Input,
+ type InputRef,
+ Row,
+ Tag,
+} from 'antd';
+import React, { useRef, useState } from 'react';
+import useStyles from './Center.style';
+import Applications from './components/Applications';
+import Articles from './components/Articles';
+import Projects from './components/Projects';
+import type { CurrentUser, TagType, tabKeyType } from './data.d';
+import { queryCurrent } from './service';
+
+const operationTabList = [
+ {
+ key: 'articles',
+ tab: (
+
+ 文章{' '}
+
+ (8)
+
+
+ ),
+ },
+ {
+ key: 'applications',
+ tab: (
+
+ 应用{' '}
+
+ (8)
+
+
+ ),
+ },
+ {
+ key: 'projects',
+ tab: (
+
+ 项目{' '}
+
+ (8)
+
+
+ ),
+ },
+];
+const TagList: React.FC<{
+ tags: CurrentUser['tags'];
+}> = ({ tags }) => {
+ const { styles } = useStyles();
+ const ref = useRef(null);
+ const [newTags, setNewTags] = useState([]);
+ const [inputVisible, setInputVisible] = useState(false);
+ const [inputValue, setInputValue] = useState('');
+ const showInput = () => {
+ setInputVisible(true);
+ if (ref.current) {
+ // eslint-disable-next-line no-unused-expressions
+ ref.current?.focus();
+ }
+ };
+ const handleInputChange = (e: React.ChangeEvent) => {
+ 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 (
+
+
标签
+ {(tags || []).concat(newTags).map((item) => (
+
{item.label}
+ ))}
+ {inputVisible && (
+
+ )}
+ {!inputVisible && (
+
+
+
+ )}
+
+ );
+};
+const Center: React.FC = () => {
+ const { styles } = useStyles();
+ const [tabKey, setTabKey] = useState('articles');
+
+ // 获取用户信息
+ const { data: currentUser, loading } = useRequest(() => {
+ return queryCurrent();
+ });
+
+ // 渲染用户信息
+ const renderUserInfo = ({
+ title,
+ group,
+ geographic,
+ }: Partial) => {
+ return (
+
+
+
+ {title}
+
+
+
+ {group}
+
+
+
+ {
+ (
+ geographic || {
+ province: {
+ label: '',
+ },
+ }
+ ).province.label
+ }
+ {
+ (
+ geographic || {
+ city: {
+ label: '',
+ },
+ }
+ ).city.label
+ }
+
+
+ );
+ };
+
+ // 渲染tab切换
+ const renderChildrenByTabKey = (tabValue: tabKeyType) => {
+ if (tabValue === 'projects') {
+ return ;
+ }
+ if (tabValue === 'applications') {
+ return ;
+ }
+ if (tabValue === 'articles') {
+ return ;
+ }
+ return null;
+ };
+ return (
+
+
+
+
+ {!loading && currentUser && (
+ <>
+
+
+
{currentUser.name}
+
{currentUser?.signature}
+
+ {renderUserInfo(currentUser)}
+
+
+
+
+ >
+ )}
+
+
+
+ {
+ setTabKey(_tabKey as tabKeyType);
+ }}
+ >
+ {renderChildrenByTabKey(tabKey)}
+
+
+
+
+ );
+};
+export default Center;
diff --git a/src/pages/account/center/service.ts b/src/pages/account/center/service.ts
new file mode 100644
index 00000000..dee1554e
--- /dev/null
+++ b/src/pages/account/center/service.ts
@@ -0,0 +1,14 @@
+import { request } from '@umijs/max';
+import type { CurrentUser, ListItemDataType } from './data.d';
+
+export async function queryCurrent(): Promise<{ data: CurrentUser }> {
+ return request('/api/currentUserDetail');
+}
+
+export async function queryFakeList(params: {
+ count: number;
+}): Promise<{ data: { list: ListItemDataType[] } }> {
+ return request('/api/fake_list_Detail', {
+ params,
+ });
+}
diff --git a/src/pages/account/settings/_mock.ts b/src/pages/account/settings/_mock.ts
new file mode 100644
index 00000000..4bec6c1b
--- /dev/null
+++ b/src/pages/account/settings/_mock.ts
@@ -0,0 +1,80 @@
+import type { Request, Response } from 'express';
+
+const city = require('./geographic/city.json');
+const province = require('./geographic/province.json');
+
+function getProvince(_: Request, res: Response) {
+ return res.json({
+ data: province,
+ });
+}
+
+function getCity(req: Request, res: Response) {
+ const province = req.params.province;
+ return res.json({
+ data: city[province as keyof typeof city],
+ });
+}
+
+function getCurrentUse(_req: Request, res: Response) {
+ return res.json({
+ data: {
+ name: 'Serati Ma',
+ avatar:
+ 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
+ userid: '00000001',
+ email: 'antdesign@alipay.com',
+ signature: '海纳百川,有容乃大',
+ title: '交互专家',
+ group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+ tags: [
+ {
+ key: '0',
+ label: '很有想法的',
+ },
+ {
+ key: '1',
+ label: '专注设计',
+ },
+ {
+ key: '2',
+ label: '辣~',
+ },
+ {
+ key: '3',
+ label: '大长腿',
+ },
+ {
+ key: '4',
+ label: '川妹子',
+ },
+ {
+ key: '5',
+ label: '海纳百川',
+ },
+ ],
+ notifyCount: 12,
+ unreadCount: 11,
+ country: 'China',
+ geographic: {
+ province: {
+ label: '浙江省',
+ key: '330000',
+ },
+ city: {
+ label: '杭州市',
+ key: '330100',
+ },
+ },
+ address: '西湖区工专路 77 号',
+ phone: '0752-268888888',
+ },
+ });
+}
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
+export default {
+ // 支持值为 Object 和 Array
+ 'GET /api/accountSettingCurrentUser': getCurrentUse,
+ 'GET /api/geographic/province': getProvince,
+ 'GET /api/geographic/city/:province': getCity,
+};
diff --git a/src/pages/account/settings/components/base.tsx b/src/pages/account/settings/components/base.tsx
new file mode 100644
index 00000000..4031876d
--- /dev/null
+++ b/src/pages/account/settings/components/base.tsx
@@ -0,0 +1,239 @@
+import { UploadOutlined } from '@ant-design/icons';
+import {
+ ProForm,
+ ProFormDependency,
+ ProFormFieldSet,
+ ProFormSelect,
+ ProFormText,
+ ProFormTextArea,
+} from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import { Button, Input, message, Upload } from 'antd';
+import React from 'react';
+import { queryCity, queryCurrent, queryProvince } from '../service';
+import useStyles from './index.style';
+
+const validatorPhone = (
+ _rule: any,
+ value: string[],
+ callback: (message?: string) => void,
+) => {
+ if (!value[0]) {
+ callback('Please input your area code!');
+ }
+ if (!value[1]) {
+ callback('Please input your phone number!');
+ }
+ callback();
+};
+
+const BaseView: React.FC = () => {
+ const { styles } = useStyles();
+
+ const { data: currentUser, loading } = useRequest(() => {
+ return queryCurrent();
+ });
+ const getAvatarURL = () => {
+ if (currentUser) {
+ if (currentUser.avatar) {
+ return currentUser.avatar;
+ }
+ const url =
+ 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
+ return url;
+ }
+ return '';
+ };
+ const handleFinish = async () => {
+ message.success('更新基本信息成功');
+ };
+ return (
+
+ {loading ? null : (
+ <>
+
+
+ >
+ )}
+
+ );
+};
+export default BaseView;
+
+const AvatarView = ({ avatar }: { avatar: string }) => {
+ const { styles } = useStyles();
+
+ return (
+ <>
+ 头像
+
+
+
+
+
+
+
+ 更换头像
+
+
+
+ >
+ );
+};
diff --git a/src/pages/account/settings/components/binding.tsx b/src/pages/account/settings/components/binding.tsx
new file mode 100644
index 00000000..4e2d1301
--- /dev/null
+++ b/src/pages/account/settings/components/binding.tsx
@@ -0,0 +1,48 @@
+import {
+ AlipayOutlined,
+ DingdingOutlined,
+ TaobaoOutlined,
+} from '@ant-design/icons';
+import { List } from 'antd';
+import React from 'react';
+
+const BindingView: React.FC = () => {
+ const getData = () => [
+ {
+ title: '绑定淘宝',
+ description: '当前未绑定淘宝账号',
+ actions: [绑定 ],
+ avatar: ,
+ },
+ {
+ title: '绑定支付宝',
+ description: '当前未绑定支付宝账号',
+ actions: [绑定 ],
+ avatar: ,
+ },
+ {
+ title: '绑定钉钉',
+ description: '当前未绑定钉钉账号',
+ actions: [绑定 ],
+ avatar: ,
+ },
+ ];
+
+ return (
+ (
+
+
+
+ )}
+ />
+ );
+};
+
+export default BindingView;
diff --git a/src/pages/account/settings/components/index.style.ts b/src/pages/account/settings/components/index.style.ts
new file mode 100644
index 00000000..5e4a7904
--- /dev/null
+++ b/src/pages/account/settings/components/index.style.ts
@@ -0,0 +1,60 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ baseView: {
+ display: 'flex',
+ paddingTop: '12px',
+ '.ant-legacy-form-item .ant-legacy-form-item-control-wrapper': {
+ width: '100%',
+ },
+ [`@media screen and (max-width: ${token.screenXL}px)`]: {
+ flexDirection: 'column-reverse',
+ },
+ },
+ left: {
+ minWidth: '224px',
+ maxWidth: '448px',
+ },
+ right: {
+ flex: '1',
+ paddingLeft: '104px',
+ [`@media screen and (max-width: ${token.screenXL}px)`]: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ maxWidth: '448px',
+ padding: '20px',
+ },
+ },
+ avatar_title: {
+ height: '22px',
+ marginBottom: '8px',
+ color: token.colorTextHeading,
+ fontSize: token.fontSize,
+ lineHeight: '22px',
+ [`@media screen and (max-width: ${token.screenXL}px)`]: {
+ display: 'none',
+ },
+ },
+ avatar: {
+ width: '144px',
+ height: '144px',
+ marginBottom: '12px',
+ overflow: 'hidden',
+ img: { width: '100%' },
+ },
+ button_view: {
+ width: '144px',
+ textAlign: 'center',
+ },
+ area_code: {
+ width: '72px',
+ },
+ phone_number: {
+ width: '214px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/account/settings/components/notification.tsx b/src/pages/account/settings/components/notification.tsx
new file mode 100644
index 00000000..85d42d7f
--- /dev/null
+++ b/src/pages/account/settings/components/notification.tsx
@@ -0,0 +1,44 @@
+import { List, Switch } from 'antd';
+import React from 'react';
+
+type Unpacked = T extends (infer U)[] ? U : T;
+
+const NotificationView: React.FC = () => {
+ const getData = () => {
+ const Action = (
+
+ );
+ return [
+ {
+ title: '用户消息',
+ description: '其他用户的消息将以站内信的形式通知',
+ actions: [Action],
+ },
+ {
+ title: '系统消息',
+ description: '系统消息将以站内信的形式通知',
+ actions: [Action],
+ },
+ {
+ title: '待办任务',
+ description: '待办任务将以站内信的形式通知',
+ actions: [Action],
+ },
+ ];
+ };
+
+ const data = getData();
+ return (
+ >
+ itemLayout="horizontal"
+ dataSource={data}
+ renderItem={(item) => (
+
+
+
+ )}
+ />
+ );
+};
+
+export default NotificationView;
diff --git a/src/pages/account/settings/components/security.tsx b/src/pages/account/settings/components/security.tsx
new file mode 100644
index 00000000..97a7436e
--- /dev/null
+++ b/src/pages/account/settings/components/security.tsx
@@ -0,0 +1,60 @@
+import { List } from 'antd';
+import React from 'react';
+
+type Unpacked = T extends (infer U)[] ? U : T;
+
+const passwordStrength = {
+ strong: 强 ,
+ medium: 中 ,
+ weak: 弱 Weak ,
+};
+
+const SecurityView: React.FC = () => {
+ const getData = () => [
+ {
+ title: '账户密码',
+ description: (
+ <>
+ 当前密码强度:
+ {passwordStrength.strong}
+ >
+ ),
+ actions: [修改 ],
+ },
+ {
+ title: '密保手机',
+ description: `已绑定手机:138****8293`,
+ actions: [修改 ],
+ },
+ {
+ title: '密保问题',
+ description: '未设置密保问题,密保问题可有效保护账户安全',
+ actions: [设置 ],
+ },
+ {
+ title: '备用邮箱',
+ description: `已绑定邮箱:ant***sign.com`,
+ actions: [修改 ],
+ },
+ {
+ title: 'MFA 设备',
+ description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
+ actions: [绑定 ],
+ },
+ ];
+
+ const data = getData();
+ return (
+ >
+ itemLayout="horizontal"
+ dataSource={data}
+ renderItem={(item) => (
+
+
+
+ )}
+ />
+ );
+};
+
+export default SecurityView;
diff --git a/src/pages/account/settings/data.d.ts b/src/pages/account/settings/data.d.ts
new file mode 100644
index 00000000..f4a88f30
--- /dev/null
+++ b/src/pages/account/settings/data.d.ts
@@ -0,0 +1,43 @@
+export type TagType = {
+ key: string;
+ label: string;
+};
+
+export type GeographicItemType = {
+ name: string;
+ id: string;
+};
+
+export type GeographicType = {
+ province: GeographicItemType;
+ city: GeographicItemType;
+};
+
+export type NoticeType = {
+ id: string;
+ title: string;
+ logo: string;
+ description: string;
+ updatedAt: string;
+ member: string;
+ href: string;
+ memberLink: string;
+};
+
+export type CurrentUser = {
+ name: string;
+ avatar: string;
+ userid: string;
+ notice: NoticeType[];
+ email: string;
+ signature: string;
+ title: string;
+ group: string;
+ tags: TagType[];
+ notifyCount: number;
+ unreadCount: number;
+ country: string;
+ geographic: GeographicType;
+ address: string;
+ phone: string;
+};
diff --git a/src/pages/account/settings/geographic/city.json b/src/pages/account/settings/geographic/city.json
new file mode 100644
index 00000000..29783747
--- /dev/null
+++ b/src/pages/account/settings/geographic/city.json
@@ -0,0 +1,1784 @@
+{
+ "110000": [
+ {
+ "province": "北京市",
+ "name": "市辖区",
+ "id": "110100"
+ }
+ ],
+ "120000": [
+ {
+ "province": "天津市",
+ "name": "市辖区",
+ "id": "120100"
+ }
+ ],
+ "130000": [
+ {
+ "province": "河北省",
+ "name": "石家庄市",
+ "id": "130100"
+ },
+ {
+ "province": "河北省",
+ "name": "唐山市",
+ "id": "130200"
+ },
+ {
+ "province": "河北省",
+ "name": "秦皇岛市",
+ "id": "130300"
+ },
+ {
+ "province": "河北省",
+ "name": "邯郸市",
+ "id": "130400"
+ },
+ {
+ "province": "河北省",
+ "name": "邢台市",
+ "id": "130500"
+ },
+ {
+ "province": "河北省",
+ "name": "保定市",
+ "id": "130600"
+ },
+ {
+ "province": "河北省",
+ "name": "张家口市",
+ "id": "130700"
+ },
+ {
+ "province": "河北省",
+ "name": "承德市",
+ "id": "130800"
+ },
+ {
+ "province": "河北省",
+ "name": "沧州市",
+ "id": "130900"
+ },
+ {
+ "province": "河北省",
+ "name": "廊坊市",
+ "id": "131000"
+ },
+ {
+ "province": "河北省",
+ "name": "衡水市",
+ "id": "131100"
+ },
+ {
+ "province": "河北省",
+ "name": "省直辖县级行政区划",
+ "id": "139000"
+ }
+ ],
+ "140000": [
+ {
+ "province": "山西省",
+ "name": "太原市",
+ "id": "140100"
+ },
+ {
+ "province": "山西省",
+ "name": "大同市",
+ "id": "140200"
+ },
+ {
+ "province": "山西省",
+ "name": "阳泉市",
+ "id": "140300"
+ },
+ {
+ "province": "山西省",
+ "name": "长治市",
+ "id": "140400"
+ },
+ {
+ "province": "山西省",
+ "name": "晋城市",
+ "id": "140500"
+ },
+ {
+ "province": "山西省",
+ "name": "朔州市",
+ "id": "140600"
+ },
+ {
+ "province": "山西省",
+ "name": "晋中市",
+ "id": "140700"
+ },
+ {
+ "province": "山西省",
+ "name": "运城市",
+ "id": "140800"
+ },
+ {
+ "province": "山西省",
+ "name": "忻州市",
+ "id": "140900"
+ },
+ {
+ "province": "山西省",
+ "name": "临汾市",
+ "id": "141000"
+ },
+ {
+ "province": "山西省",
+ "name": "吕梁市",
+ "id": "141100"
+ }
+ ],
+ "150000": [
+ {
+ "province": "内蒙古自治区",
+ "name": "呼和浩特市",
+ "id": "150100"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "包头市",
+ "id": "150200"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "乌海市",
+ "id": "150300"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "赤峰市",
+ "id": "150400"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "通辽市",
+ "id": "150500"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "鄂尔多斯市",
+ "id": "150600"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "呼伦贝尔市",
+ "id": "150700"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "巴彦淖尔市",
+ "id": "150800"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "乌兰察布市",
+ "id": "150900"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "兴安盟",
+ "id": "152200"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "锡林郭勒盟",
+ "id": "152500"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "阿拉善盟",
+ "id": "152900"
+ }
+ ],
+ "210000": [
+ {
+ "province": "辽宁省",
+ "name": "沈阳市",
+ "id": "210100"
+ },
+ {
+ "province": "辽宁省",
+ "name": "大连市",
+ "id": "210200"
+ },
+ {
+ "province": "辽宁省",
+ "name": "鞍山市",
+ "id": "210300"
+ },
+ {
+ "province": "辽宁省",
+ "name": "抚顺市",
+ "id": "210400"
+ },
+ {
+ "province": "辽宁省",
+ "name": "本溪市",
+ "id": "210500"
+ },
+ {
+ "province": "辽宁省",
+ "name": "丹东市",
+ "id": "210600"
+ },
+ {
+ "province": "辽宁省",
+ "name": "锦州市",
+ "id": "210700"
+ },
+ {
+ "province": "辽宁省",
+ "name": "营口市",
+ "id": "210800"
+ },
+ {
+ "province": "辽宁省",
+ "name": "阜新市",
+ "id": "210900"
+ },
+ {
+ "province": "辽宁省",
+ "name": "辽阳市",
+ "id": "211000"
+ },
+ {
+ "province": "辽宁省",
+ "name": "盘锦市",
+ "id": "211100"
+ },
+ {
+ "province": "辽宁省",
+ "name": "铁岭市",
+ "id": "211200"
+ },
+ {
+ "province": "辽宁省",
+ "name": "朝阳市",
+ "id": "211300"
+ },
+ {
+ "province": "辽宁省",
+ "name": "葫芦岛市",
+ "id": "211400"
+ }
+ ],
+ "220000": [
+ {
+ "province": "吉林省",
+ "name": "长春市",
+ "id": "220100"
+ },
+ {
+ "province": "吉林省",
+ "name": "吉林市",
+ "id": "220200"
+ },
+ {
+ "province": "吉林省",
+ "name": "四平市",
+ "id": "220300"
+ },
+ {
+ "province": "吉林省",
+ "name": "辽源市",
+ "id": "220400"
+ },
+ {
+ "province": "吉林省",
+ "name": "通化市",
+ "id": "220500"
+ },
+ {
+ "province": "吉林省",
+ "name": "白山市",
+ "id": "220600"
+ },
+ {
+ "province": "吉林省",
+ "name": "松原市",
+ "id": "220700"
+ },
+ {
+ "province": "吉林省",
+ "name": "白城市",
+ "id": "220800"
+ },
+ {
+ "province": "吉林省",
+ "name": "延边朝鲜族自治州",
+ "id": "222400"
+ }
+ ],
+ "230000": [
+ {
+ "province": "黑龙江省",
+ "name": "哈尔滨市",
+ "id": "230100"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "齐齐哈尔市",
+ "id": "230200"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "鸡西市",
+ "id": "230300"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "鹤岗市",
+ "id": "230400"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "双鸭山市",
+ "id": "230500"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "大庆市",
+ "id": "230600"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "伊春市",
+ "id": "230700"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "佳木斯市",
+ "id": "230800"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "七台河市",
+ "id": "230900"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "牡丹江市",
+ "id": "231000"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "黑河市",
+ "id": "231100"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "绥化市",
+ "id": "231200"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "大兴安岭地区",
+ "id": "232700"
+ }
+ ],
+ "310000": [
+ {
+ "province": "上海市",
+ "name": "市辖区",
+ "id": "310100"
+ }
+ ],
+ "320000": [
+ {
+ "province": "江苏省",
+ "name": "南京市",
+ "id": "320100"
+ },
+ {
+ "province": "江苏省",
+ "name": "无锡市",
+ "id": "320200"
+ },
+ {
+ "province": "江苏省",
+ "name": "徐州市",
+ "id": "320300"
+ },
+ {
+ "province": "江苏省",
+ "name": "常州市",
+ "id": "320400"
+ },
+ {
+ "province": "江苏省",
+ "name": "苏州市",
+ "id": "320500"
+ },
+ {
+ "province": "江苏省",
+ "name": "南通市",
+ "id": "320600"
+ },
+ {
+ "province": "江苏省",
+ "name": "连云港市",
+ "id": "320700"
+ },
+ {
+ "province": "江苏省",
+ "name": "淮安市",
+ "id": "320800"
+ },
+ {
+ "province": "江苏省",
+ "name": "盐城市",
+ "id": "320900"
+ },
+ {
+ "province": "江苏省",
+ "name": "扬州市",
+ "id": "321000"
+ },
+ {
+ "province": "江苏省",
+ "name": "镇江市",
+ "id": "321100"
+ },
+ {
+ "province": "江苏省",
+ "name": "泰州市",
+ "id": "321200"
+ },
+ {
+ "province": "江苏省",
+ "name": "宿迁市",
+ "id": "321300"
+ }
+ ],
+ "330000": [
+ {
+ "province": "浙江省",
+ "name": "杭州市",
+ "id": "330100"
+ },
+ {
+ "province": "浙江省",
+ "name": "宁波市",
+ "id": "330200"
+ },
+ {
+ "province": "浙江省",
+ "name": "温州市",
+ "id": "330300"
+ },
+ {
+ "province": "浙江省",
+ "name": "嘉兴市",
+ "id": "330400"
+ },
+ {
+ "province": "浙江省",
+ "name": "湖州市",
+ "id": "330500"
+ },
+ {
+ "province": "浙江省",
+ "name": "绍兴市",
+ "id": "330600"
+ },
+ {
+ "province": "浙江省",
+ "name": "金华市",
+ "id": "330700"
+ },
+ {
+ "province": "浙江省",
+ "name": "衢州市",
+ "id": "330800"
+ },
+ {
+ "province": "浙江省",
+ "name": "舟山市",
+ "id": "330900"
+ },
+ {
+ "province": "浙江省",
+ "name": "台州市",
+ "id": "331000"
+ },
+ {
+ "province": "浙江省",
+ "name": "丽水市",
+ "id": "331100"
+ }
+ ],
+ "340000": [
+ {
+ "province": "安徽省",
+ "name": "合肥市",
+ "id": "340100"
+ },
+ {
+ "province": "安徽省",
+ "name": "芜湖市",
+ "id": "340200"
+ },
+ {
+ "province": "安徽省",
+ "name": "蚌埠市",
+ "id": "340300"
+ },
+ {
+ "province": "安徽省",
+ "name": "淮南市",
+ "id": "340400"
+ },
+ {
+ "province": "安徽省",
+ "name": "马鞍山市",
+ "id": "340500"
+ },
+ {
+ "province": "安徽省",
+ "name": "淮北市",
+ "id": "340600"
+ },
+ {
+ "province": "安徽省",
+ "name": "铜陵市",
+ "id": "340700"
+ },
+ {
+ "province": "安徽省",
+ "name": "安庆市",
+ "id": "340800"
+ },
+ {
+ "province": "安徽省",
+ "name": "黄山市",
+ "id": "341000"
+ },
+ {
+ "province": "安徽省",
+ "name": "滁州市",
+ "id": "341100"
+ },
+ {
+ "province": "安徽省",
+ "name": "阜阳市",
+ "id": "341200"
+ },
+ {
+ "province": "安徽省",
+ "name": "宿州市",
+ "id": "341300"
+ },
+ {
+ "province": "安徽省",
+ "name": "六安市",
+ "id": "341500"
+ },
+ {
+ "province": "安徽省",
+ "name": "亳州市",
+ "id": "341600"
+ },
+ {
+ "province": "安徽省",
+ "name": "池州市",
+ "id": "341700"
+ },
+ {
+ "province": "安徽省",
+ "name": "宣城市",
+ "id": "341800"
+ }
+ ],
+ "350000": [
+ {
+ "province": "福建省",
+ "name": "福州市",
+ "id": "350100"
+ },
+ {
+ "province": "福建省",
+ "name": "厦门市",
+ "id": "350200"
+ },
+ {
+ "province": "福建省",
+ "name": "莆田市",
+ "id": "350300"
+ },
+ {
+ "province": "福建省",
+ "name": "三明市",
+ "id": "350400"
+ },
+ {
+ "province": "福建省",
+ "name": "泉州市",
+ "id": "350500"
+ },
+ {
+ "province": "福建省",
+ "name": "漳州市",
+ "id": "350600"
+ },
+ {
+ "province": "福建省",
+ "name": "南平市",
+ "id": "350700"
+ },
+ {
+ "province": "福建省",
+ "name": "龙岩市",
+ "id": "350800"
+ },
+ {
+ "province": "福建省",
+ "name": "宁德市",
+ "id": "350900"
+ }
+ ],
+ "360000": [
+ {
+ "province": "江西省",
+ "name": "南昌市",
+ "id": "360100"
+ },
+ {
+ "province": "江西省",
+ "name": "景德镇市",
+ "id": "360200"
+ },
+ {
+ "province": "江西省",
+ "name": "萍乡市",
+ "id": "360300"
+ },
+ {
+ "province": "江西省",
+ "name": "九江市",
+ "id": "360400"
+ },
+ {
+ "province": "江西省",
+ "name": "新余市",
+ "id": "360500"
+ },
+ {
+ "province": "江西省",
+ "name": "鹰潭市",
+ "id": "360600"
+ },
+ {
+ "province": "江西省",
+ "name": "赣州市",
+ "id": "360700"
+ },
+ {
+ "province": "江西省",
+ "name": "吉安市",
+ "id": "360800"
+ },
+ {
+ "province": "江西省",
+ "name": "宜春市",
+ "id": "360900"
+ },
+ {
+ "province": "江西省",
+ "name": "抚州市",
+ "id": "361000"
+ },
+ {
+ "province": "江西省",
+ "name": "上饶市",
+ "id": "361100"
+ }
+ ],
+ "370000": [
+ {
+ "province": "山东省",
+ "name": "济南市",
+ "id": "370100"
+ },
+ {
+ "province": "山东省",
+ "name": "青岛市",
+ "id": "370200"
+ },
+ {
+ "province": "山东省",
+ "name": "淄博市",
+ "id": "370300"
+ },
+ {
+ "province": "山东省",
+ "name": "枣庄市",
+ "id": "370400"
+ },
+ {
+ "province": "山东省",
+ "name": "东营市",
+ "id": "370500"
+ },
+ {
+ "province": "山东省",
+ "name": "烟台市",
+ "id": "370600"
+ },
+ {
+ "province": "山东省",
+ "name": "潍坊市",
+ "id": "370700"
+ },
+ {
+ "province": "山东省",
+ "name": "济宁市",
+ "id": "370800"
+ },
+ {
+ "province": "山东省",
+ "name": "泰安市",
+ "id": "370900"
+ },
+ {
+ "province": "山东省",
+ "name": "威海市",
+ "id": "371000"
+ },
+ {
+ "province": "山东省",
+ "name": "日照市",
+ "id": "371100"
+ },
+ {
+ "province": "山东省",
+ "name": "莱芜市",
+ "id": "371200"
+ },
+ {
+ "province": "山东省",
+ "name": "临沂市",
+ "id": "371300"
+ },
+ {
+ "province": "山东省",
+ "name": "德州市",
+ "id": "371400"
+ },
+ {
+ "province": "山东省",
+ "name": "聊城市",
+ "id": "371500"
+ },
+ {
+ "province": "山东省",
+ "name": "滨州市",
+ "id": "371600"
+ },
+ {
+ "province": "山东省",
+ "name": "菏泽市",
+ "id": "371700"
+ }
+ ],
+ "410000": [
+ {
+ "province": "河南省",
+ "name": "郑州市",
+ "id": "410100"
+ },
+ {
+ "province": "河南省",
+ "name": "开封市",
+ "id": "410200"
+ },
+ {
+ "province": "河南省",
+ "name": "洛阳市",
+ "id": "410300"
+ },
+ {
+ "province": "河南省",
+ "name": "平顶山市",
+ "id": "410400"
+ },
+ {
+ "province": "河南省",
+ "name": "安阳市",
+ "id": "410500"
+ },
+ {
+ "province": "河南省",
+ "name": "鹤壁市",
+ "id": "410600"
+ },
+ {
+ "province": "河南省",
+ "name": "新乡市",
+ "id": "410700"
+ },
+ {
+ "province": "河南省",
+ "name": "焦作市",
+ "id": "410800"
+ },
+ {
+ "province": "河南省",
+ "name": "濮阳市",
+ "id": "410900"
+ },
+ {
+ "province": "河南省",
+ "name": "许昌市",
+ "id": "411000"
+ },
+ {
+ "province": "河南省",
+ "name": "漯河市",
+ "id": "411100"
+ },
+ {
+ "province": "河南省",
+ "name": "三门峡市",
+ "id": "411200"
+ },
+ {
+ "province": "河南省",
+ "name": "南阳市",
+ "id": "411300"
+ },
+ {
+ "province": "河南省",
+ "name": "商丘市",
+ "id": "411400"
+ },
+ {
+ "province": "河南省",
+ "name": "信阳市",
+ "id": "411500"
+ },
+ {
+ "province": "河南省",
+ "name": "周口市",
+ "id": "411600"
+ },
+ {
+ "province": "河南省",
+ "name": "驻马店市",
+ "id": "411700"
+ },
+ {
+ "province": "河南省",
+ "name": "省直辖县级行政区划",
+ "id": "419000"
+ }
+ ],
+ "420000": [
+ {
+ "province": "湖北省",
+ "name": "武汉市",
+ "id": "420100"
+ },
+ {
+ "province": "湖北省",
+ "name": "黄石市",
+ "id": "420200"
+ },
+ {
+ "province": "湖北省",
+ "name": "十堰市",
+ "id": "420300"
+ },
+ {
+ "province": "湖北省",
+ "name": "宜昌市",
+ "id": "420500"
+ },
+ {
+ "province": "湖北省",
+ "name": "襄阳市",
+ "id": "420600"
+ },
+ {
+ "province": "湖北省",
+ "name": "鄂州市",
+ "id": "420700"
+ },
+ {
+ "province": "湖北省",
+ "name": "荆门市",
+ "id": "420800"
+ },
+ {
+ "province": "湖北省",
+ "name": "孝感市",
+ "id": "420900"
+ },
+ {
+ "province": "湖北省",
+ "name": "荆州市",
+ "id": "421000"
+ },
+ {
+ "province": "湖北省",
+ "name": "黄冈市",
+ "id": "421100"
+ },
+ {
+ "province": "湖北省",
+ "name": "咸宁市",
+ "id": "421200"
+ },
+ {
+ "province": "湖北省",
+ "name": "随州市",
+ "id": "421300"
+ },
+ {
+ "province": "湖北省",
+ "name": "恩施土家族苗族自治州",
+ "id": "422800"
+ },
+ {
+ "province": "湖北省",
+ "name": "省直辖县级行政区划",
+ "id": "429000"
+ }
+ ],
+ "430000": [
+ {
+ "province": "湖南省",
+ "name": "长沙市",
+ "id": "430100"
+ },
+ {
+ "province": "湖南省",
+ "name": "株洲市",
+ "id": "430200"
+ },
+ {
+ "province": "湖南省",
+ "name": "湘潭市",
+ "id": "430300"
+ },
+ {
+ "province": "湖南省",
+ "name": "衡阳市",
+ "id": "430400"
+ },
+ {
+ "province": "湖南省",
+ "name": "邵阳市",
+ "id": "430500"
+ },
+ {
+ "province": "湖南省",
+ "name": "岳阳市",
+ "id": "430600"
+ },
+ {
+ "province": "湖南省",
+ "name": "常德市",
+ "id": "430700"
+ },
+ {
+ "province": "湖南省",
+ "name": "张家界市",
+ "id": "430800"
+ },
+ {
+ "province": "湖南省",
+ "name": "益阳市",
+ "id": "430900"
+ },
+ {
+ "province": "湖南省",
+ "name": "郴州市",
+ "id": "431000"
+ },
+ {
+ "province": "湖南省",
+ "name": "永州市",
+ "id": "431100"
+ },
+ {
+ "province": "湖南省",
+ "name": "怀化市",
+ "id": "431200"
+ },
+ {
+ "province": "湖南省",
+ "name": "娄底市",
+ "id": "431300"
+ },
+ {
+ "province": "湖南省",
+ "name": "湘西土家族苗族自治州",
+ "id": "433100"
+ }
+ ],
+ "440000": [
+ {
+ "province": "广东省",
+ "name": "广州市",
+ "id": "440100"
+ },
+ {
+ "province": "广东省",
+ "name": "韶关市",
+ "id": "440200"
+ },
+ {
+ "province": "广东省",
+ "name": "深圳市",
+ "id": "440300"
+ },
+ {
+ "province": "广东省",
+ "name": "珠海市",
+ "id": "440400"
+ },
+ {
+ "province": "广东省",
+ "name": "汕头市",
+ "id": "440500"
+ },
+ {
+ "province": "广东省",
+ "name": "佛山市",
+ "id": "440600"
+ },
+ {
+ "province": "广东省",
+ "name": "江门市",
+ "id": "440700"
+ },
+ {
+ "province": "广东省",
+ "name": "湛江市",
+ "id": "440800"
+ },
+ {
+ "province": "广东省",
+ "name": "茂名市",
+ "id": "440900"
+ },
+ {
+ "province": "广东省",
+ "name": "肇庆市",
+ "id": "441200"
+ },
+ {
+ "province": "广东省",
+ "name": "惠州市",
+ "id": "441300"
+ },
+ {
+ "province": "广东省",
+ "name": "梅州市",
+ "id": "441400"
+ },
+ {
+ "province": "广东省",
+ "name": "汕尾市",
+ "id": "441500"
+ },
+ {
+ "province": "广东省",
+ "name": "河源市",
+ "id": "441600"
+ },
+ {
+ "province": "广东省",
+ "name": "阳江市",
+ "id": "441700"
+ },
+ {
+ "province": "广东省",
+ "name": "清远市",
+ "id": "441800"
+ },
+ {
+ "province": "广东省",
+ "name": "东莞市",
+ "id": "441900"
+ },
+ {
+ "province": "广东省",
+ "name": "中山市",
+ "id": "442000"
+ },
+ {
+ "province": "广东省",
+ "name": "潮州市",
+ "id": "445100"
+ },
+ {
+ "province": "广东省",
+ "name": "揭阳市",
+ "id": "445200"
+ },
+ {
+ "province": "广东省",
+ "name": "云浮市",
+ "id": "445300"
+ }
+ ],
+ "450000": [
+ {
+ "province": "广西壮族自治区",
+ "name": "南宁市",
+ "id": "450100"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "柳州市",
+ "id": "450200"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "桂林市",
+ "id": "450300"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "梧州市",
+ "id": "450400"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "北海市",
+ "id": "450500"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "防城港市",
+ "id": "450600"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "钦州市",
+ "id": "450700"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "贵港市",
+ "id": "450800"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "玉林市",
+ "id": "450900"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "百色市",
+ "id": "451000"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "贺州市",
+ "id": "451100"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "河池市",
+ "id": "451200"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "来宾市",
+ "id": "451300"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "崇左市",
+ "id": "451400"
+ }
+ ],
+ "460000": [
+ {
+ "province": "海南省",
+ "name": "海口市",
+ "id": "460100"
+ },
+ {
+ "province": "海南省",
+ "name": "三亚市",
+ "id": "460200"
+ },
+ {
+ "province": "海南省",
+ "name": "三沙市",
+ "id": "460300"
+ },
+ {
+ "province": "海南省",
+ "name": "儋州市",
+ "id": "460400"
+ },
+ {
+ "province": "海南省",
+ "name": "省直辖县级行政区划",
+ "id": "469000"
+ }
+ ],
+ "500000": [
+ {
+ "province": "重庆市",
+ "name": "市辖区",
+ "id": "500100"
+ },
+ {
+ "province": "重庆市",
+ "name": "县",
+ "id": "500200"
+ }
+ ],
+ "510000": [
+ {
+ "province": "四川省",
+ "name": "成都市",
+ "id": "510100"
+ },
+ {
+ "province": "四川省",
+ "name": "自贡市",
+ "id": "510300"
+ },
+ {
+ "province": "四川省",
+ "name": "攀枝花市",
+ "id": "510400"
+ },
+ {
+ "province": "四川省",
+ "name": "泸州市",
+ "id": "510500"
+ },
+ {
+ "province": "四川省",
+ "name": "德阳市",
+ "id": "510600"
+ },
+ {
+ "province": "四川省",
+ "name": "绵阳市",
+ "id": "510700"
+ },
+ {
+ "province": "四川省",
+ "name": "广元市",
+ "id": "510800"
+ },
+ {
+ "province": "四川省",
+ "name": "遂宁市",
+ "id": "510900"
+ },
+ {
+ "province": "四川省",
+ "name": "内江市",
+ "id": "511000"
+ },
+ {
+ "province": "四川省",
+ "name": "乐山市",
+ "id": "511100"
+ },
+ {
+ "province": "四川省",
+ "name": "南充市",
+ "id": "511300"
+ },
+ {
+ "province": "四川省",
+ "name": "眉山市",
+ "id": "511400"
+ },
+ {
+ "province": "四川省",
+ "name": "宜宾市",
+ "id": "511500"
+ },
+ {
+ "province": "四川省",
+ "name": "广安市",
+ "id": "511600"
+ },
+ {
+ "province": "四川省",
+ "name": "达州市",
+ "id": "511700"
+ },
+ {
+ "province": "四川省",
+ "name": "雅安市",
+ "id": "511800"
+ },
+ {
+ "province": "四川省",
+ "name": "巴中市",
+ "id": "511900"
+ },
+ {
+ "province": "四川省",
+ "name": "资阳市",
+ "id": "512000"
+ },
+ {
+ "province": "四川省",
+ "name": "阿坝藏族羌族自治州",
+ "id": "513200"
+ },
+ {
+ "province": "四川省",
+ "name": "甘孜藏族自治州",
+ "id": "513300"
+ },
+ {
+ "province": "四川省",
+ "name": "凉山彝族自治州",
+ "id": "513400"
+ }
+ ],
+ "520000": [
+ {
+ "province": "贵州省",
+ "name": "贵阳市",
+ "id": "520100"
+ },
+ {
+ "province": "贵州省",
+ "name": "六盘水市",
+ "id": "520200"
+ },
+ {
+ "province": "贵州省",
+ "name": "遵义市",
+ "id": "520300"
+ },
+ {
+ "province": "贵州省",
+ "name": "安顺市",
+ "id": "520400"
+ },
+ {
+ "province": "贵州省",
+ "name": "毕节市",
+ "id": "520500"
+ },
+ {
+ "province": "贵州省",
+ "name": "铜仁市",
+ "id": "520600"
+ },
+ {
+ "province": "贵州省",
+ "name": "黔西南布依族苗族自治州",
+ "id": "522300"
+ },
+ {
+ "province": "贵州省",
+ "name": "黔东南苗族侗族自治州",
+ "id": "522600"
+ },
+ {
+ "province": "贵州省",
+ "name": "黔南布依族苗族自治州",
+ "id": "522700"
+ }
+ ],
+ "530000": [
+ {
+ "province": "云南省",
+ "name": "昆明市",
+ "id": "530100"
+ },
+ {
+ "province": "云南省",
+ "name": "曲靖市",
+ "id": "530300"
+ },
+ {
+ "province": "云南省",
+ "name": "玉溪市",
+ "id": "530400"
+ },
+ {
+ "province": "云南省",
+ "name": "保山市",
+ "id": "530500"
+ },
+ {
+ "province": "云南省",
+ "name": "昭通市",
+ "id": "530600"
+ },
+ {
+ "province": "云南省",
+ "name": "丽江市",
+ "id": "530700"
+ },
+ {
+ "province": "云南省",
+ "name": "普洱市",
+ "id": "530800"
+ },
+ {
+ "province": "云南省",
+ "name": "临沧市",
+ "id": "530900"
+ },
+ {
+ "province": "云南省",
+ "name": "楚雄彝族自治州",
+ "id": "532300"
+ },
+ {
+ "province": "云南省",
+ "name": "红河哈尼族彝族自治州",
+ "id": "532500"
+ },
+ {
+ "province": "云南省",
+ "name": "文山壮族苗族自治州",
+ "id": "532600"
+ },
+ {
+ "province": "云南省",
+ "name": "西双版纳傣族自治州",
+ "id": "532800"
+ },
+ {
+ "province": "云南省",
+ "name": "大理白族自治州",
+ "id": "532900"
+ },
+ {
+ "province": "云南省",
+ "name": "德宏傣族景颇族自治州",
+ "id": "533100"
+ },
+ {
+ "province": "云南省",
+ "name": "怒江傈僳族自治州",
+ "id": "533300"
+ },
+ {
+ "province": "云南省",
+ "name": "迪庆藏族自治州",
+ "id": "533400"
+ }
+ ],
+ "540000": [
+ {
+ "province": "西藏自治区",
+ "name": "拉萨市",
+ "id": "540100"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "日喀则市",
+ "id": "540200"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "昌都市",
+ "id": "540300"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "林芝市",
+ "id": "540400"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "山南市",
+ "id": "540500"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "那曲地区",
+ "id": "542400"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "阿里地区",
+ "id": "542500"
+ }
+ ],
+ "610000": [
+ {
+ "province": "陕西省",
+ "name": "西安市",
+ "id": "610100"
+ },
+ {
+ "province": "陕西省",
+ "name": "铜川市",
+ "id": "610200"
+ },
+ {
+ "province": "陕西省",
+ "name": "宝鸡市",
+ "id": "610300"
+ },
+ {
+ "province": "陕西省",
+ "name": "咸阳市",
+ "id": "610400"
+ },
+ {
+ "province": "陕西省",
+ "name": "渭南市",
+ "id": "610500"
+ },
+ {
+ "province": "陕西省",
+ "name": "延安市",
+ "id": "610600"
+ },
+ {
+ "province": "陕西省",
+ "name": "汉中市",
+ "id": "610700"
+ },
+ {
+ "province": "陕西省",
+ "name": "榆林市",
+ "id": "610800"
+ },
+ {
+ "province": "陕西省",
+ "name": "安康市",
+ "id": "610900"
+ },
+ {
+ "province": "陕西省",
+ "name": "商洛市",
+ "id": "611000"
+ }
+ ],
+ "620000": [
+ {
+ "province": "甘肃省",
+ "name": "兰州市",
+ "id": "620100"
+ },
+ {
+ "province": "甘肃省",
+ "name": "嘉峪关市",
+ "id": "620200"
+ },
+ {
+ "province": "甘肃省",
+ "name": "金昌市",
+ "id": "620300"
+ },
+ {
+ "province": "甘肃省",
+ "name": "白银市",
+ "id": "620400"
+ },
+ {
+ "province": "甘肃省",
+ "name": "天水市",
+ "id": "620500"
+ },
+ {
+ "province": "甘肃省",
+ "name": "武威市",
+ "id": "620600"
+ },
+ {
+ "province": "甘肃省",
+ "name": "张掖市",
+ "id": "620700"
+ },
+ {
+ "province": "甘肃省",
+ "name": "平凉市",
+ "id": "620800"
+ },
+ {
+ "province": "甘肃省",
+ "name": "酒泉市",
+ "id": "620900"
+ },
+ {
+ "province": "甘肃省",
+ "name": "庆阳市",
+ "id": "621000"
+ },
+ {
+ "province": "甘肃省",
+ "name": "定西市",
+ "id": "621100"
+ },
+ {
+ "province": "甘肃省",
+ "name": "陇南市",
+ "id": "621200"
+ },
+ {
+ "province": "甘肃省",
+ "name": "临夏回族自治州",
+ "id": "622900"
+ },
+ {
+ "province": "甘肃省",
+ "name": "甘南藏族自治州",
+ "id": "623000"
+ }
+ ],
+ "630000": [
+ {
+ "province": "青海省",
+ "name": "西宁市",
+ "id": "630100"
+ },
+ {
+ "province": "青海省",
+ "name": "海东市",
+ "id": "630200"
+ },
+ {
+ "province": "青海省",
+ "name": "海北藏族自治州",
+ "id": "632200"
+ },
+ {
+ "province": "青海省",
+ "name": "黄南藏族自治州",
+ "id": "632300"
+ },
+ {
+ "province": "青海省",
+ "name": "海南藏族自治州",
+ "id": "632500"
+ },
+ {
+ "province": "青海省",
+ "name": "果洛藏族自治州",
+ "id": "632600"
+ },
+ {
+ "province": "青海省",
+ "name": "玉树藏族自治州",
+ "id": "632700"
+ },
+ {
+ "province": "青海省",
+ "name": "海西蒙古族藏族自治州",
+ "id": "632800"
+ }
+ ],
+ "640000": [
+ {
+ "province": "宁夏回族自治区",
+ "name": "银川市",
+ "id": "640100"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "石嘴山市",
+ "id": "640200"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "吴忠市",
+ "id": "640300"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "固原市",
+ "id": "640400"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "中卫市",
+ "id": "640500"
+ }
+ ],
+ "650000": [
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "乌鲁木齐市",
+ "id": "650100"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "克拉玛依市",
+ "id": "650200"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "吐鲁番市",
+ "id": "650400"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "哈密市",
+ "id": "650500"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "昌吉回族自治州",
+ "id": "652300"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "博尔塔拉蒙古自治州",
+ "id": "652700"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "巴音郭楞蒙古自治州",
+ "id": "652800"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "阿克苏地区",
+ "id": "652900"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "克孜勒苏柯尔克孜自治州",
+ "id": "653000"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "喀什地区",
+ "id": "653100"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "和田地区",
+ "id": "653200"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "伊犁哈萨克自治州",
+ "id": "654000"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "塔城地区",
+ "id": "654200"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "阿勒泰地区",
+ "id": "654300"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "自治区直辖县级行政区划",
+ "id": "659000"
+ }
+ ]
+}
diff --git a/src/pages/account/settings/geographic/province.json b/src/pages/account/settings/geographic/province.json
new file mode 100644
index 00000000..910c83f0
--- /dev/null
+++ b/src/pages/account/settings/geographic/province.json
@@ -0,0 +1,138 @@
+[
+ {
+ "name": "北京市",
+ "id": "110000"
+ },
+ {
+ "name": "天津市",
+ "id": "120000"
+ },
+ {
+ "name": "河北省",
+ "id": "130000"
+ },
+ {
+ "name": "山西省",
+ "id": "140000"
+ },
+ {
+ "name": "内蒙古自治区",
+ "id": "150000"
+ },
+ {
+ "name": "辽宁省",
+ "id": "210000"
+ },
+ {
+ "name": "吉林省",
+ "id": "220000"
+ },
+ {
+ "name": "黑龙江省",
+ "id": "230000"
+ },
+ {
+ "name": "上海市",
+ "id": "310000"
+ },
+ {
+ "name": "江苏省",
+ "id": "320000"
+ },
+ {
+ "name": "浙江省",
+ "id": "330000"
+ },
+ {
+ "name": "安徽省",
+ "id": "340000"
+ },
+ {
+ "name": "福建省",
+ "id": "350000"
+ },
+ {
+ "name": "江西省",
+ "id": "360000"
+ },
+ {
+ "name": "山东省",
+ "id": "370000"
+ },
+ {
+ "name": "河南省",
+ "id": "410000"
+ },
+ {
+ "name": "湖北省",
+ "id": "420000"
+ },
+ {
+ "name": "湖南省",
+ "id": "430000"
+ },
+ {
+ "name": "广东省",
+ "id": "440000"
+ },
+ {
+ "name": "广西壮族自治区",
+ "id": "450000"
+ },
+ {
+ "name": "海南省",
+ "id": "460000"
+ },
+ {
+ "name": "重庆市",
+ "id": "500000"
+ },
+ {
+ "name": "四川省",
+ "id": "510000"
+ },
+ {
+ "name": "贵州省",
+ "id": "520000"
+ },
+ {
+ "name": "云南省",
+ "id": "530000"
+ },
+ {
+ "name": "西藏自治区",
+ "id": "540000"
+ },
+ {
+ "name": "陕西省",
+ "id": "610000"
+ },
+ {
+ "name": "甘肃省",
+ "id": "620000"
+ },
+ {
+ "name": "青海省",
+ "id": "630000"
+ },
+ {
+ "name": "宁夏回族自治区",
+ "id": "640000"
+ },
+ {
+ "name": "新疆维吾尔自治区",
+ "id": "650000"
+ },
+ {
+ "name": "台湾省",
+ "id": "710000"
+ },
+ {
+ "name": "香港特别行政区",
+ "id": "810000"
+ },
+ {
+ "name": "澳门特别行政区",
+ "id": "820000"
+ }
+]
diff --git a/src/pages/account/settings/index.tsx b/src/pages/account/settings/index.tsx
new file mode 100644
index 00000000..fc59fbf2
--- /dev/null
+++ b/src/pages/account/settings/index.tsx
@@ -0,0 +1,108 @@
+import { GridContent } from '@ant-design/pro-components';
+import { Menu } from 'antd';
+import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
+import BaseView from './components/base';
+import BindingView from './components/binding';
+import NotificationView from './components/notification';
+import SecurityView from './components/security';
+import useStyles from './style.style';
+
+type SettingsStateKeys = 'base' | 'security' | 'binding' | 'notification';
+type SettingsState = {
+ mode: 'inline' | 'horizontal';
+ selectKey: SettingsStateKeys;
+};
+const Settings: React.FC = () => {
+ const { styles } = useStyles();
+ const menuMap: Record = {
+ base: '基本设置',
+ security: '安全设置',
+ binding: '账号绑定',
+ notification: '新消息通知',
+ };
+ const [initConfig, setInitConfig] = useState({
+ mode: 'inline',
+ selectKey: 'base',
+ });
+ const dom = useRef(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 ;
+ case 'security':
+ return ;
+ case 'binding':
+ return ;
+ case 'notification':
+ return ;
+ default:
+ return null;
+ }
+ };
+ return (
+
+ {
+ if (ref) {
+ dom.current = ref;
+ }
+ }}
+ >
+
+
{
+ setInitConfig({
+ ...initConfig,
+ selectKey: key as SettingsStateKeys,
+ });
+ }}
+ items={getMenu()}
+ />
+
+
+
{menuMap[initConfig.selectKey]}
+ {renderChildren()}
+
+
+
+ );
+};
+export default Settings;
diff --git a/src/pages/account/settings/service.ts b/src/pages/account/settings/service.ts
new file mode 100644
index 00000000..2daaf243
--- /dev/null
+++ b/src/pages/account/settings/service.ts
@@ -0,0 +1,20 @@
+import { request } from '@umijs/max';
+import type { CurrentUser, GeographicItemType } from './data';
+
+export async function queryCurrent(): Promise<{ data: CurrentUser }> {
+ return request('/api/accountSettingCurrentUser');
+}
+
+export async function queryProvince(): Promise<{ data: GeographicItemType[] }> {
+ return request('/api/geographic/province');
+}
+
+export async function queryCity(
+ province: string,
+): Promise<{ data: GeographicItemType[] }> {
+ return request(`/api/geographic/city/${province}`);
+}
+
+export async function query() {
+ return request('/api/users');
+}
diff --git a/src/pages/account/settings/style.style.ts b/src/pages/account/settings/style.style.ts
new file mode 100644
index 00000000..8ff05765
--- /dev/null
+++ b/src/pages/account/settings/style.style.ts
@@ -0,0 +1,74 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ main: {
+ display: 'flex',
+ width: '100%',
+ height: '100%',
+ paddingTop: '16px',
+ paddingBottom: '16px',
+ backgroundColor: token.colorBgContainer,
+ '.ant-list-split .ant-list-item:last-child': {
+ borderBottom: `1px solid ${token.colorSplit}`,
+ },
+ '.ant-list-item': { paddingTop: '14px', paddingBottom: '14px' },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ flexDirection: 'column',
+ },
+ },
+ leftMenu: {
+ width: '224px',
+ borderRight: `${token.lineWidth}px solid ${token.colorSplit}`,
+ '.ant-menu-inline': { border: 'none' },
+ '.ant-menu-horizontal': { fontWeight: 'bold' },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ width: '100%',
+ border: 'none',
+ },
+ },
+ right: {
+ flex: '1',
+ padding: '8px 40px',
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ padding: '40px',
+ },
+ },
+ title: {
+ marginBottom: '12px',
+ color: token.colorTextHeading,
+ fontWeight: '500',
+ fontSize: '20px',
+ lineHeight: '28px',
+ },
+ taobao: {
+ display: 'block',
+ color: '#ff4000',
+ fontSize: '48px',
+ lineHeight: '48px',
+ borderRadius: token.borderRadius,
+ },
+ dingding: {
+ margin: '2px',
+ padding: '6px',
+ color: '#fff',
+ fontSize: '32px',
+ lineHeight: '32px',
+ backgroundColor: '#2eabff',
+ borderRadius: token.borderRadius,
+ },
+ alipay: {
+ color: '#2eabff',
+ fontSize: '48px',
+ lineHeight: '48px',
+ borderRadius: token.borderRadius,
+ },
+ ':global': {
+ 'font.strong': { color: token.colorSuccess },
+ 'font.medium': { color: token.colorWarning },
+ 'font.weak': { color: token.colorError },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/analysis/_mock.ts b/src/pages/dashboard/analysis/_mock.ts
new file mode 100644
index 00000000..4b6a0767
--- /dev/null
+++ b/src/pages/dashboard/analysis/_mock.ts
@@ -0,0 +1,210 @@
+import dayjs from 'dayjs';
+import type { Request, Response } from 'express';
+import type { AnalysisData, DataItem, RadarData } from './data.d';
+
+// mock data
+const visitData: DataItem[] = [];
+const beginDay = Date.now();
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+for (let i = 0; i < fakeY.length; i += 1) {
+ visitData.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY[i],
+ });
+}
+
+const visitData2 = [];
+const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
+for (let i = 0; i < fakeY2.length; i += 1) {
+ visitData2.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY2[i],
+ });
+}
+
+const salesData = [];
+for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+}
+const searchData = [];
+for (let i = 0; i < 50; i += 1) {
+ searchData.push({
+ index: i + 1,
+ keyword: `搜索关键词-${i}`,
+ count: Math.floor(Math.random() * 1000),
+ range: Math.floor(Math.random() * 100),
+ status: Math.floor((Math.random() * 10) % 2),
+ });
+}
+const salesTypeData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+];
+
+const salesTypeDataOnline = [
+ {
+ x: '家用电器',
+ y: 244,
+ },
+ {
+ x: '食用酒水',
+ y: 321,
+ },
+ {
+ x: '个护健康',
+ y: 311,
+ },
+ {
+ x: '服饰箱包',
+ y: 41,
+ },
+ {
+ x: '母婴产品',
+ y: 121,
+ },
+ {
+ x: '其他',
+ y: 111,
+ },
+];
+
+const salesTypeDataOffline = [
+ {
+ x: '家用电器',
+ y: 99,
+ },
+ {
+ x: '食用酒水',
+ y: 188,
+ },
+ {
+ x: '个护健康',
+ y: 344,
+ },
+ {
+ x: '服饰箱包',
+ y: 255,
+ },
+ {
+ x: '其他',
+ y: 65,
+ },
+];
+
+const offlineData = [];
+for (let i = 0; i < 10; i += 1) {
+ offlineData.push({
+ name: `Stores ${i}`,
+ cvr: Math.ceil(Math.random() * 9) / 10,
+ });
+}
+const offlineChartData = [];
+for (let i = 0; i < 20; i += 1) {
+ const date = dayjs(Date.now() + 1000 * 60 * 30 * i).format('HH:mm');
+ offlineChartData.push({
+ date,
+ type: '客流量',
+ value: Math.floor(Math.random() * 100) + 10,
+ });
+ offlineChartData.push({
+ date,
+ type: '支付笔数',
+ value: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+const radarOriginData = [
+ {
+ name: '个人',
+ ref: 10,
+ koubei: 8,
+ output: 4,
+ contribute: 5,
+ hot: 7,
+ },
+ {
+ name: '团队',
+ ref: 3,
+ koubei: 9,
+ output: 6,
+ contribute: 3,
+ hot: 1,
+ },
+ {
+ name: '部门',
+ ref: 4,
+ koubei: 1,
+ output: 6,
+ contribute: 5,
+ hot: 7,
+ },
+];
+
+const radarData: RadarData[] = [];
+const radarTitleMap = {
+ ref: '引用',
+ koubei: '口碑',
+ output: '产量',
+ contribute: '贡献',
+ hot: '热度',
+};
+radarOriginData.forEach((item) => {
+ Object.keys(item).forEach((key) => {
+ if (key !== 'name') {
+ radarData.push({
+ name: item.name,
+ label: radarTitleMap[key as 'ref'],
+ value: item[key as 'ref'],
+ });
+ }
+ });
+});
+
+const getFakeChartData: AnalysisData = {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ radarData,
+};
+
+const fakeChartData = (_: Request, res: Response) => {
+ return res.json({
+ data: getFakeChartData,
+ });
+};
+
+export default {
+ 'GET /api/fake_analysis_chart_data': fakeChartData,
+};
diff --git a/src/pages/dashboard/analysis/components/Charts/ChartCard/index.less b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.less
new file mode 100644
index 00000000..d7bf6dda
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.less
@@ -0,0 +1,75 @@
+@import '~antd/es/style/themes/default.less';
+
+.chartCard {
+ position: relative;
+ .chartTop {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ }
+ .chartTopMargin {
+ margin-bottom: 12px;
+ }
+ .chartTopHasMargin {
+ margin-bottom: 20px;
+ }
+ .metaWrap {
+ float: left;
+ }
+ .avatar {
+ position: relative;
+ top: 4px;
+ float: left;
+ margin-right: 20px;
+ img {
+ border-radius: 100%;
+ }
+ }
+ .meta {
+ height: 22px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ .action {
+ position: absolute;
+ top: 4px;
+ right: 0;
+ line-height: 1;
+ cursor: pointer;
+ }
+ .total {
+ height: 38px;
+ margin-top: 4px;
+ margin-bottom: 0;
+ overflow: hidden;
+ color: @heading-color;
+ font-size: 30px;
+ line-height: 38px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ }
+ .content {
+ position: relative;
+ width: 100%;
+ margin-bottom: 12px;
+ }
+ .contentFixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ }
+ .footer {
+ margin-top: 8px;
+ padding-top: 9px;
+ border-top: 1px solid @border-color-split;
+ & > * {
+ position: relative;
+ }
+ }
+ .footerMargin {
+ margin-top: 20px;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Charts/ChartCard/index.style.ts b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.style.ts
new file mode 100644
index 00000000..b21e103e
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.style.ts
@@ -0,0 +1,77 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ chartCard: {
+ position: 'relative',
+ },
+ chartTop: {
+ position: 'relative',
+ width: '100%',
+ overflow: 'hidden',
+ },
+ chartTopMargin: {
+ marginBottom: '12px',
+ },
+ chartTopHasMargin: {
+ marginBottom: '20px',
+ },
+ metaWrap: {
+ float: 'left',
+ },
+ avatar: {
+ position: 'relative',
+ top: '4px',
+ float: 'left',
+ marginRight: '20px',
+ img: { borderRadius: '100%' },
+ },
+ meta: {
+ height: '22px',
+ color: token.colorTextSecondary,
+ fontSize: token.fontSize,
+ lineHeight: '22px',
+ },
+ action: {
+ position: 'absolute',
+ top: '4px',
+ right: '0',
+ lineHeight: '1',
+ cursor: 'pointer',
+ },
+ total: {
+ height: '38px',
+ marginTop: '4px',
+ marginBottom: '0',
+ overflow: 'hidden',
+ color: token.colorTextHeading,
+ fontSize: '30px',
+ lineHeight: '38px',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ },
+ content: {
+ position: 'relative',
+ width: '100%',
+ marginBottom: '12px',
+ },
+ contentFixed: {
+ position: 'absolute',
+ bottom: '0',
+ left: '0',
+ width: '100%',
+ },
+ footer: {
+ marginTop: '8px',
+ paddingTop: '9px',
+ borderTop: `1px solid ${token.colorSplit}`,
+ '& > *': { position: 'relative' },
+ },
+ footerMargin: {
+ marginTop: '20px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx
new file mode 100644
index 00000000..6241b931
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx
@@ -0,0 +1,110 @@
+import omit from '@rc-component/util/es/omit';
+import { Card } from 'antd';
+import type { CardProps } from 'antd/es/card';
+import classNames from 'classnames';
+import React from 'react';
+import useStyles from './index.style';
+
+type totalType = () => React.ReactNode;
+
+export type ChartCardProps = {
+ title: React.ReactNode;
+ action?: React.ReactNode;
+ total?: React.ReactNode | number | (() => React.ReactNode | number);
+ footer?: React.ReactNode;
+ contentHeight?: number;
+ avatar?: React.ReactNode;
+ style?: React.CSSProperties;
+} & CardProps;
+
+const ChartCard: React.FC = (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 = {total()}
;
+ break;
+ default:
+ totalDom = {total}
;
+ }
+ return totalDom;
+ };
+ const renderContent = () => {
+ const {
+ contentHeight,
+ title,
+ avatar,
+ action,
+ total,
+ footer,
+ children,
+ loading,
+ } = props;
+ if (loading) {
+ return false;
+ }
+ return (
+
+
+
{avatar}
+
+
+ {title}
+ {action}
+
+ {renderTotal(total)}
+
+
+ {children && (
+
+ )}
+ {footer && (
+
+ {footer}
+
+ )}
+
+ );
+ };
+
+ const { loading = false, ...rest } = props;
+ const cardProps = omit(rest, ['total', 'contentHeight', 'action']);
+ return (
+
+ {renderContent()}
+
+ );
+};
+export default ChartCard;
diff --git a/src/pages/dashboard/analysis/components/Charts/Field/index.less b/src/pages/dashboard/analysis/components/Charts/Field/index.less
new file mode 100644
index 00000000..4fe0d1f6
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Field/index.less
@@ -0,0 +1,17 @@
+@import '~antd/es/style/themes/default.less';
+
+.field {
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ .label,
+ .number {
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ .number {
+ margin-left: 8px;
+ color: @heading-color;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Charts/Field/index.style.ts b/src/pages/dashboard/analysis/components/Charts/Field/index.style.ts
new file mode 100644
index 00000000..22192d8a
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Field/index.style.ts
@@ -0,0 +1,22 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ field: {
+ margin: '0',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ },
+ label: {
+ fontSize: token.fontSize,
+ lineHeight: '22px',
+ },
+ number: {
+ marginLeft: '8px',
+ color: token.colorTextHeading,
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/analysis/components/Charts/Field/index.tsx b/src/pages/dashboard/analysis/components/Charts/Field/index.tsx
new file mode 100644
index 00000000..97e4a171
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Field/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import useStyles from './index.style';
+export type FieldProps = {
+ label: React.ReactNode;
+ value: React.ReactNode;
+ style?: React.CSSProperties;
+};
+const Field: React.FC = ({ label, value, ...rest }) => {
+ const { styles } = useStyles();
+ return (
+
+ {label}
+ {value}
+
+ );
+};
+export default Field;
diff --git a/src/pages/dashboard/analysis/components/Charts/index.less b/src/pages/dashboard/analysis/components/Charts/index.less
new file mode 100644
index 00000000..190428bc
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/index.less
@@ -0,0 +1,19 @@
+.miniChart {
+ position: relative;
+ width: 100%;
+ .chartContent {
+ position: absolute;
+ bottom: -28px;
+ width: 100%;
+ > div {
+ margin: 0 -5px;
+ overflow: hidden;
+ }
+ }
+ .chartLoading {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ margin-left: -7px;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Charts/index.style.ts b/src/pages/dashboard/analysis/components/Charts/index.style.ts
new file mode 100644
index 00000000..29f85452
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/index.style.ts
@@ -0,0 +1,23 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(() => {
+ return {
+ miniChart: {
+ position: 'relative',
+ width: '100%',
+ },
+ chartContent: {
+ position: 'absolute',
+ bottom: '-28px',
+ width: '100%',
+ '> div': { margin: '0 -5px', overflow: 'hidden' },
+ },
+ chartLoading: {
+ position: 'absolute',
+ top: '16px',
+ left: '50%',
+ marginLeft: '-7px',
+ },
+ };
+});
+export default useStyles;
diff --git a/src/pages/dashboard/analysis/components/Charts/index.tsx b/src/pages/dashboard/analysis/components/Charts/index.tsx
new file mode 100644
index 00000000..7ad687f0
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/index.tsx
@@ -0,0 +1,13 @@
+import numeral from 'numeral';
+import ChartCard from './ChartCard';
+import Field from './Field';
+
+const yuan = (val: number | string) => `¥ ${numeral(val).format('0,0')}`;
+
+const Charts = {
+ yuan,
+ ChartCard,
+ Field,
+};
+
+export { Charts as default, yuan, ChartCard, Field };
diff --git a/src/pages/dashboard/analysis/components/IntroduceRow.tsx b/src/pages/dashboard/analysis/components/IntroduceRow.tsx
new file mode 100644
index 00000000..6e21f1f3
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/IntroduceRow.tsx
@@ -0,0 +1,168 @@
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { Area, Column } from '@ant-design/plots';
+import { Col, Progress, Row, Tooltip } from 'antd';
+import numeral from 'numeral';
+import type { DataItem } from '../data.d';
+import useStyles from '../style.style';
+import Yuan from '../utils/Yuan';
+import { ChartCard, Field } from './Charts';
+import Trend from './Trend';
+
+const topColResponsiveProps = {
+ xs: 24,
+ sm: 12,
+ md: 12,
+ lg: 12,
+ xl: 6,
+ style: {
+ marginBottom: 24,
+ },
+};
+const IntroduceRow = ({
+ loading,
+ visitData,
+}: {
+ loading: boolean;
+ visitData: DataItem[];
+}) => {
+ const { styles } = useStyles();
+ return (
+
+
+
+
+
+ }
+ loading={loading}
+ total={() => 126560 }
+ footer={
+
+ }
+ contentHeight={46}
+ >
+
+ 周同比
+ 12%
+
+
+ 日同比
+ 11%
+
+
+
+
+
+
+
+
+ }
+ total={numeral(8846).format('0,0')}
+ footer={
+
+ }
+ contentHeight={46}
+ >
+
+
+
+
+
+
+
+ }
+ total={numeral(6560).format('0,0')}
+ footer={ }
+ contentHeight={46}
+ >
+
+
+
+
+
+
+
+ }
+ total="78%"
+ footer={
+
+
+ 周同比
+ 12%
+
+
+ 日同比
+ 11%
+
+
+ }
+ contentHeight={46}
+ >
+
+
+
+
+ );
+};
+export default IntroduceRow;
diff --git a/src/pages/dashboard/analysis/components/NumberInfo/index.less b/src/pages/dashboard/analysis/components/NumberInfo/index.less
new file mode 100644
index 00000000..847d25ee
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/NumberInfo/index.less
@@ -0,0 +1,68 @@
+@import '~antd/es/style/themes/default.less';
+
+.numberInfo {
+ .suffix {
+ margin-left: 4px;
+ color: @text-color;
+ font-size: 16px;
+ font-style: normal;
+ }
+ .numberInfoTitle {
+ margin-bottom: 16px;
+ color: @text-color;
+ font-size: @font-size-lg;
+ transition: all 0.3s;
+ }
+ .numberInfoSubTitle {
+ height: 22px;
+ overflow: hidden;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ }
+ .numberInfoValue {
+ margin-top: 4px;
+ overflow: hidden;
+ font-size: 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ & > span {
+ display: inline-block;
+ height: 32px;
+ margin-right: 32px;
+ color: @heading-color;
+ font-size: 24px;
+ line-height: 32px;
+ }
+ .subTotal {
+ margin-right: 0;
+ color: @text-color-secondary;
+ font-size: @font-size-lg;
+ vertical-align: top;
+ .anticon {
+ margin-left: 4px;
+ font-size: 12px;
+ transform: scale(0.82);
+ }
+ :global {
+ .anticon-caret-up {
+ color: @red-6;
+ }
+ .anticon-caret-down {
+ color: @green-6;
+ }
+ }
+ }
+ }
+}
+.numberInfolight {
+ .numberInfoValue {
+ & > span {
+ color: @text-color;
+ }
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/NumberInfo/index.style.ts b/src/pages/dashboard/analysis/components/NumberInfo/index.style.ts
new file mode 100644
index 00000000..ba70b7b7
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/NumberInfo/index.style.ts
@@ -0,0 +1,56 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ suffix: {
+ marginLeft: '4px',
+ color: token.colorText,
+ fontSize: '16px',
+ fontStyle: 'normal',
+ },
+ numberInfoTitle: {
+ marginBottom: '16px',
+ color: token.colorText,
+ fontSize: token.fontSizeLG,
+ transition: 'all 0.3s',
+ },
+ numberInfoSubTitle: {
+ height: '22px',
+ overflow: 'hidden',
+ color: token.colorTextSecondary,
+ fontSize: token.fontSize,
+ lineHeight: '22px',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ },
+ numberInfoValue: {
+ marginTop: '4px',
+ overflow: 'hidden',
+ fontSize: '0',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ '& > span': { color: token.colorText },
+ },
+ subTotal: {
+ marginRight: '0',
+ color: token.colorTextSecondary,
+ fontSize: token.fontSizeLG,
+ verticalAlign: 'top',
+ },
+ anticon: {
+ marginLeft: '4px',
+ fontSize: '12px',
+ transform: 'scale(0.82)',
+ },
+ 'anticon-caret-up': {
+ color: token['red-6'],
+ },
+ 'anticon-caret-down': {
+ color: token['green-6'],
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/analysis/components/NumberInfo/index.tsx b/src/pages/dashboard/analysis/components/NumberInfo/index.tsx
new file mode 100644
index 00000000..c34f86f5
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/NumberInfo/index.tsx
@@ -0,0 +1,79 @@
+import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
+import classNames from 'classnames';
+import React from 'react';
+import useStyles from './index.style';
+export type NumberInfoProps = {
+ title?: React.ReactNode | string;
+ subTitle?: React.ReactNode | string;
+ total?: React.ReactNode | string;
+ status?: 'up' | 'down';
+ theme?: string;
+ gap?: number;
+ subTotal?: number;
+ suffix?: string;
+ style?: React.CSSProperties;
+};
+const NumberInfo: React.FC = ({
+ theme,
+ title,
+ subTitle,
+ total,
+ subTotal,
+ status,
+ suffix,
+ gap,
+ ...rest
+}) => {
+ const { styles } = useStyles();
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+ {subTitle && (
+
+ {subTitle}
+
+ )}
+
+
+ {total}
+ {suffix && {suffix} }
+
+ {(status || subTotal) && (
+
+ {subTotal}
+ {status && status === 'up' ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+};
+export default NumberInfo;
diff --git a/src/pages/dashboard/analysis/components/OfflineData.tsx b/src/pages/dashboard/analysis/components/OfflineData.tsx
new file mode 100644
index 00000000..f271c9af
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/OfflineData.tsx
@@ -0,0 +1,110 @@
+import { Line, Tiny } from '@ant-design/plots';
+import { Card, Col, Row, Tabs } from 'antd';
+import type { DataItem, OfflineDataType } from '../data.d';
+import useStyles from '../style.style';
+import NumberInfo from './NumberInfo';
+
+const CustomTab = ({
+ data,
+ currentTabKey: currentKey,
+}: {
+ data: OfflineDataType;
+ currentTabKey: string;
+}) => (
+
+
+
+
+
+
+
+
+);
+
+const OfflineData = ({
+ activeKey,
+ loading,
+ offlineData,
+ offlineChartData,
+ handleTabChange,
+}: {
+ activeKey: string;
+ loading: boolean;
+ offlineData: OfflineDataType[];
+ offlineChartData: DataItem[];
+ handleTabChange: (activeKey: string) => void;
+}) => {
+ const { styles } = useStyles();
+ return (
+
+ ({
+ key: shop.name,
+ label: ,
+ children: (
+
+
+
+ ),
+ }))}
+ />
+
+ );
+};
+export default OfflineData;
diff --git a/src/pages/dashboard/analysis/components/PageLoading/index.tsx b/src/pages/dashboard/analysis/components/PageLoading/index.tsx
new file mode 100644
index 00000000..dd96277b
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/PageLoading/index.tsx
@@ -0,0 +1,9 @@
+import { Spin } from 'antd';
+
+// loading components from code split
+// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
+export default () => (
+
+
+
+);
diff --git a/src/pages/dashboard/analysis/components/ProportionSales.tsx b/src/pages/dashboard/analysis/components/ProportionSales.tsx
new file mode 100644
index 00000000..4466481a
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/ProportionSales.tsx
@@ -0,0 +1,67 @@
+import { Pie } from '@ant-design/plots';
+import { Card, Segmented, Typography } from 'antd';
+import numeral from 'numeral';
+import React from 'react';
+import type { DataItem } from '../data.d';
+import useStyles from '../style.style';
+
+const { Text } = Typography;
+const ProportionSales = ({
+ dropdownGroup,
+ salesType,
+ loading,
+ salesPieData,
+ handleChangeSalesType,
+}: {
+ loading: boolean;
+ dropdownGroup: React.ReactNode;
+ salesType: 'all' | 'online' | 'stores';
+ salesPieData: DataItem[];
+ handleChangeSalesType?: (value: 'all' | 'online' | 'stores') => void;
+}) => {
+ const { styles } = useStyles();
+ return (
+
+ {dropdownGroup}
+
+
+ }
+ >
+ 销售额
+
+ `${item.x}: ${numeral(item.y).format('0,0')}`,
+ }}
+ />
+
+ );
+};
+export default ProportionSales;
diff --git a/src/pages/dashboard/analysis/components/SalesCard.tsx b/src/pages/dashboard/analysis/components/SalesCard.tsx
new file mode 100644
index 00000000..31c10cde
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/SalesCard.tsx
@@ -0,0 +1,225 @@
+import { Column } from '@ant-design/plots';
+import { Button, Card, Col, DatePicker, Row, Tabs } from 'antd';
+import type { RangePickerProps } from 'antd/es/date-picker';
+import numeral from 'numeral';
+import type { DataItem } from '../data.d';
+import useStyles from '../style.style';
+
+export type TimeType = 'today' | 'week' | 'month' | 'year';
+const { RangePicker } = DatePicker;
+
+const rankingListData: {
+ title: string;
+ total: number;
+}[] = [];
+
+for (let i = 0; i < 7; i += 1) {
+ rankingListData.push({
+ title: `工专路 ${i} 号店`,
+ total: 323234,
+ });
+}
+
+const SalesCard = ({
+ rangePickerValue,
+ salesData,
+ isActive,
+ handleRangePickerChange,
+ loading,
+ selectDate,
+}: {
+ rangePickerValue: RangePickerProps['value'];
+ isActive: (key: TimeType) => string;
+ salesData: DataItem[];
+ loading: boolean;
+ handleRangePickerChange: RangePickerProps['onChange'];
+ selectDate: (key: TimeType) => void;
+}) => {
+ const { styles } = useStyles();
+ return (
+
+
+
+ selectDate('today')}
+ >
+ 今日
+
+ selectDate('week')}
+ >
+ 本周
+
+ selectDate('month')}
+ >
+ 本月
+
+ selectDate('year')}
+ >
+ 本年
+
+
+
+
+ }
+ size="large"
+ tabBarStyle={{
+ marginBottom: 24,
+ }}
+ items={[
+ {
+ key: 'sales',
+ label: '销售额',
+ children: (
+
+
+
+
+
+
+
+
+
门店销售额排名
+
+ {rankingListData.map((item, i) => (
+
+
+ {i + 1}
+
+
+ {item.title}
+
+ {numeral(item.total).format('0,0')}
+
+ ))}
+
+
+
+
+ ),
+ },
+ {
+ key: 'views',
+ label: '访问量',
+ children: (
+
+
+
+
+
+
+
+
+
门店访问量排名
+
+ {rankingListData.map((item, i) => (
+
+
+ {i + 1}
+
+
+ {item.title}
+
+ {numeral(item.total).format('0,0')}
+
+ ))}
+
+
+
+
+ ),
+ },
+ ]}
+ />
+
+ );
+};
+export default SalesCard;
diff --git a/src/pages/dashboard/analysis/components/TopSearch.tsx b/src/pages/dashboard/analysis/components/TopSearch.tsx
new file mode 100644
index 00000000..2833b0a4
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/TopSearch.tsx
@@ -0,0 +1,181 @@
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { Area } from '@ant-design/plots';
+import { Card, Col, Row, Table, Tooltip } from 'antd';
+import numeral from 'numeral';
+import React from 'react';
+import type { DataItem } from '../data.d';
+import NumberInfo from './NumberInfo';
+import Trend from './Trend';
+
+const TopSearch = ({
+ loading,
+ visitData2,
+ searchData,
+ dropdownGroup,
+}: {
+ loading: boolean;
+ visitData2: DataItem[];
+ dropdownGroup: React.ReactNode;
+ searchData: DataItem[];
+}) => {
+ const columns = [
+ {
+ title: '排名',
+ dataIndex: 'index',
+ key: 'index',
+ },
+ {
+ title: '搜索关键词',
+ dataIndex: 'keyword',
+ key: 'keyword',
+ render: (text: React.ReactNode) => {text} ,
+ },
+ {
+ 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;
+ },
+ ) => (
+
+
+ {text}%
+
+
+ ),
+ },
+ ];
+ return (
+
+
+
+
+ 搜索用户数
+
+
+
+
+ }
+ gap={8}
+ total={numeral(12321).format('0,0')}
+ status="up"
+ subTotal={17.1}
+ />
+
+
+
+
+ 人均搜索次数
+
+
+
+
+ }
+ total={2.7}
+ status="down"
+ subTotal={26.2}
+ gap={8}
+ />
+
+
+
+
+ rowKey={(record) => record.index}
+ size="small"
+ columns={columns}
+ dataSource={searchData}
+ pagination={{
+ style: {
+ marginBottom: 0,
+ },
+ pageSize: 5,
+ }}
+ />
+
+ );
+};
+export default TopSearch;
diff --git a/src/pages/dashboard/analysis/components/Trend/index.less b/src/pages/dashboard/analysis/components/Trend/index.less
new file mode 100644
index 00000000..3d7fdf99
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Trend/index.less
@@ -0,0 +1,37 @@
+@import '~antd/es/style/themes/default.less';
+
+.trendItem {
+ display: inline-block;
+ font-size: @font-size-base;
+ line-height: 22px;
+
+ .up,
+ .down {
+ position: relative;
+ top: 1px;
+ margin-left: 4px;
+ span {
+ font-size: 12px;
+ transform: scale(0.83);
+ }
+ }
+ .up {
+ color: @red-6;
+ }
+ .down {
+ top: -1px;
+ color: @green-6;
+ }
+
+ &.trendItemGrey .up,
+ &.trendItemGrey .down {
+ color: @text-color;
+ }
+
+ &.reverseColor .up {
+ color: @green-6;
+ }
+ &.reverseColor .down {
+ color: @red-6;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Trend/index.style.ts b/src/pages/dashboard/analysis/components/Trend/index.style.ts
new file mode 100644
index 00000000..6d6f9b72
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Trend/index.style.ts
@@ -0,0 +1,32 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ trendItem: {
+ display: 'inline-block',
+ fontSize: token.fontSize,
+ lineHeight: '22px',
+ },
+ up: {
+ color: token['red-6'],
+ },
+ down: {
+ top: '-1px',
+ color: token['green-6'],
+ },
+ trendItemGrey: {
+ up: {
+ color: token.colorText,
+ },
+ down: {
+ color: token.colorText,
+ },
+ },
+ reverseColor: {
+ up: { color: token['green-6'] },
+ down: { color: token['red-6'] },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/analysis/components/Trend/index.tsx b/src/pages/dashboard/analysis/components/Trend/index.tsx
new file mode 100644
index 00000000..110a4aec
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Trend/index.tsx
@@ -0,0 +1,47 @@
+import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
+import classNames from 'classnames';
+import React from 'react';
+import useStyles from './index.style';
+
+export type TrendProps = {
+ colorful?: boolean;
+ flag: 'up' | 'down';
+ style?: React.CSSProperties;
+ reverseColor?: boolean;
+ className?: string;
+ children?: React.ReactNode;
+};
+
+const Trend: React.FC = ({
+ 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 (
+
+ {children}
+ {flag && (
+
+ {flag === 'up' ? : }
+
+ )}
+
+ );
+};
+export default Trend;
diff --git a/src/pages/dashboard/analysis/data.d.ts b/src/pages/dashboard/analysis/data.d.ts
new file mode 100644
index 00000000..f9b69456
--- /dev/null
+++ b/src/pages/dashboard/analysis/data.d.ts
@@ -0,0 +1,45 @@
+export interface DataItem {
+ [field: string]: string | number | number[] | null | undefined;
+}
+export interface VisitDataType {
+ x: string;
+ y: number;
+}
+
+export type SearchDataType = {
+ index: number;
+ keyword: string;
+ count: number;
+ range: number;
+ status: number;
+};
+
+export type OfflineDataType = {
+ name: string;
+ cvr: number;
+};
+
+export interface OfflineChartData {
+ date: number;
+ type: number;
+ value: number;
+}
+
+export type RadarData = {
+ name: string;
+ label: string;
+ value: number;
+};
+
+export interface AnalysisData {
+ visitData: DataItem[];
+ visitData2: DataItem[];
+ salesData: DataItem[];
+ searchData: DataItem[];
+ offlineData: OfflineDataType[];
+ offlineChartData: DataItem[];
+ salesTypeData: DataItem[];
+ salesTypeDataOnline: DataItem[];
+ salesTypeDataOffline: DataItem[];
+ radarData: RadarData[];
+}
diff --git a/src/pages/dashboard/analysis/index.tsx b/src/pages/dashboard/analysis/index.tsx
new file mode 100644
index 00000000..b904e03e
--- /dev/null
+++ b/src/pages/dashboard/analysis/index.tsx
@@ -0,0 +1,157 @@
+import { EllipsisOutlined } from '@ant-design/icons';
+import { GridContent } from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import { Col, Dropdown, Row } from 'antd';
+import type { RangePickerProps } from 'antd/es/date-picker';
+import type { Dayjs } from 'dayjs';
+import type { FC } from 'react';
+import { Suspense, useState } from 'react';
+import IntroduceRow from './components/IntroduceRow';
+import OfflineData from './components/OfflineData';
+import PageLoading from './components/PageLoading';
+import ProportionSales from './components/ProportionSales';
+import type { TimeType } from './components/SalesCard';
+import SalesCard from './components/SalesCard';
+import TopSearch from './components/TopSearch';
+import type { AnalysisData } from './data.d';
+import { fakeChartData } from './service';
+import useStyles from './style.style';
+import { getTimeDistance } from './utils/utils';
+
+type RangePickerValue = RangePickerProps['value'];
+type AnalysisProps = {
+ dashboardAndanalysis: AnalysisData;
+ loading: boolean;
+};
+type SalesType = 'all' | 'online' | 'stores';
+const Analysis: FC = () => {
+ const { styles } = useStyles();
+ const [salesType, setSalesType] = useState('all');
+ const [currentTabKey, setCurrentTabKey] = useState('');
+ const [rangePickerValue, setRangePickerValue] = useState(
+ 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 = (
+
+
+
+
+
+ );
+ const handleChangeSalesType = (value: SalesType) => {
+ setSalesType(value);
+ };
+ const handleTabChange = (key: string) => {
+ setCurrentTabKey(key);
+ };
+ const activeKey = currentTabKey || data?.offlineData[0]?.name || '';
+ return (
+
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+export default Analysis;
diff --git a/src/pages/dashboard/analysis/service.ts b/src/pages/dashboard/analysis/service.ts
new file mode 100644
index 00000000..d24e6262
--- /dev/null
+++ b/src/pages/dashboard/analysis/service.ts
@@ -0,0 +1,6 @@
+import { request } from '@umijs/max';
+import type { AnalysisData } from './data';
+
+export async function fakeChartData(): Promise<{ data: AnalysisData }> {
+ return request('/api/fake_analysis_chart_data');
+}
diff --git a/src/pages/dashboard/analysis/style.less b/src/pages/dashboard/analysis/style.less
new file mode 100644
index 00000000..38a790b6
--- /dev/null
+++ b/src/pages/dashboard/analysis/style.less
@@ -0,0 +1,189 @@
+@import '~antd/es/style/themes/default.less';
+
+.iconGroup {
+ span.anticon {
+ margin-left: 16px;
+ color: @text-color-secondary;
+ cursor: pointer;
+ transition: color 0.32s;
+ &:hover {
+ color: @text-color;
+ }
+ }
+}
+
+.rankingList {
+ margin: 25px 0 0;
+ padding: 0;
+ list-style: none;
+ li {
+ display: flex;
+ align-items: center;
+ margin-top: 16px;
+ zoom: 1;
+ &::before,
+ &::after {
+ display: table;
+ content: ' ';
+ }
+ &::after {
+ clear: both;
+ height: 0;
+ font-size: 0;
+ visibility: hidden;
+ }
+ span {
+ color: @text-color;
+ font-size: 14px;
+ line-height: 22px;
+ }
+ .rankingItemNumber {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin-top: 1.5px;
+ margin-right: 16px;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 20px;
+ text-align: center;
+ background-color: @tag-default-bg;
+ border-radius: 20px;
+ &.active {
+ color: #fff;
+ background-color: #314659;
+ }
+ }
+ .rankingItemTitle {
+ flex: 1;
+ margin-right: 8px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+}
+
+.salesExtra {
+ display: inline-block;
+ margin-right: 24px;
+ a {
+ margin-left: 24px;
+ color: @text-color;
+ &:hover {
+ color: @primary-color;
+ }
+ &.currentDate {
+ color: @primary-color;
+ }
+ }
+}
+
+.salesCard {
+ .salesBar {
+ padding: 0 0 32px 32px;
+ }
+ .salesRank {
+ padding: 0 32px 32px 72px;
+ }
+ :global {
+ .ant-tabs-bar,
+ .ant-tabs-nav-wrap {
+ padding-left: 16px;
+ .ant-tabs-nav .ant-tabs-tab {
+ padding-top: 16px;
+ padding-bottom: 14px;
+ line-height: 24px;
+ }
+ }
+ .ant-tabs-extra-content {
+ padding-right: 24px;
+ line-height: 55px;
+ }
+ .ant-card-head {
+ position: relative;
+ }
+ .ant-card-head-title {
+ align-items: normal;
+ }
+ }
+}
+
+.salesCardExtra {
+ height: inherit;
+}
+
+.salesTypeRadio {
+ position: absolute;
+ right: 54px;
+ bottom: 12px;
+}
+
+.offlineCard {
+ :global {
+ .ant-tabs-ink-bar {
+ bottom: auto;
+ }
+ .ant-tabs-bar {
+ border-bottom: none;
+ }
+ .ant-tabs-nav-container-scrolling {
+ padding-right: 40px;
+ padding-left: 40px;
+ }
+ .ant-tabs-tab-prev-icon::before {
+ position: relative;
+ left: 6px;
+ }
+ .ant-tabs-tab-next-icon::before {
+ position: relative;
+ right: 6px;
+ }
+ .ant-tabs-tab-active h4 {
+ color: @primary-color;
+ }
+ }
+}
+
+.trendText {
+ margin-left: 8px;
+ color: @heading-color;
+}
+
+@media screen and (max-width: @screen-lg) {
+ .salesExtra {
+ display: none;
+ }
+
+ .rankingList {
+ li {
+ span:first-child {
+ margin-right: 8px;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: @screen-md) {
+ .rankingTitle {
+ margin-top: 16px;
+ }
+
+ .salesCard .salesBar {
+ padding: 16px;
+ }
+}
+
+@media screen and (max-width: @screen-sm) {
+ .salesExtraWrap {
+ display: none;
+ }
+
+ .salesCard {
+ :global {
+ .ant-tabs-content {
+ padding-top: 30px;
+ }
+ }
+ }
+}
diff --git a/src/pages/dashboard/analysis/style.style.ts b/src/pages/dashboard/analysis/style.style.ts
new file mode 100644
index 00000000..a9a5ac97
--- /dev/null
+++ b/src/pages/dashboard/analysis/style.style.ts
@@ -0,0 +1,160 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ iconGroup: {
+ 'span.anticon': {
+ marginLeft: '16px',
+ color: token.colorTextSecondary,
+ cursor: 'pointer',
+ transition: 'color 0.32s',
+ '&:hover': {
+ color: token.colorText,
+ },
+ },
+ },
+ rankingList: {
+ margin: '25px 0 0',
+ padding: '0',
+ listStyle: 'none',
+ li: {
+ display: 'flex',
+ alignItems: 'center',
+ marginTop: '16px',
+ zoom: '1',
+ '&::before, &::after': {
+ display: 'table',
+ content: "' '",
+ },
+ '&::after': {
+ clear: 'both',
+ height: '0',
+ fontSize: '0',
+ visibility: 'hidden',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ li: {
+ 'span:first-child': { marginRight: '8px' },
+ },
+ },
+ },
+ rankingItemNumber: {
+ display: 'inline-block',
+ width: '20px',
+ height: '20px',
+ marginTop: '1.5px',
+ marginRight: '16px',
+ fontWeight: '600',
+ fontSize: '12px',
+ lineHeight: '20px',
+ textAlign: 'center',
+ borderRadius: '20px',
+ backgroundColor: token.colorBgContainerDisabled,
+ },
+ rankingItemTitle: {
+ flex: '1',
+ marginRight: '8px',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ },
+ rankingItemNumberActive: {
+ display: 'inline-block',
+ width: '20px',
+ height: '20px',
+ marginTop: '1.5px',
+ marginRight: '16px',
+ fontWeight: '600',
+ fontSize: '12px',
+ lineHeight: '20px',
+ textAlign: 'center',
+ borderRadius: '20px',
+ color: '#fff',
+ backgroundColor: token.colorBgSpotlight,
+ },
+ salesExtra: {
+ display: 'inline-block',
+ marginRight: '24px',
+ a: {
+ marginLeft: '24px',
+ color: token.colorText,
+ '&:hover': {
+ color: token.colorPrimary,
+ },
+ },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ display: 'none',
+ },
+ },
+ currentDate: {
+ color: token.colorPrimary,
+ fontWeight: 'bold',
+ },
+ salesBar: {
+ padding: '0 0 32px 32px',
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ padding: '16px',
+ },
+ },
+ salesRank: {
+ padding: '0 32px 32px 72px',
+ },
+ salesCard: {
+ '.ant-tabs-bar, .ant-tabs-nav-wrap': {
+ paddingLeft: '16px',
+ '.ant-tabs-nav .ant-tabs-tab': {
+ paddingTop: '16px',
+ paddingBottom: '14px',
+ lineHeight: '24px',
+ },
+ },
+ '.ant-tabs-extra-content': { paddingRight: '24px', lineHeight: '55px' },
+ '.ant-card-head': { position: 'relative' },
+ '.ant-card-head-title': { alignItems: 'normal' },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ padding: '16px',
+ },
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ '.ant-tabs-content': {
+ paddingTop: '30px',
+ },
+ },
+ },
+ salesCardExtra: {
+ height: 'inherit',
+ },
+ salesTypeRadio: {
+ position: 'absolute',
+ right: '54px',
+ bottom: '12px',
+ },
+ offlineCard: {
+ '.ant-tabs-ink-bar': { bottom: 'auto' },
+ '.ant-tabs-bar': { borderBottom: 'none' },
+ '.ant-tabs-nav-container-scrolling': {
+ paddingRight: '40px',
+ paddingLeft: '40px',
+ },
+ '.ant-tabs-tab-prev-icon::before': { position: 'relative', left: '6px' },
+ '.ant-tabs-tab-next-icon::before': { position: 'relative', right: '6px' },
+ '.ant-tabs-tab-active h4': { color: token.colorPrimary },
+ },
+ trendText: {
+ marginLeft: '8px',
+ color: token.colorTextHeading,
+ },
+ rankingTitle: {
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ marginTop: '16px',
+ },
+ },
+ salesExtraWrap: {
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ display: 'none',
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/analysis/utils/Yuan.tsx b/src/pages/dashboard/analysis/utils/Yuan.tsx
new file mode 100644
index 00000000..516d59fa
--- /dev/null
+++ b/src/pages/dashboard/analysis/utils/Yuan.tsx
@@ -0,0 +1,17 @@
+import { useEffect, useRef } from 'react';
+import { yuan } from '../components/Charts';
+
+/** 减少使用 dangerouslySetInnerHTML */
+const Yuan: React.FC<{ children: string | number }> = ({ children }) => {
+ const spanRef = useRef(null);
+
+ useEffect(() => {
+ if (spanRef.current) {
+ spanRef.current.innerHTML = yuan(children);
+ }
+ }, [children]);
+
+ return ;
+};
+
+export default Yuan;
diff --git a/src/pages/dashboard/analysis/utils/utils.ts b/src/pages/dashboard/analysis/utils/utils.ts
new file mode 100644
index 00000000..6a1d4d5e
--- /dev/null
+++ b/src/pages/dashboard/analysis/utils/utils.ts
@@ -0,0 +1,57 @@
+import type { RangePickerProps } from 'antd/es/date-picker';
+import dayjs from 'dayjs';
+
+type RangePickerValue = RangePickerProps['value'];
+
+export function fixedZero(val: number) {
+ return val * 1 < 10 ? `0${val}` : val;
+}
+
+export function getTimeDistance(
+ type: 'today' | 'week' | 'month' | 'year',
+): RangePickerValue {
+ const now = new Date();
+ const oneDay = 1000 * 60 * 60 * 24;
+
+ if (type === 'today') {
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+ return [dayjs(now), dayjs(now.getTime() + (oneDay - 1000))];
+ }
+
+ if (type === 'week') {
+ let day = now.getDay();
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+
+ if (day === 0) {
+ day = 6;
+ } else {
+ day -= 1;
+ }
+
+ const beginTime = now.getTime() - day * oneDay;
+
+ return [dayjs(beginTime), dayjs(beginTime + (7 * oneDay - 1000))];
+ }
+ const year = now.getFullYear();
+
+ if (type === 'month') {
+ const month = now.getMonth();
+ const nextDate = dayjs(now).add(1, 'months');
+ const nextYear = nextDate.year();
+ const nextMonth = nextDate.month();
+
+ return [
+ dayjs(`${year}-${fixedZero(month + 1)}-01 00:00:00`),
+ dayjs(
+ dayjs(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() -
+ 1000,
+ ),
+ ];
+ }
+
+ return [dayjs(`${year}-01-01 00:00:00`), dayjs(`${year}-12-31 23:59:59`)];
+}
diff --git a/src/pages/dashboard/monitor/_mock.ts b/src/pages/dashboard/monitor/_mock.ts
new file mode 100644
index 00000000..dcd1b179
--- /dev/null
+++ b/src/pages/dashboard/monitor/_mock.ts
@@ -0,0 +1,14 @@
+import type { Request, Response } from 'express';
+import mockjs from 'mockjs';
+
+const getTags = (_: Request, res: Response) => {
+ return res.json({
+ data: mockjs.mock({
+ 'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }],
+ }),
+ });
+};
+
+export default {
+ 'GET /api/tags': getTags,
+};
diff --git a/src/pages/dashboard/monitor/components/ActiveChart/index.less b/src/pages/dashboard/monitor/components/ActiveChart/index.less
new file mode 100644
index 00000000..2f5d15f2
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/ActiveChart/index.less
@@ -0,0 +1,51 @@
+.activeChart {
+ position: relative;
+}
+.activeChartGrid {
+ p {
+ position: absolute;
+ top: 80px;
+ }
+ p:last-child {
+ top: 115px;
+ }
+}
+.activeChartLegend {
+ position: relative;
+ height: 20px;
+ margin-top: 8px;
+ font-size: 0;
+ line-height: 20px;
+ span {
+ display: inline-block;
+ width: 33.33%;
+ font-size: 12px;
+ text-align: center;
+ }
+ span:first-child {
+ text-align: left;
+ }
+ span:last-child {
+ text-align: right;
+ }
+}
+.dashedLine {
+ position: relative;
+ top: -70px;
+ left: -3px;
+ height: 1px;
+
+ .line {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: linear-gradient(to right, transparent 50%, #e9e9e9 50%);
+ background-size: 6px;
+ }
+}
+
+.dashedLine:last-child {
+ top: -36px;
+}
diff --git a/src/pages/dashboard/monitor/components/ActiveChart/index.style.ts b/src/pages/dashboard/monitor/components/ActiveChart/index.style.ts
new file mode 100644
index 00000000..af0e058c
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/ActiveChart/index.style.ts
@@ -0,0 +1,48 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(() => {
+ return {
+ activeChart: {
+ position: 'relative',
+ },
+ activeChartGrid: {
+ p: { position: 'absolute', top: '80px' },
+ 'p:last-child': { top: '115px' },
+ },
+ activeChartLegend: {
+ position: 'relative',
+ height: '20px',
+ marginTop: '8px',
+ fontSize: '0',
+ lineHeight: '20px',
+ span: {
+ display: 'inline-block',
+ width: '33.33%',
+ fontSize: '12px',
+ textAlign: 'center',
+ },
+ 'span:first-child': { textAlign: 'left' },
+ 'span:last-child': { textAlign: 'right' },
+ },
+ dashedLine: {
+ position: 'relative',
+ top: '-70px',
+ left: '-3px',
+ height: '1px',
+ },
+ line: {
+ position: 'absolute',
+ top: '0',
+ left: '0',
+ width: '100%',
+ height: '100%',
+ backgroundImage:
+ 'linear-gradient(to right, transparent 50%, #e9e9e9 50%)',
+ backgroundSize: '6px',
+ },
+ 'dashedLine:last-child': {
+ top: '-36px',
+ },
+ };
+});
+export default useStyles;
diff --git a/src/pages/dashboard/monitor/components/ActiveChart/index.tsx b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx
new file mode 100644
index 00000000..1a6197de
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx
@@ -0,0 +1,94 @@
+import { Area } from '@ant-design/plots';
+import { Statistic } from 'antd';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import useStyles from './index.style';
+
+function fixedZero(val: number) {
+ return val * 1 < 10 ? `0${val}` : val;
+}
+function getActiveData() {
+ const activeData = [];
+ for (let i = 0; i < 24; i += 1) {
+ activeData.push({
+ x: `${fixedZero(i)}:00`,
+ y: Math.floor(Math.random() * 200) + i * 50,
+ });
+ }
+ return activeData;
+}
+
+const ActiveChart = () => {
+ const timerRef = useRef(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 (
+
+
+
+ {activeData && (
+
+
+
{maxValue + 200} 亿元
+
{medianValue} 亿元
+
+
+
+
+ )}
+ {activeData && (
+
+ 00:00
+ {activeData[Math.floor(activeData.length / 2)]?.x}
+ {activeData[activeData.length - 1]?.x}
+
+ )}
+
+ );
+};
+
+export default ActiveChart;
diff --git a/src/pages/dashboard/monitor/components/Map/index.tsx b/src/pages/dashboard/monitor/components/Map/index.tsx
new file mode 100644
index 00000000..34be85f0
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Map/index.tsx
@@ -0,0 +1,150 @@
+import { PageLoading } from '@ant-design/pro-components';
+import { HeatmapLayer, MapboxScene, PointLayer } from '@antv/l7-react';
+import { useEffect, useState } from 'react';
+
+const colors = [
+ '#eff3ff',
+ '#c6dbef',
+ '#9ecae1',
+ '#6baed6',
+ '#4292c6',
+ '#2171b5',
+ '#084594',
+];
+
+export default function MonitorMap() {
+ const [data, setData] = useState[] | null>(null);
+ const [grid, setGrid] = useState[] | 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 ? (
+
+ ) : (
+
+ {grid && (
+
+ )}
+ {data && [
+ ,
+ {
+ return v > 2000;
+ },
+ }}
+ size={{
+ values: 12,
+ }}
+ style={{
+ opacity: 1,
+ strokeOpacity: 1,
+ strokeWidth: 0,
+ }}
+ />,
+ ]}
+
+ );
+}
diff --git a/src/pages/dashboard/monitor/data.d.ts b/src/pages/dashboard/monitor/data.d.ts
new file mode 100644
index 00000000..b6efef35
--- /dev/null
+++ b/src/pages/dashboard/monitor/data.d.ts
@@ -0,0 +1,5 @@
+export type TagType = {
+ name: string;
+ value: number;
+ type: string;
+};
diff --git a/src/pages/dashboard/monitor/index.tsx b/src/pages/dashboard/monitor/index.tsx
new file mode 100644
index 00000000..a17fb72d
--- /dev/null
+++ b/src/pages/dashboard/monitor/index.tsx
@@ -0,0 +1,203 @@
+import { Gauge, Liquid, WordCloud } from '@ant-design/plots';
+import { GridContent } from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import { Card, Col, Progress, Row, Statistic } from 'antd';
+import numeral from 'numeral';
+import type { FC } from 'react';
+import ActiveChart from './components/ActiveChart';
+import MonitorMap from './components/Map';
+import { queryTags } from './service';
+import useStyles from './style.style';
+
+const deadline = Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30; // Moment is also OK
+
+const Monitor: FC = () => {
+ const { styles } = useStyles();
+ const { loading, data } = useRequest(queryTags);
+ const wordCloudData = (data?.list || []).map((item) => {
+ return {
+ id: +Date.now(),
+ word: item.name,
+ weight: item.value,
+ };
+ });
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ '优',
+ }}
+ meta={{
+ color: {
+ range: [
+ '#6395FA',
+ '#62DAAB',
+ '#657798',
+ '#F7C128',
+ '#1F8718',
+ ],
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+export default Monitor;
diff --git a/src/pages/dashboard/monitor/service.ts b/src/pages/dashboard/monitor/service.ts
new file mode 100644
index 00000000..2f4f5bce
--- /dev/null
+++ b/src/pages/dashboard/monitor/service.ts
@@ -0,0 +1,6 @@
+import { request } from '@umijs/max';
+import type { TagType } from './data';
+
+export async function queryTags(): Promise<{ data: { list: TagType[] } }> {
+ return request('/api/tags');
+}
diff --git a/src/pages/dashboard/monitor/style.less b/src/pages/dashboard/monitor/style.less
new file mode 100644
index 00000000..d2f0c686
--- /dev/null
+++ b/src/pages/dashboard/monitor/style.less
@@ -0,0 +1,21 @@
+@import '~antd/es/style/themes/default.less';
+
+.mapChart {
+ height: 452px;
+ padding-top: 24px;
+ img {
+ display: inline-block;
+ max-width: 100%;
+ max-height: 437px;
+ }
+}
+
+.pieCard :global(.pie-stat) {
+ font-size: 24px !important;
+}
+
+@media screen and (max-width: @screen-lg) {
+ .mapChart {
+ height: auto;
+ }
+}
diff --git a/src/pages/dashboard/monitor/style.style.ts b/src/pages/dashboard/monitor/style.style.ts
new file mode 100644
index 00000000..e36e78ca
--- /dev/null
+++ b/src/pages/dashboard/monitor/style.style.ts
@@ -0,0 +1,16 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ mapChart: {
+ height: '452px',
+ paddingTop: '24px',
+ img: { display: 'inline-block', maxWidth: '100%', maxHeight: '437px' },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ height: 'auto',
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/workplace/_mock.ts b/src/pages/dashboard/workplace/_mock.ts
new file mode 100644
index 00000000..c0dc9b73
--- /dev/null
+++ b/src/pages/dashboard/workplace/_mock.ts
@@ -0,0 +1,410 @@
+import dayjs from 'dayjs';
+import type { Request, Response } from 'express';
+import type { DataItem, OfflineDataType, SearchDataType } from './data.d';
+
+// mock data
+const visitData: DataItem[] = [];
+const beginDay = Date.now();
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+for (let i = 0; i < fakeY.length; i += 1) {
+ visitData.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY[i],
+ });
+}
+
+const visitData2: DataItem[] = [];
+const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
+for (let i = 0; i < fakeY2.length; i += 1) {
+ visitData2.push({
+ x: dayjs(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY2[i],
+ });
+}
+
+const salesData: DataItem[] = [];
+for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+}
+const searchData: SearchDataType[] = [];
+for (let i = 0; i < 50; i += 1) {
+ searchData.push({
+ index: i + 1,
+ keyword: `搜索关键词-${i}`,
+ count: Math.floor(Math.random() * 1000),
+ range: Math.floor(Math.random() * 100),
+ status: Math.floor((Math.random() * 10) % 2),
+ });
+}
+const salesTypeData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+];
+
+const salesTypeDataOnline = [
+ {
+ x: '家用电器',
+ y: 244,
+ },
+ {
+ x: '食用酒水',
+ y: 321,
+ },
+ {
+ x: '个护健康',
+ y: 311,
+ },
+ {
+ x: '服饰箱包',
+ y: 41,
+ },
+ {
+ x: '母婴产品',
+ y: 121,
+ },
+ {
+ x: '其他',
+ y: 111,
+ },
+];
+
+const salesTypeDataOffline = [
+ {
+ x: '家用电器',
+ y: 99,
+ },
+ {
+ x: '食用酒水',
+ y: 188,
+ },
+ {
+ x: '个护健康',
+ y: 344,
+ },
+ {
+ x: '服饰箱包',
+ y: 255,
+ },
+ {
+ x: '其他',
+ y: 65,
+ },
+];
+
+const offlineData: OfflineDataType[] = [];
+for (let i = 0; i < 10; i += 1) {
+ offlineData.push({
+ name: `Stores ${i}`,
+ cvr: Math.ceil(Math.random() * 9) / 10,
+ });
+}
+const offlineChartData: DataItem[] = [];
+for (let i = 0; i < 20; i += 1) {
+ offlineChartData.push({
+ x: Date.now() + 1000 * 60 * 30 * i,
+ y1: Math.floor(Math.random() * 100) + 10,
+ y2: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+const titles = [
+ 'Alipay',
+ 'Angular',
+ 'Ant Design',
+ 'Ant Design Pro',
+ 'Bootstrap',
+ 'React',
+ 'Vue',
+ 'Webpack',
+];
+const avatars = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
+ 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
+ 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
+ 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
+ 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
+ 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
+ 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
+ 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
+];
+
+const avatars2 = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
+];
+
+const getNotice = (_: Request, res: Response) => {
+ res.json({
+ data: [
+ {
+ id: 'xxx1',
+ title: titles[0],
+ logo: avatars[0],
+ description: '那是一种内在的东西,他们到达不了,也无法触及的',
+ updatedAt: new Date(),
+ member: '科学搬砖组',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx2',
+ title: titles[1],
+ logo: avatars[1],
+ description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ updatedAt: new Date('2017-07-24'),
+ member: '全组都是吴彦祖',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx3',
+ title: titles[2],
+ logo: avatars[2],
+ description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ updatedAt: new Date(),
+ member: '中二少女团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx4',
+ title: titles[3],
+ logo: avatars[3],
+ description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+ updatedAt: new Date('2017-07-23'),
+ member: '程序员日常',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx5',
+ title: titles[4],
+ logo: avatars[4],
+ description: '凛冬将至',
+ updatedAt: new Date('2017-07-23'),
+ member: '高逼格设计天团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx6',
+ title: titles[5],
+ logo: avatars[5],
+ description: '生命就像一盒巧克力,结果往往出人意料',
+ updatedAt: new Date('2017-07-23'),
+ member: '骗你来学计算机',
+ href: '',
+ memberLink: '',
+ },
+ ],
+ });
+};
+
+const getActivities = (_: Request, res: Response) => {
+ res.json({
+ data: [
+ {
+ id: 'trend-1',
+ updatedAt: new Date(),
+ user: {
+ name: '曲丽丽',
+ avatar: avatars2[0],
+ },
+ group: {
+ name: '高逼格设计天团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-2',
+ updatedAt: new Date(),
+ user: {
+ name: '付小小',
+ avatar: avatars2[1],
+ },
+ group: {
+ name: '高逼格设计天团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-3',
+ updatedAt: new Date(),
+ user: {
+ name: '林东东',
+ avatar: avatars2[2],
+ },
+ group: {
+ name: '中二少女团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-4',
+ updatedAt: new Date(),
+ user: {
+ name: '周星星',
+ avatar: avatars2[4],
+ },
+ project: {
+ name: '5 月日常迭代',
+ link: 'http://github.com/',
+ },
+ template: '将 @{project} 更新至已发布状态',
+ },
+ {
+ id: 'trend-5',
+ updatedAt: new Date(),
+ user: {
+ name: '朱偏右',
+ avatar: avatars2[3],
+ },
+ project: {
+ name: '工程效能',
+ link: 'http://github.com/',
+ },
+ comment: {
+ name: '留言',
+ link: 'http://github.com/',
+ },
+ template: '在 @{project} 发布了 @{comment}',
+ },
+ {
+ id: 'trend-6',
+ updatedAt: new Date(),
+ user: {
+ name: '乐哥',
+ avatar: avatars2[5],
+ },
+ group: {
+ name: '程序员日常',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '品牌迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ ],
+ });
+};
+
+const radarOriginData = [
+ {
+ name: '个人',
+ ref: 10,
+ koubei: 8,
+ output: 4,
+ contribute: 5,
+ hot: 7,
+ },
+ {
+ name: '团队',
+ ref: 3,
+ koubei: 9,
+ output: 6,
+ contribute: 3,
+ hot: 1,
+ },
+ {
+ name: '部门',
+ ref: 4,
+ koubei: 1,
+ output: 6,
+ contribute: 5,
+ hot: 7,
+ },
+];
+
+const radarData: any[] = [];
+const radarTitleMap = {
+ ref: '引用',
+ koubei: '口碑',
+ output: '产量',
+ contribute: '贡献',
+ hot: '热度',
+};
+radarOriginData.forEach((item) => {
+ Object.keys(item).forEach((key) => {
+ if (key !== 'name') {
+ radarData.push({
+ name: item.name,
+ label: radarTitleMap[key as 'ref'],
+ value: item[key as 'ref'],
+ });
+ }
+ });
+});
+
+const getChartData = (_: Request, res: Response) => {
+ res.json({
+ data: {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ radarData,
+ },
+ });
+};
+
+export default {
+ 'GET /api/project/notice': getNotice,
+ 'GET /api/activities': getActivities,
+ 'GET /api/fake_workplace_chart_data': getChartData,
+};
diff --git a/src/pages/dashboard/workplace/components/EditableLinkGroup/index.less b/src/pages/dashboard/workplace/components/EditableLinkGroup/index.less
new file mode 100644
index 00000000..5add1b0b
--- /dev/null
+++ b/src/pages/dashboard/workplace/components/EditableLinkGroup/index.less
@@ -0,0 +1,16 @@
+@import '~antd/es/style/themes/default.less';
+
+.linkGroup {
+ padding: 20px 0 8px 24px;
+ font-size: 0;
+ & > a {
+ display: inline-block;
+ width: 25%;
+ margin-bottom: 13px;
+ color: @text-color;
+ font-size: @font-size-base;
+ &:hover {
+ color: @primary-color;
+ }
+ }
+}
diff --git a/src/pages/dashboard/workplace/components/EditableLinkGroup/index.style.ts b/src/pages/dashboard/workplace/components/EditableLinkGroup/index.style.ts
new file mode 100644
index 00000000..dae8f253
--- /dev/null
+++ b/src/pages/dashboard/workplace/components/EditableLinkGroup/index.style.ts
@@ -0,0 +1,21 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ linkGroup: {
+ fontSize: '0',
+ '& > a': {
+ display: 'inline-block',
+ width: '25%',
+ marginBottom: '13px',
+ color: token.colorText,
+ fontSize: token.fontSize,
+ '&:hover': {
+ color: token.colorPrimary,
+ },
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/dashboard/workplace/components/EditableLinkGroup/index.tsx b/src/pages/dashboard/workplace/components/EditableLinkGroup/index.tsx
new file mode 100644
index 00000000..f1c0f4f5
--- /dev/null
+++ b/src/pages/dashboard/workplace/components/EditableLinkGroup/index.tsx
@@ -0,0 +1,38 @@
+import { PlusOutlined } from '@ant-design/icons';
+import { Button } from 'antd';
+import React, { createElement } from 'react';
+import useStyles from './index.style';
+export type EditableLink = {
+ title: string;
+ href: string;
+ id?: string;
+};
+type EditableLinkGroupProps = {
+ onAdd: () => void;
+ links: EditableLink[];
+ linkElement: any;
+};
+const EditableLinkGroup: React.FC = (props) => {
+ const { styles } = useStyles();
+ const { links = [], linkElement = 'a', onAdd = () => {} } = props;
+ return (
+
+ {links.map((link) =>
+ createElement(
+ linkElement,
+ {
+ key: `linkGroup-item-${link.id || link.title}`,
+ to: link.href,
+ href: link.href,
+ },
+ link.title,
+ ),
+ )}
+
+ 添加
+
+
+ );
+};
+
+export default EditableLinkGroup;
diff --git a/src/pages/dashboard/workplace/data.d.ts b/src/pages/dashboard/workplace/data.d.ts
new file mode 100644
index 00000000..e9ccf288
--- /dev/null
+++ b/src/pages/dashboard/workplace/data.d.ts
@@ -0,0 +1,111 @@
+export interface DataItem {
+ [field: string]: string | number | number[] | null | undefined;
+}
+export interface TagType {
+ key: string;
+ label: string;
+}
+
+export type SearchDataType = {
+ index: number;
+ keyword: string;
+ count: number;
+ range: number;
+ status: number;
+};
+
+export type OfflineDataType = {
+ name: string;
+ cvr: number;
+};
+
+export interface RadarData {
+ name: string;
+ label: string;
+ value: number;
+}
+
+export type AnalysisData = {
+ visitData: VisitDataType[];
+ visitData2: VisitDataType[];
+ salesData: VisitDataType[];
+ searchData: SearchDataType[];
+ offlineData: OfflineDataType[];
+ offlineChartData: OfflineChartData[];
+ salesTypeData: VisitDataType[];
+ salesTypeDataOnline: VisitDataType[];
+ salesTypeDataOffline: VisitDataType[];
+ radarData: DataItem[];
+};
+
+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 ActivitiesType = {
+ id: string;
+ updatedAt: string;
+ user: {
+ link?: string;
+ name: string;
+ avatar: string;
+ };
+ group: {
+ name: string;
+ link: string;
+ };
+ project: {
+ name: string;
+ link: string;
+ };
+
+ template: string;
+};
+
+export type RadarDataType = {
+ label: string;
+ name: string;
+ value: number;
+};
diff --git a/src/pages/dashboard/workplace/index.tsx b/src/pages/dashboard/workplace/index.tsx
new file mode 100644
index 00000000..23772a09
--- /dev/null
+++ b/src/pages/dashboard/workplace/index.tsx
@@ -0,0 +1,286 @@
+import { Radar } from '@ant-design/plots';
+import { PageContainer } from '@ant-design/pro-components';
+import { Link, useRequest } from '@umijs/max';
+import { Avatar, Card, Col, List, Row, Skeleton, Statistic } from 'antd';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import type { FC } from 'react';
+import EditableLinkGroup from './components/EditableLinkGroup';
+import type { ActivitiesType, CurrentUser } from './data.d';
+import { fakeChartData, queryActivities, queryProjectNotice } from './service';
+import useStyles from './style.style';
+
+dayjs.extend(relativeTime);
+
+const links = [
+ {
+ title: '操作一',
+ href: '',
+ },
+ {
+ title: '操作二',
+ href: '',
+ },
+ {
+ title: '操作三',
+ href: '',
+ },
+ {
+ title: '操作四',
+ href: '',
+ },
+ {
+ title: '操作五',
+ href: '',
+ },
+ {
+ title: '操作六',
+ href: '',
+ },
+];
+const PageHeaderContent: FC<{
+ currentUser: Partial;
+}> = ({ currentUser }) => {
+ const { styles } = useStyles();
+ const loading = currentUser && Object.keys(currentUser).length;
+ if (!loading) {
+ return (
+
+ );
+ }
+ return (
+
+
+
+
+ 早安,
+ {currentUser.name}
+ ,祝你开心每一天!
+
+
+ {currentUser.title} |{currentUser.group}
+
+
+
+ );
+};
+const ExtraContent: FC> = () => {
+ const { styles } = useStyles();
+ return (
+
+ );
+};
+const Workplace: FC = () => {
+ const { styles } = useStyles();
+ const { loading: projectLoading, data: projectNotice = [] } =
+ useRequest(queryProjectNotice);
+ const { loading: activitiesLoading, data: activities = [] } =
+ useRequest(queryActivities);
+ const { data } = useRequest(fakeChartData);
+ const renderActivities = (item: ActivitiesType) => {
+ const events = item.template.split(/@\{([^{}]*)\}/gi).map((key) => {
+ if (item[key as keyof ActivitiesType]) {
+ const value = item[key as 'user'];
+ return (
+
+ {value.name}
+
+ );
+ }
+ return key;
+ });
+ return (
+
+ }
+ title={
+
+ {item.user.name}
+
+ {events}
+
+ }
+ description={
+
+ {dayjs(item.updatedAt).fromNow()}
+
+ }
+ />
+
+ );
+ };
+
+ return (
+
+ }
+ extraContent={ }
+ >
+
+
+ 全部项目}
+ loading={projectLoading}
+ >
+ {projectNotice.map((item) => (
+
+
+
+ {item.title}
+
+ }
+ description={item.description}
+ style={{
+ width: '100%',
+ }}
+ />
+
+ {item.member || ''}
+ {item.updatedAt && (
+
+ {dayjs(item.updatedAt).fromNow()}
+
+ )}
+
+
+ ))}
+
+
+
+ loading={activitiesLoading}
+ renderItem={(item) => renderActivities(item)}
+ dataSource={activities}
+ className={styles.activitiesList}
+ size="large"
+ />
+
+
+
+
+ {}}
+ links={links}
+ linkElement={Link}
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+};
+export default Workplace;
diff --git a/src/pages/dashboard/workplace/service.ts b/src/pages/dashboard/workplace/service.ts
new file mode 100644
index 00000000..84d15931
--- /dev/null
+++ b/src/pages/dashboard/workplace/service.ts
@@ -0,0 +1,14 @@
+import { request } from '@umijs/max';
+import type { ActivitiesType, AnalysisData, NoticeType } from './data';
+
+export async function queryProjectNotice(): Promise<{ data: NoticeType[] }> {
+ return request('/api/project/notice');
+}
+
+export async function queryActivities(): Promise<{ data: ActivitiesType[] }> {
+ return request('/api/activities');
+}
+
+export async function fakeChartData(): Promise<{ data: AnalysisData }> {
+ return request('/api/fake_workplace_chart_data');
+}
diff --git a/src/pages/dashboard/workplace/style.less b/src/pages/dashboard/workplace/style.less
new file mode 100644
index 00000000..18fddb86
--- /dev/null
+++ b/src/pages/dashboard/workplace/style.less
@@ -0,0 +1,251 @@
+@import '~antd/es/style/themes/default.less';
+
+.textOverflow() {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+}
+
+// mixins for clearfix
+// ------------------------
+.clearfix() {
+ zoom: 1;
+ &::before,
+ &::after {
+ display: table;
+ content: ' ';
+ }
+ &::after {
+ clear: both;
+ height: 0;
+ font-size: 0;
+ visibility: hidden;
+ }
+}
+
+.activitiesList {
+ padding: 0 24px 8px 24px;
+
+ .username {
+ color: @text-color;
+ }
+ .event {
+ font-weight: normal;
+ }
+}
+
+.pageHeaderContent {
+ display: flex;
+ .avatar {
+ flex: 0 1 72px;
+ & > span {
+ display: block;
+ width: 72px;
+ height: 72px;
+ border-radius: 72px;
+ }
+ }
+ .content {
+ position: relative;
+ top: 4px;
+ flex: 1 1 auto;
+ margin-left: 24px;
+ color: @text-color-secondary;
+ line-height: 22px;
+ .contentTitle {
+ margin-bottom: 12px;
+ color: @heading-color;
+ font-weight: 500;
+ font-size: 20px;
+ line-height: 28px;
+ }
+ }
+}
+
+.extraContent {
+ .clearfix();
+
+ float: right;
+ white-space: nowrap;
+ .statItem {
+ position: relative;
+ display: inline-block;
+ padding: 0 32px;
+ > p:first-child {
+ margin-bottom: 4px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ > p {
+ margin: 0;
+ color: @heading-color;
+ font-size: 30px;
+ line-height: 38px;
+ > span {
+ color: @text-color-secondary;
+ font-size: 20px;
+ }
+ }
+ &::after {
+ position: absolute;
+ top: 8px;
+ right: 0;
+ width: 1px;
+ height: 40px;
+ background-color: @border-color-split;
+ content: '';
+ }
+ &:last-child {
+ padding-right: 0;
+ &::after {
+ display: none;
+ }
+ }
+ }
+}
+
+.members {
+ a {
+ display: block;
+ height: 24px;
+ margin: 12px 0;
+ color: @text-color;
+ transition: all 0.3s;
+ .textOverflow();
+ .member {
+ margin-left: 12px;
+ font-size: @font-size-base;
+ line-height: 24px;
+ vertical-align: top;
+ }
+ &:hover {
+ color: @primary-color;
+ }
+ }
+}
+
+.projectList {
+ :global {
+ .ant-card-meta-description {
+ height: 44px;
+ overflow: hidden;
+ color: @text-color-secondary;
+ line-height: 22px;
+ }
+ }
+ .cardTitle {
+ font-size: 0;
+ a {
+ display: inline-block;
+ height: 24px;
+ margin-left: 12px;
+ color: @heading-color;
+ font-size: @font-size-base;
+ line-height: 24px;
+ vertical-align: top;
+ &:hover {
+ color: @primary-color;
+ }
+ }
+ }
+ .projectGrid {
+ width: 33.33%;
+ }
+ .projectItemContent {
+ display: flex;
+ height: 20px;
+ margin-top: 8px;
+ overflow: hidden;
+ font-size: 12px;
+ line-height: 20px;
+ .textOverflow();
+ a {
+ display: inline-block;
+ flex: 1 1 0;
+ color: @text-color-secondary;
+ .textOverflow();
+ &:hover {
+ color: @primary-color;
+ }
+ }
+ .datetime {
+ flex: 0 0 auto;
+ float: right;
+ color: @disabled-color;
+ }
+ }
+}
+
+.datetime {
+ color: @disabled-color;
+}
+
+@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) {
+ .activeCard {
+ margin-bottom: 24px;
+ }
+ .members {
+ margin-bottom: 0;
+ }
+ .extraContent {
+ margin-left: -44px;
+ .statItem {
+ padding: 0 16px;
+ }
+ }
+}
+
+@media screen and (max-width: @screen-lg) {
+ .activeCard {
+ margin-bottom: 24px;
+ }
+ .members {
+ margin-bottom: 0;
+ }
+ .extraContent {
+ float: none;
+ margin-right: 0;
+ .statItem {
+ padding: 0 16px;
+ text-align: left;
+ &::after {
+ display: none;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: @screen-md) {
+ .extraContent {
+ margin-left: -16px;
+ }
+ .projectList {
+ .projectGrid {
+ width: 50%;
+ }
+ }
+}
+
+@media screen and (max-width: @screen-sm) {
+ .pageHeaderContent {
+ display: block;
+ .content {
+ margin-left: 0;
+ }
+ }
+ .extraContent {
+ .statItem {
+ float: none;
+ }
+ }
+}
+
+@media screen and (max-width: @screen-xs) {
+ .projectList {
+ .projectGrid {
+ width: 100%;
+ }
+ }
+}
diff --git a/src/pages/dashboard/workplace/style.style.ts b/src/pages/dashboard/workplace/style.style.ts
new file mode 100644
index 00000000..2580b975
--- /dev/null
+++ b/src/pages/dashboard/workplace/style.style.ts
@@ -0,0 +1,215 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ activitiesList: {
+ padding: 0,
+ },
+ username: {
+ color: token.colorText,
+ },
+ event: {
+ fontWeight: 'normal',
+ },
+ pageHeaderContent: {
+ display: 'flex',
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ display: 'block',
+ },
+ },
+ avatar: {
+ flex: '0 1 72px',
+ '& > span': {
+ display: 'block',
+ width: '72px',
+ height: '72px',
+ borderRadius: '72px',
+ },
+ },
+ content: {
+ position: 'relative',
+ top: '4px',
+ flex: '1 1 auto',
+ marginLeft: '24px',
+ color: token.colorTextSecondary,
+ lineHeight: '22px',
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ marginLeft: '0',
+ },
+ },
+ contentTitle: {
+ marginBottom: '12px',
+ color: token.colorTextHeading,
+ fontWeight: '500',
+ fontSize: '20px',
+ lineHeight: '28px',
+ },
+ extraContent: {
+ zoom: '1',
+ '&::before, &::after': { display: 'table', content: "' '" },
+ '&::after': {
+ clear: 'both',
+ height: '0',
+ fontSize: '0',
+ visibility: 'hidden',
+ },
+ float: 'right',
+ whiteSpace: 'nowrap',
+ [`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]:
+ {
+ marginLeft: '-44px',
+ },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ float: 'none',
+ marginRight: '0',
+ },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ marginLeft: '-16px',
+ },
+ },
+ statItem: {
+ position: 'relative',
+ display: 'inline-block',
+ padding: '0 32px',
+ '> p:first-child': {
+ marginBottom: '4px',
+ color: token.colorTextSecondary,
+ fontSize: token.fontSize,
+ lineHeight: '22px',
+ },
+ '> p': {
+ margin: '0',
+ color: token.colorTextHeading,
+ fontSize: '30px',
+ lineHeight: '38px',
+ '> span': {
+ color: token.colorTextSecondary,
+ fontSize: '20px',
+ },
+ },
+ '&::after': {
+ position: 'absolute',
+ top: '8px',
+ right: '0',
+ width: '1px',
+ height: '40px',
+ backgroundColor: token.colorSplit,
+ content: "''",
+ },
+ '&:last-child': {
+ paddingRight: '0',
+ '&::after': {
+ display: 'none',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]:
+ {
+ padding: '0 16px',
+ },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ padding: '0 16px',
+ textAlign: 'left',
+ '&::after': {
+ display: 'none',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenSM}px)`]: { float: 'none' },
+ },
+ members: {
+ a: {
+ display: 'block',
+ height: '24px',
+ margin: '12px 0',
+ color: token.colorText,
+ transition: 'all 0.3s',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ '&:hover': {
+ color: token.colorPrimary,
+ },
+ },
+ [`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]:
+ {
+ marginBottom: '0',
+ },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ marginBottom: '0',
+ },
+ },
+ member: {
+ marginLeft: '12px',
+ fontSize: token.fontSize,
+ lineHeight: '24px',
+ verticalAlign: 'top',
+ },
+ projectList: {
+ '.ant-card-meta-description': {
+ height: '44px',
+ overflow: 'hidden',
+ color: token.colorTextSecondary,
+ lineHeight: '22px',
+ },
+ },
+ cardTitle: {
+ fontSize: '0',
+ a: {
+ display: 'inline-block',
+ height: '24px',
+ marginLeft: '12px',
+ color: token.colorTextHeading,
+ fontSize: token.fontSize,
+ lineHeight: '24px',
+ verticalAlign: 'top',
+ '&:hover': {
+ color: token.colorPrimary,
+ },
+ },
+ },
+ projectGrid: {
+ width: '33.33%',
+ [`@media screen and (max-width: ${token.screenMD}px)`]: { width: '50%' },
+ [`@media screen and (max-width: ${token.screenXS}px)`]: { width: '100%' },
+ },
+ projectItemContent: {
+ display: 'flex',
+ height: '20px',
+ marginTop: '8px',
+ overflow: 'hidden',
+ fontSize: '12px',
+ gap: 'epx',
+ lineHeight: '20px',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ a: {
+ display: 'inline-block',
+ flex: '1 1 0',
+ color: token.colorTextSecondary,
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ '&:hover': {
+ color: token.colorPrimary,
+ },
+ },
+ },
+ datetime: {
+ flex: '0 0 auto',
+ color: token.colorTextDisabled,
+ },
+ activeCard: {
+ [`@media screen and (max-width: ${token.screenXL}px) and (min-width: @screen-lg)`]:
+ {
+ marginBottom: '24px',
+ },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ marginBottom: '24px',
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/exception/403/index.tsx b/src/pages/exception/403/index.tsx
new file mode 100644
index 00000000..b5397c0b
--- /dev/null
+++ b/src/pages/exception/403/index.tsx
@@ -0,0 +1,17 @@
+import { Link } from '@umijs/max';
+import { Button, Card, Result } from 'antd';
+
+export default () => (
+
+
+ Back to home
+
+ }
+ />
+
+);
diff --git a/src/pages/exception/404/index.tsx b/src/pages/exception/404/index.tsx
new file mode 100644
index 00000000..f2ae5149
--- /dev/null
+++ b/src/pages/exception/404/index.tsx
@@ -0,0 +1,17 @@
+import { Link } from '@umijs/max';
+import { Button, Card, Result } from 'antd';
+
+export default () => (
+
+
+ Back Home
+
+ }
+ />
+
+);
diff --git a/src/pages/exception/500/index.tsx b/src/pages/exception/500/index.tsx
new file mode 100644
index 00000000..55d69de5
--- /dev/null
+++ b/src/pages/exception/500/index.tsx
@@ -0,0 +1,17 @@
+import { Link } from '@umijs/max';
+import { Button, Card, Result } from 'antd';
+
+export default () => (
+
+
+ Back Home
+
+ }
+ />
+
+);
diff --git a/src/pages/form/advanced-form/_mock.ts b/src/pages/form/advanced-form/_mock.ts
new file mode 100644
index 00000000..b719dbc0
--- /dev/null
+++ b/src/pages/form/advanced-form/_mock.ts
@@ -0,0 +1,7 @@
+import type { Request, Response } from 'express';
+
+export default {
+ 'POST /api/advancedForm': (_: Request, res: Response) => {
+ res.send({ data: { message: 'Ok' } });
+ },
+};
diff --git a/src/pages/form/advanced-form/index.tsx b/src/pages/form/advanced-form/index.tsx
new file mode 100644
index 00000000..280648b9
--- /dev/null
+++ b/src/pages/form/advanced-form/index.tsx
@@ -0,0 +1,549 @@
+import { CloseCircleOutlined } from '@ant-design/icons';
+import type { ProColumnType } from '@ant-design/pro-components';
+import {
+ EditableProTable,
+ FooterToolbar,
+ PageContainer,
+ ProForm,
+ ProFormDateRangePicker,
+ ProFormSelect,
+ ProFormText,
+ ProFormTimePicker,
+} from '@ant-design/pro-components';
+import { Card, Col, message, Popover, Row } from 'antd';
+import type { FC } from 'react';
+import { useState } from 'react';
+import { fakeSubmitForm } from './service';
+import useStyles from './style.style';
+
+interface TableFormDateType {
+ key: string;
+ workId?: string;
+ name?: string;
+ department?: string;
+ isNew?: boolean;
+ editable?: boolean;
+}
+type InternalNamePath = (string | number)[];
+const fieldLabels = {
+ name: '仓库名',
+ url: '仓库域名',
+ owner: '仓库管理员',
+ approver: '审批人',
+ dateRange: '生效日期',
+ type: '仓库类型',
+ name2: '任务名',
+ url2: '任务描述',
+ owner2: '执行人',
+ approver2: '责任人',
+ dateRange2: '生效日期',
+ type2: '任务类型',
+};
+const tableData = [
+ {
+ key: '1',
+ workId: '00001',
+ name: 'John Brown',
+ department: 'New York No. 1 Lake Park',
+ },
+ {
+ key: '2',
+ workId: '00002',
+ name: 'Jim Green',
+ department: 'London No. 1 Lake Park',
+ },
+ {
+ key: '3',
+ workId: '00003',
+ name: 'Joe Black',
+ department: 'Sidney No. 1 Lake Park',
+ },
+];
+interface ErrorField {
+ name: InternalNamePath;
+ errors: string[];
+}
+const AdvancedForm: FC> = () => {
+ const { styles } = useStyles();
+ const [error, setError] = useState([]);
+ const getErrorInfo = (errors: ErrorField[]) => {
+ const errorCount = errors.filter((item) => item.errors.length > 0).length;
+ if (!errors || errorCount === 0) {
+ return null;
+ }
+ const scrollToField = (fieldKey: string) => {
+ const labelNode = document.querySelector(`label[for="${fieldKey}"]`);
+ if (labelNode) {
+ labelNode.scrollIntoView(true);
+ }
+ };
+ const errorList = errors.map((err) => {
+ if (!err || err.errors.length === 0) {
+ return null;
+ }
+ const key = err.name[0] as
+ | 'name'
+ | 'url'
+ | 'owner'
+ | 'approver'
+ | 'dateRange'
+ | 'type';
+ return (
+ scrollToField(key)}
+ >
+
+ {err.errors[0]}
+ {fieldLabels[key]}
+
+ );
+ });
+ return (
+
+ {
+ if (trigger?.parentNode) {
+ return trigger.parentNode as HTMLElement;
+ }
+ return trigger;
+ }}
+ >
+
+
+ {errorCount}
+
+ );
+ };
+ const onFinish = async (values: Record) => {
+ setError([]);
+ try {
+ await fakeSubmitForm(values);
+ message.success('提交成功');
+ } catch {
+ // console.log
+ }
+ };
+ const onFinishFailed = (errorInfo: any) => {
+ setError(errorInfo.errorFields);
+ };
+ const columns: ProColumnType[] = [
+ {
+ title: '成员姓名',
+ dataIndex: 'name',
+ key: 'name',
+ width: '20%',
+ },
+ {
+ title: '工号',
+ dataIndex: 'workId',
+ key: 'workId',
+ width: '20%',
+ },
+ {
+ title: '所属部门',
+ dataIndex: 'department',
+ key: 'department',
+ width: '40%',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ valueType: 'option',
+ render: (_, record: TableFormDateType, _index, action) => {
+ return [
+ {
+ action?.startEditable(record.key);
+ }}
+ >
+ 编辑
+ ,
+ ];
+ },
+ },
+ ];
+ return (
+ {
+ return (
+
+ {getErrorInfo(error)}
+ {dom}
+
+ );
+ },
+ }}
+ initialValues={{
+ members: tableData,
+ }}
+ onFinish={onFinish}
+ onFinishFailed={onFinishFailed}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ recordCreatorProps={{
+ record: () => {
+ return {
+ key: `0${Date.now()}`,
+ };
+ },
+ }}
+ columns={columns}
+ rowKey="key"
+ />
+
+
+
+
+ );
+};
+export default AdvancedForm;
diff --git a/src/pages/form/advanced-form/service.ts b/src/pages/form/advanced-form/service.ts
new file mode 100644
index 00000000..652759d0
--- /dev/null
+++ b/src/pages/form/advanced-form/service.ts
@@ -0,0 +1,8 @@
+import { request } from '@umijs/max';
+
+export async function fakeSubmitForm(params: any) {
+ return request('/api/advancedForm', {
+ method: 'POST',
+ data: params,
+ });
+}
diff --git a/src/pages/form/advanced-form/style.less b/src/pages/form/advanced-form/style.less
new file mode 100644
index 00000000..e83ac949
--- /dev/null
+++ b/src/pages/form/advanced-form/style.less
@@ -0,0 +1,65 @@
+@import '~antd/es/style/themes/default.less';
+
+.card {
+ margin-bottom: 24px;
+
+ :global {
+ .ant-legacy-form-item .ant-legacy-form-item-control-wrapper {
+ width: 100%;
+ }
+ }
+}
+
+.errorIcon {
+ margin-right: 24px;
+ color: @error-color;
+ cursor: pointer;
+
+ span.anticon {
+ margin-right: 4px;
+ }
+}
+
+.errorPopover {
+ :global {
+ .ant-popover-inner-content {
+ min-width: 256px;
+ max-height: 290px;
+ padding: 0;
+ overflow: auto;
+ }
+ }
+}
+
+.errorListItem {
+ padding: 8px 16px;
+ list-style: none;
+ border-bottom: 1px solid @border-color-split;
+ cursor: pointer;
+ transition: all 0.3s;
+ &:hover {
+ background: @item-active-bg;
+ }
+ &:last-child {
+ border: 0;
+ }
+ .errorIcon {
+ float: left;
+ margin-top: 4px;
+ margin-right: 12px;
+ padding-bottom: 22px;
+ color: @error-color;
+ }
+ .errorField {
+ margin-top: 2px;
+ color: @text-color-secondary;
+ font-size: 12px;
+ }
+}
+
+.editable {
+ td {
+ padding-top: 13px !important;
+ padding-bottom: 12.5px !important;
+ }
+}
diff --git a/src/pages/form/advanced-form/style.style.ts b/src/pages/form/advanced-form/style.style.ts
new file mode 100644
index 00000000..d06c8c31
--- /dev/null
+++ b/src/pages/form/advanced-form/style.style.ts
@@ -0,0 +1,48 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ card: {
+ marginBottom: '24px',
+ '.ant-legacy-form-item .ant-legacy-form-item-control-wrapper': {
+ width: '100%',
+ },
+ },
+ errorIcon: {
+ marginRight: '12px',
+ color: token.colorError,
+ cursor: 'pointer',
+ 'span.anticon': { marginRight: '4px' },
+ float: 'left',
+ marginTop: '4px',
+ paddingBottom: '22px',
+ },
+ errorPopover: {
+ '.ant-popover-inner-content': {
+ minWidth: '256px',
+ maxHeight: '290px',
+ padding: '0',
+ overflow: 'auto',
+ },
+ },
+ errorListItem: {
+ padding: '8px 16px',
+ listStyle: 'none',
+ borderBottom: `1px solid ${token.colorSplit}`,
+ cursor: 'pointer',
+ transition: 'all 0.3s',
+ '&:hover': { background: token.colorBgTextActive },
+ '&:last-child': { border: '0' },
+ },
+ errorField: {
+ marginTop: '2px',
+ color: token.colorTextSecondary,
+ fontSize: '12px',
+ },
+ editable: {
+ td: { paddingTop: '13px', paddingBottom: '12.5px' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/form/basic-form/_mock.ts b/src/pages/form/basic-form/_mock.ts
new file mode 100644
index 00000000..b1f2235c
--- /dev/null
+++ b/src/pages/form/basic-form/_mock.ts
@@ -0,0 +1,7 @@
+import type { Request, Response } from 'express';
+
+export default {
+ 'POST /api/basicForm': (_: Request, res: Response) => {
+ res.send({ data: { message: 'Ok' } });
+ },
+};
diff --git a/src/pages/form/basic-form/index.tsx b/src/pages/form/basic-form/index.tsx
new file mode 100644
index 00000000..b2a44aca
--- /dev/null
+++ b/src/pages/form/basic-form/index.tsx
@@ -0,0 +1,194 @@
+import {
+ PageContainer,
+ ProForm,
+ ProFormDateRangePicker,
+ ProFormDependency,
+ ProFormDigit,
+ ProFormRadio,
+ ProFormSelect,
+ ProFormText,
+ ProFormTextArea,
+} from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import { Card, message } from 'antd';
+import type { FC } from 'react';
+import { fakeSubmitForm } from './service';
+import useStyles from './style.style';
+
+const BasicForm: FC> = () => {
+ const { styles } = useStyles();
+ const { run } = useRequest(fakeSubmitForm, {
+ manual: true,
+ onSuccess: () => {
+ message.success('提交成功');
+ },
+ });
+ const onFinish = async (values: Record) => {
+ run(values);
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+ 客户
+ (选填)
+
+ }
+ tooltip="目标的服务对象"
+ name="client"
+ placeholder="请描述你服务的客户,内部客户直接 @姓名/工号"
+ />
+
+
+ 邀评人
+ (选填)
+
+ }
+ name="invites"
+ placeholder="请直接 @姓名/工号,最多可邀请 5 人"
+ />
+
+
+ 权重
+ (选填)
+
+ }
+ name="weight"
+ placeholder="请输入"
+ min={0}
+ max={100}
+ width="xs"
+ fieldProps={{
+ formatter: (value) => `${value || 0}%`,
+ parser: (value) => Number(value ? value.replace('%', '') : '0'),
+ }}
+ />
+
+
+
+ {({ publicType }) => {
+ return (
+
+ );
+ }}
+
+
+
+
+ );
+};
+export default BasicForm;
diff --git a/src/pages/form/basic-form/service.ts b/src/pages/form/basic-form/service.ts
new file mode 100644
index 00000000..7553f9c4
--- /dev/null
+++ b/src/pages/form/basic-form/service.ts
@@ -0,0 +1,8 @@
+import { request } from '@umijs/max';
+
+export async function fakeSubmitForm(params: any) {
+ return request('/api/basicForm', {
+ method: 'POST',
+ data: params,
+ });
+}
diff --git a/src/pages/form/basic-form/style.less b/src/pages/form/basic-form/style.less
new file mode 100644
index 00000000..60a374ba
--- /dev/null
+++ b/src/pages/form/basic-form/style.less
@@ -0,0 +1,6 @@
+@import '~antd/es/style/themes/default.less';
+
+.optional {
+ color: @text-color-secondary;
+ font-style: normal;
+}
diff --git a/src/pages/form/basic-form/style.style.ts b/src/pages/form/basic-form/style.style.ts
new file mode 100644
index 00000000..e8a8406b
--- /dev/null
+++ b/src/pages/form/basic-form/style.style.ts
@@ -0,0 +1,12 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ optional: {
+ color: token.colorTextSecondary,
+ fontStyle: 'normal',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/form/step-form/_mock.ts b/src/pages/form/step-form/_mock.ts
new file mode 100644
index 00000000..c010228c
--- /dev/null
+++ b/src/pages/form/step-form/_mock.ts
@@ -0,0 +1,7 @@
+import type { Request, Response } from 'express';
+
+export default {
+ 'POST /api/stepForm': (_: Request, res: Response) => {
+ res.send({ data: { message: 'Ok' } });
+ },
+};
diff --git a/src/pages/form/step-form/data.d.ts b/src/pages/form/step-form/data.d.ts
new file mode 100644
index 00000000..4bddb0c8
--- /dev/null
+++ b/src/pages/form/step-form/data.d.ts
@@ -0,0 +1,9 @@
+export interface StepDataType {
+ payAccount: string;
+ receiverAccount: string;
+ receiverName: string;
+ amount: string;
+ receiverMode: string;
+}
+
+export type CurrentTypes = 'base' | 'confirm' | 'result';
diff --git a/src/pages/form/step-form/index.tsx b/src/pages/form/step-form/index.tsx
new file mode 100644
index 00000000..0354f0dd
--- /dev/null
+++ b/src/pages/form/step-form/index.tsx
@@ -0,0 +1,250 @@
+import {
+ PageContainer,
+ ProForm,
+ ProFormDigit,
+ ProFormSelect,
+ ProFormText,
+ StepsForm,
+} from '@ant-design/pro-components';
+import {
+ Alert,
+ Button,
+ Card,
+ Descriptions,
+ Divider,
+ Form,
+ Result,
+ Statistic,
+} from 'antd';
+import React, { useState } from 'react';
+import type { StepDataType } from './data.d';
+import useStyles from './style.style';
+
+const StepDescriptions: React.FC<{
+ stepData: StepDataType;
+ bordered?: boolean;
+}> = ({ stepData, bordered }) => {
+ const { payAccount, receiverAccount, receiverName, amount } = stepData;
+ return (
+
+ {payAccount}
+ {receiverAccount}
+ {receiverName}
+
+
+ 元
+
+ }
+ precision={2}
+ />
+
+
+ );
+};
+const StepResult: React.FC<{
+ onFinish: () => Promise;
+ children?: React.ReactNode;
+}> = (props) => {
+ const { styles } = useStyles();
+ return (
+
+
+ 再转一笔
+
+ 查看账单
+ >
+ }
+ className={styles.result}
+ >
+ {props.children}
+
+ );
+};
+const StepForm: React.FC> = () => {
+ const { styles } = useStyles();
+ const [stepData, setStepData] = useState({
+ payAccount: 'ant-design@alipay.com',
+ receiverAccount: 'test@example.com',
+ receiverName: 'Alex',
+ amount: '500',
+ receiverMode: 'alipay',
+ });
+ const [current, setCurrent] = useState(0);
+ const [form] = Form.useForm();
+ return (
+
+
+ {
+ if (props.step === 2) {
+ return null;
+ }
+ return dom;
+ },
+ }}
+ >
+
+ formRef={{
+ current: form,
+ }}
+ title="填写转账信息"
+ initialValues={stepData}
+ onFinish={async (values) => {
+ setStepData(values);
+ return true;
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setCurrent(0);
+ form.resetFields();
+ }}
+ >
+
+
+
+
+
+
+
说明
+
转账到支付宝账户
+
+ 如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
+
+
转账到银行卡
+
+ 如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
+
+
+
+
+ );
+};
+export default StepForm;
diff --git a/src/pages/form/step-form/style.less b/src/pages/form/step-form/style.less
new file mode 100644
index 00000000..443eb2ef
--- /dev/null
+++ b/src/pages/form/step-form/style.less
@@ -0,0 +1,11 @@
+@import '~antd/es/style/themes/default.less';
+
+.card {
+ margin-bottom: 24px;
+}
+
+.result {
+ max-width: 560px;
+ margin: 0 auto;
+ padding: 24px 0 8px;
+}
diff --git a/src/pages/form/step-form/style.style.ts b/src/pages/form/step-form/style.style.ts
new file mode 100644
index 00000000..c4133149
--- /dev/null
+++ b/src/pages/form/step-form/style.style.ts
@@ -0,0 +1,15 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(() => {
+ return {
+ card: {
+ marginBottom: '24px',
+ },
+ result: {
+ maxWidth: '560px',
+ margin: '0 auto',
+ padding: '24px 0 8px',
+ },
+ };
+});
+export default useStyles;
diff --git a/src/pages/list/basic-list/_mock.ts b/src/pages/list/basic-list/_mock.ts
new file mode 100644
index 00000000..680f6883
--- /dev/null
+++ b/src/pages/list/basic-list/_mock.ts
@@ -0,0 +1,165 @@
+import type { Request, Response } from 'express';
+import type { BasicListItemDataType } 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 = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+];
+
+function fakeList(count: number): BasicListItemDataType[] {
+ 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;
+}
+
+let sourceData: BasicListItemDataType[] = [];
+
+function getFakeList(req: Request, res: Response) {
+ const params = req.query as any;
+
+ const count = Number(params.count) * 1 || 20;
+
+ const result = fakeList(count);
+ sourceData = result;
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+function postFakeList(req: Request, res: Response) {
+ const { /* url = '', */ body } = req;
+ // const params = getUrlParams(url);
+ const { method, id } = body;
+ // const count = (params.count * 1) || 20;
+ let result = sourceData || [];
+
+ switch (method) {
+ case 'delete':
+ result = result.filter((item) => item.id !== id);
+ break;
+ case 'update':
+ result.forEach((item, i) => {
+ if (item.id === id) {
+ result[i] = { ...item, ...body };
+ }
+ });
+ break;
+ case 'post':
+ result.unshift({
+ ...body,
+ id: `fake-list-${result.length}`,
+ createdAt: Date.now(),
+ });
+ break;
+ default:
+ break;
+ }
+
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+export default {
+ 'GET /api/get_list': getFakeList,
+ 'POST /api/post_fake_list': postFakeList,
+};
diff --git a/src/pages/list/basic-list/components/OperationModal.tsx b/src/pages/list/basic-list/components/OperationModal.tsx
new file mode 100644
index 00000000..ebb2e082
--- /dev/null
+++ b/src/pages/list/basic-list/components/OperationModal.tsx
@@ -0,0 +1,129 @@
+import {
+ ModalForm,
+ ProFormDateTimePicker,
+ ProFormSelect,
+ ProFormText,
+ ProFormTextArea,
+} from '@ant-design/pro-components';
+import { Button, Result } from 'antd';
+import type { FC } from 'react';
+import type { BasicListItemDataType } from '../data.d';
+import useStyles from '../style.style';
+
+type OperationModalProps = {
+ done: boolean;
+ open: boolean;
+ current: Partial | undefined;
+ onDone: () => void;
+ onSubmit: (values: BasicListItemDataType) => void;
+ children?: React.ReactNode;
+};
+const OperationModal: FC = (props) => {
+ const { styles } = useStyles();
+ const { done, open, current, onDone, onSubmit, children } = props;
+ if (!open) {
+ return null;
+ }
+ return (
+
+ open={open}
+ title={done ? null : `任务${current ? '编辑' : '添加'}`}
+ className={styles.standardListForm}
+ width={640}
+ onFinish={async (values) => {
+ onSubmit(values);
+ }}
+ initialValues={current}
+ submitter={{
+ render: (_, dom) => (done ? null : dom),
+ }}
+ trigger={children}
+ modalProps={{
+ onCancel: () => onDone(),
+ destroyOnHidden: true,
+ bodyStyle: done
+ ? {
+ padding: '72px 0',
+ }
+ : {},
+ }}
+ >
+ {!done ? (
+ <>
+
+
+
+
+ >
+ ) : (
+
+ 知道了
+
+ }
+ className={styles.formResult}
+ />
+ )}
+
+ );
+};
+export default OperationModal;
diff --git a/src/pages/list/basic-list/data.d.ts b/src/pages/list/basic-list/data.d.ts
new file mode 100644
index 00000000..f1cfa668
--- /dev/null
+++ b/src/pages/list/basic-list/data.d.ts
@@ -0,0 +1,29 @@
+export type Member = {
+ avatar: string;
+ name: string;
+ id: string;
+};
+
+export type BasicListItemDataType = {
+ 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[];
+};
diff --git a/src/pages/list/basic-list/index.tsx b/src/pages/list/basic-list/index.tsx
new file mode 100644
index 00000000..af97803c
--- /dev/null
+++ b/src/pages/list/basic-list/index.tsx
@@ -0,0 +1,282 @@
+import { DownOutlined, PlusOutlined } from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import {
+ Avatar,
+ Button,
+ Card,
+ Col,
+ Dropdown,
+ Input,
+ List,
+ Modal,
+ Progress,
+ Row,
+ Segmented,
+} from 'antd';
+import dayjs from 'dayjs';
+import type { FC } from 'react';
+import React, { useState } from 'react';
+import OperationModal from './components/OperationModal';
+import type { BasicListItemDataType } from './data.d';
+import {
+ addFakeList,
+ queryFakeList,
+ removeFakeList,
+ updateFakeList,
+} from './service';
+import useStyles from './style.style';
+
+const { Search } = Input;
+const Info: FC<{
+ title: React.ReactNode;
+ value: React.ReactNode;
+ bordered?: boolean;
+}> = ({ title, value, bordered }) => {
+ const { styles } = useStyles();
+ return (
+
+
{title}
+
{value}
+ {bordered &&
}
+
+ );
+};
+const ListContent = ({
+ data: { owner, createdAt, percent, status },
+}: {
+ data: BasicListItemDataType;
+}) => {
+ const { styles } = useStyles();
+ return (
+
+
+
+
开始时间
+
{dayjs(createdAt).format('YYYY-MM-DD HH:mm')}
+
+
+
+ );
+};
+export const BasicList: FC = () => {
+ const { styles } = useStyles();
+ const [done, setDone] = useState(false);
+ const [open, setVisible] = useState(false);
+ const [current, setCurrent] = useState<
+ Partial | undefined
+ >(undefined);
+ const {
+ data: listData,
+ loading,
+ mutate,
+ } = useRequest(() => {
+ return queryFakeList({
+ count: 50,
+ });
+ });
+ const { run: postRun } = useRequest(
+ (method, params) => {
+ if (method === 'remove') {
+ return removeFakeList(params);
+ }
+ if (method === 'update') {
+ return updateFakeList(params);
+ }
+ return addFakeList(params);
+ },
+ {
+ manual: true,
+ onSuccess: (result) => {
+ mutate(result);
+ },
+ },
+ );
+ const list = listData?.list || [];
+ const paginationProps = {
+ showSizeChanger: true,
+ showQuickJumper: true,
+ pageSize: 5,
+ total: list.length,
+ };
+ const showEditModal = (item: BasicListItemDataType) => {
+ setVisible(true);
+ setCurrent(item);
+ };
+ const deleteItem = (id: string) => {
+ postRun('remove', {
+ id,
+ });
+ };
+ const editAndDelete = (
+ key: string | number,
+ currentItem: BasicListItemDataType,
+ ) => {
+ if (key === 'edit') showEditModal(currentItem);
+ else if (key === 'delete') {
+ Modal.confirm({
+ title: '删除任务',
+ content: '确定删除该任务吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: () => deleteItem(currentItem.id),
+ });
+ }
+ };
+ const extraContent = (
+
+
+ ({})}
+ variant="filled"
+ />
+
+ );
+
+ const renderMoreBtn = (item: BasicListItemDataType) => {
+ return (
+ editAndDelete(key, item),
+ items: [
+ {
+ key: 'edit',
+ label: '编辑',
+ },
+ {
+ key: 'delete',
+ label: '删除',
+ },
+ ],
+ }}
+ >
+
+ 更多
+
+
+ );
+ };
+
+ const handleDone = () => {
+ setDone(false);
+ setVisible(false);
+ setCurrent({});
+ };
+ const handleSubmit = (values: BasicListItemDataType) => {
+ setDone(true);
+ const method = values?.id ? 'update' : 'add';
+ postRun(method, values);
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (
+ {
+ e.preventDefault();
+ showEditModal(item);
+ }}
+ >
+ 编辑
+ ,
+ renderMoreBtn(item),
+ ]}
+ >
+
+ }
+ title={{item.title} }
+ description={item.subDescription}
+ />
+
+
+ )}
+ />
+
+
+
+
{
+ setVisible(true);
+ }}
+ style={{
+ width: '100%',
+ marginBottom: 8,
+ }}
+ >
+
+ 添加
+
+
+
+ );
+};
+export default BasicList;
diff --git a/src/pages/list/basic-list/service.ts b/src/pages/list/basic-list/service.ts
new file mode 100644
index 00000000..99eb3541
--- /dev/null
+++ b/src/pages/list/basic-list/service.ts
@@ -0,0 +1,50 @@
+import { request } from '@umijs/max';
+import type { BasicListItemDataType } from './data.d';
+
+type ParamsType = {
+ count?: number;
+} & Partial;
+
+export async function queryFakeList(
+ params: ParamsType,
+): Promise<{ data: { list: BasicListItemDataType[] } }> {
+ return request('/api/get_list', {
+ params,
+ });
+}
+
+export async function removeFakeList(
+ params: ParamsType,
+): Promise<{ data: { list: BasicListItemDataType[] } }> {
+ return request('/api/post_fake_list', {
+ method: 'POST',
+ data: {
+ ...params,
+ method: 'delete',
+ },
+ });
+}
+
+export async function addFakeList(
+ params: ParamsType,
+): Promise<{ data: { list: BasicListItemDataType[] } }> {
+ return request('/api/post_fake_list', {
+ method: 'POST',
+ data: {
+ ...params,
+ method: 'post',
+ },
+ });
+}
+
+export async function updateFakeList(
+ params: ParamsType,
+): Promise<{ data: { list: BasicListItemDataType[] } }> {
+ return request('/api/post_fake_list', {
+ method: 'POST',
+ data: {
+ ...params,
+ method: 'update',
+ },
+ });
+}
diff --git a/src/pages/list/basic-list/style.style.ts b/src/pages/list/basic-list/style.style.ts
new file mode 100644
index 00000000..7e120666
--- /dev/null
+++ b/src/pages/list/basic-list/style.style.ts
@@ -0,0 +1,141 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ standardList: {
+ '.ant-card-head': { borderBottom: 'none' },
+ '.ant-card-head-title': { padding: '24px 0', lineHeight: '32px' },
+ '.ant-card-extra': { padding: '24px 0' },
+ '.ant-list-pagination': { marginTop: '24px', textAlign: 'right' },
+ '.ant-avatar-lg': { width: '48px', height: '48px', lineHeight: '48px' },
+ [`@media screen and (max-width: ${token.screenXS}px)`]: {
+ '.ant-list-item-content': {
+ display: 'block',
+ flex: 'none',
+ width: '100%',
+ },
+ '.ant-list-item-action': {
+ marginLeft: '0',
+ },
+ },
+ },
+ headerInfo: {
+ position: 'relative',
+ textAlign: 'center',
+ '& > span': {
+ display: 'inline-block',
+ marginBottom: '4px',
+ color: token.colorTextSecondary,
+ fontSize: token.fontSize,
+ lineHeight: '22px',
+ },
+ '& > p': {
+ margin: '0',
+ color: token.colorTextHeading,
+ fontSize: '24px',
+ lineHeight: '32px',
+ },
+ '& > em': {
+ position: 'absolute',
+ top: '0',
+ right: '0',
+ width: '1px',
+ height: '56px',
+ backgroundColor: token.colorSplit,
+ },
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ marginBottom: '16px',
+ '& > em': {
+ display: 'none',
+ },
+ },
+ },
+ listContent: {
+ fontSize: '0',
+ [`@media screen and (max-width: ${token.screenXS}px)`]: {
+ marginLeft: '0',
+ '& > div': {
+ marginLeft: '0',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ '& > div': {
+ display: 'block',
+ },
+ '& > div:last-child': {
+ top: '0',
+ width: '100%',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenLG}px) and (min-width: @screen-md)`]:
+ {
+ '& > div': {
+ display: 'block',
+ },
+ '& > div:last-child': {
+ top: '0',
+ width: '100%',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenXL}px)`]: {
+ '& > div': {
+ marginLeft: '24px',
+ },
+ '& > div:last-child': {
+ top: '0',
+ },
+ },
+ '@media screen and (max-width: 1400px)': {
+ textAlign: 'right',
+ '& > div:last-child': {
+ top: '0',
+ },
+ },
+ },
+ listContentItem: {
+ display: 'inline-block',
+ marginLeft: '40px',
+ color: token.colorTextSecondary,
+ fontSize: token.fontSize,
+ verticalAlign: 'middle',
+ '> span': { lineHeight: '20px' },
+ '> p': { marginTop: '4px', marginBottom: '0', lineHeight: '22px' },
+ },
+ extraContentSearch: {
+ width: '272px',
+ marginLeft: '16px',
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ width: '100%',
+ marginLeft: '0',
+ },
+ },
+ listCard: {
+ [`@media screen and (max-width: ${token.screenXS}px)`]: {
+ '.ant-card-head-title': {
+ overflow: 'open',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ '.ant-radio-group': {
+ display: 'block',
+ marginBottom: '8px',
+ },
+ },
+ },
+ standardListForm: {
+ '.ant-form-item': {
+ marginBottom: '12px',
+ '&:last-child': {
+ marginBottom: '32px',
+ paddingTop: '4px',
+ },
+ },
+ },
+ formResult: {
+ width: '100%',
+ "[class^='title']": { marginBottom: '8px' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/basic-list/utils/utils.style.ts b/src/pages/list/basic-list/utils/utils.style.ts
new file mode 100644
index 00000000..0ad5e64a
--- /dev/null
+++ b/src/pages/list/basic-list/utils/utils.style.ts
@@ -0,0 +1,6 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(() => {
+ return {};
+});
+export default useStyles;
diff --git a/src/pages/list/card-list/_mock.ts b/src/pages/list/card-list/_mock.ts
new file mode 100644
index 00000000..36b5d755
--- /dev/null
+++ b/src/pages/list/card-list/_mock.ts
@@ -0,0 +1,125 @@
+import type { Request, Response } from 'express';
+import type { CardListItemDataType } 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 = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+];
+
+function fakeList(count: number): CardListItemDataType[] {
+ 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 || 20;
+
+ const result = fakeList(count);
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+export default {
+ 'GET /api/card_fake_list': getFakeList,
+};
diff --git a/src/pages/list/card-list/data.d.ts b/src/pages/list/card-list/data.d.ts
new file mode 100644
index 00000000..c7e663ff
--- /dev/null
+++ b/src/pages/list/card-list/data.d.ts
@@ -0,0 +1,29 @@
+export type Member = {
+ avatar: string;
+ name: string;
+ id: string;
+};
+
+export type CardListItemDataType = {
+ 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[];
+};
diff --git a/src/pages/list/card-list/index.tsx b/src/pages/list/card-list/index.tsx
new file mode 100644
index 00000000..a7b52927
--- /dev/null
+++ b/src/pages/list/card-list/index.tsx
@@ -0,0 +1,124 @@
+import { PlusOutlined } from '@ant-design/icons';
+import { PageContainer } from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import { Button, Card, List, Typography } from 'antd';
+import type { CardListItemDataType } from './data.d';
+import { queryFakeList } from './service';
+import useStyles from './style.style';
+
+const { Paragraph } = Typography;
+const CardList = () => {
+ const { styles } = useStyles();
+ const { data, loading } = useRequest(() => {
+ return queryFakeList({
+ count: 8,
+ });
+ });
+ const list = data?.list || [];
+ const content = (
+
+
+ 段落示意:蚂蚁金服务设计平台
+ ant.design,用最小的工作量,无缝接入蚂蚁金服生态,
+ 提供跨越设计与开发的体验解决方案。
+
+
+
+ );
+ const extraContent = (
+
+
+
+ );
+ const nullData: Partial = {};
+ return (
+
+
+
>
+ rowKey="id"
+ loading={loading}
+ grid={{
+ gutter: 16,
+ xs: 1,
+ sm: 2,
+ md: 3,
+ lg: 3,
+ xl: 4,
+ xxl: 4,
+ }}
+ dataSource={[nullData, ...list]}
+ renderItem={(item) => {
+ if (item?.id) {
+ return (
+
+ 操作一,
+ 操作二 ,
+ ]}
+ >
+
+ }
+ title={{item.title} }
+ description={
+
+ {item.description}
+
+ }
+ />
+
+
+ );
+ }
+ return (
+
+
+ 新增产品
+
+
+ );
+ }}
+ />
+
+
+ );
+};
+export default CardList;
diff --git a/src/pages/list/card-list/service.ts b/src/pages/list/card-list/service.ts
new file mode 100644
index 00000000..8095b5f0
--- /dev/null
+++ b/src/pages/list/card-list/service.ts
@@ -0,0 +1,10 @@
+import { request } from '@umijs/max';
+import type { CardListItemDataType } from './data.d';
+
+export async function queryFakeList(params: {
+ count: number;
+}): Promise<{ data: { list: CardListItemDataType[] } }> {
+ return request('/api/card_fake_list', {
+ params,
+ });
+}
diff --git a/src/pages/list/card-list/style.style.ts b/src/pages/list/card-list/style.style.ts
new file mode 100644
index 00000000..69bbff47
--- /dev/null
+++ b/src/pages/list/card-list/style.style.ts
@@ -0,0 +1,89 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ card: {
+ '.ant-card-meta-title': {
+ marginBottom: '12px',
+ '& > a': {
+ display: 'inline-block',
+ maxWidth: '100%',
+ color: token.colorTextHeading,
+ },
+ },
+ '.ant-card-body:hover': {
+ '.ant-card-meta-title > a': {
+ color: token.colorPrimary,
+ },
+ },
+ },
+ item: {
+ height: '64px',
+ },
+ cardList: {
+ '.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
+ },
+ extraImg: {
+ width: '155px',
+ marginTop: '-20px',
+ textAlign: 'center',
+ img: { width: '100%' },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ display: 'none',
+ },
+ },
+ newButton: {
+ width: '100%',
+ height: '201px',
+ color: token.colorTextSecondary,
+ backgroundColor: token.colorBgContainer,
+ borderColor: token.colorBorder,
+ },
+ cardAvatar: {
+ width: '48px',
+ height: '48px',
+ borderRadius: '48px',
+ },
+ cardDescription: {
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ wordBreak: 'break-all',
+ },
+ pageHeaderContent: {
+ position: 'relative',
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ paddingBottom: '30px',
+ },
+ },
+ contentLink: {
+ marginTop: '16px',
+ a: {
+ marginRight: '32px',
+ img: {
+ width: '24px',
+ },
+ },
+ img: { marginRight: '8px', verticalAlign: 'middle' },
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ a: {
+ marginRight: '16px',
+ },
+ },
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ position: 'absolute',
+ bottom: '-4px',
+ left: '0',
+ width: '1000px',
+ a: {
+ marginRight: '16px',
+ },
+ img: {
+ marginRight: '4px',
+ },
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/card-list/utils/utils.style.ts b/src/pages/list/card-list/utils/utils.style.ts
new file mode 100644
index 00000000..0ad5e64a
--- /dev/null
+++ b/src/pages/list/card-list/utils/utils.style.ts
@@ -0,0 +1,6 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(() => {
+ return {};
+});
+export default useStyles;
diff --git a/src/pages/list/mock/index.ts b/src/pages/list/mock/index.ts
new file mode 100644
index 00000000..0fb50b14
--- /dev/null
+++ b/src/pages/list/mock/index.ts
@@ -0,0 +1,6 @@
+import type { DefaultOptionType } from 'antd/es/select';
+
+export const categoryOptions: DefaultOptionType[] = Array.from({ length: 12 }).map((_, index) => ({
+ value: `cat${index + 1}`,
+ label: `类目${index + 1}`,
+}));
diff --git a/src/pages/list/search/applications/_mock.ts b/src/pages/list/search/applications/_mock.ts
new file mode 100644
index 00000000..4c39b899
--- /dev/null
+++ b/src/pages/list/search/applications/_mock.ts
@@ -0,0 +1,124 @@
+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 = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+];
+
+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: any = req.query;
+
+ const count = params.count * 1 || 20;
+
+ const result = fakeList(count);
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+export default {
+ 'GET /api/fake_list': getFakeList,
+};
diff --git a/src/pages/list/search/applications/components/StandardFormRow/index.style.ts b/src/pages/list/search/applications/components/StandardFormRow/index.style.ts
new file mode 100644
index 00000000..89156097
--- /dev/null
+++ b/src/pages/list/search/applications/components/StandardFormRow/index.style.ts
@@ -0,0 +1,60 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ standardFormRow: {
+ display: 'flex',
+ marginBottom: '16px',
+ paddingBottom: '16px',
+ borderBottom: `1px dashed ${token.colorSplit}`,
+ '.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
+ '.ant-form-item-label, .ant-legacy-form-item-label': {
+ label: {
+ marginRight: '0',
+ color: token.colorText,
+ },
+ },
+ '.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
+ { padding: '0', lineHeight: '32px' },
+ },
+ label: {
+ flex: '0 0 auto',
+ marginRight: '24px',
+ color: token.colorTextHeading,
+ fontSize: token.fontSize,
+ textAlign: 'right',
+ '& > span': {
+ display: 'inline-block',
+ height: '32px',
+ lineHeight: '32px',
+ '&::after': {
+ content: "':'",
+ },
+ },
+ },
+ content: {
+ flex: '1 1 0',
+ '.ant-form-item, .ant-legacy-form-item': {
+ '&:last-child': {
+ marginRight: '0',
+ },
+ },
+ },
+ standardFormRowLast: {
+ marginBottom: '0',
+ paddingBottom: '0',
+ border: 'none',
+ },
+ standardFormRowBlock: {
+ '.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
+ { display: 'block' },
+ },
+ standardFormRowGrid: {
+ '.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
+ { display: 'block' },
+ '.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/applications/components/StandardFormRow/index.tsx b/src/pages/list/search/applications/components/StandardFormRow/index.tsx
new file mode 100644
index 00000000..f2ed2175
--- /dev/null
+++ b/src/pages/list/search/applications/components/StandardFormRow/index.tsx
@@ -0,0 +1,38 @@
+import classNames from 'classnames';
+import React from 'react';
+import useStyles from './index.style';
+
+type StandardFormRowProps = {
+ title?: string;
+ last?: boolean;
+ block?: boolean;
+ grid?: boolean;
+ style?: React.CSSProperties;
+ children?: React.ReactNode;
+};
+const StandardFormRow: React.FC = ({
+ title,
+ children,
+ last,
+ block,
+ grid,
+ ...rest
+}) => {
+ const { styles } = useStyles();
+ const cls = classNames(styles.standardFormRow, {
+ [styles.standardFormRowBlock]: block,
+ [styles.standardFormRowLast]: last,
+ [styles.standardFormRowGrid]: grid,
+ });
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
{children}
+
+ );
+};
+export default StandardFormRow;
diff --git a/src/pages/list/search/applications/components/TagSelect/index.style.ts b/src/pages/list/search/applications/components/TagSelect/index.style.ts
new file mode 100644
index 00000000..d198ab7a
--- /dev/null
+++ b/src/pages/list/search/applications/components/TagSelect/index.style.ts
@@ -0,0 +1,35 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ tagSelect: {
+ position: 'relative',
+ maxHeight: '32px',
+ marginLeft: '-8px',
+ overflow: 'hidden',
+ lineHeight: '32px',
+ transition: 'all 0.3s',
+ userSelect: 'none',
+ '.ant-tag': {
+ marginRight: '24px',
+ padding: '0 8px',
+ fontSize: token.fontSize,
+ },
+ },
+ trigger: {
+ position: 'absolute',
+ top: '0',
+ right: '0',
+ 'span.anticon': { fontSize: '12px' },
+ },
+ expanded: {
+ maxHeight: '200px',
+ transition: 'all 0.3s',
+ },
+ hasExpandTag: {
+ paddingRight: '50px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/applications/components/TagSelect/index.tsx b/src/pages/list/search/applications/components/TagSelect/index.tsx
new file mode 100644
index 00000000..80107cd2
--- /dev/null
+++ b/src/pages/list/search/applications/components/TagSelect/index.tsx
@@ -0,0 +1,160 @@
+import { DownOutlined, UpOutlined } from '@ant-design/icons';
+import { useMergedState } from '@rc-component/util';
+import { Tag } from 'antd';
+import classNames from 'classnames';
+import React, { type FC, useMemo, useState } from 'react';
+import useStyles from './index.style';
+
+const { CheckableTag } = Tag;
+export interface TagSelectOptionProps {
+ value: string | number;
+ style?: React.CSSProperties;
+ checked?: boolean;
+ onChange?: (value: string | number, state: boolean) => void;
+ children?: React.ReactNode;
+}
+const TagSelectOption: React.FC & {
+ isTagSelectOption: boolean;
+} = ({ children, checked, onChange, value }) => (
+ onChange?.(value, state)}
+ >
+ {children}
+
+);
+
+TagSelectOption.isTagSelectOption = true;
+
+type TagSelectOptionElement = React.ReactElement<
+ TagSelectOptionProps,
+ typeof TagSelectOption
+>;
+
+export interface TagSelectProps {
+ onChange?: (value: (string | number)[]) => void;
+ expandable?: boolean;
+ value?: (string | number)[];
+ defaultValue?: (string | number)[];
+ style?: React.CSSProperties;
+ hideCheckAll?: boolean;
+ actionsText?: {
+ expandText?: React.ReactNode;
+ collapseText?: React.ReactNode;
+ selectAllText?: React.ReactNode;
+ };
+ className?: string;
+ Option?: TagSelectOptionProps;
+ children?: TagSelectOptionElement | TagSelectOptionElement[];
+}
+const TagSelect: FC & {
+ Option: typeof TagSelectOption;
+} = (props) => {
+ const { styles } = useStyles();
+ const {
+ children,
+ hideCheckAll = false,
+ className,
+ style,
+ expandable,
+ actionsText = {},
+ } = props;
+ const [expand, setExpand] = useState(false);
+
+ const [value, setValue] = useMergedState<(string | number)[]>(
+ props.defaultValue || [],
+ {
+ value: props.value,
+ defaultValue: props.defaultValue,
+ onChange: props.onChange,
+ },
+ );
+
+ const isTagSelectOption = (node: TagSelectOptionElement) =>
+ node?.type &&
+ (node.type.isTagSelectOption ||
+ node.type.displayName === 'TagSelectOption');
+
+ // Memoize all tags to avoid recalculating on every render
+ const allTags = useMemo(() => {
+ const childrenArray = React.Children.toArray(
+ children,
+ ) as TagSelectOptionElement[];
+ return childrenArray
+ .filter((child) => isTagSelectOption(child))
+ .map((child) => child.props.value);
+ }, [children]);
+
+ // Use Set for O(1) lookups
+ const valueSet = useMemo(() => new Set(value || []), [value]);
+
+ const onSelectAll = (checked: boolean) => {
+ setValue(checked ? [...allTags] : []);
+ };
+ const handleTagChange = (tag: string | number, checked: boolean) => {
+ const checkedTags = new Set(value || []);
+ if (checked) {
+ checkedTags.add(tag);
+ } else {
+ checkedTags.delete(tag);
+ }
+ setValue([...checkedTags]);
+ };
+ const checkedAll = allTags.length === value?.length && allTags.length > 0;
+ const {
+ expandText = '展开',
+ collapseText = '收起',
+ selectAllText = '全部',
+ } = actionsText;
+ const cls = classNames(styles.tagSelect, className, {
+ [styles.hasExpandTag]: expandable,
+ [styles.expanded]: expand,
+ });
+ return (
+
+ );
+};
+TagSelect.Option = TagSelectOption;
+export default TagSelect;
diff --git a/src/pages/list/search/applications/data.d.ts b/src/pages/list/search/applications/data.d.ts
new file mode 100644
index 00000000..f39b0a60
--- /dev/null
+++ b/src/pages/list/search/applications/data.d.ts
@@ -0,0 +1,33 @@
+export type Member = {
+ avatar: string;
+ name: string;
+ id: string;
+};
+
+export interface Params {
+ count: number;
+}
+
+export interface 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[];
+}
diff --git a/src/pages/list/search/applications/index.tsx b/src/pages/list/search/applications/index.tsx
new file mode 100644
index 00000000..4b69ccad
--- /dev/null
+++ b/src/pages/list/search/applications/index.tsx
@@ -0,0 +1,239 @@
+import {
+ DownloadOutlined,
+ EditOutlined,
+ EllipsisOutlined,
+ ShareAltOutlined,
+} from '@ant-design/icons';
+import { useRequest } from '@umijs/max';
+import {
+ Avatar,
+ Card,
+ Col,
+ Dropdown,
+ Form,
+ List,
+ Row,
+ Select,
+ Tooltip,
+} from 'antd';
+import numeral from 'numeral';
+import type { FC } from 'react';
+import React from 'react';
+import { categoryOptions } from '../../mock';
+import StandardFormRow from './components/StandardFormRow';
+import TagSelect from './components/TagSelect';
+import type { ListItemDataType } from './data.d';
+import { queryFakeList } from './service';
+import useStyles from './style.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 = (
+
+ {Math.floor(val / 10000)}
+
+ 万
+
+
+ );
+ }
+ return result;
+}
+const formItemLayout = {
+ wrapperCol: {
+ xs: {
+ span: 24,
+ },
+ sm: {
+ span: 16,
+ },
+ },
+};
+const CardInfo: React.FC<{
+ activeUser: React.ReactNode;
+ newUser: React.ReactNode;
+}> = ({ activeUser, newUser }) => {
+ const { styles } = useStyles();
+ return (
+
+ );
+};
+export const Applications: FC> = () => {
+ const { styles } = useStyles();
+ const { data, loading, run } = useRequest((values: any) => {
+ console.log('form data', values);
+ return queryFakeList({
+ count: 8,
+ });
+ });
+
+ const list = data?.list || [];
+
+ return (
+
+
+
+
+
+
+ rowKey="id"
+ grid={{
+ gutter: 16,
+ xs: 1,
+ sm: 2,
+ md: 3,
+ lg: 3,
+ xl: 4,
+ xxl: 4,
+ }}
+ loading={loading}
+ dataSource={list}
+ renderItem={(item) => (
+
+
+
+ ,
+
+
+ ,
+
+
+ ,
+
+
+ ,
+ ]}
+ >
+ }
+ title={item.title}
+ />
+
+
+
+
+
+ )}
+ />
+
+ );
+};
+export default Applications;
diff --git a/src/pages/list/search/applications/service.ts b/src/pages/list/search/applications/service.ts
new file mode 100644
index 00000000..c90e68c8
--- /dev/null
+++ b/src/pages/list/search/applications/service.ts
@@ -0,0 +1,10 @@
+import { request } from '@umijs/max';
+import type { ListItemDataType, Params } from './data.d';
+
+export async function queryFakeList(
+ params: Params,
+): Promise<{ data: { list: ListItemDataType[] } }> {
+ return request('/api/fake_list', {
+ params,
+ });
+}
diff --git a/src/pages/list/search/applications/style.style.ts b/src/pages/list/search/applications/style.style.ts
new file mode 100644
index 00000000..f304d857
--- /dev/null
+++ b/src/pages/list/search/applications/style.style.ts
@@ -0,0 +1,42 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ filterCardList: {
+ '.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;
diff --git a/src/pages/list/search/applications/utils/utils.style.ts b/src/pages/list/search/applications/utils/utils.style.ts
new file mode 100644
index 00000000..0ad5e64a
--- /dev/null
+++ b/src/pages/list/search/applications/utils/utils.style.ts
@@ -0,0 +1,6 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(() => {
+ return {};
+});
+export default useStyles;
diff --git a/src/pages/list/search/articles/_mock.ts b/src/pages/list/search/articles/_mock.ts
new file mode 100644
index 00000000..e988dc15
--- /dev/null
+++ b/src/pages/list/search/articles/_mock.ts
@@ -0,0 +1,124 @@
+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 = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+];
+
+function fakeList(count: number): ListItemDataType[] {
+ const list = [];
+ for (let i = 0; i < count; i += 1) {
+ list.push({
+ id: `fake-list-${Math.random().toString(36).slice(2, 6)}${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: any = req.query;
+
+ const count = params.count * 1 || 20;
+
+ const result = fakeList(count);
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+export default {
+ 'GET /api/fake_list': getFakeList,
+};
diff --git a/src/pages/list/search/articles/components/ArticleListContent/index.style.ts b/src/pages/list/search/articles/components/ArticleListContent/index.style.ts
new file mode 100644
index 00000000..c993635f
--- /dev/null
+++ b/src/pages/list/search/articles/components/ArticleListContent/index.style.ts
@@ -0,0 +1,29 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ description: {
+ maxWidth: '720px',
+ lineHeight: '22px',
+ },
+ extra: {
+ marginTop: '16px',
+ color: token.colorTextSecondary,
+ lineHeight: '22px',
+ '& > em': {
+ marginLeft: '16px',
+ color: token.colorTextDisabled,
+ fontStyle: 'normal',
+ },
+ [`@media screen and (max-width: ${token.screenXS}px)`]: {
+ '& > em': {
+ display: 'block',
+ marginTop: '8px',
+ marginLeft: '0',
+ },
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/articles/components/ArticleListContent/index.tsx b/src/pages/list/search/articles/components/ArticleListContent/index.tsx
new file mode 100644
index 00000000..0e5f60c0
--- /dev/null
+++ b/src/pages/list/search/articles/components/ArticleListContent/index.tsx
@@ -0,0 +1,30 @@
+import { Avatar } from 'antd';
+import dayjs from 'dayjs';
+import React from 'react';
+import useStyles from './index.style';
+
+type ArticleListContentProps = {
+ data: {
+ content: React.ReactNode;
+ updatedAt: number;
+ avatar: string;
+ owner: string;
+ href: string;
+ };
+};
+const ArticleListContent: React.FC = ({
+ data: { content, updatedAt, avatar, owner, href },
+}) => {
+ const { styles } = useStyles();
+ return (
+
+
{content}
+
+
+
{owner} 发布在
{href}
+
{dayjs(updatedAt).format('YYYY-MM-DD HH:mm')}
+
+
+ );
+};
+export default ArticleListContent;
diff --git a/src/pages/list/search/articles/components/StandardFormRow/index.style.ts b/src/pages/list/search/articles/components/StandardFormRow/index.style.ts
new file mode 100644
index 00000000..7b5f569d
--- /dev/null
+++ b/src/pages/list/search/articles/components/StandardFormRow/index.style.ts
@@ -0,0 +1,62 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ standardFormRow: {
+ display: 'flex',
+ width: '100%',
+ marginBottom: '16px',
+ paddingBottom: '16px',
+ borderBottom: `1px dashed ${token.colorSplit}`,
+ '.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
+ '.ant-form-item-label, .ant-legacy-form-item-label': {
+ label: {
+ marginRight: '0',
+ color: token.colorText,
+ },
+ },
+ '.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
+ { padding: '0', lineHeight: '32px' },
+ },
+ label: {
+ flex: '0 0 auto',
+ marginRight: '24px',
+ color: token.colorTextHeading,
+ fontSize: token.fontSize,
+ textAlign: 'right',
+ '& > span': {
+ display: 'inline-block',
+ height: '32px',
+ lineHeight: '32px',
+ '&::after': {
+ content: "':'",
+ },
+ },
+ },
+ content: {
+ flex: '1 1 0',
+ '.ant-form-item, .ant-legacy-form-item': {
+ '&:last-child': {
+ display: 'block',
+ marginRight: '0',
+ },
+ },
+ },
+ standardFormRowLast: {
+ marginBottom: '0',
+ paddingBottom: '0',
+ border: 'none',
+ },
+ standardFormRowBlock: {
+ '.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
+ { display: 'block' },
+ },
+ standardFormRowGrid: {
+ '.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
+ { display: 'block' },
+ '.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/articles/components/StandardFormRow/index.tsx b/src/pages/list/search/articles/components/StandardFormRow/index.tsx
new file mode 100644
index 00000000..01236990
--- /dev/null
+++ b/src/pages/list/search/articles/components/StandardFormRow/index.tsx
@@ -0,0 +1,38 @@
+import classNames from 'classnames';
+import React from 'react';
+import useStyles from './index.style';
+
+type StandardFormRowProps = {
+ title?: string;
+ last?: boolean;
+ block?: boolean;
+ grid?: boolean;
+ children?: React.ReactNode;
+ style?: React.CSSProperties;
+};
+const StandardFormRow: React.FC = ({
+ title,
+ children,
+ last,
+ block,
+ grid,
+ ...rest
+}) => {
+ const { styles } = useStyles();
+ const cls = classNames(styles.standardFormRow, {
+ [styles.standardFormRowBlock]: block,
+ [styles.standardFormRowLast]: last,
+ [styles.standardFormRowGrid]: grid,
+ });
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
{children}
+
+ );
+};
+export default StandardFormRow;
diff --git a/src/pages/list/search/articles/components/TagSelect/index.style.ts b/src/pages/list/search/articles/components/TagSelect/index.style.ts
new file mode 100644
index 00000000..d198ab7a
--- /dev/null
+++ b/src/pages/list/search/articles/components/TagSelect/index.style.ts
@@ -0,0 +1,35 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ tagSelect: {
+ position: 'relative',
+ maxHeight: '32px',
+ marginLeft: '-8px',
+ overflow: 'hidden',
+ lineHeight: '32px',
+ transition: 'all 0.3s',
+ userSelect: 'none',
+ '.ant-tag': {
+ marginRight: '24px',
+ padding: '0 8px',
+ fontSize: token.fontSize,
+ },
+ },
+ trigger: {
+ position: 'absolute',
+ top: '0',
+ right: '0',
+ 'span.anticon': { fontSize: '12px' },
+ },
+ expanded: {
+ maxHeight: '200px',
+ transition: 'all 0.3s',
+ },
+ hasExpandTag: {
+ paddingRight: '50px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/articles/components/TagSelect/index.tsx b/src/pages/list/search/articles/components/TagSelect/index.tsx
new file mode 100644
index 00000000..80107cd2
--- /dev/null
+++ b/src/pages/list/search/articles/components/TagSelect/index.tsx
@@ -0,0 +1,160 @@
+import { DownOutlined, UpOutlined } from '@ant-design/icons';
+import { useMergedState } from '@rc-component/util';
+import { Tag } from 'antd';
+import classNames from 'classnames';
+import React, { type FC, useMemo, useState } from 'react';
+import useStyles from './index.style';
+
+const { CheckableTag } = Tag;
+export interface TagSelectOptionProps {
+ value: string | number;
+ style?: React.CSSProperties;
+ checked?: boolean;
+ onChange?: (value: string | number, state: boolean) => void;
+ children?: React.ReactNode;
+}
+const TagSelectOption: React.FC & {
+ isTagSelectOption: boolean;
+} = ({ children, checked, onChange, value }) => (
+ onChange?.(value, state)}
+ >
+ {children}
+
+);
+
+TagSelectOption.isTagSelectOption = true;
+
+type TagSelectOptionElement = React.ReactElement<
+ TagSelectOptionProps,
+ typeof TagSelectOption
+>;
+
+export interface TagSelectProps {
+ onChange?: (value: (string | number)[]) => void;
+ expandable?: boolean;
+ value?: (string | number)[];
+ defaultValue?: (string | number)[];
+ style?: React.CSSProperties;
+ hideCheckAll?: boolean;
+ actionsText?: {
+ expandText?: React.ReactNode;
+ collapseText?: React.ReactNode;
+ selectAllText?: React.ReactNode;
+ };
+ className?: string;
+ Option?: TagSelectOptionProps;
+ children?: TagSelectOptionElement | TagSelectOptionElement[];
+}
+const TagSelect: FC & {
+ Option: typeof TagSelectOption;
+} = (props) => {
+ const { styles } = useStyles();
+ const {
+ children,
+ hideCheckAll = false,
+ className,
+ style,
+ expandable,
+ actionsText = {},
+ } = props;
+ const [expand, setExpand] = useState(false);
+
+ const [value, setValue] = useMergedState<(string | number)[]>(
+ props.defaultValue || [],
+ {
+ value: props.value,
+ defaultValue: props.defaultValue,
+ onChange: props.onChange,
+ },
+ );
+
+ const isTagSelectOption = (node: TagSelectOptionElement) =>
+ node?.type &&
+ (node.type.isTagSelectOption ||
+ node.type.displayName === 'TagSelectOption');
+
+ // Memoize all tags to avoid recalculating on every render
+ const allTags = useMemo(() => {
+ const childrenArray = React.Children.toArray(
+ children,
+ ) as TagSelectOptionElement[];
+ return childrenArray
+ .filter((child) => isTagSelectOption(child))
+ .map((child) => child.props.value);
+ }, [children]);
+
+ // Use Set for O(1) lookups
+ const valueSet = useMemo(() => new Set(value || []), [value]);
+
+ const onSelectAll = (checked: boolean) => {
+ setValue(checked ? [...allTags] : []);
+ };
+ const handleTagChange = (tag: string | number, checked: boolean) => {
+ const checkedTags = new Set(value || []);
+ if (checked) {
+ checkedTags.add(tag);
+ } else {
+ checkedTags.delete(tag);
+ }
+ setValue([...checkedTags]);
+ };
+ const checkedAll = allTags.length === value?.length && allTags.length > 0;
+ const {
+ expandText = '展开',
+ collapseText = '收起',
+ selectAllText = '全部',
+ } = actionsText;
+ const cls = classNames(styles.tagSelect, className, {
+ [styles.hasExpandTag]: expandable,
+ [styles.expanded]: expand,
+ });
+ return (
+
+ );
+};
+TagSelect.Option = TagSelectOption;
+export default TagSelect;
diff --git a/src/pages/list/search/articles/data.d.ts b/src/pages/list/search/articles/data.d.ts
new file mode 100644
index 00000000..0a4597c9
--- /dev/null
+++ b/src/pages/list/search/articles/data.d.ts
@@ -0,0 +1,32 @@
+export type Member = {
+ avatar: string;
+ name: string;
+ id: string;
+};
+
+export interface Params {
+ count: number;
+}
+export interface 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[];
+}
diff --git a/src/pages/list/search/articles/index.tsx b/src/pages/list/search/articles/index.tsx
new file mode 100644
index 00000000..df44ede0
--- /dev/null
+++ b/src/pages/list/search/articles/index.tsx
@@ -0,0 +1,261 @@
+import {
+ LikeOutlined,
+ LoadingOutlined,
+ MessageOutlined,
+ StarOutlined,
+} from '@ant-design/icons';
+import { useRequest } from '@umijs/max';
+import { Button, Card, Col, Form, List, Row, Select, Tag } from 'antd';
+import type { DefaultOptionType } from 'antd/es/select';
+import type { FC } from 'react';
+import React, { useMemo } from 'react';
+import { categoryOptions } from '../../mock';
+import ArticleListContent from './components/ArticleListContent';
+import StandardFormRow from './components/StandardFormRow';
+import TagSelect from './components/TagSelect';
+import type { ListItemDataType } from './data.d';
+import { queryFakeList } from './service';
+import useStyles from './style.style';
+
+const FormItem = Form.Item;
+
+const pageSize = 5;
+
+const IconText: React.FC<{
+ type: string;
+ text: React.ReactNode;
+}> = ({ type, text }) => {
+ switch (type) {
+ case 'star-o':
+ return (
+
+
+ {text}
+
+ );
+ case 'like-o':
+ return (
+
+
+ {text}
+
+ );
+ case 'message':
+ return (
+
+
+ {text}
+
+ );
+ default:
+ return null;
+ }
+};
+
+const Articles: FC = () => {
+ const [form] = Form.useForm();
+
+ const { styles } = useStyles();
+
+ const { data, reload, loading, loadMore, loadingMore } = useRequest(
+ () => {
+ return queryFakeList({
+ count: pageSize,
+ });
+ },
+ {
+ loadMore: true,
+ },
+ );
+
+ const list = data?.list || [];
+
+ const setOwner = () => {
+ form.setFieldsValue({
+ owner: ['wzj'],
+ });
+ };
+
+ const owners = [
+ {
+ id: 'wzj',
+ name: '我自己',
+ },
+ {
+ id: 'wjh',
+ name: '吴家豪',
+ },
+ {
+ id: 'zxx',
+ name: '周星星',
+ },
+ {
+ id: 'zly',
+ name: '赵丽颖',
+ },
+ {
+ id: 'ym',
+ name: '姚明',
+ },
+ ];
+
+ const formItemLayout = {
+ wrapperCol: {
+ xs: { span: 24 },
+ sm: { span: 24 },
+ md: { span: 12 },
+ },
+ };
+
+ const loadMoreDom = list.length > 0 && (
+
+
+ {loadingMore ? (
+
+ 加载中...
+
+ ) : (
+ '加载更多'
+ )}
+
+
+ );
+
+ const ownerOptions = useMemo(
+ () =>
+ owners.map((item) => ({
+ label: item.name,
+ value: item.id,
+ })),
+ [],
+ );
+
+ return (
+ <>
+
+
+
+
+
+ size="large"
+ loading={loading}
+ rowKey="id"
+ itemLayout="vertical"
+ loadMore={loadMoreDom}
+ dataSource={list}
+ renderItem={(item) => (
+ ,
+ ,
+ ,
+ ]}
+ extra={
}
+ >
+
+ {item.title}
+
+ }
+ description={
+
+ Ant Design
+ 设计语言
+ 蚂蚁金服
+
+ }
+ />
+
+
+ )}
+ />
+
+ >
+ );
+};
+
+export default Articles;
diff --git a/src/pages/list/search/articles/service.ts b/src/pages/list/search/articles/service.ts
new file mode 100644
index 00000000..c90e68c8
--- /dev/null
+++ b/src/pages/list/search/articles/service.ts
@@ -0,0 +1,10 @@
+import { request } from '@umijs/max';
+import type { ListItemDataType, Params } from './data.d';
+
+export async function queryFakeList(
+ params: Params,
+): Promise<{ data: { list: ListItemDataType[] } }> {
+ return request('/api/fake_list', {
+ params,
+ });
+}
diff --git a/src/pages/list/search/articles/style.style.ts b/src/pages/list/search/articles/style.style.ts
new file mode 100644
index 00000000..5d8db5b4
--- /dev/null
+++ b/src/pages/list/search/articles/style.style.ts
@@ -0,0 +1,30 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ listItemMetaTitle: {
+ color: token.colorTextHeading,
+ },
+ listItemExtra: {
+ width: '272px',
+ height: '1px',
+ [`@media screen and (max-width: ${token.screenLG}px)`]: {
+ width: '0',
+ height: '1px',
+ },
+ },
+ selfTrigger: {
+ marginLeft: '12px',
+ [`@media screen and (max-width: ${token.screenXS}px)`]: {
+ display: 'block',
+ marginLeft: '0',
+ },
+ [`@media screen and (max-width: ${token.screenMD}px)`]: {
+ display: 'block',
+ marginLeft: '0',
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/index.tsx b/src/pages/list/search/index.tsx
new file mode 100644
index 00000000..43a5075e
--- /dev/null
+++ b/src/pages/list/search/index.tsx
@@ -0,0 +1,85 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { history, Outlet, useLocation, useMatch } from '@umijs/max';
+import { Input } from 'antd';
+import type { FC } from 'react';
+
+type SearchProps = {
+ children?: React.ReactNode;
+};
+
+const tabList = [
+ {
+ key: 'articles',
+ tab: '文章',
+ },
+ {
+ key: 'projects',
+ tab: '项目',
+ },
+ {
+ key: 'applications',
+ tab: '应用',
+ },
+];
+
+const Search: FC = () => {
+ const location = useLocation();
+ const match = useMatch(location.pathname);
+ const handleTabChange = (key: string) => {
+ const url =
+ match?.pathname === '/'
+ ? ''
+ : match?.pathname.substring(0, match.pathname.lastIndexOf('/'));
+ switch (key) {
+ case 'articles':
+ history.push(`${url}/articles`);
+ break;
+ case 'applications':
+ history.push(`${url}/applications`);
+ break;
+ case 'projects':
+ history.push(`${url}/projects`);
+ break;
+ default:
+ break;
+ }
+ };
+
+ const handleFormSubmit = (value: string) => {
+ // eslint-disable-next-line no-console
+ console.log(value);
+ };
+
+ const getTabKey = () => {
+ const tabKey = location.pathname.substring(
+ location.pathname.lastIndexOf('/') + 1,
+ );
+ if (tabKey && tabKey !== '/') {
+ return tabKey;
+ }
+ return 'articles';
+ };
+
+ return (
+
+
+
+ }
+ tabList={tabList}
+ tabActiveKey={getTabKey()}
+ onTabChange={handleTabChange}
+ >
+
+
+ );
+};
+
+export default Search;
diff --git a/src/pages/list/search/projects/_mock.ts b/src/pages/list/search/projects/_mock.ts
new file mode 100644
index 00000000..4c39b899
--- /dev/null
+++ b/src/pages/list/search/projects/_mock.ts
@@ -0,0 +1,124 @@
+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 = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+];
+
+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: any = req.query;
+
+ const count = params.count * 1 || 20;
+
+ const result = fakeList(count);
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+export default {
+ 'GET /api/fake_list': getFakeList,
+};
diff --git a/src/pages/list/search/projects/components/AvatarList/index.style.ts b/src/pages/list/search/projects/components/AvatarList/index.style.ts
new file mode 100644
index 00000000..d601a1d8
--- /dev/null
+++ b/src/pages/list/search/projects/components/AvatarList/index.style.ts
@@ -0,0 +1,41 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ avatarList: {
+ display: 'inline-block',
+ ul: { display: 'inline-block', marginLeft: '8px', fontSize: '0' },
+ },
+ avatarItem: {
+ display: 'inline-block',
+ width: token.controlHeight,
+ height: token.controlHeight,
+ marginLeft: '-8px',
+ fontSize: token.fontSize,
+ '.ant-avatar': { border: `1px solid ${token.colorBorder}` },
+ },
+ avatarItemLarge: {
+ width: token.controlHeightLG,
+ height: token.controlHeightLG,
+ },
+ avatarItemSmall: {
+ width: token.controlHeightSM,
+ height: token.controlHeightSM,
+ },
+ avatarItemMini: {
+ width: '20px',
+ height: '20px',
+ '.ant-avatar': {
+ width: '20px',
+ height: '20px',
+ lineHeight: '20px',
+ '.ant-avatar-string': {
+ fontSize: '12px',
+ lineHeight: '18px',
+ },
+ },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/projects/components/AvatarList/index.tsx b/src/pages/list/search/projects/components/AvatarList/index.tsx
new file mode 100644
index 00000000..91d01e4b
--- /dev/null
+++ b/src/pages/list/search/projects/components/AvatarList/index.tsx
@@ -0,0 +1,91 @@
+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;
+ size?: SizeType;
+ maxLength?: number;
+ excessItemsStyle?: React.CSSProperties;
+ style?: React.CSSProperties;
+ children:
+ | React.ReactElement
+ | React.ReactElement[];
+};
+const avatarSizeToClassName = (size: SizeType | 'mini', styles: any) =>
+ classNames(styles.avatarItem, {
+ [styles.avatarItemLarge]: size === 'large',
+ [styles.avatarItemSmall]: size === 'small',
+ [styles.avatarItemMini]: size === 'mini',
+ });
+
+const Item: React.FC = ({
+ src,
+ size,
+ tips,
+ onClick = () => {},
+}) => {
+ const { styles } = useStyles();
+
+ const cls = avatarSizeToClassName(size || 'default', styles);
+
+ return (
+
+ {tips ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
+const AvatarList: React.FC & {
+ 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[];
+
+ const childrenWithProps = childrenArray.slice(0, numToShow).map((child) =>
+ React.cloneElement(child, {
+ size,
+ }),
+ );
+ if (numToShow < numOfChildren) {
+ const cls = avatarSizeToClassName(size || 'default', styles);
+ childrenWithProps.push(
+
+ {`+${numOfChildren - maxLength}`}
+ ,
+ );
+ }
+ return (
+
+ );
+};
+AvatarList.Item = Item;
+export default AvatarList;
diff --git a/src/pages/list/search/projects/components/StandardFormRow/index.style.ts b/src/pages/list/search/projects/components/StandardFormRow/index.style.ts
new file mode 100644
index 00000000..7b5f569d
--- /dev/null
+++ b/src/pages/list/search/projects/components/StandardFormRow/index.style.ts
@@ -0,0 +1,62 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ standardFormRow: {
+ display: 'flex',
+ width: '100%',
+ marginBottom: '16px',
+ paddingBottom: '16px',
+ borderBottom: `1px dashed ${token.colorSplit}`,
+ '.ant-form-item, .ant-legacy-form-item': { marginRight: '24px' },
+ '.ant-form-item-label, .ant-legacy-form-item-label': {
+ label: {
+ marginRight: '0',
+ color: token.colorText,
+ },
+ },
+ '.ant-form-item-label, .ant-legacy-form-item-label, .ant-form-item-control, .ant-legacy-form-item-control':
+ { padding: '0', lineHeight: '32px' },
+ },
+ label: {
+ flex: '0 0 auto',
+ marginRight: '24px',
+ color: token.colorTextHeading,
+ fontSize: token.fontSize,
+ textAlign: 'right',
+ '& > span': {
+ display: 'inline-block',
+ height: '32px',
+ lineHeight: '32px',
+ '&::after': {
+ content: "':'",
+ },
+ },
+ },
+ content: {
+ flex: '1 1 0',
+ '.ant-form-item, .ant-legacy-form-item': {
+ '&:last-child': {
+ display: 'block',
+ marginRight: '0',
+ },
+ },
+ },
+ standardFormRowLast: {
+ marginBottom: '0',
+ paddingBottom: '0',
+ border: 'none',
+ },
+ standardFormRowBlock: {
+ '.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
+ { display: 'block' },
+ },
+ standardFormRowGrid: {
+ '.ant-form-item, .ant-legacy-form-item, div.ant-form-item-control-wrapper, div.ant-legacy-form-item-control-wrapper':
+ { display: 'block' },
+ '.ant-form-item-label, .ant-legacy-form-item-label': { float: 'left' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/projects/components/StandardFormRow/index.tsx b/src/pages/list/search/projects/components/StandardFormRow/index.tsx
new file mode 100644
index 00000000..01236990
--- /dev/null
+++ b/src/pages/list/search/projects/components/StandardFormRow/index.tsx
@@ -0,0 +1,38 @@
+import classNames from 'classnames';
+import React from 'react';
+import useStyles from './index.style';
+
+type StandardFormRowProps = {
+ title?: string;
+ last?: boolean;
+ block?: boolean;
+ grid?: boolean;
+ children?: React.ReactNode;
+ style?: React.CSSProperties;
+};
+const StandardFormRow: React.FC = ({
+ title,
+ children,
+ last,
+ block,
+ grid,
+ ...rest
+}) => {
+ const { styles } = useStyles();
+ const cls = classNames(styles.standardFormRow, {
+ [styles.standardFormRowBlock]: block,
+ [styles.standardFormRowLast]: last,
+ [styles.standardFormRowGrid]: grid,
+ });
+ return (
+
+ {title && (
+
+ {title}
+
+ )}
+
{children}
+
+ );
+};
+export default StandardFormRow;
diff --git a/src/pages/list/search/projects/components/TagSelect/index.style.ts b/src/pages/list/search/projects/components/TagSelect/index.style.ts
new file mode 100644
index 00000000..d198ab7a
--- /dev/null
+++ b/src/pages/list/search/projects/components/TagSelect/index.style.ts
@@ -0,0 +1,35 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ tagSelect: {
+ position: 'relative',
+ maxHeight: '32px',
+ marginLeft: '-8px',
+ overflow: 'hidden',
+ lineHeight: '32px',
+ transition: 'all 0.3s',
+ userSelect: 'none',
+ '.ant-tag': {
+ marginRight: '24px',
+ padding: '0 8px',
+ fontSize: token.fontSize,
+ },
+ },
+ trigger: {
+ position: 'absolute',
+ top: '0',
+ right: '0',
+ 'span.anticon': { fontSize: '12px' },
+ },
+ expanded: {
+ maxHeight: '200px',
+ transition: 'all 0.3s',
+ },
+ hasExpandTag: {
+ paddingRight: '50px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/list/search/projects/components/TagSelect/index.tsx b/src/pages/list/search/projects/components/TagSelect/index.tsx
new file mode 100644
index 00000000..80107cd2
--- /dev/null
+++ b/src/pages/list/search/projects/components/TagSelect/index.tsx
@@ -0,0 +1,160 @@
+import { DownOutlined, UpOutlined } from '@ant-design/icons';
+import { useMergedState } from '@rc-component/util';
+import { Tag } from 'antd';
+import classNames from 'classnames';
+import React, { type FC, useMemo, useState } from 'react';
+import useStyles from './index.style';
+
+const { CheckableTag } = Tag;
+export interface TagSelectOptionProps {
+ value: string | number;
+ style?: React.CSSProperties;
+ checked?: boolean;
+ onChange?: (value: string | number, state: boolean) => void;
+ children?: React.ReactNode;
+}
+const TagSelectOption: React.FC & {
+ isTagSelectOption: boolean;
+} = ({ children, checked, onChange, value }) => (
+ onChange?.(value, state)}
+ >
+ {children}
+
+);
+
+TagSelectOption.isTagSelectOption = true;
+
+type TagSelectOptionElement = React.ReactElement<
+ TagSelectOptionProps,
+ typeof TagSelectOption
+>;
+
+export interface TagSelectProps {
+ onChange?: (value: (string | number)[]) => void;
+ expandable?: boolean;
+ value?: (string | number)[];
+ defaultValue?: (string | number)[];
+ style?: React.CSSProperties;
+ hideCheckAll?: boolean;
+ actionsText?: {
+ expandText?: React.ReactNode;
+ collapseText?: React.ReactNode;
+ selectAllText?: React.ReactNode;
+ };
+ className?: string;
+ Option?: TagSelectOptionProps;
+ children?: TagSelectOptionElement | TagSelectOptionElement[];
+}
+const TagSelect: FC & {
+ Option: typeof TagSelectOption;
+} = (props) => {
+ const { styles } = useStyles();
+ const {
+ children,
+ hideCheckAll = false,
+ className,
+ style,
+ expandable,
+ actionsText = {},
+ } = props;
+ const [expand, setExpand] = useState(false);
+
+ const [value, setValue] = useMergedState<(string | number)[]>(
+ props.defaultValue || [],
+ {
+ value: props.value,
+ defaultValue: props.defaultValue,
+ onChange: props.onChange,
+ },
+ );
+
+ const isTagSelectOption = (node: TagSelectOptionElement) =>
+ node?.type &&
+ (node.type.isTagSelectOption ||
+ node.type.displayName === 'TagSelectOption');
+
+ // Memoize all tags to avoid recalculating on every render
+ const allTags = useMemo(() => {
+ const childrenArray = React.Children.toArray(
+ children,
+ ) as TagSelectOptionElement[];
+ return childrenArray
+ .filter((child) => isTagSelectOption(child))
+ .map((child) => child.props.value);
+ }, [children]);
+
+ // Use Set for O(1) lookups
+ const valueSet = useMemo(() => new Set(value || []), [value]);
+
+ const onSelectAll = (checked: boolean) => {
+ setValue(checked ? [...allTags] : []);
+ };
+ const handleTagChange = (tag: string | number, checked: boolean) => {
+ const checkedTags = new Set(value || []);
+ if (checked) {
+ checkedTags.add(tag);
+ } else {
+ checkedTags.delete(tag);
+ }
+ setValue([...checkedTags]);
+ };
+ const checkedAll = allTags.length === value?.length && allTags.length > 0;
+ const {
+ expandText = '展开',
+ collapseText = '收起',
+ selectAllText = '全部',
+ } = actionsText;
+ const cls = classNames(styles.tagSelect, className, {
+ [styles.hasExpandTag]: expandable,
+ [styles.expanded]: expand,
+ });
+ return (
+
+ );
+};
+TagSelect.Option = TagSelectOption;
+export default TagSelect;
diff --git a/src/pages/list/search/projects/data.d.ts b/src/pages/list/search/projects/data.d.ts
new file mode 100644
index 00000000..0a4597c9
--- /dev/null
+++ b/src/pages/list/search/projects/data.d.ts
@@ -0,0 +1,32 @@
+export type Member = {
+ avatar: string;
+ name: string;
+ id: string;
+};
+
+export interface Params {
+ count: number;
+}
+export interface 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[];
+}
diff --git a/src/pages/list/search/projects/index.tsx b/src/pages/list/search/projects/index.tsx
new file mode 100644
index 00000000..fa0401e7
--- /dev/null
+++ b/src/pages/list/search/projects/index.tsx
@@ -0,0 +1,176 @@
+import { useRequest } from '@umijs/max';
+import { Card, Col, Form, List, Row, Select, Typography } from 'antd';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+import type { FC } from 'react';
+import { categoryOptions } from '../../mock';
+import AvatarList from './components/AvatarList';
+import StandardFormRow from './components/StandardFormRow';
+import TagSelect from './components/TagSelect';
+import type { ListItemDataType } from './data.d';
+import { queryFakeList } from './service';
+import useStyles from './style.style';
+
+dayjs.extend(relativeTime);
+
+const FormItem = Form.Item;
+const { Paragraph } = Typography;
+const getKey = (id: string, index: number) => `${id}-${index}`;
+const Projects: FC = () => {
+ const { styles } = useStyles();
+ const { data, loading, run } = useRequest((values: any) => {
+ console.log('form data', values);
+ return queryFakeList({
+ count: 8,
+ });
+ });
+ const list = data?.list || [];
+ const cardList = list && (
+
+ rowKey="id"
+ loading={loading}
+ grid={{
+ gutter: 16,
+ xs: 1,
+ sm: 2,
+ md: 3,
+ lg: 3,
+ xl: 4,
+ xxl: 4,
+ }}
+ dataSource={list}
+ renderItem={(item) => (
+
+ }
+ >
+ {item.title}}
+ description={
+
+ {item.subDescription}
+
+ }
+ />
+
+
{dayjs(item.updatedAt).fromNow()}
+
+
+ {item.members.map((member, i) => (
+
+ ))}
+
+
+
+
+
+ )}
+ />
+ );
+ const formItemLayout = {
+ wrapperCol: {
+ xs: {
+ span: 24,
+ },
+ sm: {
+ span: 16,
+ },
+ },
+ };
+ return (
+
+ );
+};
+export default Projects;
diff --git a/src/pages/list/search/projects/service.ts b/src/pages/list/search/projects/service.ts
new file mode 100644
index 00000000..9f96cc6f
--- /dev/null
+++ b/src/pages/list/search/projects/service.ts
@@ -0,0 +1,10 @@
+import { request } from '@umijs/max';
+import type { ListItemDataType, Params } from './data';
+
+export async function queryFakeList(
+ params: Params,
+): Promise<{ data: { list: ListItemDataType[] } }> {
+ return request('/api/fake_list', {
+ params,
+ });
+}
diff --git a/src/pages/list/search/projects/style.style.ts b/src/pages/list/search/projects/style.style.ts
new file mode 100644
index 00000000..ca34a15b
--- /dev/null
+++ b/src/pages/list/search/projects/style.style.ts
@@ -0,0 +1,49 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ card: {
+ '.ant-card-meta-title': {
+ marginBottom: '4px',
+ '& > a': {
+ display: 'inline-block',
+ maxWidth: '100%',
+ color: token.colorTextHeading,
+ },
+ },
+ '.ant-card-meta-description': {
+ height: '44px',
+ overflow: 'hidden',
+ lineHeight: '22px',
+ },
+ '&:hover': {
+ '.ant-card-meta-title > a': {
+ color: token.colorPrimary,
+ },
+ },
+ },
+ cardItemContent: {
+ display: 'flex',
+ height: '20px',
+ marginTop: '16px',
+ marginBottom: '-4px',
+ lineHeight: '20px',
+ '& > span': {
+ flex: '1',
+ color: token.colorTextSecondary,
+ fontSize: '12px',
+ },
+ },
+ avatarList: {
+ flex: '0 1 auto',
+ },
+ cardList: {
+ marginTop: '24px',
+ },
+ coverCardList: {
+ '.ant-list .ant-list-item-content-single': { maxWidth: '100%' },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/profile/advanced/_mock.ts b/src/pages/profile/advanced/_mock.ts
new file mode 100644
index 00000000..cc13c2f2
--- /dev/null
+++ b/src/pages/profile/advanced/_mock.ts
@@ -0,0 +1,81 @@
+import type { Request, Response } from 'express';
+
+const advancedOperation1 = [
+ {
+ key: 'op1',
+ type: '订购关系生效',
+ name: '曲丽丽',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+ {
+ key: 'op2',
+ type: '财务复审',
+ name: '付小小',
+ status: 'reject',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '不通过原因',
+ },
+ {
+ key: 'op3',
+ type: '部门初审',
+ name: '周毛毛',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+ {
+ key: 'op4',
+ type: '提交订单',
+ name: '林东东',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '很棒',
+ },
+ {
+ key: 'op5',
+ type: '创建订单',
+ name: '汗牙牙',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+];
+
+const advancedOperation2 = [
+ {
+ key: 'op1',
+ type: '订购关系生效',
+ name: '曲丽丽',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+];
+
+const advancedOperation3 = [
+ {
+ key: 'op1',
+ type: '创建订单',
+ name: '汗牙牙',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+];
+
+function getProfileAdvancedData(_req: Request, res: Response) {
+ const result = {
+ data: {
+ advancedOperation1,
+ advancedOperation2,
+ advancedOperation3,
+ },
+ };
+ return res.json(result);
+}
+
+export default {
+ 'GET /api/profile/advanced': getProfileAdvancedData,
+};
diff --git a/src/pages/profile/advanced/data.d.ts b/src/pages/profile/advanced/data.d.ts
new file mode 100644
index 00000000..95f7d31e
--- /dev/null
+++ b/src/pages/profile/advanced/data.d.ts
@@ -0,0 +1,32 @@
+export type AdvancedOperation1 = {
+ key: string;
+ type: string;
+ name: string;
+ status: string;
+ updatedAt: string;
+ memo: string;
+};
+
+export type AdvancedOperation2 = {
+ key: string;
+ type: string;
+ name: string;
+ status: string;
+ updatedAt: string;
+ memo: string;
+};
+
+export type AdvancedOperation3 = {
+ key: string;
+ type: string;
+ name: string;
+ status: string;
+ updatedAt: string;
+ memo: string;
+};
+
+export interface AdvancedProfileData {
+ advancedOperation1?: AdvancedOperation1[];
+ advancedOperation2?: AdvancedOperation2[];
+ advancedOperation3?: AdvancedOperation3[];
+}
diff --git a/src/pages/profile/advanced/index.tsx b/src/pages/profile/advanced/index.tsx
new file mode 100644
index 00000000..c2833833
--- /dev/null
+++ b/src/pages/profile/advanced/index.tsx
@@ -0,0 +1,492 @@
+import {
+ DingdingOutlined,
+ DownOutlined,
+ EllipsisOutlined,
+ InfoCircleOutlined,
+} from '@ant-design/icons';
+import {
+ GridContent,
+ PageContainer,
+ RouteContext,
+} from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import {
+ Badge,
+ Button,
+ Card,
+ Descriptions,
+ Divider,
+ Dropdown,
+ Empty,
+ Popover,
+ Space,
+ Statistic,
+ Steps,
+ Table,
+ Tooltip,
+} from 'antd';
+import classNames from 'classnames';
+import type { FC } from 'react';
+import React, { useState } from 'react';
+import type { AdvancedProfileData } from './data.d';
+import { queryAdvancedProfile } from './service';
+import useStyles from './style.style';
+
+const action = (
+
+ {({ isMobile }) => {
+ if (isMobile) {
+ return (
+ }
+ menu={{
+ items: [
+ {
+ key: '1',
+ label: '操作一',
+ },
+ {
+ key: '2',
+ label: '操作二',
+ },
+ {
+ key: '3',
+ label: '操作三',
+ },
+ ],
+ }}
+ placement="bottomRight"
+ >
+ 主操作
+
+ );
+ }
+ return (
+
+
+ 操作一
+ 操作二
+
+
+
+
+
+
+ 主操作
+
+ );
+ }}
+
+);
+
+const operationTabList = [
+ {
+ key: 'tab1',
+ tab: '操作日志一',
+ },
+ {
+ key: 'tab2',
+ tab: '操作日志二',
+ },
+ {
+ key: 'tab3',
+ tab: '操作日志三',
+ },
+];
+const columns = [
+ {
+ title: '操作类型',
+ dataIndex: 'type',
+ key: 'type',
+ },
+ {
+ title: '操作人',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: '执行结果',
+ dataIndex: 'status',
+ key: 'status',
+ render: (text: string) => {
+ if (text === 'agree') {
+ return ;
+ }
+ return ;
+ },
+ },
+ {
+ title: '操作时间',
+ dataIndex: 'updatedAt',
+ key: 'updatedAt',
+ },
+ {
+ title: '备注',
+ dataIndex: 'memo',
+ key: 'memo',
+ },
+];
+type AdvancedState = {
+ operationKey: 'tab1' | 'tab2' | 'tab3';
+ tabActiveKey: string;
+};
+const Advanced: FC = () => {
+ const { styles } = useStyles();
+
+ const extra = (
+
+
+
+
+ );
+ const description = (
+
+ {({ isMobile }) => (
+
+ 曲丽丽
+ XX 服务
+ 2017-07-07
+
+ 12421
+
+
+ 2017-07-07 ~ 2017-08-08
+
+
+ 请于两个工作日内确认
+
+
+ )}
+
+ );
+ const desc1 = (
+
+ 曲丽丽
+
+
2016-12-12 12:32
+
+ );
+ const desc2 = (
+
+ );
+
+ const [tabStatus, seTabStatus] = useState({
+ operationKey: 'tab1',
+ tabActiveKey: 'detail',
+ });
+
+ const customDot = (
+ dot: React.ReactNode,
+ {
+ status,
+ }: {
+ status: string;
+ },
+ ) => {
+ const popoverContent = (
+
+ 吴加号
+
+
+ 未响应
+
+ }
+ />
+
+
+ 耗时:2小时25分钟
+
+
+ );
+ if (status === 'process') {
+ return (
+
+ {dot}
+
+ );
+ }
+ return dot;
+ };
+
+ const { data = {}, loading } = useRequest<{
+ data: AdvancedProfileData;
+ }>(queryAdvancedProfile);
+ const { advancedOperation1, advancedOperation2, advancedOperation3 } = data;
+ const contentList = {
+ tab1: (
+
+ ),
+ tab2: (
+
+ ),
+ tab3: (
+
+ ),
+ };
+ const onTabChange = (tabActiveKey: string) => {
+ seTabStatus({
+ ...tabStatus,
+ tabActiveKey,
+ });
+ };
+ const onOperationTabChange = (key: string) => {
+ seTabStatus({
+ ...tabStatus,
+ operationKey: key as 'tab1',
+ });
+ };
+ return (
+
+
+
+
+
+ {({ isMobile }) => (
+
+ )}
+
+
+
+
+ 付小小
+
+ 32943898021309809423
+
+
+ 3321944288191034921
+
+
+ 18112345678
+
+
+ 曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口
+
+
+
+ 725
+
+ 2017-08-08
+
+
+ 某某数据
+
+
+
+
+ }
+ >
+ 725
+
+
+ 2017-08-08
+
+
+
+ 信息组
+
+
+
+ 林东东
+ 1234567
+
+ XX公司 - YY部
+
+
+ 2017-08-08
+
+
+ 这段描述很长很长很长很长很长很长很长很长很长很长很长很长很长很长...
+
+
+
+
+
+ Citrullus lanatus (Thunb.) Matsum. et
+ Nakai一年生蔓生藤本;茎、枝粗壮,具明显的棱。卷须较粗..
+
+
+
+
+ 付小小
+ 1234568
+
+
+
+
+
+
+
+ {contentList[tabStatus.operationKey] as React.ReactNode}
+
+
+
+
+ );
+};
+export default Advanced;
diff --git a/src/pages/profile/advanced/service.ts b/src/pages/profile/advanced/service.ts
new file mode 100644
index 00000000..beea5e90
--- /dev/null
+++ b/src/pages/profile/advanced/service.ts
@@ -0,0 +1,5 @@
+import { request } from '@umijs/max';
+
+export async function queryAdvancedProfile() {
+ return request('/api/profile/advanced');
+}
diff --git a/src/pages/profile/advanced/style.style.ts b/src/pages/profile/advanced/style.style.ts
new file mode 100644
index 00000000..9594971f
--- /dev/null
+++ b/src/pages/profile/advanced/style.style.ts
@@ -0,0 +1,38 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ main: {
+ '.ant-descriptions-row > td': { paddingBottom: '8px' },
+ '.ant-page-header-heading-extra': { flexDirection: 'column' },
+ },
+ headerList: {
+ marginBottom: '4px',
+ '.ant-descriptions-row > td': { paddingBottom: '8px' },
+ },
+ stepDescription: {
+ position: 'relative',
+ left: '38px',
+ paddingTop: '8px',
+ fontSize: '14px',
+ textAlign: 'left',
+ '> div': { marginTop: '8px', marginBottom: '4px' },
+ [`@media screen and (max-width: ${token.screenSM}px)`]: { left: '8px' },
+ },
+ pageHeader: {
+ '.ant-page-header-heading-extra > * + *': { marginLeft: '8px' },
+ [`@media screen and (max-width: ${token.screenSM}px)`]: {
+ '.ant-pro-page-header-wrap-row': {
+ flexDirection: 'column',
+ },
+ },
+ },
+ moreInfo: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ width: '200px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/profile/basic/_mock.ts b/src/pages/profile/basic/_mock.ts
new file mode 100644
index 00000000..f432f8d5
--- /dev/null
+++ b/src/pages/profile/basic/_mock.ts
@@ -0,0 +1,92 @@
+import type { Request, Response } from 'express';
+
+const basicGoods = [
+ {
+ id: '1234561',
+ name: '矿泉水 550ml',
+ barcode: '12421432143214321',
+ price: '2.00',
+ num: '1',
+ amount: '2.00',
+ },
+ {
+ id: '1234562',
+ name: '凉茶 300ml',
+ barcode: '12421432143214322',
+ price: '3.00',
+ num: '2',
+ amount: '6.00',
+ },
+ {
+ id: '1234563',
+ name: '好吃的薯片',
+ barcode: '12421432143214323',
+ price: '7.00',
+ num: '4',
+ amount: '28.00',
+ },
+ {
+ id: '1234564',
+ name: '特别好吃的蛋卷',
+ barcode: '12421432143214324',
+ price: '8.50',
+ num: '3',
+ amount: '25.50',
+ },
+];
+
+const basicProgress = [
+ {
+ key: '1',
+ time: '2017-10-01 14:10',
+ rate: '联系客户',
+ status: 'processing',
+ operator: '取货员 ID1234',
+ cost: '5mins',
+ },
+ {
+ key: '2',
+ time: '2017-10-01 14:05',
+ rate: '取货员出发',
+ status: 'success',
+ operator: '取货员 ID1234',
+ cost: '1h',
+ },
+ {
+ key: '3',
+ time: '2017-10-01 13:05',
+ rate: '取货员接单',
+ status: 'success',
+ operator: '取货员 ID1234',
+ cost: '5mins',
+ },
+ {
+ key: '4',
+ time: '2017-10-01 13:00',
+ rate: '申请审批通过',
+ status: 'success',
+ operator: '系统',
+ cost: '1h',
+ },
+ {
+ key: '5',
+ time: '2017-10-01 12:00',
+ rate: '发起退货申请',
+ status: 'success',
+ operator: '用户',
+ cost: '5mins',
+ },
+];
+
+function getProfileBasic(_: Request, res: Response) {
+ return res.json({
+ data: {
+ basicProgress,
+ basicGoods,
+ },
+ });
+}
+
+export default {
+ 'GET /api/profile/basic': getProfileBasic,
+};
diff --git a/src/pages/profile/basic/data.d.ts b/src/pages/profile/basic/data.d.ts
new file mode 100644
index 00000000..5b47678e
--- /dev/null
+++ b/src/pages/profile/basic/data.d.ts
@@ -0,0 +1,17 @@
+export type BasicGood = {
+ id: string;
+ name?: string;
+ barcode?: string;
+ price?: string;
+ num?: string | number;
+ amount?: string | number;
+};
+
+export type BasicProgress = {
+ key: string;
+ time: string;
+ rate: string;
+ status: string;
+ operator: string;
+ cost: string;
+};
diff --git a/src/pages/profile/basic/index.tsx b/src/pages/profile/basic/index.tsx
new file mode 100644
index 00000000..08e6fdbc
--- /dev/null
+++ b/src/pages/profile/basic/index.tsx
@@ -0,0 +1,236 @@
+import type { ProColumns } from '@ant-design/pro-components';
+import { PageContainer, ProTable } from '@ant-design/pro-components';
+import { useRequest } from '@umijs/max';
+import { Badge, Card, Descriptions, Divider } from 'antd';
+import type { FC } from 'react';
+import React from 'react';
+import type { BasicGood, BasicProgress } from './data.d';
+import { queryBasicProfile } from './service';
+import useStyles from './style.style';
+
+const progressColumns: ProColumns[] = [
+ {
+ title: '时间',
+ dataIndex: 'time',
+ key: 'time',
+ },
+ {
+ title: '当前进度',
+ dataIndex: 'rate',
+ key: 'rate',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ render: (text: React.ReactNode) => {
+ if (text === 'success') {
+ return ;
+ }
+ return ;
+ },
+ },
+ {
+ title: '操作员ID',
+ dataIndex: 'operator',
+ key: 'operator',
+ },
+ {
+ title: '耗时',
+ dataIndex: 'cost',
+ key: 'cost',
+ },
+];
+const Basic: FC = () => {
+ const { styles } = useStyles();
+ const { data, loading } = useRequest(() => {
+ return queryBasicProfile();
+ });
+ const { basicGoods, basicProgress } = data || {
+ basicGoods: [],
+ basicProgress: [],
+ };
+ let goodsData: typeof basicGoods = [];
+ if (basicGoods.length) {
+ let num = 0;
+ let amount = 0;
+ basicGoods.forEach((item) => {
+ num += Number(item.num);
+ amount += Number(item.amount);
+ });
+ goodsData = basicGoods.concat({
+ id: '总计',
+ num,
+ amount,
+ });
+ }
+ const renderContent = (value: any, _: any, index: any) => {
+ const obj: {
+ children: any;
+ props: {
+ colSpan?: number;
+ };
+ } = {
+ children: value,
+ props: {},
+ };
+ if (index === basicGoods.length) {
+ obj.props.colSpan = 0;
+ }
+ return obj;
+ };
+ const goodsColumns: ProColumns[] = [
+ {
+ title: '商品编号',
+ dataIndex: 'id',
+ key: 'id',
+ render: (text: React.ReactNode, _: any, index: number) => {
+ if (index < basicGoods.length) {
+ return {text} ;
+ }
+ return {
+ children: (
+
+ 总计
+
+ ),
+ props: {
+ colSpan: 4,
+ },
+ };
+ },
+ },
+ {
+ title: '商品名称',
+ dataIndex: 'name',
+ key: 'name',
+ render: renderContent,
+ },
+ {
+ title: '商品条码',
+ dataIndex: 'barcode',
+ key: 'barcode',
+ render: renderContent,
+ },
+ {
+ title: '单价',
+ dataIndex: 'price',
+ key: 'price',
+ align: 'right' as 'left' | 'right' | 'center',
+ render: renderContent,
+ },
+ {
+ title: '数量(件)',
+ dataIndex: 'num',
+ key: 'num',
+ align: 'right' as 'left' | 'right' | 'center',
+ render: (text: React.ReactNode, _: any, index: number) => {
+ if (index < basicGoods.length) {
+ return text;
+ }
+ return (
+
+ {text}
+
+ );
+ },
+ },
+ {
+ title: '金额',
+ dataIndex: 'amount',
+ key: 'amount',
+ align: 'right' as 'left' | 'right' | 'center',
+ render: (text: React.ReactNode, _: any, index: number) => {
+ if (index < basicGoods.length) {
+ return text;
+ }
+ return (
+
+ {text}
+
+ );
+ },
+ },
+ ];
+ return (
+
+
+
+ 1000000000
+ 已取货
+ 1234123421
+ 3214321432
+
+
+
+ 付小小
+ 18100000000
+ 菜鸟仓储
+
+ 浙江省杭州市西湖区万塘路18号
+
+ 无
+
+
+ 退货商品
+
+ 退货进度
+
+
+
+ );
+};
+export default Basic;
diff --git a/src/pages/profile/basic/service.ts b/src/pages/profile/basic/service.ts
new file mode 100644
index 00000000..57602486
--- /dev/null
+++ b/src/pages/profile/basic/service.ts
@@ -0,0 +1,11 @@
+import { request } from '@umijs/max';
+import type { BasicGood, BasicProgress } from './data.d';
+
+export async function queryBasicProfile(): Promise<{
+ data: {
+ basicProgress: BasicProgress[];
+ basicGoods: BasicGood[];
+ };
+}> {
+ return request('/api/profile/basic');
+}
diff --git a/src/pages/profile/basic/style.style.ts b/src/pages/profile/basic/style.style.ts
new file mode 100644
index 00000000..154eb819
--- /dev/null
+++ b/src/pages/profile/basic/style.style.ts
@@ -0,0 +1,14 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ title: {
+ marginBottom: '16px',
+ color: token.colorTextHeading,
+ fontWeight: '500',
+ fontSize: '16px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/result/fail/index.style.ts b/src/pages/result/fail/index.style.ts
new file mode 100644
index 00000000..7cb682d9
--- /dev/null
+++ b/src/pages/result/fail/index.style.ts
@@ -0,0 +1,17 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ error_icon: {
+ color: token.colorBgTextActive,
+ },
+ title: {
+ marginBottom: '16px',
+ color: token.colorTextHeading,
+ fontWeight: '500',
+ fontSize: '16px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/result/fail/index.tsx b/src/pages/result/fail/index.tsx
new file mode 100644
index 00000000..fed1ec75
--- /dev/null
+++ b/src/pages/result/fail/index.tsx
@@ -0,0 +1,75 @@
+import { CloseCircleOutlined, RightOutlined } from '@ant-design/icons';
+import { GridContent } from '@ant-design/pro-components';
+import { Button, Card, Result } from 'antd';
+import useStyles from './index.style';
+
+export default () => {
+ const { styles } = useStyles();
+ const Content = (
+ <>
+
+ 您提交的内容有如下错误:
+
+
+
+ >
+ );
+ return (
+
+
+
+ 返回修改
+
+ }
+ style={{
+ marginTop: 48,
+ marginBottom: 16,
+ }}
+ >
+ {Content}
+
+
+
+ );
+};
diff --git a/src/pages/result/success/index.style.ts b/src/pages/result/success/index.style.ts
new file mode 100644
index 00000000..169a71f1
--- /dev/null
+++ b/src/pages/result/success/index.style.ts
@@ -0,0 +1,20 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ title: {
+ position: 'relative',
+ color: token.colorText,
+ fontSize: '12px',
+ textAlign: 'center',
+ },
+ 'head-title': {
+ marginBottom: '20px',
+ color: token.colorTextHeading,
+ fontWeight: '500px',
+ fontSize: '16px',
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/pages/result/success/index.tsx b/src/pages/result/success/index.tsx
new file mode 100644
index 00000000..ba1f00ce
--- /dev/null
+++ b/src/pages/result/success/index.tsx
@@ -0,0 +1,139 @@
+import { DingdingOutlined } from '@ant-design/icons';
+import { GridContent } from '@ant-design/pro-components';
+import { Button, Card, Descriptions, Result, Steps } from 'antd';
+import useStyles from './index.style';
+
+export default () => {
+ const { styles } = useStyles();
+ const desc1 = (
+
+
+ 曲丽丽
+
+
+
2016-12-12 12:32
+
+ );
+ const desc2 = (
+
+ );
+ const content = (
+ <>
+
+ 23421
+ 曲丽丽
+
+ 2016-12-12 ~ 2017-12-12
+
+
+
+
+ 创建项目
+
+ ),
+ description: desc1,
+ },
+ {
+ title: (
+
+ 部门初审
+
+ ),
+ description: desc2,
+ },
+ {
+ title: (
+
+ 财务复核
+
+ ),
+ },
+ {
+ title: (
+
+ 完成
+
+ ),
+ },
+ ]}
+ />
+ >
+ );
+ const extra = (
+ <>
+ 返回列表
+ 查看项目
+ 打印
+ >
+ );
+ return (
+
+
+
+ {content}
+
+
+
+ );
+};
diff --git a/src/pages/user/login/__snapshots__/login.test.tsx.snap b/src/pages/user/login/__snapshots__/login.test.tsx.snap
index b2185b21..8cc10ba8 100644
--- a/src/pages/user/login/__snapshots__/login.test.tsx.snap
+++ b/src/pages/user/login/__snapshots__/login.test.tsx.snap
@@ -365,9 +365,6 @@ exports[`Login Page should login success 1`] = `
id="autoLogin"
type="checkbox"
/>
-
-
> = () => {
+ const { styles } = useStyles();
+ const [params] = useSearchParams();
+
+ const actions = (
+
+ );
+
+ const email = params?.get('account') || 'AntDesign@example.com';
+ return (
+
+ 你的账户:{email} 注册成功
+
+ }
+ subTitle="激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。"
+ extra={actions}
+ />
+ );
+};
+export default RegisterResult;
diff --git a/src/pages/user/register-result/style.style.ts b/src/pages/user/register-result/style.style.ts
new file mode 100644
index 00000000..ebc1b282
--- /dev/null
+++ b/src/pages/user/register-result/style.style.ts
@@ -0,0 +1,26 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(() => {
+ return {
+ registerResult: {
+ width: '800px',
+ minHeight: '400px',
+ margin: 'auto',
+ padding: '80px',
+ background: 'none',
+ },
+ anticon: {
+ fontSize: '64px',
+ },
+ title: {
+ marginTop: '32px',
+ fontSize: '20px',
+ lineHeight: '28px',
+ },
+ actions: {
+ marginTop: '40px',
+ 'a + a': { marginLeft: '8px' },
+ },
+ };
+});
+export default useStyles;
diff --git a/src/pages/user/register/_mock.ts b/src/pages/user/register/_mock.ts
new file mode 100644
index 00000000..1edfe6b8
--- /dev/null
+++ b/src/pages/user/register/_mock.ts
@@ -0,0 +1,9 @@
+import type { Request, Response } from 'express';
+
+export default {
+ 'POST /api/register': (_: Request, res: Response) => {
+ res.send({
+ data: { status: 'ok', currentAuthority: 'user' },
+ });
+ },
+};
diff --git a/src/pages/user/register/index.tsx b/src/pages/user/register/index.tsx
new file mode 100644
index 00000000..72f909d1
--- /dev/null
+++ b/src/pages/user/register/index.tsx
@@ -0,0 +1,309 @@
+import { history, Link, useRequest } from '@umijs/max';
+import {
+ Button,
+ Col,
+ Form,
+ Input,
+ message,
+ Popover,
+ Progress,
+ Row,
+ Select,
+ Space,
+} from 'antd';
+import type { Store } from 'antd/es/form/interface';
+import type { FC } from 'react';
+import { useEffect, useState } from 'react';
+import type { StateType } from './service';
+import { fakeRegister } from './service';
+import useStyles from './styles';
+
+const FormItem = Form.Item;
+const { Option } = Select;
+
+const passwordProgressMap: {
+ ok: 'success';
+ pass: 'normal';
+ poor: 'exception';
+} = {
+ ok: 'success',
+ pass: 'normal',
+ poor: 'exception',
+};
+const Register: FC = () => {
+ const { styles } = useStyles();
+ const [count, setCount]: [number, any] = useState(0);
+ const [open, setVisible]: [boolean, any] = useState(false);
+ const [prefix, setPrefix]: [string, any] = useState('86');
+ const [popover, setPopover]: [boolean, any] = useState(false);
+ const confirmDirty = false;
+ let interval: number | undefined;
+
+ const passwordStatusMap = {
+ ok: (
+
+ 强度:强
+
+ ),
+ pass: (
+
+ 强度:中
+
+ ),
+ poor: (
+
+ 强度:太短
+
+ ),
+ };
+
+ const [form] = Form.useForm();
+ useEffect(
+ () => () => {
+ clearInterval(interval);
+ },
+ [interval],
+ );
+ const onGetCaptcha = () => {
+ let counts = 59;
+ setCount(counts);
+ interval = window.setInterval(() => {
+ counts -= 1;
+ setCount(counts);
+ if (counts === 0) {
+ clearInterval(interval);
+ }
+ }, 1000);
+ };
+ const getPasswordStatus = () => {
+ const value = form.getFieldValue('password');
+ if (value && value.length > 9) {
+ return 'ok';
+ }
+ if (value && value.length > 5) {
+ return 'pass';
+ }
+ return 'poor';
+ };
+ const { loading: submitting, run: register } = useRequest<{
+ data: StateType;
+ }>(fakeRegister, {
+ manual: true,
+ onSuccess: (data, params) => {
+ if (data.status === 'ok') {
+ message.success('注册成功!');
+ history.push({
+ pathname: `/user/register-result?account=${params[0].email}`,
+ });
+ }
+ },
+ });
+ const onFinish = (values: Store) => {
+ register(values);
+ };
+ const checkConfirm = (_: any, value: string) => {
+ const promise = Promise;
+ if (value && value !== form.getFieldValue('password')) {
+ return promise.reject('两次输入的密码不匹配!');
+ }
+ return promise.resolve();
+ };
+ const checkPassword = (_: any, value: string) => {
+ const promise = Promise;
+ // 没有值的情况
+ if (!value) {
+ setVisible(!!value);
+ return promise.reject('请输入密码!');
+ }
+ // 有值的情况
+ if (!open) {
+ setVisible(!!value);
+ }
+ setPopover(!popover);
+ if (value.length < 6) {
+ return promise.reject('');
+ }
+ if (value && confirmDirty) {
+ form.validateFields(['confirm']);
+ }
+ return promise.resolve();
+ };
+ const changePrefix = (value: string) => {
+ setPrefix(value);
+ };
+ const renderPasswordProgress = () => {
+ const value = form.getFieldValue('password');
+ const passwordStatus = getPasswordStatus();
+ return value?.length ? (
+
+
100 ? 100 : value.length * 10}
+ showInfo={false}
+ />
+
+ ) : null;
+ };
+ return (
+
+ );
+};
+export default Register;
diff --git a/src/pages/user/register/service.ts b/src/pages/user/register/service.ts
new file mode 100644
index 00000000..69caab3d
--- /dev/null
+++ b/src/pages/user/register/service.ts
@@ -0,0 +1,22 @@
+import { request } from '@umijs/max';
+
+export interface StateType {
+ status?: 'ok' | 'error';
+ currentAuthority?: 'user' | 'guest' | 'admin';
+}
+
+export interface UserRegisterParams {
+ mail: string;
+ password: string;
+ confirm: string;
+ mobile: string;
+ captcha: string;
+ prefix: string;
+}
+
+export async function fakeRegister(params: UserRegisterParams) {
+ return request('/api/register', {
+ method: 'POST',
+ data: params,
+ });
+}
diff --git a/src/pages/user/register/styles.ts b/src/pages/user/register/styles.ts
new file mode 100644
index 00000000..cad6b68c
--- /dev/null
+++ b/src/pages/user/register/styles.ts
@@ -0,0 +1,46 @@
+import { createStyles } from 'antd-style';
+
+const useStyles = createStyles(({ token }) => {
+ return {
+ main: {
+ width: '368px',
+ margin: '0 auto',
+ h3: { marginBottom: '20px', fontSize: '16px' },
+ },
+ password: {
+ marginBottom: '24px',
+ '.ant-form-item-explain': { display: 'none' },
+ },
+ getCaptcha: {
+ display: 'block',
+ width: '100%',
+ },
+
+ footer: {
+ width: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ submit: {
+ width: '50%',
+ },
+ success: {
+ transition: 'color 0.3s',
+ color: token.colorSuccess,
+ },
+ warning: {
+ transition: 'color 0.3s',
+ color: token.colorWarning,
+ },
+ error: {
+ transition: 'color 0.3s',
+ color: token.colorError,
+ },
+ 'progress-pass > .progress': {
+ '.ant-progress-bg': { backgroundColor: token.colorWarning },
+ },
+ };
+});
+
+export default useStyles;
diff --git a/src/services/swagger/index.ts b/src/services/swagger/index.ts
deleted file mode 100644
index 83cf97ca..00000000
--- a/src/services/swagger/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// @ts-ignore
-/* eslint-disable */
-// API 更新时间:
-// API 唯一标识:
-import * as pet from './pet';
-import * as store from './store';
-import * as user from './user';
-export default {
- pet,
- store,
- user,
-};
diff --git a/src/services/swagger/pet.ts b/src/services/swagger/pet.ts
deleted file mode 100644
index b887475a..00000000
--- a/src/services/swagger/pet.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-// @ts-ignore
-/* eslint-disable */
-import { request } from '@umijs/max';
-
-/** Update an existing pet PUT /pet */
-export async function updatePet(body: API.Pet, options?: { [key: string]: any }) {
- return request('/pet', {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- data: body,
- ...(options || {}),
- });
-}
-
-/** Add a new pet to the store POST /pet */
-export async function addPet(body: API.Pet, options?: { [key: string]: any }) {
- return request('/pet', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- data: body,
- ...(options || {}),
- });
-}
-
-/** Find pet by ID Returns a single pet GET /pet/${param0} */
-export async function getPetById(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.getPetByIdParams,
- options?: { [key: string]: any },
-) {
- const { petId: param0, ...queryParams } = params;
- return request(`/pet/${param0}`, {
- method: 'GET',
- params: { ...queryParams },
- ...(options || {}),
- });
-}
-
-/** Updates a pet in the store with form data POST /pet/${param0} */
-export async function updatePetWithForm(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.updatePetWithFormParams,
- body: { name?: string; status?: string },
- options?: { [key: string]: any },
-) {
- const { petId: param0, ...queryParams } = params;
- const formData = new FormData();
-
- Object.keys(body).forEach((ele) => {
- const item = (body as any)[ele];
-
- if (item !== undefined && item !== null) {
- formData.append(
- ele,
- typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item,
- );
- }
- });
-
- return request(`/pet/${param0}`, {
- method: 'POST',
- params: { ...queryParams },
- data: formData,
- ...(options || {}),
- });
-}
-
-/** Deletes a pet DELETE /pet/${param0} */
-export async function deletePet(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.deletePetParams & {
- // header
- api_key?: string;
- },
- options?: { [key: string]: any },
-) {
- const { petId: param0, ...queryParams } = params;
- return request(`/pet/${param0}`, {
- method: 'DELETE',
- headers: {},
- params: { ...queryParams },
- ...(options || {}),
- });
-}
-
-/** uploads an image POST /pet/${param0}/uploadImage */
-export async function uploadFile(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.uploadFileParams,
- body: { additionalMetadata?: string; file?: string },
- file?: File,
- options?: { [key: string]: any },
-) {
- const { petId: param0, ...queryParams } = params;
- const formData = new FormData();
-
- if (file) {
- formData.append('file', file);
- }
-
- Object.keys(body).forEach((ele) => {
- const item = (body as any)[ele];
-
- if (item !== undefined && item !== null) {
- formData.append(
- ele,
- typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item,
- );
- }
- });
-
- return request(`/pet/${param0}/uploadImage`, {
- method: 'POST',
- params: { ...queryParams },
- data: formData,
- requestType: 'form',
- ...(options || {}),
- });
-}
-
-/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
-export async function findPetsByStatus(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.findPetsByStatusParams,
- options?: { [key: string]: any },
-) {
- return request('/pet/findByStatus', {
- method: 'GET',
- params: {
- ...params,
- },
- ...(options || {}),
- });
-}
-
-/** Finds Pets by tags Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */
-export async function findPetsByTags(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.findPetsByTagsParams,
- options?: { [key: string]: any },
-) {
- return request('/pet/findByTags', {
- method: 'GET',
- params: {
- ...params,
- },
- ...(options || {}),
- });
-}
diff --git a/src/services/swagger/store.ts b/src/services/swagger/store.ts
deleted file mode 100644
index b9c689a6..00000000
--- a/src/services/swagger/store.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-// @ts-ignore
-/* eslint-disable */
-import { request } from '@umijs/max';
-
-/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
-export async function getInventory(options?: { [key: string]: any }) {
- return request>('/store/inventory', {
- method: 'GET',
- ...(options || {}),
- });
-}
-
-/** Place an order for a pet POST /store/order */
-export async function placeOrder(body: API.Order, options?: { [key: string]: any }) {
- return request('/store/order', {
- method: 'POST',
- data: body,
- ...(options || {}),
- });
-}
-
-/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */
-export async function getOrderById(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.getOrderByIdParams,
- options?: { [key: string]: any },
-) {
- const { orderId: param0, ...queryParams } = params;
- return request(`/store/order/${param0}`, {
- method: 'GET',
- params: { ...queryParams },
- ...(options || {}),
- });
-}
-
-/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
-export async function deleteOrder(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.deleteOrderParams,
- options?: { [key: string]: any },
-) {
- const { orderId: param0, ...queryParams } = params;
- return request(`/store/order/${param0}`, {
- method: 'DELETE',
- params: { ...queryParams },
- ...(options || {}),
- });
-}
diff --git a/src/services/swagger/typings.d.ts b/src/services/swagger/typings.d.ts
deleted file mode 100644
index d06bcfcb..00000000
--- a/src/services/swagger/typings.d.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-declare namespace API {
- type ApiResponse = {
- code?: number;
- type?: string;
- message?: string;
- };
-
- type Category = {
- id?: number;
- name?: string;
- };
-
- type deleteOrderParams = {
- /** ID of the order that needs to be deleted */
- orderId: number;
- };
-
- type deletePetParams = {
- api_key?: string;
- /** Pet id to delete */
- petId: number;
- };
-
- type deleteUserParams = {
- /** The name that needs to be deleted */
- username: string;
- };
-
- type findPetsByStatusParams = {
- /** Status values that need to be considered for filter */
- status: ('available' | 'pending' | 'sold')[];
- };
-
- type findPetsByTagsParams = {
- /** Tags to filter by */
- tags: string[];
- };
-
- type getOrderByIdParams = {
- /** ID of pet that needs to be fetched */
- orderId: number;
- };
-
- type getPetByIdParams = {
- /** ID of pet to return */
- petId: number;
- };
-
- type getUserByNameParams = {
- /** The name that needs to be fetched. Use user1 for testing. */
- username: string;
- };
-
- type loginUserParams = {
- /** The user name for login */
- username: string;
- /** The password for login in clear text */
- password: string;
- };
-
- type Order = {
- id?: number;
- petId?: number;
- quantity?: number;
- shipDate?: string;
- /** Order Status */
- status?: 'placed' | 'approved' | 'delivered';
- complete?: boolean;
- };
-
- type Pet = {
- id?: number;
- category?: Category;
- name: string;
- photoUrls: string[];
- tags?: Tag[];
- /** pet status in the store */
- status?: 'available' | 'pending' | 'sold';
- };
-
- type Tag = {
- id?: number;
- name?: string;
- };
-
- type updatePetWithFormParams = {
- /** ID of pet that needs to be updated */
- petId: number;
- };
-
- type updateUserParams = {
- /** name that need to be updated */
- username: string;
- };
-
- type uploadFileParams = {
- /** ID of pet to update */
- petId: number;
- };
-
- type User = {
- id?: number;
- username?: string;
- firstName?: string;
- lastName?: string;
- email?: string;
- password?: string;
- phone?: string;
- /** User Status */
- userStatus?: number;
- };
-}
diff --git a/src/services/swagger/user.ts b/src/services/swagger/user.ts
deleted file mode 100644
index 4dd6f421..00000000
--- a/src/services/swagger/user.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-// @ts-ignore
-/* eslint-disable */
-import { request } from '@umijs/max';
-
-/** Create user This can only be done by the logged in user. POST /user */
-export async function createUser(body: API.User, options?: { [key: string]: any }) {
- return request('/user', {
- method: 'POST',
- data: body,
- ...(options || {}),
- });
-}
-
-/** Get user by user name GET /user/${param0} */
-export async function getUserByName(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.getUserByNameParams,
- options?: { [key: string]: any },
-) {
- const { username: param0, ...queryParams } = params;
- return request(`/user/${param0}`, {
- method: 'GET',
- params: { ...queryParams },
- ...(options || {}),
- });
-}
-
-/** Updated user This can only be done by the logged in user. PUT /user/${param0} */
-export async function updateUser(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.updateUserParams,
- body: API.User,
- options?: { [key: string]: any },
-) {
- const { username: param0, ...queryParams } = params;
- return request(`/user/${param0}`, {
- method: 'PUT',
- params: { ...queryParams },
- data: body,
- ...(options || {}),
- });
-}
-
-/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
-export async function deleteUser(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.deleteUserParams,
- options?: { [key: string]: any },
-) {
- const { username: param0, ...queryParams } = params;
- return request(`/user/${param0}`, {
- method: 'DELETE',
- params: { ...queryParams },
- ...(options || {}),
- });
-}
-
-/** Creates list of users with given input array POST /user/createWithArray */
-export async function createUsersWithArrayInput(
- body: API.User[],
- options?: { [key: string]: any },
-) {
- return request('/user/createWithArray', {
- method: 'POST',
- data: body,
- ...(options || {}),
- });
-}
-
-/** Creates list of users with given input array POST /user/createWithList */
-export async function createUsersWithListInput(body: API.User[], options?: { [key: string]: any }) {
- return request('/user/createWithList', {
- method: 'POST',
- data: body,
- ...(options || {}),
- });
-}
-
-/** Logs user into the system GET /user/login */
-export async function loginUser(
- // 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
- params: API.loginUserParams,
- options?: { [key: string]: any },
-) {
- return request('/user/login', {
- method: 'GET',
- params: {
- ...params,
- },
- ...(options || {}),
- });
-}
-
-/** Logs out current logged in user session GET /user/logout */
-export async function logoutUser(options?: { [key: string]: any }) {
- return request('/user/logout', {
- method: 'GET',
- ...(options || {}),
- });
-}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 00000000..a403104a
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ content: ['./src/**/*.tsx'],
+};
diff --git a/tailwind.css b/tailwind.css
new file mode 100644
index 00000000..b5c61c95
--- /dev/null
+++ b/tailwind.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/types/cache/mock/login.mock.cache.js b/types/cache/mock/login.mock.cache.js
new file mode 100644
index 00000000..6c59e198
--- /dev/null
+++ b/types/cache/mock/login.mock.cache.js
@@ -0,0 +1,324 @@
+module.exports = {
+ 'GET /api/currentUser': {
+ 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',
+ },
+ },
+ 'GET /api/rule': {
+ data: [
+ {
+ key: 99,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 99',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 503,
+ status: '0',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 81,
+ },
+ {
+ key: 98,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 98',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 164,
+ status: '0',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 12,
+ },
+ {
+ key: 97,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 97',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 174,
+ status: '1',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 81,
+ },
+ {
+ key: 96,
+ disabled: true,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 96',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 914,
+ status: '0',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 7,
+ },
+ {
+ key: 95,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 95',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 698,
+ status: '2',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 82,
+ },
+ {
+ key: 94,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 94',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 488,
+ status: '1',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 14,
+ },
+ {
+ key: 93,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 93',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 580,
+ status: '2',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 77,
+ },
+ {
+ key: 92,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 92',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 244,
+ status: '3',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 58,
+ },
+ {
+ key: 91,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 91',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 959,
+ status: '0',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 66,
+ },
+ {
+ key: 90,
+ disabled: true,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 90',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 958,
+ status: '0',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 72,
+ },
+ {
+ key: 89,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 89',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 301,
+ status: '2',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 2,
+ },
+ {
+ key: 88,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 88',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 277,
+ status: '1',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 12,
+ },
+ {
+ key: 87,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 87',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 810,
+ status: '1',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 82,
+ },
+ {
+ key: 86,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 86',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 780,
+ status: '3',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 22,
+ },
+ {
+ key: 85,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 85',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 705,
+ status: '3',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 12,
+ },
+ {
+ key: 84,
+ disabled: true,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 84',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 203,
+ status: '0',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 79,
+ },
+ {
+ key: 83,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 83',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 491,
+ status: '2',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 59,
+ },
+ {
+ key: 82,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 82',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 73,
+ status: '0',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 100,
+ },
+ {
+ key: 81,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
+ name: 'TradeCode 81',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 406,
+ status: '3',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 61,
+ },
+ {
+ key: 80,
+ disabled: false,
+ href: 'https://ant.design',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
+ name: 'TradeCode 80',
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: 112,
+ status: '2',
+ updatedAt: '2022-12-06T05:00:57.040Z',
+ createdAt: '2022-12-06T05:00:57.040Z',
+ progress: 20,
+ },
+ ],
+ total: 100,
+ success: true,
+ pageSize: 20,
+ current: 1,
+ },
+ 'POST /api/login/outLogin': { data: {}, success: true },
+ 'POST /api/login/account': {
+ status: 'ok',
+ type: 'account',
+ currentAuthority: 'admin',
+ },
+};