Browse Source
* init * remove Authorized * remove login * remove request * remove path-to-regexp" * remove create-umi * clean dependencies * remove unused less * better login mock * add ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION=site env * support settings dynamic * support umi-plugin-setting-drawer * 🎨 format: clean code * remove clean fetch:blocks * add NoticeIcon * add NoticeIcon * use "@ant-design/pro-layout": "6.0.0-1" * support errorHandler * remove SelectLang * update version * PageHeaderWrapper to PageContainer * PageHeaderWrapper to PageContainer (#6622) * 📌 versions: use alpha version (#6634) * use alpha version 5.0.0 no yet * less test * fix lint error * rename setting Co-authored-by: chenshuai2144 <qixian.cs@outlook.com> * fix 401 do not to login error * 405 start:no-mock * siderWidth: 208 * side * false * username * remove resolutions * update list demo * correct the text * fix tabs style in antd@4.3 * fix login error * use "@ant-design/pro-layout": "6.0.0-5" * eslint * access * pro-layout 6.0.0-7 * merge master * up layout version * 💄 UI: fix login style warning * upgrade dependency * delete throw error * versions: up @umijs/fabric * change login * do not open defaultOpenAll * rebase error * update demos * deps upgreade * 🐛 修复切换不同权限的账户登录后,access的鉴权不生效问题 (#6997) * 🐛 修复切换不同权限的账户登录后,access的鉴权不生效问题 * 🎨 prettier code * 修改hash路由下search参数在hash之前,search最后一个参数错误bug (#6908) * 🐛 bug: fix tsc error * config.js里面 history.type 为 hash时,退出重登录后,拼接返回路径时,#号前路径会重复 * prettier all code * merge master * 🐛 bug: fix the problem that modifying URL can bypass permissions * reset hash * fix infinite loop of login page (#7095) * merge master * rm unuse code * rm unuse code * remove unuse code * 🐛 bug: fix redirect error in hash mode * fix redirect * 💥 feat: support defaultSettings close #7407 * sort * sorter * default close umi ui * goto reload window.location.href * add setupTests.js * add localStorageMock init * open esbuild * add exportStatic * better code * add initialStateConfig * remove unuse code * login 翻译 * add key * update siderWidth close #7585 * fix redirect error close #7632 * add childrenRender type * better styles * better styles * remove unuse code close #7648 * default use browser history * close * support unAccessible * up @umijs/preset-react version * merge master * support dumi function * ✨ feat: add react-dev-inspector plugins * 🚑 hotfix: support InspectorWrapper * 🚑 hotfix: support InspectorWrapper * add dumi docs href * 👷 CI: fix Deploy CI * 👷 CI : fix azure-pipelines * fix keys error * fix: version (#7789) * fix lint * fix lint * fix typo * fix type * better code * add openAPI plugins * use openapi plugins * support schemaPath = url * add umi-plugins * add dumi doc * add plugin doc * add config doc * fix: outLogin request method (#8059) (#8061) Co-authored-by: luqili <a@luqili.com> * 非 dev 环境不展示 openapi * locale pt_br Pages added (#8088) * revert surge preview * test: update rebase action * add persian local and fix menu direction for rtl display (#8079) * up @umijs/preset-dumi version * up typescript verison * add waterMark * default open webpack5 * default open webpack5 * support webpack5 * support openAPI is array * upgrade deps * dir * 页面代码结构推荐 * Update global.tsx * Update index.tsx * refactor(app.tsx): dev links use Link * loginPath * fix ci * fix tsc * fix tsc * fix(app.tsx): dev links use umi Link * fix tes * fix warn * refactor(app.tsx): optimization import Link * resetv5-app-tsx-links #8226 * use link to hash * default open fastRefresh * Update README.md (#8356) Ant Design Pro V5 is not released yet. `trial` is often used for released paid product, and `preview` is the regularly used word for pre-release version. * Fixing a few issues with internationalization on the login (#7945) Co-authored-by: chenshuai2144 <qixian.cs@outlook.com> * Update api.ts (#8424) * chore: support verify commit * fix locale error * support all block * locale: 增加标记,以便删除国际化脚本识别 * fix(locale): 删除request错误捕捉 * chore: remove tag * chore: use fabric verify-commit * chore: up to 5.0.0-beta.3 * chore: remove home page png * use latest * merge master * fix conflict type and eslint waring! (#8644) * chore: open mfsu and webpack5 * try fix ci * fix ci * try fix ci * add test * add test * 增加加载时间 * fix test * prettier all code * docs: change oneapi doc * chore: add umi * rmeove unuse code Co-authored-by: lijiehua <41830859@qq.com> Co-authored-by: 偏右 <afc163@gmail.com> Co-authored-by: Jerry <510846@qq.com> Co-authored-by: Vern Brandl <tkvern@users.noreply.github.com> Co-authored-by: Lee Fan <535536456@qq.com> Co-authored-by: zhangshuling <Zsl0516> Co-authored-by: Justin <zcodeworld@gmail.com> Co-authored-by: Jeff Tian <jeff.tian@outlook.com> Co-authored-by: luqili <33475522+luqil@users.noreply.github.com> Co-authored-by: luqili <a@luqili.com> Co-authored-by: Thiago Tognoli <thiagotognoli@users.noreply.github.com> Co-authored-by: abolfazl <Abolfazl.rajabpour@gmail.com> Co-authored-by: HouKunLin <houkunlin@aliyun.com> Co-authored-by: SHINCHVEN <shinchven@gmail.com> Co-authored-by: Scott Goci <scottjg@gmail.com> Co-authored-by: _XiaoTian <istianlei@qq.com> Co-authored-by: 丁田秀 <295434665@qq.com> Co-authored-by: Jiankian <i@anline.cn>pull/8660/head
committed by
GitHub
98 changed files with 3178 additions and 2510 deletions
@ -1,23 +1,21 @@ |
|||
import { Settings as ProSettings } from '@ant-design/pro-layout'; |
|||
import { Settings as LayoutSettings } from '@ant-design/pro-layout'; |
|||
|
|||
type DefaultSettings = Partial<ProSettings> & { |
|||
pwa: boolean; |
|||
}; |
|||
|
|||
const proSettings: DefaultSettings = { |
|||
navTheme: 'dark', |
|||
const Settings: LayoutSettings & { |
|||
pwa?: boolean; |
|||
logo?: string; |
|||
} = { |
|||
navTheme: 'light', |
|||
// 拂晓蓝
|
|||
primaryColor: '#1890ff', |
|||
layout: 'side', |
|||
layout: 'mix', |
|||
contentWidth: 'Fluid', |
|||
fixedHeader: false, |
|||
fixSiderbar: true, |
|||
colorWeak: false, |
|||
title: 'Ant Design Pro', |
|||
pwa: false, |
|||
logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', |
|||
iconfontUrl: '', |
|||
}; |
|||
|
|||
export type { DefaultSettings }; |
|||
|
|||
export default proSettings; |
|||
export default Settings; |
|||
|
|||
@ -0,0 +1,593 @@ |
|||
{ |
|||
"openapi": "3.0.1", |
|||
"info": { |
|||
"title": "Ant Design Pro", |
|||
"version": "1.0.0" |
|||
}, |
|||
"servers": [ |
|||
{ |
|||
"url": "http://localhost:8000/" |
|||
}, |
|||
{ |
|||
"url": "https://localhost:8000/" |
|||
} |
|||
], |
|||
"paths": { |
|||
"/api/currentUser": { |
|||
"get": { |
|||
"tags": ["api"], |
|||
"description": "获取当前的用户", |
|||
"operationId": "currentUser", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/CurrentUser" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/ErrorResponse" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"x-swagger-router-controller": "api" |
|||
}, |
|||
"/api/login/captcha": { |
|||
"post": { |
|||
"description": "发送验证码", |
|||
"operationId": "getFakeCaptcha", |
|||
"tags": ["login"], |
|||
"parameters": [ |
|||
{ |
|||
"name": "phone", |
|||
"in": "query", |
|||
"description": "手机号", |
|||
"schema": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
], |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/FakeCaptcha" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"/api/login/outLogin": { |
|||
"post": { |
|||
"description": "登录接口", |
|||
"operationId": "outLogin", |
|||
"tags": ["login"], |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"type": "object" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/ErrorResponse" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"x-swagger-router-controller": "api" |
|||
}, |
|||
"/api/login/account": { |
|||
"post": { |
|||
"tags": ["login"], |
|||
"description": "登录接口", |
|||
"operationId": "login", |
|||
"requestBody": { |
|||
"description": "登录系统", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/LoginParams" |
|||
} |
|||
} |
|||
}, |
|||
"required": true |
|||
}, |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/LoginResult" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/ErrorResponse" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"x-codegen-request-body-name": "body" |
|||
}, |
|||
"x-swagger-router-controller": "api" |
|||
}, |
|||
"/api/notices": { |
|||
"summary": "getNotices", |
|||
"description": "NoticeIconItem", |
|||
"get": { |
|||
"tags": ["api"], |
|||
"operationId": "getNotices", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/NoticeIconList" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"/api/rule": { |
|||
"get": { |
|||
"tags": ["rule"], |
|||
"description": "获取规则列表", |
|||
"operationId": "rule", |
|||
"parameters": [ |
|||
{ |
|||
"name": "current", |
|||
"in": "query", |
|||
"description": "当前的页码", |
|||
"schema": { |
|||
"type": "number" |
|||
} |
|||
}, |
|||
{ |
|||
"name": "pageSize", |
|||
"in": "query", |
|||
"description": "页面的容量", |
|||
"schema": { |
|||
"type": "number" |
|||
} |
|||
} |
|||
], |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/RuleList" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/ErrorResponse" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"post": { |
|||
"tags": ["rule"], |
|||
"description": "新建规则", |
|||
"operationId": "addRule", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/RuleListItem" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/ErrorResponse" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"put": { |
|||
"tags": ["rule"], |
|||
"description": "新建规则", |
|||
"operationId": "updateRule", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/RuleListItem" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/ErrorResponse" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"delete": { |
|||
"tags": ["rule"], |
|||
"description": "删除规则", |
|||
"operationId": "removeRule", |
|||
"responses": { |
|||
"200": { |
|||
"description": "Success", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"type": "object" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"401": { |
|||
"description": "Error", |
|||
"content": { |
|||
"application/json": { |
|||
"schema": { |
|||
"$ref": "#/components/schemas/ErrorResponse" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"x-swagger-router-controller": "api" |
|||
}, |
|||
"/swagger": { |
|||
"x-swagger-pipe": "swagger_raw" |
|||
} |
|||
}, |
|||
"components": { |
|||
"schemas": { |
|||
"CurrentUser": { |
|||
"type": "object", |
|||
"properties": { |
|||
"name": { |
|||
"type": "string" |
|||
}, |
|||
"avatar": { |
|||
"type": "string" |
|||
}, |
|||
"userid": { |
|||
"type": "string" |
|||
}, |
|||
"email": { |
|||
"type": "string" |
|||
}, |
|||
"signature": { |
|||
"type": "string" |
|||
}, |
|||
"title": { |
|||
"type": "string" |
|||
}, |
|||
"group": { |
|||
"type": "string" |
|||
}, |
|||
"tags": { |
|||
"type": "array", |
|||
"items": { |
|||
"type": "object", |
|||
"properties": { |
|||
"key": { |
|||
"type": "string" |
|||
}, |
|||
"label": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"notifyCount": { |
|||
"type": "integer", |
|||
"format": "int32" |
|||
}, |
|||
"unreadCount": { |
|||
"type": "integer", |
|||
"format": "int32" |
|||
}, |
|||
"country": { |
|||
"type": "string" |
|||
}, |
|||
"access": { |
|||
"type": "string" |
|||
}, |
|||
"geographic": { |
|||
"type": "object", |
|||
"properties": { |
|||
"province": { |
|||
"type": "object", |
|||
"properties": { |
|||
"label": { |
|||
"type": "string" |
|||
}, |
|||
"key": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"city": { |
|||
"type": "object", |
|||
"properties": { |
|||
"label": { |
|||
"type": "string" |
|||
}, |
|||
"key": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
"address": { |
|||
"type": "string" |
|||
}, |
|||
"phone": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"LoginResult": { |
|||
"type": "object", |
|||
"properties": { |
|||
"status": { |
|||
"type": "string" |
|||
}, |
|||
"type": { |
|||
"type": "string" |
|||
}, |
|||
"currentAuthority": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"PageParams": { |
|||
"type": "object", |
|||
"properties": { |
|||
"current": { |
|||
"type": "number" |
|||
}, |
|||
"pageSize": { |
|||
"type": "number" |
|||
} |
|||
} |
|||
}, |
|||
"RuleListItem": { |
|||
"type": "object", |
|||
"properties": { |
|||
"key": { |
|||
"type": "integer", |
|||
"format": "int32" |
|||
}, |
|||
"disabled": { |
|||
"type": "boolean" |
|||
}, |
|||
"href": { |
|||
"type": "string" |
|||
}, |
|||
"avatar": { |
|||
"type": "string" |
|||
}, |
|||
"name": { |
|||
"type": "string" |
|||
}, |
|||
"owner": { |
|||
"type": "string" |
|||
}, |
|||
"desc": { |
|||
"type": "string" |
|||
}, |
|||
"callNo": { |
|||
"type": "integer", |
|||
"format": "int32" |
|||
}, |
|||
"status": { |
|||
"type": "integer", |
|||
"format": "int32" |
|||
}, |
|||
"updatedAt": { |
|||
"type": "string", |
|||
"format": "datetime" |
|||
}, |
|||
"createdAt": { |
|||
"type": "string", |
|||
"format": "datetime" |
|||
}, |
|||
"progress": { |
|||
"type": "integer", |
|||
"format": "int32" |
|||
} |
|||
} |
|||
}, |
|||
"RuleList": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": { |
|||
"type": "array", |
|||
"items": { |
|||
"$ref": "#/components/schemas/RuleListItem" |
|||
} |
|||
}, |
|||
"total": { |
|||
"type": "integer", |
|||
"description": "列表的内容总数", |
|||
"format": "int32" |
|||
}, |
|||
"success": { |
|||
"type": "boolean" |
|||
} |
|||
} |
|||
}, |
|||
"FakeCaptcha": { |
|||
"type": "object", |
|||
"properties": { |
|||
"code": { |
|||
"type": "integer", |
|||
"format": "int32" |
|||
}, |
|||
"status": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"LoginParams": { |
|||
"type": "object", |
|||
"properties": { |
|||
"username": { |
|||
"type": "string" |
|||
}, |
|||
"password": { |
|||
"type": "string" |
|||
}, |
|||
"autoLogin": { |
|||
"type": "boolean" |
|||
}, |
|||
"type": { |
|||
"type": "string" |
|||
} |
|||
} |
|||
}, |
|||
"ErrorResponse": { |
|||
"required": ["errorCode"], |
|||
"type": "object", |
|||
"properties": { |
|||
"errorCode": { |
|||
"type": "string", |
|||
"description": "业务约定的错误码" |
|||
}, |
|||
"errorMessage": { |
|||
"type": "string", |
|||
"description": "业务上的错误信息" |
|||
}, |
|||
"success": { |
|||
"type": "boolean", |
|||
"description": "业务上的请求是否成功" |
|||
} |
|||
} |
|||
}, |
|||
"NoticeIconList": { |
|||
"type": "object", |
|||
"properties": { |
|||
"data": { |
|||
"type": "array", |
|||
"items": { |
|||
"$ref": "#/components/schemas/NoticeIconItem" |
|||
} |
|||
}, |
|||
"total": { |
|||
"type": "integer", |
|||
"description": "列表的内容总数", |
|||
"format": "int32" |
|||
}, |
|||
"success": { |
|||
"type": "boolean" |
|||
} |
|||
} |
|||
}, |
|||
"NoticeIconItemType": { |
|||
"title": "NoticeIconItemType", |
|||
"description": "已读未读列表的枚举", |
|||
"type": "string", |
|||
"properties": {}, |
|||
"enum": ["notification", "message", "event"] |
|||
}, |
|||
"NoticeIconItem": { |
|||
"type": "object", |
|||
"properties": { |
|||
"id": { |
|||
"type": "string" |
|||
}, |
|||
"extra": { |
|||
"type": "string", |
|||
"format": "any" |
|||
}, |
|||
"key": { "type": "string" }, |
|||
"read": { |
|||
"type": "boolean" |
|||
}, |
|||
"avatar": { |
|||
"type": "string" |
|||
}, |
|||
"title": { |
|||
"type": "string" |
|||
}, |
|||
"status": { |
|||
"type": "string" |
|||
}, |
|||
"datetime": { |
|||
"type": "string", |
|||
"format": "date" |
|||
}, |
|||
"description": { |
|||
"type": "string" |
|||
}, |
|||
"type": { |
|||
"extensions": { |
|||
"x-is-enum": true |
|||
}, |
|||
"$ref": "#/components/schemas/NoticeIconItemType" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
Before Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,9 @@ |
|||
/** |
|||
* @see https://umijs.org/zh-CN/plugins/plugin-access
|
|||
* */ |
|||
export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) { |
|||
const { currentUser } = initialState || {}; |
|||
return { |
|||
canAdmin: currentUser && currentUser.access === 'admin', |
|||
}; |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
import type { Settings as LayoutSettings } from '@ant-design/pro-layout'; |
|||
import { PageLoading } from '@ant-design/pro-layout'; |
|||
import { notification } from 'antd'; |
|||
import type { RequestConfig, RunTimeLayoutConfig } from 'umi'; |
|||
import { history, Link } from 'umi'; |
|||
import RightContent from '@/components/RightContent'; |
|||
import Footer from '@/components/Footer'; |
|||
import { currentUser as queryCurrentUser } from './services/ant-design-pro/api'; |
|||
import { BookOutlined, LinkOutlined } from '@ant-design/icons'; |
|||
|
|||
const isDev = process.env.NODE_ENV === 'development'; |
|||
const loginPath = '/user/login'; |
|||
|
|||
/** 获取用户信息比较慢的时候会展示一个 loading */ |
|||
export const initialStateConfig = { |
|||
loading: <PageLoading />, |
|||
}; |
|||
|
|||
/** |
|||
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
|
|||
* */ |
|||
export async function getInitialState(): Promise<{ |
|||
settings?: Partial<LayoutSettings>; |
|||
currentUser?: API.CurrentUser; |
|||
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>; |
|||
}> { |
|||
const fetchUserInfo = async () => { |
|||
try { |
|||
const currentUser = await queryCurrentUser(); |
|||
return currentUser; |
|||
} catch (error) { |
|||
history.push(loginPath); |
|||
} |
|||
return undefined; |
|||
}; |
|||
// 如果是登录页面,不执行
|
|||
if (history.location.pathname !== loginPath) { |
|||
const currentUser = await fetchUserInfo(); |
|||
return { |
|||
fetchUserInfo, |
|||
currentUser, |
|||
settings: {}, |
|||
}; |
|||
} |
|||
return { |
|||
fetchUserInfo, |
|||
settings: {}, |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* 异常处理程序 |
|||
200: '服务器成功返回请求的数据。', |
|||
201: '新建或修改数据成功。', |
|||
202: '一个请求已经进入后台排队(异步任务)。', |
|||
204: '删除数据成功。', |
|||
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', |
|||
401: '用户没有权限(令牌、用户名、密码错误)。', |
|||
403: '用户得到授权,但是访问是被禁止的。', |
|||
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', |
|||
405: '请求方法不被允许。', |
|||
406: '请求的格式不可得。', |
|||
410: '请求的资源被永久删除,且不会再得到的。', |
|||
422: '当创建一个对象时,发生一个验证错误。', |
|||
500: '服务器发生错误,请检查服务器。', |
|||
502: '网关错误。', |
|||
503: '服务不可用,服务器暂时过载或维护。', |
|||
504: '网关超时。', |
|||
//-----English
|
|||
200: The server successfully returned the requested data. ', |
|||
201: New or modified data is successful. ', |
|||
202: A request has entered the background queue (asynchronous task). ', |
|||
204: Data deleted successfully. ', |
|||
400: 'There was an error in the request sent, and the server did not create or modify data. ', |
|||
401: The user does not have permission (token, username, password error). ', |
|||
403: The user is authorized, but access is forbidden. ', |
|||
404: The request sent was for a record that did not exist. ', |
|||
405: The request method is not allowed. ', |
|||
406: The requested format is not available. ', |
|||
410': |
|||
'The requested resource is permanently deleted and will no longer be available. ', |
|||
422: When creating an object, a validation error occurred. ', |
|||
500: An error occurred on the server, please check the server. ', |
|||
502: Gateway error. ', |
|||
503: The service is unavailable. ', |
|||
504: The gateway timed out. ', |
|||
* @see https://beta-pro.ant.design/docs/request-cn
|
|||
*/ |
|||
export const request: RequestConfig = { |
|||
errorHandler: (error: any) => { |
|||
const { response } = error; |
|||
|
|||
if (!response) { |
|||
notification.error({ |
|||
description: '您的网络发生异常,无法连接服务器', |
|||
message: '网络异常', |
|||
}); |
|||
} |
|||
throw error; |
|||
}, |
|||
}; |
|||
|
|||
// ProLayout 支持的api https://procomponents.ant.design/components/layout
|
|||
export const layout: RunTimeLayoutConfig = ({ initialState }) => { |
|||
return { |
|||
rightContentRender: () => <RightContent />, |
|||
disableContentMargin: false, |
|||
waterMarkProps: { |
|||
content: initialState?.currentUser?.name, |
|||
}, |
|||
footerRender: () => <Footer />, |
|||
onPageChange: () => { |
|||
const { location } = history; |
|||
// 如果没有登录,重定向到 login
|
|||
if (!initialState?.currentUser && location.pathname !== loginPath) { |
|||
history.push(loginPath); |
|||
} |
|||
}, |
|||
links: isDev |
|||
? [ |
|||
<Link to="/umi/plugin/openapi" target="_blank"> |
|||
<LinkOutlined /> |
|||
<span>OpenAPI 文档</span> |
|||
</Link>, |
|||
<Link to="/~docs"> |
|||
<BookOutlined /> |
|||
<span>业务组件文档</span> |
|||
</Link>, |
|||
] |
|||
: [], |
|||
menuHeaderRender: undefined, |
|||
// 自定义 403 页面
|
|||
// unAccessible: <div>unAccessible</div>,
|
|||
...initialState?.settings, |
|||
}; |
|||
}; |
|||
@ -1,35 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Result } from 'antd'; |
|||
import check from './CheckPermissions'; |
|||
import type { IAuthorityType } from './CheckPermissions'; |
|||
import type AuthorizedRoute from './AuthorizedRoute'; |
|||
import type Secured from './Secured'; |
|||
|
|||
type AuthorizedProps = { |
|||
authority: IAuthorityType; |
|||
noMatch?: React.ReactNode; |
|||
}; |
|||
|
|||
type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & { |
|||
Secured: typeof Secured; |
|||
check: typeof check; |
|||
AuthorizedRoute: typeof AuthorizedRoute; |
|||
}; |
|||
|
|||
const Authorized: React.FunctionComponent<AuthorizedProps> = ({ |
|||
children, |
|||
authority, |
|||
noMatch = ( |
|||
<Result |
|||
status="403" |
|||
title="403" |
|||
subTitle="Sorry, you are not authorized to access this page." |
|||
/> |
|||
), |
|||
}) => { |
|||
const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; |
|||
const dom = check(authority, childrenRender, noMatch); |
|||
return <>{dom}</>; |
|||
}; |
|||
|
|||
export default Authorized as IAuthorizedType; |
|||
@ -1,33 +0,0 @@ |
|||
import { Redirect, Route } from 'umi'; |
|||
|
|||
import React from 'react'; |
|||
import Authorized from './Authorized'; |
|||
import type { IAuthorityType } from './CheckPermissions'; |
|||
|
|||
type AuthorizedRouteProps = { |
|||
currentAuthority: string; |
|||
component: React.ComponentClass<any, any>; |
|||
render: (props: any) => React.ReactNode; |
|||
redirectPath: string; |
|||
authority: IAuthorityType; |
|||
}; |
|||
|
|||
const AuthorizedRoute: React.SFC<AuthorizedRouteProps> = ({ |
|||
component: Component, |
|||
render, |
|||
authority, |
|||
redirectPath, |
|||
...rest |
|||
}) => ( |
|||
<Authorized |
|||
authority={authority} |
|||
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />} |
|||
> |
|||
<Route |
|||
{...rest} |
|||
render={(props: any) => (Component ? <Component {...props} /> : render(props))} |
|||
/> |
|||
</Authorized> |
|||
); |
|||
|
|||
export default AuthorizedRoute; |
|||
@ -1,88 +0,0 @@ |
|||
import React from 'react'; |
|||
import { CURRENT } from './renderAuthorize'; |
|||
// eslint-disable-next-line import/no-cycle
|
|||
import PromiseRender from './PromiseRender'; |
|||
|
|||
export type IAuthorityType = |
|||
| undefined |
|||
| string |
|||
| string[] |
|||
| Promise<boolean> |
|||
| ((currentAuthority: string | string[]) => IAuthorityType); |
|||
|
|||
/** |
|||
* @en-US |
|||
* General permission check method |
|||
* Common check permissions method |
|||
* @param {Permission judgment} authority |
|||
* @param {Your permission | Your permission description} currentAuthority |
|||
* @param {Passing components} target |
|||
* @param {no pass components | no pass components} Exception |
|||
* ------------------------------------------------------- |
|||
* @zh-CN |
|||
* 通用权限检查方法 Common check permissions method |
|||
* |
|||
* @param { 权限判定 | Permission judgment } authority |
|||
* @param { 你的权限 | Your permission description } currentAuthority |
|||
* @param { 通过的组件 | Passing components } target |
|||
* @param { 未通过的组件 | no pass components } Exception |
|||
*/ |
|||
const checkPermissions = <T, K>( |
|||
authority: IAuthorityType, |
|||
currentAuthority: string | string[], |
|||
target: T, |
|||
Exception: K, |
|||
): T | K | React.ReactNode => { |
|||
// No judgment permission. View all by default
|
|||
// Retirement authority, return target;
|
|||
if (!authority) { |
|||
return target; |
|||
} |
|||
// Array processing
|
|||
if (Array.isArray(authority)) { |
|||
if (Array.isArray(currentAuthority)) { |
|||
if (currentAuthority.some((item) => authority.includes(item))) { |
|||
return target; |
|||
} |
|||
} else if (authority.includes(currentAuthority)) { |
|||
return target; |
|||
} |
|||
return Exception; |
|||
} |
|||
// Deal with string
|
|||
if (typeof authority === 'string') { |
|||
if (Array.isArray(currentAuthority)) { |
|||
if (currentAuthority.some((item) => authority === item)) { |
|||
return target; |
|||
} |
|||
} else if (authority === currentAuthority) { |
|||
return target; |
|||
} |
|||
return Exception; |
|||
} |
|||
// Deal with promise
|
|||
if (authority instanceof Promise) { |
|||
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />; |
|||
} |
|||
// Deal with function
|
|||
if (typeof authority === 'function') { |
|||
const bool = authority(currentAuthority); |
|||
// The return value after the function is executed is Promise
|
|||
if (bool instanceof Promise) { |
|||
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />; |
|||
} |
|||
if (bool) { |
|||
return target; |
|||
} |
|||
return Exception; |
|||
} |
|||
throw new Error('unsupported parameters'); |
|||
}; |
|||
|
|||
export { checkPermissions }; |
|||
|
|||
function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode { |
|||
return checkPermissions<T, K>(authority, CURRENT, target, Exception); |
|||
} |
|||
|
|||
export default check; |
|||
@ -1,96 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Spin } from 'antd'; |
|||
import isEqual from 'lodash/isEqual'; |
|||
import { isComponentClass } from './Secured'; |
|||
// eslint-disable-next-line import/no-cycle
|
|||
|
|||
type PromiseRenderProps<T, K> = { |
|||
ok: T; |
|||
error: K; |
|||
promise: Promise<boolean>; |
|||
}; |
|||
|
|||
type PromiseRenderState = { |
|||
component: React.ComponentClass | React.FunctionComponent; |
|||
}; |
|||
|
|||
export default class PromiseRender<T, K> extends React.Component< |
|||
PromiseRenderProps<T, K>, |
|||
PromiseRenderState |
|||
> { |
|||
state: PromiseRenderState = { |
|||
component: () => null, |
|||
}; |
|||
|
|||
componentDidMount(): void { |
|||
this.setRenderComponent(this.props); |
|||
} |
|||
|
|||
shouldComponentUpdate = ( |
|||
nextProps: PromiseRenderProps<T, K>, |
|||
nextState: PromiseRenderState, |
|||
): boolean => { |
|||
const { component } = this.state; |
|||
if (!isEqual(nextProps, this.props)) { |
|||
this.setRenderComponent(nextProps); |
|||
} |
|||
if (nextState.component !== component) return true; |
|||
return false; |
|||
}; |
|||
|
|||
// set render Component : ok or error
|
|||
setRenderComponent(props: PromiseRenderProps<T, K>): void { |
|||
const ok = this.checkIsInstantiation(props.ok); |
|||
const error = this.checkIsInstantiation(props.error); |
|||
props.promise |
|||
.then(() => { |
|||
this.setState({ |
|||
component: ok, |
|||
}); |
|||
return true; |
|||
}) |
|||
.catch(() => { |
|||
this.setState({ |
|||
component: error, |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
// Determine whether the incoming component has been instantiated
|
|||
// AuthorizedRoute is already instantiated
|
|||
// Authorized render is already instantiated, children is no instantiated
|
|||
// Secured is not instantiated
|
|||
checkIsInstantiation = ( |
|||
target: React.ReactNode | React.ComponentClass, |
|||
): React.FunctionComponent => { |
|||
if (isComponentClass(target)) { |
|||
const Target = target as React.ComponentClass; |
|||
return (props: any) => <Target {...props} />; |
|||
} |
|||
if (React.isValidElement(target)) { |
|||
return (props: any) => React.cloneElement(target, props); |
|||
} |
|||
return () => target as React.ReactNode & null; |
|||
}; |
|||
|
|||
render() { |
|||
const { component: Component } = this.state; |
|||
const { ok, error, promise, ...rest } = this.props; |
|||
|
|||
return Component ? ( |
|||
<Component {...rest} /> |
|||
) : ( |
|||
<div |
|||
style={{ |
|||
width: '100%', |
|||
height: '100%', |
|||
margin: 'auto', |
|||
paddingTop: 50, |
|||
textAlign: 'center', |
|||
}} |
|||
> |
|||
<Spin size="large" /> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
@ -1,80 +0,0 @@ |
|||
import React from 'react'; |
|||
import CheckPermissions from './CheckPermissions'; |
|||
|
|||
/** |
|||
* @en-US No pages can be accessed by default,default is "NULL" |
|||
* @zh-CN 默认不能访问任何页面 default is "NULL" |
|||
* */ |
|||
const Exception403 = () => 403; |
|||
|
|||
export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => { |
|||
if (!component) return false; |
|||
const proto = Object.getPrototypeOf(component); |
|||
if (proto === React.Component || proto === Function.prototype) return true; |
|||
return isComponentClass(proto); |
|||
}; |
|||
|
|||
// Determine whether the incoming component has been instantiated
|
|||
// AuthorizedRoute is already instantiated
|
|||
// Authorized render is already instantiated, children is no instantiated
|
|||
// Secured is not instantiated
|
|||
const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { |
|||
if (isComponentClass(target)) { |
|||
const Target = target as React.ComponentClass; |
|||
return (props: any) => <Target {...props} />; |
|||
} |
|||
if (React.isValidElement(target)) { |
|||
return (props: any) => React.cloneElement(target, props); |
|||
} |
|||
return () => target; |
|||
}; |
|||
|
|||
/** |
|||
* @en-US |
|||
* Used to determine whether you have permission to access this view permission |
|||
* authority supports incoming string, () => boolean | Promise |
|||
* e.g.'user' Only user user can access |
|||
* e.g.'user,admin' user and admin can access |
|||
* e.g. ()=>boolean return true to access, return false to not access |
|||
* e.g. Promise then can be accessed, catch can not be accessed |
|||
* e.g. authority support incoming string, () => boolean | Promise |
|||
* e.g.'user' only user user can access |
|||
* e.g.'user, admin' user and admin can access |
|||
* e.g. () => boolean true to be able to visit, return false can not be accessed |
|||
* e.g. Promise then can not access the visit to catch |
|||
*------------------------------------------------------------- |
|||
* @zh-CN |
|||
* 用于判断是否拥有权限访问此 view 权限 authority 支持传入 string, () => boolean | Promise e.g. 'user' 只有 user 用户能访问 |
|||
* e.g. 'user,admin' user 和 admin 都能访问 e.g. ()=>boolean 返回true能访问,返回false不能访问 e.g. Promise then 能访问 |
|||
* catch不能访问 e.g. authority support incoming string, () => boolean | Promise e.g. 'user' only user |
|||
* user can access e.g. 'user, admin' user and admin can access e.g. () => boolean true to be able |
|||
* to visit, return false can not be accessed e.g. Promise then can not access the visit to catch |
|||
* |
|||
* @param {string | function | Promise} authority |
|||
* @param {ReactNode} error non-required parameter |
|||
*/ |
|||
const authorize = (authority: string, error?: React.ReactNode) => { |
|||
/** |
|||
* @en-US |
|||
* conversion into a class |
|||
* Prevent the staticContext from being found to cause an error when the string is passed in |
|||
* String parameters can cause staticContext not found error |
|||
*------------------------------------------------------------- |
|||
* @zh-CN |
|||
* Conversion into a class 防止传入字符串时找不到staticContext造成报错 String parameters can cause staticContext |
|||
* not found error |
|||
*/ |
|||
let classError: boolean | React.FunctionComponent = false; |
|||
if (error) { |
|||
classError = (() => error) as React.FunctionComponent; |
|||
} |
|||
if (!authority) { |
|||
throw new Error('authority is required'); |
|||
} |
|||
return function decideAuthority(target: React.ComponentClass | React.ReactNode) { |
|||
const component = CheckPermissions(authority, target, classError || Exception403); |
|||
return checkIsInstantiation(component); |
|||
}; |
|||
}; |
|||
|
|||
export default authorize; |
|||
@ -1,11 +0,0 @@ |
|||
import Authorized from './Authorized'; |
|||
import Secured from './Secured'; |
|||
import check from './CheckPermissions'; |
|||
import renderAuthorize from './renderAuthorize'; |
|||
|
|||
Authorized.Secured = Secured; |
|||
Authorized.check = check; |
|||
|
|||
const RenderAuthorize = renderAuthorize(Authorized); |
|||
|
|||
export default RenderAuthorize; |
|||
@ -1,31 +0,0 @@ |
|||
/* eslint-disable eslint-comments/disable-enable-pair */ |
|||
/* eslint-disable import/no-mutable-exports */ |
|||
let CURRENT: string | string[] = 'NULL'; |
|||
|
|||
type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); |
|||
/** |
|||
* Use authority or getAuthority |
|||
* |
|||
* @param {string|()=>String} currentAuthority |
|||
*/ |
|||
const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( |
|||
currentAuthority: CurrentAuthorityType, |
|||
): T => { |
|||
if (currentAuthority) { |
|||
if (typeof currentAuthority === 'function') { |
|||
CURRENT = currentAuthority(); |
|||
} |
|||
if ( |
|||
Object.prototype.toString.call(currentAuthority) === '[object String]' || |
|||
Array.isArray(currentAuthority) |
|||
) { |
|||
CURRENT = currentAuthority as string[]; |
|||
} |
|||
} else { |
|||
CURRENT = 'NULL'; |
|||
} |
|||
return Authorized; |
|||
}; |
|||
|
|||
export { CURRENT }; |
|||
export default <T>(Authorized: T) => renderAuthorize<T>(Authorized); |
|||
@ -0,0 +1,37 @@ |
|||
import { useIntl } from 'umi'; |
|||
import { GithubOutlined } from '@ant-design/icons'; |
|||
import { DefaultFooter } from '@ant-design/pro-layout'; |
|||
|
|||
export default () => { |
|||
const intl = useIntl(); |
|||
const defaultMessage = intl.formatMessage({ |
|||
id: 'app.copyright.produced', |
|||
defaultMessage: '蚂蚁集团体验技术部出品', |
|||
}); |
|||
|
|||
return ( |
|||
<DefaultFooter |
|||
copyright={`2020 ${defaultMessage}`} |
|||
links={[ |
|||
{ |
|||
key: 'Ant Design Pro', |
|||
title: 'Ant Design Pro', |
|||
href: 'https://pro.ant.design', |
|||
blankTarget: true, |
|||
}, |
|||
{ |
|||
key: 'github', |
|||
title: <GithubOutlined />, |
|||
href: 'https://github.com/ant-design/ant-design-pro', |
|||
blankTarget: true, |
|||
}, |
|||
{ |
|||
key: 'Ant Design', |
|||
title: 'Ant Design', |
|||
href: 'https://ant.design', |
|||
blankTarget: true, |
|||
}, |
|||
]} |
|||
/> |
|||
); |
|||
}; |
|||
@ -1,88 +0,0 @@ |
|||
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; |
|||
import { Avatar, Menu, Spin } from 'antd'; |
|||
import React from 'react'; |
|||
import type { ConnectProps } from 'umi'; |
|||
import { history, connect } from 'umi'; |
|||
import type { ConnectState } from '@/models/connect'; |
|||
import type { CurrentUser } from '@/models/user'; |
|||
import HeaderDropdown from '../HeaderDropdown'; |
|||
import styles from './index.less'; |
|||
|
|||
export type GlobalHeaderRightProps = { |
|||
currentUser?: CurrentUser; |
|||
menu?: boolean; |
|||
} & Partial<ConnectProps>; |
|||
|
|||
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> { |
|||
onMenuClick = (event: { key: React.Key; keyPath: React.Key[]; item: React.ReactInstance }) => { |
|||
const { key } = event; |
|||
|
|||
if (key === 'logout') { |
|||
const { dispatch } = this.props; |
|||
|
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'login/logout', |
|||
}); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
history.push(`/account/${key}`); |
|||
}; |
|||
|
|||
render(): React.ReactNode { |
|||
const { |
|||
currentUser = { |
|||
avatar: '', |
|||
name: '', |
|||
}, |
|||
menu, |
|||
} = this.props; |
|||
const menuHeaderDropdown = ( |
|||
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}> |
|||
{menu && ( |
|||
<Menu.Item key="center"> |
|||
<UserOutlined /> |
|||
个人中心 |
|||
</Menu.Item> |
|||
)} |
|||
{menu && ( |
|||
<Menu.Item key="settings"> |
|||
<SettingOutlined /> |
|||
个人设置 |
|||
</Menu.Item> |
|||
)} |
|||
{menu && <Menu.Divider />} |
|||
|
|||
<Menu.Item key="logout"> |
|||
<LogoutOutlined /> |
|||
退出登录 |
|||
</Menu.Item> |
|||
</Menu> |
|||
); |
|||
return currentUser && currentUser.name ? ( |
|||
<HeaderDropdown overlay={menuHeaderDropdown}> |
|||
<span className={`${styles.action} ${styles.account}`}> |
|||
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" /> |
|||
<span className={`${styles.name} anticon`}>{currentUser.name}</span> |
|||
</span> |
|||
</HeaderDropdown> |
|||
) : ( |
|||
<span className={`${styles.action} ${styles.account}`}> |
|||
<Spin |
|||
size="small" |
|||
style={{ |
|||
marginLeft: 8, |
|||
marginRight: 8, |
|||
}} |
|||
/> |
|||
</span> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default connect(({ user }: ConnectState) => ({ |
|||
currentUser: user.currentUser, |
|||
}))(AvatarDropdown); |
|||
@ -1,168 +0,0 @@ |
|||
import { Component } from 'react'; |
|||
import type { ConnectProps } from 'umi'; |
|||
import { connect } from 'umi'; |
|||
import { Tag, message } from 'antd'; |
|||
import groupBy from 'lodash/groupBy'; |
|||
import moment from 'moment'; |
|||
import type { NoticeItem } from '@/models/global'; |
|||
import type { CurrentUser } from '@/models/user'; |
|||
import type { ConnectState } from '@/models/connect'; |
|||
import NoticeIcon from '../NoticeIcon'; |
|||
import styles from './index.less'; |
|||
|
|||
export type GlobalHeaderRightProps = { |
|||
notices?: NoticeItem[]; |
|||
currentUser?: CurrentUser; |
|||
fetchingNotices?: boolean; |
|||
onNoticeVisibleChange?: (visible: boolean) => void; |
|||
onNoticeClear?: (tabName?: string) => void; |
|||
} & Partial<ConnectProps>; |
|||
|
|||
class GlobalHeaderRight extends Component<GlobalHeaderRightProps> { |
|||
componentDidMount() { |
|||
const { dispatch } = this.props; |
|||
|
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'global/fetchNotices', |
|||
}); |
|||
} |
|||
} |
|||
|
|||
changeReadState = (clickedItem: NoticeItem): void => { |
|||
const { id } = clickedItem; |
|||
const { dispatch } = this.props; |
|||
|
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'global/changeNoticeReadState', |
|||
payload: id, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
handleNoticeClear = (title: string, key: string) => { |
|||
const { dispatch } = this.props; |
|||
message.success(`${'Emptied'} ${title}`); |
|||
|
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'global/clearNotices', |
|||
payload: key, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
getNoticeData = (): Record<string, NoticeItem[]> => { |
|||
const { notices = [] } = this.props; |
|||
|
|||
if (!notices || notices.length === 0 || !Array.isArray(notices)) { |
|||
return {}; |
|||
} |
|||
|
|||
const newNotices = notices.map((notice) => { |
|||
const newNotice = { ...notice }; |
|||
|
|||
if (newNotice.datetime) { |
|||
newNotice.datetime = moment(notice.datetime as string).fromNow(); |
|||
} |
|||
|
|||
if (newNotice.id) { |
|||
newNotice.key = newNotice.id; |
|||
} |
|||
|
|||
if (newNotice.extra && newNotice.status) { |
|||
const color = { |
|||
todo: '', |
|||
processing: 'blue', |
|||
urgent: 'red', |
|||
doing: 'gold', |
|||
}[newNotice.status]; |
|||
newNotice.extra = ( |
|||
<Tag |
|||
color={color} |
|||
style={{ |
|||
marginRight: 0, |
|||
}} |
|||
> |
|||
{newNotice.extra} |
|||
</Tag> |
|||
); |
|||
} |
|||
|
|||
return newNotice; |
|||
}); |
|||
return groupBy(newNotices, 'type'); |
|||
}; |
|||
|
|||
getUnreadData = (noticeData: Record<string, NoticeItem[]>) => { |
|||
const unreadMsg: Record<string, number> = {}; |
|||
Object.keys(noticeData).forEach((key) => { |
|||
const value = noticeData[key]; |
|||
|
|||
if (!unreadMsg[key]) { |
|||
unreadMsg[key] = 0; |
|||
} |
|||
|
|||
if (Array.isArray(value)) { |
|||
unreadMsg[key] = value.filter((item) => !item.read).length; |
|||
} |
|||
}); |
|||
return unreadMsg; |
|||
}; |
|||
|
|||
render() { |
|||
const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props; |
|||
const noticeData = this.getNoticeData(); |
|||
const unreadMsg = this.getUnreadData(noticeData); |
|||
return ( |
|||
<NoticeIcon |
|||
className={styles.action} |
|||
count={currentUser && currentUser.unreadCount} |
|||
onItemClick={(item) => { |
|||
this.changeReadState(item as NoticeItem); |
|||
}} |
|||
loading={fetchingNotices} |
|||
clearText="Empty" |
|||
viewMoreText="See more" |
|||
onClear={this.handleNoticeClear} |
|||
onPopupVisibleChange={onNoticeVisibleChange} |
|||
onViewMore={() => message.info('Click on view more')} |
|||
clearClose |
|||
> |
|||
<NoticeIcon.Tab |
|||
tabKey="notification" |
|||
count={unreadMsg.notification} |
|||
list={noticeData.notification} |
|||
title="Notification" |
|||
emptyText="You have viewed all notifications" |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="message" |
|||
count={unreadMsg.message} |
|||
list={noticeData.message} |
|||
title="Message" |
|||
emptyText="You have read all messages" |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="event" |
|||
title="To do" |
|||
emptyText="You have completed all to-dos" |
|||
count={unreadMsg.event} |
|||
list={noticeData.event} |
|||
showViewMore |
|||
/> |
|||
</NoticeIcon> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default connect(({ user, global, loading }: ConnectState) => ({ |
|||
currentUser: user.currentUser, |
|||
collapsed: global.collapsed, |
|||
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], |
|||
fetchingNotices: loading.effects['global/fetchNotices'], |
|||
notices: global.notices, |
|||
}))(GlobalHeaderRight); |
|||
@ -1,83 +0,0 @@ |
|||
import { Tooltip, Tag } from 'antd'; |
|||
import type { Settings as ProSettings } from '@ant-design/pro-layout'; |
|||
import { QuestionCircleOutlined } from '@ant-design/icons'; |
|||
import React from 'react'; |
|||
import type { ConnectProps } from 'umi'; |
|||
import { connect, SelectLang } from 'umi'; |
|||
import type { ConnectState } from '@/models/connect'; |
|||
import Avatar from './AvatarDropdown'; |
|||
import HeaderSearch from '../HeaderSearch'; |
|||
import styles from './index.less'; |
|||
|
|||
export type GlobalHeaderRightProps = { |
|||
theme?: ProSettings['navTheme'] | 'realDark'; |
|||
} & Partial<ConnectProps> & |
|||
Partial<ProSettings>; |
|||
|
|||
const ENVTagColor = { |
|||
dev: 'orange', |
|||
test: 'green', |
|||
pre: '#87d068', |
|||
}; |
|||
|
|||
const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = (props) => { |
|||
const { theme, layout } = props; |
|||
let className = styles.right; |
|||
|
|||
if (theme === 'dark' && layout === 'top') { |
|||
className = `${styles.right} ${styles.dark}`; |
|||
} |
|||
|
|||
return ( |
|||
<div className={className}> |
|||
<HeaderSearch |
|||
className={`${styles.action} ${styles.search}`} |
|||
placeholder="Site Search" |
|||
defaultValue="umi ui" |
|||
options={[ |
|||
{ label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' }, |
|||
{ |
|||
label: <a href="next.ant.design">Ant Design</a>, |
|||
value: 'Ant Design', |
|||
}, |
|||
{ |
|||
label: <a href="https://protable.ant.design/">Pro Table</a>, |
|||
value: 'Pro Table', |
|||
}, |
|||
{ |
|||
label: <a href="https://prolayout.ant.design/">Pro Layout</a>, |
|||
value: 'Pro Layout', |
|||
}, |
|||
]} |
|||
// onSearch={value => {
|
|||
// //console.log('input', value);
|
|||
// }}
|
|||
/> |
|||
<Tooltip title="Use documentation"> |
|||
<a |
|||
style={{ |
|||
color: 'inherit', |
|||
}} |
|||
target="_blank" |
|||
href="https://pro.ant.design/docs/getting-started" |
|||
rel="noopener noreferrer" |
|||
className={styles.action} |
|||
> |
|||
<QuestionCircleOutlined /> |
|||
</a> |
|||
</Tooltip> |
|||
<Avatar /> |
|||
{REACT_APP_ENV && ( |
|||
<span> |
|||
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag> |
|||
</span> |
|||
)} |
|||
<SelectLang className={styles.action} /> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default connect(({ settings }: ConnectState) => ({ |
|||
theme: settings.navTheme, |
|||
layout: settings.layout, |
|||
}))(GlobalHeaderRight); |
|||
@ -0,0 +1,126 @@ |
|||
import { BellOutlined } from '@ant-design/icons'; |
|||
import { Badge, Spin, Tabs } from 'antd'; |
|||
import useMergedState from 'rc-util/es/hooks/useMergedState'; |
|||
import React from 'react'; |
|||
import classNames from 'classnames'; |
|||
import type { NoticeIconTabProps } from './NoticeList'; |
|||
import NoticeList from './NoticeList'; |
|||
import HeaderDropdown from '../HeaderDropdown'; |
|||
import styles from './index.less'; |
|||
|
|||
const { TabPane } = Tabs; |
|||
|
|||
export type NoticeIconProps = { |
|||
count?: number; |
|||
bell?: React.ReactNode; |
|||
className?: string; |
|||
loading?: boolean; |
|||
onClear?: (tabName: string, tabKey: string) => void; |
|||
onItemClick?: (item: API.NoticeIconItem, tabProps: NoticeIconTabProps) => void; |
|||
onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void; |
|||
onTabChange?: (tabTile: string) => void; |
|||
style?: React.CSSProperties; |
|||
onPopupVisibleChange?: (visible: boolean) => void; |
|||
popupVisible?: boolean; |
|||
clearText?: string; |
|||
viewMoreText?: string; |
|||
clearClose?: boolean; |
|||
emptyImage?: string; |
|||
children?: React.ReactElement<NoticeIconTabProps>[]; |
|||
}; |
|||
|
|||
const NoticeIcon: React.FC<NoticeIconProps> & { |
|||
Tab: typeof NoticeList; |
|||
} = (props) => { |
|||
const getNotificationBox = (): React.ReactNode => { |
|||
const { |
|||
children, |
|||
loading, |
|||
onClear, |
|||
onTabChange, |
|||
onItemClick, |
|||
onViewMore, |
|||
clearText, |
|||
viewMoreText, |
|||
} = props; |
|||
if (!children) { |
|||
return null; |
|||
} |
|||
const panes: React.ReactNode[] = []; |
|||
React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => { |
|||
if (!child) { |
|||
return; |
|||
} |
|||
const { list, title, count, tabKey, showClear, showViewMore } = child.props; |
|||
const len = list && list.length ? list.length : 0; |
|||
const msgCount = count || count === 0 ? count : len; |
|||
const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title; |
|||
panes.push( |
|||
<TabPane tab={tabTitle} key={tabKey}> |
|||
<NoticeList |
|||
clearText={clearText} |
|||
viewMoreText={viewMoreText} |
|||
list={list} |
|||
tabKey={tabKey} |
|||
onClear={(): void => onClear && onClear(title, tabKey)} |
|||
onClick={(item): void => onItemClick && onItemClick(item, child.props)} |
|||
onViewMore={(event): void => onViewMore && onViewMore(child.props, event)} |
|||
showClear={showClear} |
|||
showViewMore={showViewMore} |
|||
title={title} |
|||
/> |
|||
</TabPane>, |
|||
); |
|||
}); |
|||
return ( |
|||
<> |
|||
<Spin spinning={loading} delay={300}> |
|||
<Tabs className={styles.tabs} onChange={onTabChange}> |
|||
{panes} |
|||
</Tabs> |
|||
</Spin> |
|||
</> |
|||
); |
|||
}; |
|||
|
|||
const { className, count, bell } = props; |
|||
|
|||
const [visible, setVisible] = useMergedState<boolean>(false, { |
|||
value: props.popupVisible, |
|||
onChange: props.onPopupVisibleChange, |
|||
}); |
|||
const noticeButtonClass = classNames(className, styles.noticeButton); |
|||
const notificationBox = getNotificationBox(); |
|||
const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />; |
|||
const trigger = ( |
|||
<span className={classNames(noticeButtonClass, { opened: visible })}> |
|||
<Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}> |
|||
{NoticeBellIcon} |
|||
</Badge> |
|||
</span> |
|||
); |
|||
if (!notificationBox) { |
|||
return trigger; |
|||
} |
|||
|
|||
return ( |
|||
<HeaderDropdown |
|||
placement="bottomRight" |
|||
overlay={notificationBox} |
|||
overlayClassName={styles.popover} |
|||
trigger={['click']} |
|||
visible={visible} |
|||
onVisibleChange={setVisible} |
|||
> |
|||
{trigger} |
|||
</HeaderDropdown> |
|||
); |
|||
}; |
|||
|
|||
NoticeIcon.defaultProps = { |
|||
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', |
|||
}; |
|||
|
|||
NoticeIcon.Tab = NoticeList; |
|||
|
|||
export default NoticeIcon; |
|||
@ -1,142 +1,152 @@ |
|||
import { BellOutlined } from '@ant-design/icons'; |
|||
import { Badge, Spin, Tabs } from 'antd'; |
|||
import useMergedState from 'rc-util/es/hooks/useMergedState'; |
|||
import React from 'react'; |
|||
import classNames from 'classnames'; |
|||
import type { NoticeIconTabProps } from './NoticeList'; |
|||
import NoticeList from './NoticeList'; |
|||
|
|||
import HeaderDropdown from '../HeaderDropdown'; |
|||
import { useEffect, useState } from 'react'; |
|||
import { Tag, message } from 'antd'; |
|||
import { groupBy } from 'lodash'; |
|||
import moment from 'moment'; |
|||
import { useModel } from 'umi'; |
|||
import { getNotices } from '@/services/ant-design-pro/api'; |
|||
|
|||
import NoticeIcon from './NoticeIcon'; |
|||
import styles from './index.less'; |
|||
|
|||
const { TabPane } = Tabs; |
|||
|
|||
export type NoticeIconData = { |
|||
avatar?: string | React.ReactNode; |
|||
title?: React.ReactNode; |
|||
description?: React.ReactNode; |
|||
datetime?: React.ReactNode; |
|||
extra?: React.ReactNode; |
|||
style?: React.CSSProperties; |
|||
key?: string | number; |
|||
read?: boolean; |
|||
export type GlobalHeaderRightProps = { |
|||
fetchingNotices?: boolean; |
|||
onNoticeVisibleChange?: (visible: boolean) => void; |
|||
onNoticeClear?: (tabName?: string) => void; |
|||
}; |
|||
|
|||
export type NoticeIconProps = { |
|||
count?: number; |
|||
bell?: React.ReactNode; |
|||
className?: string; |
|||
loading?: boolean; |
|||
onClear?: (tabName: string, tabKey: string) => void; |
|||
onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void; |
|||
onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void; |
|||
onTabChange?: (tabTile: string) => void; |
|||
style?: React.CSSProperties; |
|||
onPopupVisibleChange?: (visible: boolean) => void; |
|||
popupVisible?: boolean; |
|||
clearText?: string; |
|||
viewMoreText?: string; |
|||
clearClose?: boolean; |
|||
emptyImage?: string; |
|||
children: React.ReactElement<NoticeIconTabProps>[]; |
|||
}; |
|||
const getNoticeData = (notices: API.NoticeIconItem[]): Record<string, API.NoticeIconItem[]> => { |
|||
if (!notices || notices.length === 0 || !Array.isArray(notices)) { |
|||
return {}; |
|||
} |
|||
|
|||
const NoticeIcon: React.FC<NoticeIconProps> & { |
|||
Tab: typeof NoticeList; |
|||
} = (props) => { |
|||
const getNotificationBox = (): React.ReactNode => { |
|||
const { |
|||
children, |
|||
loading, |
|||
onClear, |
|||
onTabChange, |
|||
onItemClick, |
|||
onViewMore, |
|||
clearText, |
|||
viewMoreText, |
|||
} = props; |
|||
if (!children) { |
|||
return null; |
|||
const newNotices = notices.map((notice) => { |
|||
const newNotice = { ...notice }; |
|||
|
|||
if (newNotice.datetime) { |
|||
newNotice.datetime = moment(notice.datetime as string).fromNow(); |
|||
} |
|||
const panes: React.ReactNode[] = []; |
|||
React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => { |
|||
if (!child) { |
|||
return; |
|||
|
|||
if (newNotice.id) { |
|||
newNotice.key = newNotice.id; |
|||
} |
|||
const { list, title, count, tabKey, showClear, showViewMore } = child.props; |
|||
const len = list && list.length ? list.length : 0; |
|||
const msgCount = count || count === 0 ? count : len; |
|||
const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title; |
|||
panes.push( |
|||
<TabPane tab={tabTitle} key={tabKey}> |
|||
<NoticeList |
|||
{...child.props} |
|||
clearText={clearText} |
|||
viewMoreText={viewMoreText} |
|||
data={list} |
|||
onClear={(): void => { |
|||
onClear?.(title, tabKey); |
|||
}} |
|||
onClick={(item): void => { |
|||
onItemClick?.(item, child.props); |
|||
}} |
|||
onViewMore={(event): void => { |
|||
onViewMore?.(child.props, event); |
|||
|
|||
if (newNotice.extra && newNotice.status) { |
|||
const color = { |
|||
todo: '', |
|||
processing: 'blue', |
|||
urgent: 'red', |
|||
doing: 'gold', |
|||
}[newNotice.status]; |
|||
newNotice.extra = ( |
|||
<Tag |
|||
color={color} |
|||
style={{ |
|||
marginRight: 0, |
|||
}} |
|||
showClear={showClear} |
|||
showViewMore={showViewMore} |
|||
title={title} |
|||
/> |
|||
</TabPane>, |
|||
); |
|||
> |
|||
{newNotice.extra} |
|||
</Tag> |
|||
) as any; |
|||
} |
|||
|
|||
return newNotice; |
|||
}); |
|||
return ( |
|||
<Spin spinning={loading} delay={300}> |
|||
<Tabs className={styles.tabs} onChange={onTabChange}> |
|||
{panes} |
|||
</Tabs> |
|||
</Spin> |
|||
); |
|||
}; |
|||
return groupBy(newNotices, 'type'); |
|||
}; |
|||
|
|||
const { className, count, bell } = props; |
|||
const getUnreadData = (noticeData: Record<string, API.NoticeIconItem[]>) => { |
|||
const unreadMsg: Record<string, number> = {}; |
|||
Object.keys(noticeData).forEach((key) => { |
|||
const value = noticeData[key]; |
|||
|
|||
if (!unreadMsg[key]) { |
|||
unreadMsg[key] = 0; |
|||
} |
|||
|
|||
const [visible, setVisible] = useMergedState<boolean>(false, { |
|||
value: props.popupVisible, |
|||
onChange: props.onPopupVisibleChange, |
|||
if (Array.isArray(value)) { |
|||
unreadMsg[key] = value.filter((item) => !item.read).length; |
|||
} |
|||
}); |
|||
const noticeButtonClass = classNames(className, styles.noticeButton); |
|||
const notificationBox = getNotificationBox(); |
|||
const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />; |
|||
const trigger = ( |
|||
<span className={classNames(noticeButtonClass, { opened: visible })}> |
|||
<Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}> |
|||
{NoticeBellIcon} |
|||
</Badge> |
|||
</span> |
|||
return unreadMsg; |
|||
}; |
|||
|
|||
const NoticeIconView = () => { |
|||
const { initialState } = useModel('@@initialState'); |
|||
const { currentUser } = initialState || {}; |
|||
const [notices, setNotices] = useState<API.NoticeIconItem[]>([]); |
|||
|
|||
useEffect(() => { |
|||
getNotices().then(({ data }) => setNotices(data || [])); |
|||
}, []); |
|||
|
|||
const noticeData = getNoticeData(notices); |
|||
const unreadMsg = getUnreadData(noticeData || {}); |
|||
|
|||
const changeReadState = (id: string) => { |
|||
setNotices( |
|||
notices.map((item) => { |
|||
const notice = { ...item }; |
|||
if (notice.id === id) { |
|||
notice.read = true; |
|||
} |
|||
return notice; |
|||
}), |
|||
); |
|||
if (!notificationBox) { |
|||
return trigger; |
|||
}; |
|||
|
|||
const clearReadState = (title: string, key: string) => { |
|||
setNotices( |
|||
notices.map((item) => { |
|||
const notice = { ...item }; |
|||
if (notice.type === key) { |
|||
notice.read = true; |
|||
} |
|||
return notice; |
|||
}), |
|||
); |
|||
message.success(`${'清空了'} ${title}`); |
|||
}; |
|||
|
|||
return ( |
|||
<HeaderDropdown |
|||
placement="bottomRight" |
|||
overlay={notificationBox} |
|||
overlayClassName={styles.popover} |
|||
trigger={['click']} |
|||
visible={visible} |
|||
onVisibleChange={setVisible} |
|||
<NoticeIcon |
|||
className={styles.action} |
|||
count={currentUser && currentUser.unreadCount} |
|||
onItemClick={(item) => { |
|||
changeReadState(item.id!); |
|||
}} |
|||
onClear={(title: string, key: string) => clearReadState(title, key)} |
|||
loading={false} |
|||
clearText="清空" |
|||
viewMoreText="查看更多" |
|||
onViewMore={() => message.info('Click on view more')} |
|||
clearClose |
|||
> |
|||
{trigger} |
|||
</HeaderDropdown> |
|||
<NoticeIcon.Tab |
|||
tabKey="notification" |
|||
count={unreadMsg.notification} |
|||
list={noticeData.notification} |
|||
title="通知" |
|||
emptyText="你已查看所有通知" |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="message" |
|||
count={unreadMsg.message} |
|||
list={noticeData.message} |
|||
title="消息" |
|||
emptyText="您已读完所有消息" |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="event" |
|||
title="待办" |
|||
emptyText="你已完成所有待办" |
|||
count={unreadMsg.event} |
|||
list={noticeData.event} |
|||
showViewMore |
|||
/> |
|||
</NoticeIcon> |
|||
); |
|||
}; |
|||
|
|||
NoticeIcon.defaultProps = { |
|||
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', |
|||
}; |
|||
|
|||
NoticeIcon.Tab = NoticeList; |
|||
|
|||
export default NoticeIcon; |
|||
export default NoticeIconView; |
|||
|
|||
@ -1,5 +0,0 @@ |
|||
import { PageLoading } from '@ant-design/pro-layout'; |
|||
|
|||
// loading components from code split
|
|||
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
|
|||
export default PageLoading; |
|||
@ -0,0 +1,103 @@ |
|||
import React, { useCallback } from 'react'; |
|||
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; |
|||
import { Avatar, Menu, Spin } from 'antd'; |
|||
import { history, useModel } from 'umi'; |
|||
import { stringify } from 'querystring'; |
|||
import HeaderDropdown from '../HeaderDropdown'; |
|||
import styles from './index.less'; |
|||
import { outLogin } from '@/services/ant-design-pro/api'; |
|||
import type { MenuInfo } from 'rc-menu/lib/interface'; |
|||
|
|||
export type GlobalHeaderRightProps = { |
|||
menu?: boolean; |
|||
}; |
|||
|
|||
/** |
|||
* 退出登录,并且将当前的 url 保存 |
|||
*/ |
|||
const loginOut = async () => { |
|||
await outLogin(); |
|||
const { query = {}, pathname } = history.location; |
|||
const { redirect } = query; |
|||
// Note: There may be security issues, please note
|
|||
if (window.location.pathname !== '/user/login' && !redirect) { |
|||
history.replace({ |
|||
pathname: '/user/login', |
|||
search: stringify({ |
|||
redirect: pathname, |
|||
}), |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { |
|||
const { initialState, setInitialState } = useModel('@@initialState'); |
|||
|
|||
const onMenuClick = useCallback( |
|||
(event: MenuInfo) => { |
|||
const { key } = event; |
|||
if (key === 'logout' && initialState) { |
|||
setInitialState({ ...initialState, currentUser: undefined }); |
|||
loginOut(); |
|||
return; |
|||
} |
|||
history.push(`/account/${key}`); |
|||
}, |
|||
[initialState, setInitialState], |
|||
); |
|||
|
|||
const loading = ( |
|||
<span className={`${styles.action} ${styles.account}`}> |
|||
<Spin |
|||
size="small" |
|||
style={{ |
|||
marginLeft: 8, |
|||
marginRight: 8, |
|||
}} |
|||
/> |
|||
</span> |
|||
); |
|||
|
|||
if (!initialState) { |
|||
return loading; |
|||
} |
|||
|
|||
const { currentUser } = initialState; |
|||
|
|||
if (!currentUser || !currentUser.name) { |
|||
return loading; |
|||
} |
|||
|
|||
const menuHeaderDropdown = ( |
|||
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}> |
|||
{menu && ( |
|||
<Menu.Item key="center"> |
|||
<UserOutlined /> |
|||
个人中心 |
|||
</Menu.Item> |
|||
)} |
|||
{menu && ( |
|||
<Menu.Item key="settings"> |
|||
<SettingOutlined /> |
|||
个人设置 |
|||
</Menu.Item> |
|||
)} |
|||
{menu && <Menu.Divider />} |
|||
|
|||
<Menu.Item key="logout"> |
|||
<LogoutOutlined /> |
|||
退出登录 |
|||
</Menu.Item> |
|||
</Menu> |
|||
); |
|||
return ( |
|||
<HeaderDropdown overlay={menuHeaderDropdown}> |
|||
<span className={`${styles.action} ${styles.account}`}> |
|||
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" /> |
|||
<span className={`${styles.name} anticon`}>{currentUser.name}</span> |
|||
</span> |
|||
</HeaderDropdown> |
|||
); |
|||
}; |
|||
|
|||
export default AvatarDropdown; |
|||
@ -0,0 +1,62 @@ |
|||
import { Space } from 'antd'; |
|||
import { QuestionCircleOutlined } from '@ant-design/icons'; |
|||
import React from 'react'; |
|||
import { useModel, SelectLang } from 'umi'; |
|||
import Avatar from './AvatarDropdown'; |
|||
import HeaderSearch from '../HeaderSearch'; |
|||
import styles from './index.less'; |
|||
|
|||
export type SiderTheme = 'light' | 'dark'; |
|||
|
|||
const GlobalHeaderRight: React.FC = () => { |
|||
const { initialState } = useModel('@@initialState'); |
|||
|
|||
if (!initialState || !initialState.settings) { |
|||
return null; |
|||
} |
|||
|
|||
const { navTheme, layout } = initialState.settings; |
|||
let className = styles.right; |
|||
|
|||
if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') { |
|||
className = `${styles.right} ${styles.dark}`; |
|||
} |
|||
return ( |
|||
<Space className={className}> |
|||
<HeaderSearch |
|||
className={`${styles.action} ${styles.search}`} |
|||
placeholder="站内搜索" |
|||
defaultValue="umi ui" |
|||
options={[ |
|||
{ label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' }, |
|||
{ |
|||
label: <a href="next.ant.design">Ant Design</a>, |
|||
value: 'Ant Design', |
|||
}, |
|||
{ |
|||
label: <a href="https://protable.ant.design/">Pro Table</a>, |
|||
value: 'Pro Table', |
|||
}, |
|||
{ |
|||
label: <a href="https://prolayout.ant.design/">Pro Layout</a>, |
|||
value: 'Pro Layout', |
|||
}, |
|||
]} |
|||
// onSearch={value => {
|
|||
// console.log('input', value);
|
|||
// }}
|
|||
/> |
|||
<span |
|||
className={styles.action} |
|||
onClick={() => { |
|||
window.open('https://pro.ant.design/docs/getting-started'); |
|||
}} |
|||
> |
|||
<QuestionCircleOutlined /> |
|||
</span> |
|||
<Avatar /> |
|||
<SelectLang className={styles.action} /> |
|||
</Space> |
|||
); |
|||
}; |
|||
export default GlobalHeaderRight; |
|||
@ -0,0 +1,272 @@ |
|||
--- |
|||
title: 业务组件 |
|||
sidemenu: false |
|||
--- |
|||
|
|||
> 此功能由[dumi](https://d.umijs.org/zh-CN/guide/advanced#umi-%E9%A1%B9%E7%9B%AE%E9%9B%86%E6%88%90%E6%A8%A1%E5%BC%8F)提供,dumi 是一个 📖 为组件开发场景而生的文档工具,用过的都说好。 |
|||
|
|||
# 业务组件 |
|||
|
|||
这里列举了 Pro 中所有用到的组件,这些组件不适合作为组件库,但是在业务中却真实需要。所以我们准备了这个文档,来指导大家是否需要使用这个组件。 |
|||
|
|||
## Footer 页脚组件 |
|||
|
|||
这个组件自带了一些 Pro 的配置,你一般都需要改掉它的信息。 |
|||
|
|||
```tsx |
|||
/** |
|||
* background: '#f0f2f5' |
|||
*/ |
|||
import React from 'react'; |
|||
import Footer from '@/components/Footer'; |
|||
|
|||
export default () => <Footer />; |
|||
``` |
|||
|
|||
## HeaderDropdown 头部下拉列表 |
|||
|
|||
HeaderDropdown 是 antd Dropdown 的封装,但是增加了移动端的特殊处理,用法也是相同的。 |
|||
|
|||
```tsx |
|||
/** |
|||
* background: '#f0f2f5' |
|||
*/ |
|||
import { Button, Menu } from 'antd'; |
|||
import React from 'react'; |
|||
import HeaderDropdown from '@/components/HeaderDropdown'; |
|||
|
|||
export default () => { |
|||
const menuHeaderDropdown = ( |
|||
<Menu selectedKeys={[]}> |
|||
<Menu.Item key="center">个人中心</Menu.Item> |
|||
<Menu.Item key="settings">个人设置</Menu.Item> |
|||
<Menu.Divider /> |
|||
<Menu.Item key="logout">退出登录</Menu.Item> |
|||
</Menu> |
|||
); |
|||
return ( |
|||
<HeaderDropdown overlay={menuHeaderDropdown}> |
|||
<Button>hover 展示菜单</Button> |
|||
</HeaderDropdown> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
## HeaderSearch 头部搜索框 |
|||
|
|||
一个带补全数据的输入框,支持收起和展开 Input |
|||
|
|||
```tsx |
|||
/** |
|||
* background: '#f0f2f5' |
|||
*/ |
|||
import { Button, Menu } from 'antd'; |
|||
import React from 'react'; |
|||
import HeaderSearch from '@/components/HeaderSearch'; |
|||
|
|||
export default () => { |
|||
return ( |
|||
<HeaderSearch |
|||
placeholder="站内搜索" |
|||
defaultValue="umi ui" |
|||
options={[ |
|||
{ label: 'Ant Design Pro', value: 'Ant Design Pro' }, |
|||
{ |
|||
label: 'Ant Design', |
|||
value: 'Ant Design', |
|||
}, |
|||
{ |
|||
label: 'Pro Table', |
|||
value: 'Pro Table', |
|||
}, |
|||
{ |
|||
label: 'Pro Layout', |
|||
value: 'Pro Layout', |
|||
}, |
|||
]} |
|||
onSearch={(value) => { |
|||
console.log('input', value); |
|||
}} |
|||
/> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
### API |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
| --------------- | ---------------------------------- | ---------------------------- | ------ | |
|||
| value | 输入框的值 | `string` | - | |
|||
| onChange | 值修改后触发 | `(value?: string) => void` | - | |
|||
| onSearch | 查询后触发 | `(value?: string) => void` | - | |
|||
| options | 选项菜单的的列表 | `{label,value}[]` | - | |
|||
| defaultVisible | 输入框默认是否显示,只有第一次生效 | `boolean` | - | |
|||
| visible | 输入框是否显示 | `boolean` | - | |
|||
| onVisibleChange | 输入框显示隐藏的回调函数 | `(visible: boolean) => void` | - | |
|||
|
|||
## NoticeIcon 通知工具 |
|||
|
|||
通知工具提供一个展示多种通知信息的界面。 |
|||
|
|||
```tsx |
|||
/** |
|||
* background: '#f0f2f5' |
|||
*/ |
|||
import { message } from 'antd'; |
|||
import React from 'react'; |
|||
import NoticeIcon from '@/components/NoticeIcon/NoticeIcon'; |
|||
|
|||
export default () => { |
|||
const list = [ |
|||
{ |
|||
id: '000000001', |
|||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', |
|||
title: '你收到了 14 份新周报', |
|||
datetime: '2017-08-09', |
|||
type: 'notification', |
|||
}, |
|||
{ |
|||
id: '000000002', |
|||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', |
|||
title: '你推荐的 曲妮妮 已通过第三轮面试', |
|||
datetime: '2017-08-08', |
|||
type: 'notification', |
|||
}, |
|||
]; |
|||
return ( |
|||
<NoticeIcon |
|||
count={10} |
|||
onItemClick={(item) => { |
|||
message.info(`${item.title} 被点击了`); |
|||
}} |
|||
onClear={(title: string, key: string) => message.info('点击了清空更多')} |
|||
loading={false} |
|||
clearText="清空" |
|||
viewMoreText="查看更多" |
|||
onViewMore={() => message.info('点击了查看更多')} |
|||
clearClose |
|||
> |
|||
<NoticeIcon.Tab |
|||
tabKey="notification" |
|||
count={2} |
|||
list={list} |
|||
title="通知" |
|||
emptyText="你已查看所有通知" |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="message" |
|||
count={2} |
|||
list={list} |
|||
title="消息" |
|||
emptyText="您已读完所有消息" |
|||
showViewMore |
|||
/> |
|||
<NoticeIcon.Tab |
|||
tabKey="event" |
|||
title="待办" |
|||
emptyText="你已完成所有待办" |
|||
count={2} |
|||
list={list} |
|||
showViewMore |
|||
/> |
|||
</NoticeIcon> |
|||
); |
|||
}; |
|||
``` |
|||
|
|||
### NoticeIcon API |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
| --- | --- | --- | --- | |
|||
| count | 有多少未读通知 | `number` | - | |
|||
| bell | 铃铛的图表 | `ReactNode` | - | |
|||
| onClear | 点击清空数据按钮 | `(tabName: string, tabKey: string) => void` | - | |
|||
| onItemClick | 未读消息列被点击 | `(item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void` | - | |
|||
| onViewMore | 查看更多的按钮点击 | `(tabProps: NoticeIconTabProps, e: MouseEvent) => void` | - | |
|||
| onTabChange | 通知 Tab 的切换 | `(tabTile: string) => void;` | - | |
|||
| popupVisible | 通知显示是否展示 | `boolean` | - | |
|||
| onPopupVisibleChange | 通知信息显示隐藏的回调函数 | `(visible: boolean) => void` | - | |
|||
| clearText | 清空按钮的文字 | `string` | - | |
|||
| viewMoreText | 查看更多的按钮文字 | `string` | - | |
|||
| clearClose | 展示清空按钮 | `boolean` | - | |
|||
| emptyImage | 列表为空时的兜底展示 | `ReactNode` | - | |
|||
|
|||
### NoticeIcon.Tab API |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
| ------------ | ------------------ | ------------------------------------ | ------ | |
|||
| count | 有多少未读通知 | `number` | - | |
|||
| title | 通知 Tab 的标题 | `ReactNode` | - | |
|||
| showClear | 展示清除按钮 | `boolean` | `true` | |
|||
| showViewMore | 展示加载更 | `boolean` | `true` | |
|||
| tabKey | Tab 的唯一 key | `string` | - | |
|||
| onClick | 子项的单击事件 | `(item: API.NoticeIconData) => void` | - | |
|||
| onClear | 清楚按钮的点击 | `()=>void` | - | |
|||
| emptyText | 为空的时候测试 | `()=>void` | - | |
|||
| viewMoreText | 查看更多的按钮文字 | `string` | - | |
|||
| onViewMore | 查看更多的按钮点击 | `( e: MouseEvent) => void` | - | |
|||
| list | 通知信息的列表 | `API.NoticeIconData` | - | |
|||
|
|||
### NoticeIconData |
|||
|
|||
```tsx | pure |
|||
export interface NoticeIconData { |
|||
id: string; |
|||
key: string; |
|||
avatar: string; |
|||
title: string; |
|||
datetime: string; |
|||
type: string; |
|||
read?: boolean; |
|||
description: string; |
|||
clickClose?: boolean; |
|||
extra: any; |
|||
status: string; |
|||
} |
|||
``` |
|||
|
|||
## RightContent |
|||
|
|||
RightContent 是以上几个组件的组合,同时新增了 plugins 的 `SelectLang` 插件。 |
|||
|
|||
```tsx | pure |
|||
<Space> |
|||
<HeaderSearch |
|||
placeholder="站内搜索" |
|||
defaultValue="umi ui" |
|||
options={[ |
|||
{ label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' }, |
|||
{ |
|||
label: <a href="next.ant.design">Ant Design</a>, |
|||
value: 'Ant Design', |
|||
}, |
|||
{ |
|||
label: <a href="https://protable.ant.design/">Pro Table</a>, |
|||
value: 'Pro Table', |
|||
}, |
|||
{ |
|||
label: <a href="https://prolayout.ant.design/">Pro Layout</a>, |
|||
value: 'Pro Layout', |
|||
}, |
|||
]} |
|||
/> |
|||
<Tooltip title="使用文档"> |
|||
<span |
|||
className={styles.action} |
|||
onClick={() => { |
|||
window.location.href = 'https://pro.ant.design/docs/getting-started'; |
|||
}} |
|||
> |
|||
<QuestionCircleOutlined /> |
|||
</span> |
|||
</Tooltip> |
|||
<Avatar /> |
|||
{REACT_APP_ENV && ( |
|||
<span> |
|||
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag> |
|||
</span> |
|||
)} |
|||
<SelectLang className={styles.action} /> |
|||
</Space> |
|||
``` |
|||
@ -1 +0,0 @@ |
|||
export default undefined; |
|||
@ -1,183 +0,0 @@ |
|||
/** |
|||
* Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout. |
|||
* |
|||
* @see You can view component api by: https://github.com/ant-design/ant-design-pro-layout
|
|||
*/ |
|||
import type { |
|||
MenuDataItem, |
|||
BasicLayoutProps as ProLayoutProps, |
|||
Settings, |
|||
} from '@ant-design/pro-layout'; |
|||
import ProLayout, { DefaultFooter } from '@ant-design/pro-layout'; |
|||
import React, { useEffect, useMemo, useRef } from 'react'; |
|||
import type { Dispatch } from 'umi'; |
|||
import { Link, useIntl, connect, history } from 'umi'; |
|||
import { GithubOutlined } from '@ant-design/icons'; |
|||
import { Result, Button } from 'antd'; |
|||
import Authorized from '@/utils/Authorized'; |
|||
import RightContent from '@/components/GlobalHeader/RightContent'; |
|||
import type { ConnectState } from '@/models/connect'; |
|||
import { getMatchMenu } from '@umijs/route-utils'; |
|||
import logo from '../assets/logo.svg'; |
|||
|
|||
const noMatch = ( |
|||
<Result |
|||
status={403} |
|||
title="403" |
|||
subTitle="Sorry, you are not authorized to access this page." |
|||
extra={ |
|||
<Button type="primary"> |
|||
<Link to="/user/login">Go Login</Link> |
|||
</Button> |
|||
} |
|||
/> |
|||
); |
|||
export type BasicLayoutProps = { |
|||
breadcrumbNameMap: Record<string, MenuDataItem>; |
|||
route: ProLayoutProps['route'] & { |
|||
authority: string[]; |
|||
}; |
|||
settings: Settings; |
|||
dispatch: Dispatch; |
|||
} & ProLayoutProps; |
|||
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & { |
|||
breadcrumbNameMap: Record<string, MenuDataItem>; |
|||
}; |
|||
/** Use Authorized check all menu item */ |
|||
|
|||
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => |
|||
menuList.map((item) => { |
|||
const localItem = { |
|||
...item, |
|||
children: item.children ? menuDataRender(item.children) : undefined, |
|||
}; |
|||
return Authorized.check(item.authority, localItem, null) as MenuDataItem; |
|||
}); |
|||
|
|||
const defaultFooterDom = ( |
|||
<DefaultFooter |
|||
copyright={`${new Date().getFullYear()} Produced by Ant Group Experience Technology Department`} |
|||
links={[ |
|||
{ |
|||
key: 'Ant Design Pro', |
|||
title: 'Ant Design Pro', |
|||
href: 'https://pro.ant.design', |
|||
blankTarget: true, |
|||
}, |
|||
{ |
|||
key: 'github', |
|||
title: <GithubOutlined />, |
|||
href: 'https://github.com/ant-design/ant-design-pro', |
|||
blankTarget: true, |
|||
}, |
|||
{ |
|||
key: 'Ant Design', |
|||
title: 'Ant Design', |
|||
href: 'https://ant.design', |
|||
blankTarget: true, |
|||
}, |
|||
]} |
|||
/> |
|||
); |
|||
|
|||
const BasicLayout: React.FC<BasicLayoutProps> = (props) => { |
|||
const { |
|||
dispatch, |
|||
children, |
|||
settings, |
|||
location = { |
|||
pathname: '/', |
|||
}, |
|||
} = props; |
|||
|
|||
const menuDataRef = useRef<MenuDataItem[]>([]); |
|||
|
|||
useEffect(() => { |
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'user/fetchCurrent', |
|||
}); |
|||
} |
|||
}, []); |
|||
/** Init variables */ |
|||
|
|||
const handleMenuCollapse = (payload: boolean): void => { |
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'global/changeLayoutCollapsed', |
|||
payload, |
|||
}); |
|||
} |
|||
}; |
|||
// get children authority
|
|||
const authorized = useMemo( |
|||
() => |
|||
getMatchMenu(location.pathname || '/', menuDataRef.current).pop() || { |
|||
authority: undefined, |
|||
}, |
|||
[location.pathname], |
|||
); |
|||
|
|||
const { formatMessage } = useIntl(); |
|||
|
|||
return ( |
|||
<ProLayout |
|||
logo={logo} |
|||
formatMessage={formatMessage} |
|||
{...props} |
|||
{...settings} |
|||
onCollapse={handleMenuCollapse} |
|||
onMenuHeaderClick={() => history.push('/')} |
|||
menuItemRender={(menuItemProps, defaultDom) => { |
|||
if ( |
|||
menuItemProps.isUrl || |
|||
!menuItemProps.path || |
|||
location.pathname === menuItemProps.path |
|||
) { |
|||
return defaultDom; |
|||
} |
|||
return <Link to={menuItemProps.path}>{defaultDom}</Link>; |
|||
}} |
|||
breadcrumbRender={(routers = []) => [ |
|||
{ |
|||
path: '/', |
|||
breadcrumbName: formatMessage({ id: 'menu.home' }), |
|||
}, |
|||
...routers, |
|||
]} |
|||
itemRender={(route, params, routes, paths) => { |
|||
const first = routes.indexOf(route) === 0; |
|||
return first ? ( |
|||
<Link to={paths.join('/')}>{route.breadcrumbName}</Link> |
|||
) : ( |
|||
<span>{route.breadcrumbName}</span> |
|||
); |
|||
}} |
|||
footerRender={() => { |
|||
if (settings.footerRender || settings.footerRender === undefined) { |
|||
return defaultFooterDom; |
|||
} |
|||
return null; |
|||
}} |
|||
menuDataRender={menuDataRender} |
|||
rightContentRender={() => <RightContent />} |
|||
postMenuData={(menuData) => { |
|||
menuDataRef.current = menuData || []; |
|||
return menuData || []; |
|||
}} |
|||
waterMarkProps={{ |
|||
content: 'Ant Design Pro', |
|||
fontColor: 'rgba(24,144,255,0.15)', |
|||
}} |
|||
> |
|||
<Authorized authority={authorized!.authority} noMatch={noMatch}> |
|||
{children} |
|||
</Authorized> |
|||
</ProLayout> |
|||
); |
|||
}; |
|||
|
|||
export default connect(({ global, settings }: ConnectState) => ({ |
|||
collapsed: global.collapsed, |
|||
settings, |
|||
}))(BasicLayout); |
|||
@ -1,10 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Inspector } from 'react-dev-inspector'; |
|||
|
|||
const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : React.Fragment; |
|||
|
|||
const Layout: React.FC = ({ children }) => { |
|||
return <InspectorWrapper>{children}</InspectorWrapper>; |
|||
}; |
|||
|
|||
export default Layout; |
|||
@ -1,58 +0,0 @@ |
|||
import React from 'react'; |
|||
import { PageLoading } from '@ant-design/pro-layout'; |
|||
import type { ConnectProps } from 'umi'; |
|||
import { Redirect, connect } from 'umi'; |
|||
import { stringify } from 'querystring'; |
|||
import type { ConnectState } from '@/models/connect'; |
|||
import type { CurrentUser } from '@/models/user'; |
|||
|
|||
type SecurityLayoutProps = { |
|||
loading?: boolean; |
|||
currentUser?: CurrentUser; |
|||
} & ConnectProps; |
|||
|
|||
type SecurityLayoutState = { |
|||
isReady: boolean; |
|||
}; |
|||
|
|||
class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> { |
|||
state: SecurityLayoutState = { |
|||
isReady: false, |
|||
}; |
|||
|
|||
componentDidMount() { |
|||
this.setState({ |
|||
isReady: true, |
|||
}); |
|||
const { dispatch } = this.props; |
|||
if (dispatch) { |
|||
dispatch({ |
|||
type: 'user/fetchCurrent', |
|||
}); |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { isReady } = this.state; |
|||
const { children, loading, currentUser } = this.props; |
|||
// You can replace it to your authentication rule (such as check token exists)
|
|||
// You can replace it with your own login authentication rules (such as judging whether the token exists)
|
|||
const isLogin = currentUser && currentUser.userid; |
|||
const queryString = stringify({ |
|||
redirect: window.location.href, |
|||
}); |
|||
|
|||
if ((!isLogin && loading) || !isReady) { |
|||
return <PageLoading />; |
|||
} |
|||
if (!isLogin && window.location.pathname !== '/user/login') { |
|||
return <Redirect to={`/user/login?${queryString}`} />; |
|||
} |
|||
return children; |
|||
} |
|||
} |
|||
|
|||
export default connect(({ user, loading }: ConnectState) => ({ |
|||
currentUser: user.currentUser, |
|||
loading: loading.models.user, |
|||
}))(SecurityLayout); |
|||
@ -1,70 +0,0 @@ |
|||
import type { MenuDataItem } from '@ant-design/pro-layout'; |
|||
import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout'; |
|||
import { Helmet, HelmetProvider } from 'react-helmet-async'; |
|||
import type { ConnectProps } from 'umi'; |
|||
import { Link, SelectLang, useIntl, connect, FormattedMessage } from 'umi'; |
|||
import React from 'react'; |
|||
import type { ConnectState } from '@/models/connect'; |
|||
import logo from '../assets/logo.svg'; |
|||
import styles from './UserLayout.less'; |
|||
|
|||
export type UserLayoutProps = { |
|||
breadcrumbNameMap: Record<string, MenuDataItem>; |
|||
} & Partial<ConnectProps>; |
|||
|
|||
const UserLayout: React.FC<UserLayoutProps> = (props) => { |
|||
const { |
|||
route = { |
|||
routes: [], |
|||
}, |
|||
} = props; |
|||
const { routes = [] } = route; |
|||
const { |
|||
children, |
|||
location = { |
|||
pathname: '', |
|||
}, |
|||
} = props; |
|||
const { formatMessage } = useIntl(); |
|||
const { breadcrumb } = getMenuData(routes); |
|||
const title = getPageTitle({ |
|||
pathname: location.pathname, |
|||
formatMessage, |
|||
breadcrumb, |
|||
...props, |
|||
}); |
|||
return ( |
|||
<HelmetProvider> |
|||
<Helmet> |
|||
<title>{title}</title> |
|||
<meta name="description" content={title} /> |
|||
</Helmet> |
|||
|
|||
<div className={styles.container}> |
|||
<div className={styles.lang}> |
|||
<SelectLang /> |
|||
</div> |
|||
<div className={styles.content}> |
|||
<div className={styles.top}> |
|||
<div className={styles.header}> |
|||
<Link to="/"> |
|||
<img alt="logo" className={styles.logo} src={logo} /> |
|||
<span className={styles.title}>Ant Design</span> |
|||
</Link> |
|||
</div> |
|||
<div className={styles.desc}> |
|||
<FormattedMessage |
|||
id="pages.layouts.userLayout.title" |
|||
defaultMessage="Ant Design. The most influential Web design specification in Xihu District." |
|||
/> |
|||
</div> |
|||
</div> |
|||
{children} |
|||
</div> |
|||
<DefaultFooter /> |
|||
</div> |
|||
</HelmetProvider> |
|||
); |
|||
}; |
|||
|
|||
export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout); |
|||
@ -0,0 +1,24 @@ |
|||
import component from './fa-IR/component'; |
|||
import globalHeader from './fa-IR/globalHeader'; |
|||
import menu from './fa-IR/menu'; |
|||
import pwa from './fa-IR/pwa'; |
|||
import settingDrawer from './fa-IR/settingDrawer'; |
|||
import settings from './fa-IR/settings'; |
|||
import pages from './fa-IR/pages'; |
|||
|
|||
export default { |
|||
'navBar.lang': 'زبان ها ', |
|||
'layout.user.link.help': 'کمک', |
|||
'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, |
|||
...settings, |
|||
...pwa, |
|||
...component, |
|||
...pages, |
|||
}; |
|||
@ -0,0 +1,5 @@ |
|||
export default { |
|||
'component.tagSelect.expand': 'باز', |
|||
'component.tagSelect.collapse': 'بسته ', |
|||
'component.tagSelect.all': 'همه', |
|||
}; |
|||
@ -0,0 +1,17 @@ |
|||
export default { |
|||
'component.globalHeader.search': 'جستجو ', |
|||
'component.globalHeader.search.example1': 'مثال 1 را جستجو کنید', |
|||
'component.globalHeader.search.example2': 'مثال 2 را جستجو کنید', |
|||
'component.globalHeader.search.example3': 'مثال 3 را جستجو کنید', |
|||
'component.globalHeader.help': 'کمک', |
|||
'component.globalHeader.notification': 'اعلان', |
|||
'component.globalHeader.notification.empty': 'شما همه اعلان ها را مشاهده کرده اید.', |
|||
'component.globalHeader.message': 'پیام', |
|||
'component.globalHeader.message.empty': 'شما همه پیام ها را مشاهده کرده اید.', |
|||
'component.globalHeader.event': 'رویداد', |
|||
'component.globalHeader.event.empty': 'شما همه رویدادها را مشاهده کرده اید.', |
|||
'component.noticeIcon.clear': 'پاک کردن', |
|||
'component.noticeIcon.cleared': 'پاک شد', |
|||
'component.noticeIcon.empty': 'بدون اعلان', |
|||
'component.noticeIcon.view-more': 'نمایش بیشتر', |
|||
}; |
|||
@ -0,0 +1,52 @@ |
|||
export default { |
|||
'menu.welcome': 'خوش آمدید', |
|||
'menu.more-blocks': 'بلوک های بیشتر', |
|||
'menu.home': 'خانه', |
|||
'menu.admin': 'مدیر', |
|||
'menu.admin.sub-page': 'زیر صفحه', |
|||
'menu.login': 'ورود', |
|||
'menu.register': 'ثبت نام', |
|||
'menu.register.result': 'ثبت نام نتیجه', |
|||
'menu.dashboard': 'داشبورد', |
|||
'menu.dashboard.analysis': 'تحلیل و بررسی', |
|||
'menu.dashboard.monitor': 'نظارت', |
|||
'menu.dashboard.workplace': 'محل کار', |
|||
'menu.exception.403': '403', |
|||
'menu.exception.404': '404', |
|||
'menu.exception.500': '500', |
|||
'menu.form': 'فرم', |
|||
'menu.form.basic-form': 'فرم اساسی', |
|||
'menu.form.step-form': 'فرم مرحله', |
|||
'menu.form.step-form.info': 'فرم مرحله (نوشتن اطلاعات انتقال)', |
|||
'menu.form.step-form.confirm': 'فرم مرحله (تأیید اطلاعات انتقال)', |
|||
'menu.form.step-form.result': 'فرم مرحله (تمام شده)', |
|||
'menu.form.advanced-form': 'فرم پیشرفته', |
|||
'menu.list': 'لیست', |
|||
'menu.list.table-list': 'جدول جستجو', |
|||
'menu.list.basic-list': 'لیست اصلی', |
|||
'menu.list.card-list': 'لیست کارت', |
|||
'menu.list.search-list': 'لیست جستجو', |
|||
'menu.list.search-list.articles': 'لیست جستجو (مقالات)', |
|||
'menu.list.search-list.projects': 'لیست جستجو (پروژه ها)', |
|||
'menu.list.search-list.applications': 'لیست جستجو (برنامه ها)', |
|||
'menu.profile': 'مشخصات', |
|||
'menu.profile.basic': 'مشخصات عمومی', |
|||
'menu.profile.advanced': 'مشخصات پیشرفته', |
|||
'menu.result': 'نتیجه', |
|||
'menu.result.success': 'موفق', |
|||
'menu.result.fail': 'ناموفق', |
|||
'menu.exception': 'استثنا', |
|||
'menu.exception.not-permission': '403', |
|||
'menu.exception.not-find': '404', |
|||
'menu.exception.server-error': '500', |
|||
'menu.exception.trigger': 'راه اندازی', |
|||
'menu.account': 'حساب', |
|||
'menu.account.center': 'مرکز حساب', |
|||
'menu.account.settings': 'تنظیمات حساب', |
|||
'menu.account.trigger': 'خطای راه اندازی', |
|||
'menu.account.logout': 'خروج', |
|||
'menu.editor': 'ویرایشگر گرافیک', |
|||
'menu.editor.flow': 'ویرایشگر جریان', |
|||
'menu.editor.mind': 'ویرایشگر ذهن', |
|||
'menu.editor.koni': 'ویرایشگر Koni', |
|||
}; |
|||
@ -0,0 +1,67 @@ |
|||
export default { |
|||
'pages.layouts.userLayout.title': 'طراحی مورچه تأثیرگذارترین مشخصات طراحی وب در منطقه Xihu است', |
|||
'pages.login.accountLogin.tab': 'ورود به حساب کاربری', |
|||
'pages.login.accountLogin.errorMessage': 'نام کاربری / رمزعبور نادرست (مدیر / ant.design)', |
|||
'pages.login.username.placeholder': 'نام کاربری: مدیر یا کاربر', |
|||
'pages.login.username.required': 'لطفا نام کاربری خود را وارد کنید!', |
|||
'pages.login.password.placeholder': 'رمز عبور: ant.design', |
|||
'pages.login.password.required': 'لطفاً رمز ورود خود را وارد کنید!', |
|||
'pages.login.phoneLogin.tab': 'ورود به سیستم تلفن', |
|||
'pages.login.phoneLogin.errorMessage': 'خطای کد تأیید', |
|||
'pages.login.phoneNumber.placeholder': 'شماره تلفن', |
|||
'pages.login.phoneNumber.required': 'لطفاً شماره تلفن خود را وارد کنید!', |
|||
'pages.login.phoneNumber.invalid': 'شماره تلفن نامعتبر است!', |
|||
'pages.login.captcha.placeholder': 'کد تایید', |
|||
'pages.login.captcha.required': 'لطفا کد تأیید را وارد کنید!', |
|||
'pages.login.phoneLogin.getVerificationCode': 'دریافت کد', |
|||
'pages.getCaptchaSecondText': 'ثانیه', |
|||
'pages.login.rememberMe': 'مرا به خاطر بسپار', |
|||
'pages.login.forgotPassword': 'رمز عبور را فراموش کرده اید ?', |
|||
'pages.login.submit': 'ارسال', |
|||
'pages.login.loginWith': 'وارد شوید با :', |
|||
'pages.login.registerAccount': 'ثبت نام', |
|||
'pages.welcome.advancedComponent': 'مولفه پیشرفته', |
|||
'pages.welcome.link': 'خوش آمدید', |
|||
'pages.welcome.advancedLayout': 'چیدمان پیشرفته', |
|||
'pages.welcome.alertMessage': 'اجزای سنگین تر سریعتر و قوی تر آزاد شده اند.', |
|||
'pages.admin.subPage.title': 'این صفحه فقط توسط مدیر قابل مشاهده است', |
|||
'pages.admin.subPage.alertMessage': |
|||
'رابط کاربری Umi اکنون منتشر شده است ، برای شروع تجربه استفاده از npm run ui خوش آمدید.', |
|||
'pages.searchTable.createForm.newRule': 'قانون جدید', |
|||
'pages.searchTable.updateForm.ruleConfig': 'پیکربندی قانون', |
|||
'pages.searchTable.updateForm.basicConfig': 'اطلاعات اولیه', |
|||
'pages.searchTable.updateForm.ruleName.nameLabel': ' نام قانون', |
|||
'pages.searchTable.updateForm.ruleName.nameRules': 'لطفاً نام قانون را وارد کنید!', |
|||
'pages.searchTable.updateForm.ruleDesc.descLabel': 'شرح قانون', |
|||
'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'لطفاً حداقل پنج حرف وارد کنید', |
|||
'pages.searchTable.updateForm.ruleDesc.descRules': |
|||
'لطفاً حداقل یک قانون حاوی پنج کاراکتر شرح دهید!', |
|||
'pages.searchTable.updateForm.ruleProps.title': 'پیکربندی خصوصیات', |
|||
'pages.searchTable.updateForm.object': 'نظارت بر شی', |
|||
'pages.searchTable.updateForm.ruleProps.templateLabel': 'الگوی قانون', |
|||
'pages.searchTable.updateForm.ruleProps.typeLabel': 'نوع قانون', |
|||
'pages.searchTable.updateForm.schedulingPeriod.title': 'تنظیم دوره زمان بندی', |
|||
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'زمان شروع', |
|||
'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'لطفاً زمان شروع را انتخاب کنید!', |
|||
'pages.searchTable.titleDesc': 'شرح', |
|||
'pages.searchTable.ruleName': 'نام قانون لازم است', |
|||
'pages.searchTable.titleCallNo': 'تعداد تماس های خدماتی', |
|||
'pages.searchTable.titleStatus': 'وضعیت', |
|||
'pages.searchTable.nameStatus.default': 'پیش فرض', |
|||
'pages.searchTable.nameStatus.running': 'در حال دویدن', |
|||
'pages.searchTable.nameStatus.online': 'برخط', |
|||
'pages.searchTable.nameStatus.abnormal': 'غیرطبیعی', |
|||
'pages.searchTable.titleUpdatedAt': 'آخرین برنامه ریزی در', |
|||
'pages.searchTable.exception': 'لطفا دلیل استثنا را وارد کنید!', |
|||
'pages.searchTable.titleOption': 'گزینه', |
|||
'pages.searchTable.config': 'پیکربندی', |
|||
'pages.searchTable.subscribeAlert': 'مشترک شدن در هشدارها', |
|||
'pages.searchTable.title': 'فرم درخواست', |
|||
'pages.searchTable.new': 'جدید', |
|||
'pages.searchTable.chosen': 'انتخاب شده', |
|||
'pages.searchTable.item': 'مورد', |
|||
'pages.searchTable.totalServiceCalls': 'تعداد کل تماس های خدماتی', |
|||
'pages.searchTable.tenThousand': '0000', |
|||
'pages.searchTable.batchDeletion': 'حذف دسته ای', |
|||
'pages.searchTable.batchApproval': 'تصویب دسته ای', |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
export default { |
|||
'app.pwa.offline': 'شما اکنون آفلاین هستید', |
|||
'app.pwa.serviceworker.updated': 'مطالب جدید در دسترس است', |
|||
'app.pwa.serviceworker.updated.hint': |
|||
'لطفاً برای بارگیری مجدد صفحه فعلی ، دکمه "تازه سازی" را فشار دهید', |
|||
'app.pwa.serviceworker.updated.ok': 'تازه سازی', |
|||
}; |
|||
@ -0,0 +1,32 @@ |
|||
export default { |
|||
'app.setting.pagestyle': 'تنظیم نوع صفحه', |
|||
'app.setting.pagestyle.dark': 'سبک تیره', |
|||
'app.setting.pagestyle.light': 'سبک سبک', |
|||
'app.setting.content-width': 'عرض محتوا', |
|||
'app.setting.content-width.fixed': 'ثابت', |
|||
'app.setting.content-width.fluid': 'شناور', |
|||
'app.setting.themecolor': 'رنگ تم', |
|||
'app.setting.themecolor.dust': 'گرد و غبار قرمز', |
|||
'app.setting.themecolor.volcano': 'آتشفشان', |
|||
'app.setting.themecolor.sunset': 'غروب نارنجی', |
|||
'app.setting.themecolor.cyan': 'فیروزه ای', |
|||
'app.setting.themecolor.green': 'سبز قطبی', |
|||
'app.setting.themecolor.daybreak': 'آبی روشن(پیشفرض)', |
|||
'app.setting.themecolor.geekblue': 'چسب گیک', |
|||
'app.setting.themecolor.purple': 'بنفش طلایی', |
|||
'app.setting.navigationmode': 'حالت پیمایش', |
|||
'app.setting.sidemenu': 'طرح منوی کناری', |
|||
'app.setting.topmenu': 'طرح منوی بالایی', |
|||
'app.setting.fixedheader': 'سرصفحه ثابت', |
|||
'app.setting.fixedsidebar': 'نوار کناری ثابت', |
|||
'app.setting.fixedsidebar.hint': 'کار بر روی منوی کناری', |
|||
'app.setting.hideheader': 'هدر پنهان هنگام پیمایش', |
|||
'app.setting.hideheader.hint': 'وقتی Hidden Header فعال باشد کار می کند', |
|||
'app.setting.othersettings': 'تنظیمات دیگر', |
|||
'app.setting.weakmode': 'حالت ضعیف', |
|||
'app.setting.copy': 'تنظیمات کپی', |
|||
'app.setting.copyinfo': |
|||
'موفقیت در کپی کردن , لطفا defaultSettings را در src / models / setting.js جایگزین کنید', |
|||
'app.setting.production.hint': |
|||
'صفحه تنظیم فقط در محیط توسعه نمایش داده می شود ، لطفاً دستی تغییر دهید', |
|||
}; |
|||
@ -0,0 +1,60 @@ |
|||
export default { |
|||
'app.settings.menuMap.basic': 'تنظیمات پایه ', |
|||
'app.settings.menuMap.security': 'تنظیمات امنیتی', |
|||
'app.settings.menuMap.binding': 'صحافی حساب', |
|||
'app.settings.menuMap.notification': 'اعلان پیام جدید', |
|||
'app.settings.basic.avatar': 'آواتار', |
|||
'app.settings.basic.change-avatar': 'آواتار را تغییر دهید', |
|||
'app.settings.basic.email': 'ایمیل', |
|||
'app.settings.basic.email-message': 'لطفا ایمیل خود را وارد کنید!', |
|||
'app.settings.basic.nickname': 'نام مستعار', |
|||
'app.settings.basic.nickname-message': 'لطفاً نام مستعار خود را وارد کنید!', |
|||
'app.settings.basic.profile': 'پروفایل شخصی', |
|||
'app.settings.basic.profile-message': 'لطفاً مشخصات شخصی خود را وارد کنید!', |
|||
'app.settings.basic.profile-placeholder': 'معرفی مختصر خودتان', |
|||
'app.settings.basic.country': 'کشور / منطقه', |
|||
'app.settings.basic.country-message': 'لطفاً کشور خود را وارد کنید!', |
|||
'app.settings.basic.geographic': 'استان یا شهر', |
|||
'app.settings.basic.geographic-message': 'لطفاً اطلاعات جغرافیایی خود را وارد کنید!', |
|||
'app.settings.basic.address': 'آدرس خیابان', |
|||
'app.settings.basic.address-message': 'لطفا آدرس خود را وارد کنید!', |
|||
'app.settings.basic.phone': 'شماره تلفن', |
|||
'app.settings.basic.phone-message': 'لطفاً تلفن خود را وارد کنید!', |
|||
'app.settings.basic.update': 'به روز رسانی اطلاعات', |
|||
'app.settings.security.strong': 'قوی', |
|||
'app.settings.security.medium': 'متوسط', |
|||
'app.settings.security.weak': 'ضعیف', |
|||
'app.settings.security.password': 'رمز عبور حساب کاربری', |
|||
'app.settings.security.password-description': 'قدرت رمز عبور فعلی', |
|||
'app.settings.security.phone': 'تلفن امنیتی', |
|||
'app.settings.security.phone-description': 'تلفن مقید', |
|||
'app.settings.security.question': 'سوال امنیتی', |
|||
'app.settings.security.question-description': |
|||
'سوال امنیتی تنظیم نشده است و سیاست امنیتی می تواند به طور موثر از امنیت حساب محافظت کند', |
|||
'app.settings.security.email': 'ایمیل پشتیبان', |
|||
'app.settings.security.email-description': 'ایمیل مقید', |
|||
'app.settings.security.mfa': 'دستگاه MFA', |
|||
'app.settings.security.mfa-description': |
|||
'دستگاه MFA بسته نشده ، پس از اتصال ، می تواند دو بار تأیید شود', |
|||
'app.settings.security.modify': 'تغییر', |
|||
'app.settings.security.set': 'تنظیم', |
|||
'app.settings.security.bind': 'بستن', |
|||
'app.settings.binding.taobao': 'اتصال Taobao', |
|||
'app.settings.binding.taobao-description': 'حساب Taobao در حال حاضر بسته نشده است', |
|||
'app.settings.binding.alipay': 'اتصال Alipay', |
|||
'app.settings.binding.alipay-description': 'حساب Alipay در حال حاضر بسته نشده است', |
|||
'app.settings.binding.dingding': 'اتصال DingTalk', |
|||
'app.settings.binding.dingding-description': 'حساب DingTalk در حال حاضر محدود نشده است', |
|||
'app.settings.binding.bind': 'بستن', |
|||
'app.settings.notification.password': 'رمز عبور حساب کاربری', |
|||
'app.settings.notification.password-description': |
|||
'پیام های سایر کاربران در قالب یک نامه ایستگاهی اعلام خواهد شد', |
|||
'app.settings.notification.messages': 'پیام های سیستم', |
|||
'app.settings.notification.messages-description': |
|||
'پیام های سیستم به صورت نامه ایستگاه مطلع می شوند', |
|||
'app.settings.notification.todo': 'اعلان کارها', |
|||
'app.settings.notification.todo-description': |
|||
'لیست کارها به صورت نامه ای از ایستگاه اطلاع داده می شود', |
|||
'app.settings.open': 'باز کن', |
|||
'app.settings.close': 'بستن', |
|||
}; |
|||
@ -0,0 +1,70 @@ |
|||
export default { |
|||
'pages.layouts.userLayout.title': |
|||
'Ant Design é a especificação de web design mais influente no distrito de Xihu', |
|||
'pages.login.accountLogin.tab': 'Login da conta', |
|||
'pages.login.accountLogin.errorMessage': 'usuário/senha incorreto(admin/ant.design)', |
|||
'pages.login.username.placeholder': 'Usuário: admin or user', |
|||
'pages.login.username.required': 'Por favor insira seu usuário!', |
|||
'pages.login.password.placeholder': 'Senha: ant.design', |
|||
'pages.login.password.required': 'Por favor insira sua senha!', |
|||
'pages.login.phoneLogin.tab': 'Login com Telefone', |
|||
'pages.login.phoneLogin.errorMessage': 'Erro de Código de Verificação', |
|||
'pages.login.phoneNumber.placeholder': 'Telefone', |
|||
'pages.login.phoneNumber.required': 'Por favor entre com seu telefone!', |
|||
'pages.login.phoneNumber.invalid': 'Telefone é inválido!', |
|||
'pages.login.captcha.placeholder': 'Código de Verificação', |
|||
'pages.login.captcha.required': 'Por favor entre com o código de verificação!', |
|||
'pages.login.phoneLogin.getVerificationCode': 'Obter Código', |
|||
'pages.getCaptchaSecondText': 'seg(s)', |
|||
'pages.login.rememberMe': 'Lembre-me', |
|||
'pages.login.forgotPassword': 'Perdeu a Senha ?', |
|||
'pages.login.submit': 'Enviar', |
|||
'pages.login.loginWith': 'Login com :', |
|||
'pages.login.registerAccount': 'Registra Conta', |
|||
'pages.welcome.advancedComponent': 'Componente Avançado', |
|||
'pages.welcome.link': 'Bem-vindo', |
|||
'pages.welcome.advancedLayout': 'Layout Avançado', |
|||
'pages.welcome.alertMessage': 'Componentes pesados mais rápidos e mais fortes foram lançados.', |
|||
'pages.admin.subPage.title': 'Esta página só pode ser vista pelo Admin', |
|||
'pages.admin.subPage.alertMessage': |
|||
'O Umi ui foi lançado, bem-vindo ao usar o npm run ui para iniciar a experiência.', |
|||
'pages.searchTable.createForm.newRule': 'Neva Regra', |
|||
'pages.searchTable.updateForm.ruleConfig': 'Configuração de Regra', |
|||
'pages.searchTable.updateForm.basicConfig': 'Informação básica', |
|||
'pages.searchTable.updateForm.ruleName.nameLabel': 'Nome da Regra', |
|||
'pages.searchTable.updateForm.ruleName.nameRules': 'Por favor entre com o nome da regra!', |
|||
'pages.searchTable.updateForm.ruleDesc.descLabel': 'Descrição da Regra', |
|||
'pages.searchTable.updateForm.ruleDesc.descPlaceholder': |
|||
'Por favor insira ao menos cinco caracteres', |
|||
'pages.searchTable.updateForm.ruleDesc.descRules': |
|||
'Insira uma descrição de regra de pelo menos cinco caracteres!', |
|||
'pages.searchTable.updateForm.ruleProps.title': 'Configurar Propriedades', |
|||
'pages.searchTable.updateForm.object': 'Objeto de Monitoramento', |
|||
'pages.searchTable.updateForm.ruleProps.templateLabel': 'Modelo de Regra', |
|||
'pages.searchTable.updateForm.ruleProps.typeLabel': 'Tipo de Regra', |
|||
'pages.searchTable.updateForm.schedulingPeriod.title': 'Definir Período de Agendamento', |
|||
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Hora de Início', |
|||
'pages.searchTable.updateForm.schedulingPeriod.timeRules': |
|||
'Por favor selecione um horáriod e início!', |
|||
'pages.searchTable.titleDesc': 'Descrição', |
|||
'pages.searchTable.ruleName': 'O nome da regra é obrigatório', |
|||
'pages.searchTable.titleCallNo': 'Número de chamadas de serviço', |
|||
'pages.searchTable.titleStatus': 'Status', |
|||
'pages.searchTable.nameStatus.default': 'padrão', |
|||
'pages.searchTable.nameStatus.running': 'executando', |
|||
'pages.searchTable.nameStatus.online': 'online', |
|||
'pages.searchTable.nameStatus.abnormal': 'anormal', |
|||
'pages.searchTable.titleUpdatedAt': 'Última programação em', |
|||
'pages.searchTable.exception': 'Por favor, indique o motivo da exceção!', |
|||
'pages.searchTable.titleOption': 'Opção', |
|||
'pages.searchTable.config': 'Configuração', |
|||
'pages.searchTable.subscribeAlert': 'Inscreva-se para receber alertas', |
|||
'pages.searchTable.title': 'Formulário de Consulta', |
|||
'pages.searchTable.new': 'Novo', |
|||
'pages.searchTable.chosen': 'selecionado', |
|||
'pages.searchTable.item': 'item', |
|||
'pages.searchTable.totalServiceCalls': 'Número total de chamadas de serviço', |
|||
'pages.searchTable.tenThousand': '0000', |
|||
'pages.searchTable.batchDeletion': 'deleção em lote', |
|||
'pages.searchTable.batchApproval': 'aprovação em lote', |
|||
}; |
|||
@ -1,30 +0,0 @@ |
|||
import type { MenuDataItem, Settings as ProSettings } from '@ant-design/pro-layout'; |
|||
import { GlobalModelState } from './global'; |
|||
import { UserModelState } from './user'; |
|||
import type { StateType } from './login'; |
|||
|
|||
export { GlobalModelState, UserModelState }; |
|||
|
|||
export type Loading = { |
|||
global: boolean; |
|||
effects: Record<string, boolean | undefined>; |
|||
models: { |
|||
global?: boolean; |
|||
menu?: boolean; |
|||
setting?: boolean; |
|||
user?: boolean; |
|||
login?: boolean; |
|||
}; |
|||
}; |
|||
|
|||
export type ConnectState = { |
|||
global: GlobalModelState; |
|||
loading: Loading; |
|||
settings: ProSettings; |
|||
user: UserModelState; |
|||
login: StateType; |
|||
}; |
|||
|
|||
export type Route = { |
|||
routes?: Route[]; |
|||
} & MenuDataItem; |
|||
@ -1,126 +0,0 @@ |
|||
import type { Reducer, Effect } from 'umi'; |
|||
|
|||
import type { NoticeIconData } from '@/components/NoticeIcon'; |
|||
import { queryNotices } from '@/services/user'; |
|||
import type { ConnectState } from './connect.d'; |
|||
|
|||
export type NoticeItem = { |
|||
id: string; |
|||
type: string; |
|||
status: string; |
|||
} & NoticeIconData; |
|||
|
|||
export type GlobalModelState = { |
|||
collapsed: boolean; |
|||
notices: NoticeItem[]; |
|||
}; |
|||
|
|||
export type GlobalModelType = { |
|||
namespace: 'global'; |
|||
state: GlobalModelState; |
|||
effects: { |
|||
fetchNotices: Effect; |
|||
clearNotices: Effect; |
|||
changeNoticeReadState: Effect; |
|||
}; |
|||
reducers: { |
|||
changeLayoutCollapsed: Reducer<GlobalModelState>; |
|||
saveNotices: Reducer<GlobalModelState>; |
|||
saveClearedNotices: Reducer<GlobalModelState>; |
|||
}; |
|||
}; |
|||
|
|||
const GlobalModel: GlobalModelType = { |
|||
namespace: 'global', |
|||
|
|||
state: { |
|||
collapsed: false, |
|||
notices: [], |
|||
}, |
|||
|
|||
effects: { |
|||
*fetchNotices(_, { call, put, select }) { |
|||
const data = yield call(queryNotices); |
|||
yield put({ |
|||
type: 'saveNotices', |
|||
payload: data, |
|||
}); |
|||
const unreadCount: number = yield select( |
|||
(state: ConnectState) => state.global.notices.filter((item) => !item.read).length, |
|||
); |
|||
yield put({ |
|||
type: 'user/changeNotifyCount', |
|||
payload: { |
|||
totalCount: data.length, |
|||
unreadCount, |
|||
}, |
|||
}); |
|||
}, |
|||
*clearNotices({ payload }, { put, select }) { |
|||
yield put({ |
|||
type: 'saveClearedNotices', |
|||
payload, |
|||
}); |
|||
const count: number = yield select((state: ConnectState) => state.global.notices.length); |
|||
const unreadCount: number = yield select( |
|||
(state: ConnectState) => state.global.notices.filter((item) => !item.read).length, |
|||
); |
|||
yield put({ |
|||
type: 'user/changeNotifyCount', |
|||
payload: { |
|||
totalCount: count, |
|||
unreadCount, |
|||
}, |
|||
}); |
|||
}, |
|||
*changeNoticeReadState({ payload }, { put, select }) { |
|||
const notices: NoticeItem[] = yield select((state: ConnectState) => |
|||
state.global.notices.map((item) => { |
|||
const notice = { ...item }; |
|||
if (notice.id === payload) { |
|||
notice.read = true; |
|||
} |
|||
return notice; |
|||
}), |
|||
); |
|||
|
|||
yield put({ |
|||
type: 'saveNotices', |
|||
payload: notices, |
|||
}); |
|||
|
|||
yield put({ |
|||
type: 'user/changeNotifyCount', |
|||
payload: { |
|||
totalCount: notices.length, |
|||
unreadCount: notices.filter((item) => !item.read).length, |
|||
}, |
|||
}); |
|||
}, |
|||
}, |
|||
|
|||
reducers: { |
|||
changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { |
|||
return { |
|||
...state, |
|||
collapsed: payload, |
|||
}; |
|||
}, |
|||
saveNotices(state, { payload }): GlobalModelState { |
|||
return { |
|||
collapsed: false, |
|||
...state, |
|||
notices: payload, |
|||
}; |
|||
}, |
|||
saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { |
|||
return { |
|||
...state, |
|||
collapsed: false, |
|||
notices: state.notices.filter((item): boolean => item.type !== payload), |
|||
}; |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
export default GlobalModel; |
|||
@ -1,93 +0,0 @@ |
|||
import { stringify } from 'querystring'; |
|||
import type { Reducer, Effect } from 'umi'; |
|||
import { history } from 'umi'; |
|||
|
|||
import { fakeAccountLogin } from '@/services/login'; |
|||
import { setAuthority } from '@/utils/authority'; |
|||
import { getPageQuery } from '@/utils/utils'; |
|||
import { message } from 'antd'; |
|||
|
|||
export type StateType = { |
|||
status?: 'ok' | 'error'; |
|||
type?: string; |
|||
currentAuthority?: 'user' | 'guest' | 'admin'; |
|||
}; |
|||
|
|||
export type LoginModelType = { |
|||
namespace: string; |
|||
state: StateType; |
|||
effects: { |
|||
login: Effect; |
|||
logout: Effect; |
|||
}; |
|||
reducers: { |
|||
changeLoginStatus: Reducer<StateType>; |
|||
}; |
|||
}; |
|||
|
|||
const Model: LoginModelType = { |
|||
namespace: 'login', |
|||
|
|||
state: { |
|||
status: undefined, |
|||
}, |
|||
|
|||
effects: { |
|||
*login({ payload }, { call, put }) { |
|||
const response = yield call(fakeAccountLogin, payload); |
|||
yield put({ |
|||
type: 'changeLoginStatus', |
|||
payload: response, |
|||
}); |
|||
// Login successfully
|
|||
if (response.status === 'ok') { |
|||
const urlParams = new URL(window.location.href); |
|||
const params = getPageQuery(); |
|||
message.success('🎉 🎉 🎉 登录成功!'); |
|||
let { redirect } = params as { redirect: string }; |
|||
if (redirect) { |
|||
const redirectUrlParams = new URL(redirect); |
|||
if (redirectUrlParams.origin === urlParams.origin) { |
|||
redirect = redirect.substr(urlParams.origin.length); |
|||
if (window.routerBase !== '/') { |
|||
redirect = redirect.replace(window.routerBase, '/'); |
|||
} |
|||
if (redirect.match(/^\/.*#/)) { |
|||
redirect = redirect.substr(redirect.indexOf('#') + 1); |
|||
} |
|||
} else { |
|||
window.location.href = '/'; |
|||
return; |
|||
} |
|||
} |
|||
history.replace(redirect || '/'); |
|||
} |
|||
}, |
|||
|
|||
logout() { |
|||
const { redirect } = getPageQuery(); |
|||
// Note: There may be security issues, please note
|
|||
if (window.location.pathname !== '/user/login' && !redirect) { |
|||
history.replace({ |
|||
pathname: '/user/login', |
|||
search: stringify({ |
|||
redirect: window.location.href, |
|||
}), |
|||
}); |
|||
} |
|||
}, |
|||
}, |
|||
|
|||
reducers: { |
|||
changeLoginStatus(state, { payload }) { |
|||
setAuthority(payload.currentAuthority); |
|||
return { |
|||
...state, |
|||
status: payload.status, |
|||
type: payload.type, |
|||
}; |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
export default Model; |
|||
@ -1,38 +0,0 @@ |
|||
import type { Reducer } from 'umi'; |
|||
import type { DefaultSettings } from '../../config/defaultSettings'; |
|||
import defaultSettings from '../../config/defaultSettings'; |
|||
|
|||
export type SettingModelType = { |
|||
namespace: 'settings'; |
|||
state: DefaultSettings; |
|||
reducers: { |
|||
changeSetting: Reducer<DefaultSettings>; |
|||
}; |
|||
}; |
|||
|
|||
const updateColorWeak: (colorWeak: boolean) => void = (colorWeak) => { |
|||
const root = document.getElementById('root'); |
|||
if (root) { |
|||
root.className = colorWeak ? 'colorWeak' : ''; |
|||
} |
|||
}; |
|||
|
|||
const SettingModel: SettingModelType = { |
|||
namespace: 'settings', |
|||
state: defaultSettings, |
|||
reducers: { |
|||
changeSetting(state = defaultSettings, { payload }) { |
|||
const { colorWeak, contentWidth } = payload; |
|||
|
|||
if (state.contentWidth !== contentWidth && window.dispatchEvent) { |
|||
window.dispatchEvent(new Event('resize')); |
|||
} |
|||
updateColorWeak(!!colorWeak); |
|||
return { |
|||
...state, |
|||
...payload, |
|||
}; |
|||
}, |
|||
}, |
|||
}; |
|||
export default SettingModel; |
|||
@ -1,85 +0,0 @@ |
|||
import type { Effect, Reducer } from 'umi'; |
|||
|
|||
import { queryCurrent, query as queryUsers } from '@/services/user'; |
|||
|
|||
export type CurrentUser = { |
|||
avatar?: string; |
|||
name?: string; |
|||
title?: string; |
|||
group?: string; |
|||
signature?: string; |
|||
tags?: { |
|||
key: string; |
|||
label: string; |
|||
}[]; |
|||
userid?: string; |
|||
unreadCount?: number; |
|||
}; |
|||
|
|||
export type UserModelState = { |
|||
currentUser?: CurrentUser; |
|||
}; |
|||
|
|||
export type UserModelType = { |
|||
namespace: 'user'; |
|||
state: UserModelState; |
|||
effects: { |
|||
fetch: Effect; |
|||
fetchCurrent: Effect; |
|||
}; |
|||
reducers: { |
|||
saveCurrentUser: Reducer<UserModelState>; |
|||
changeNotifyCount: Reducer<UserModelState>; |
|||
}; |
|||
}; |
|||
|
|||
const UserModel: UserModelType = { |
|||
namespace: 'user', |
|||
|
|||
state: { |
|||
currentUser: {}, |
|||
}, |
|||
|
|||
effects: { |
|||
*fetch(_, { call, put }) { |
|||
const response = yield call(queryUsers); |
|||
yield put({ |
|||
type: 'save', |
|||
payload: response, |
|||
}); |
|||
}, |
|||
*fetchCurrent(_, { call, put }) { |
|||
const response = yield call(queryCurrent); |
|||
yield put({ |
|||
type: 'saveCurrentUser', |
|||
payload: response, |
|||
}); |
|||
}, |
|||
}, |
|||
|
|||
reducers: { |
|||
saveCurrentUser(state, action) { |
|||
return { |
|||
...state, |
|||
currentUser: action.payload || {}, |
|||
}; |
|||
}, |
|||
changeNotifyCount( |
|||
state = { |
|||
currentUser: {}, |
|||
}, |
|||
action, |
|||
) { |
|||
return { |
|||
...state, |
|||
currentUser: { |
|||
...state.currentUser, |
|||
notifyCount: action.payload.totalCount, |
|||
unreadCount: action.payload.unreadCount, |
|||
}, |
|||
}; |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
export default UserModel; |
|||
@ -1,36 +0,0 @@ |
|||
export type TableListItem = { |
|||
key: number; |
|||
disabled?: boolean; |
|||
href: string; |
|||
avatar: string; |
|||
name: string; |
|||
owner: string; |
|||
desc: string; |
|||
callNo: number; |
|||
status: number; |
|||
updatedAt: Date; |
|||
createdAt: Date; |
|||
progress: number; |
|||
}; |
|||
|
|||
export type TableListPagination = { |
|||
total: number; |
|||
pageSize: number; |
|||
current: number; |
|||
}; |
|||
|
|||
export type TableListData = { |
|||
list: TableListItem[]; |
|||
pagination: Partial<TableListPagination>; |
|||
}; |
|||
|
|||
export type TableListParams = { |
|||
status?: string; |
|||
name?: string; |
|||
desc?: string; |
|||
key?: number; |
|||
pageSize?: number; |
|||
currentPage?: number; |
|||
filter?: Record<string, any[]>; |
|||
sorter?: Record<string, any>; |
|||
}; |
|||
@ -1,38 +0,0 @@ |
|||
import request from '@/utils/request'; |
|||
import type { TableListParams, TableListItem } from './data.d'; |
|||
|
|||
export async function queryRule(params?: TableListParams) { |
|||
return request('/api/rule', { |
|||
params, |
|||
}); |
|||
} |
|||
|
|||
export async function removeRule(params: { key: number[] }) { |
|||
return request('/api/rule', { |
|||
method: 'POST', |
|||
data: { |
|||
...params, |
|||
method: 'delete', |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
export async function addRule(params: TableListItem) { |
|||
return request('/api/rule', { |
|||
method: 'POST', |
|||
data: { |
|||
...params, |
|||
method: 'post', |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
export async function updateRule(params: TableListParams) { |
|||
return request('/api/rule', { |
|||
method: 'POST', |
|||
data: { |
|||
...params, |
|||
method: 'update', |
|||
}, |
|||
}); |
|||
} |
|||
@ -1,44 +0,0 @@ |
|||
@import '~antd/es/style/themes/default.less'; |
|||
|
|||
.main { |
|||
width: 328px; |
|||
margin: 0 auto; |
|||
@media screen and (max-width: @screen-sm) { |
|||
width: 95%; |
|||
max-width: 328px; |
|||
} |
|||
|
|||
:global { |
|||
.@{ant-prefix}-tabs-nav-list { |
|||
margin: auto; |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
|
|||
.icon { |
|||
margin-left: 16px; |
|||
color: rgba(0, 0, 0, 0.2); |
|||
font-size: 24px; |
|||
vertical-align: middle; |
|||
cursor: pointer; |
|||
transition: color 0.3s; |
|||
|
|||
&:hover { |
|||
color: @primary-color; |
|||
} |
|||
} |
|||
|
|||
.other { |
|||
margin-top: 24px; |
|||
line-height: 22px; |
|||
text-align: left; |
|||
.register { |
|||
float: right; |
|||
} |
|||
} |
|||
|
|||
.prefixIcon { |
|||
color: @primary-color; |
|||
font-size: @font-size-base; |
|||
} |
|||
} |
|||
@ -1,263 +0,0 @@ |
|||
import { |
|||
AlipayCircleOutlined, |
|||
LockOutlined, |
|||
MailOutlined, |
|||
MobileOutlined, |
|||
TaobaoCircleOutlined, |
|||
UserOutlined, |
|||
WeiboCircleOutlined, |
|||
} from '@ant-design/icons'; |
|||
import { Alert, Space, message, Tabs } from 'antd'; |
|||
import React, { useState } from 'react'; |
|||
import ProForm, { ProFormCaptcha, ProFormCheckbox, ProFormText } from '@ant-design/pro-form'; |
|||
import { useIntl, connect, FormattedMessage } from 'umi'; |
|||
import { getFakeCaptcha } from '@/services/login'; |
|||
import type { Dispatch } from 'umi'; |
|||
import type { StateType } from '@/models/login'; |
|||
import type { LoginParamsType } from '@/services/login'; |
|||
import type { ConnectState } from '@/models/connect'; |
|||
|
|||
import styles from './index.less'; |
|||
|
|||
export type LoginProps = { |
|||
dispatch: Dispatch; |
|||
userLogin: StateType; |
|||
submitting?: boolean; |
|||
}; |
|||
|
|||
const LoginMessage: React.FC<{ |
|||
content: string; |
|||
}> = ({ content }) => ( |
|||
<Alert |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
message={content} |
|||
type="error" |
|||
showIcon |
|||
/> |
|||
); |
|||
|
|||
const Login: React.FC<LoginProps> = (props) => { |
|||
const { userLogin = {}, submitting } = props; |
|||
const { status, type: loginType } = userLogin; |
|||
const [type, setType] = useState<string>('account'); |
|||
const intl = useIntl(); |
|||
|
|||
const handleSubmit = (values: LoginParamsType) => { |
|||
const { dispatch } = props; |
|||
dispatch({ |
|||
type: 'login/login', |
|||
payload: { ...values, type }, |
|||
}); |
|||
}; |
|||
return ( |
|||
<div className={styles.main}> |
|||
<ProForm |
|||
initialValues={{ |
|||
autoLogin: true, |
|||
}} |
|||
submitter={{ |
|||
render: (_, dom) => dom.pop(), |
|||
submitButtonProps: { |
|||
loading: submitting, |
|||
size: 'large', |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}} |
|||
onFinish={(values) => { |
|||
handleSubmit(values as LoginParamsType); |
|||
return Promise.resolve(); |
|||
}} |
|||
> |
|||
<Tabs activeKey={type} onChange={setType}> |
|||
<Tabs.TabPane |
|||
key="account" |
|||
tab={intl.formatMessage({ |
|||
id: 'pages.login.accountLogin.tab', |
|||
defaultMessage: 'Account password login', |
|||
})} |
|||
/> |
|||
<Tabs.TabPane |
|||
key="mobile" |
|||
tab={intl.formatMessage({ |
|||
id: 'pages.login.phoneLogin.tab', |
|||
defaultMessage: 'Mobile phone number login', |
|||
})} |
|||
/> |
|||
</Tabs> |
|||
|
|||
{status === 'error' && loginType === 'account' && !submitting && ( |
|||
<LoginMessage |
|||
content={intl.formatMessage({ |
|||
id: 'pages.login.accountLogin.errorMessage', |
|||
defaultMessage: 'Incorrect account or password(admin/ant.design)', |
|||
})} |
|||
/> |
|||
)} |
|||
{type === 'account' && ( |
|||
<> |
|||
<ProFormText |
|||
name="userName" |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <UserOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.username.placeholder', |
|||
defaultMessage: 'Username: admin or user', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.username.required" |
|||
defaultMessage="Please enter user name!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormText.Password |
|||
name="password" |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <LockOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.password.placeholder', |
|||
defaultMessage: 'Password: ant.design', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.password.required" |
|||
defaultMessage="Please enter password!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
</> |
|||
)} |
|||
|
|||
{status === 'error' && loginType === 'mobile' && !submitting && ( |
|||
<LoginMessage content="Verification code error" /> |
|||
)} |
|||
{type === 'mobile' && ( |
|||
<> |
|||
<ProFormText |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <MobileOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
name="mobile" |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.phoneNumber.placeholder', |
|||
defaultMessage: 'Phone number', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.phoneNumber.required" |
|||
defaultMessage="Please enter phone number!" |
|||
/> |
|||
), |
|||
}, |
|||
{ |
|||
pattern: /^1\d{10}$/, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.phoneNumber.invalid" |
|||
defaultMessage="Malformed phone number!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormCaptcha |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <MailOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
captchaProps={{ |
|||
size: 'large', |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.captcha.placeholder', |
|||
defaultMessage: 'Please enter verification code', |
|||
})} |
|||
captchaTextRender={(timing, count) => { |
|||
if (timing) { |
|||
return `${count} ${intl.formatMessage({ |
|||
id: 'pages.getCaptchaSecondText', |
|||
defaultMessage: 'Get verification code', |
|||
})}`;
|
|||
} |
|||
return intl.formatMessage({ |
|||
id: 'pages.login.phoneLogin.getVerificationCode', |
|||
defaultMessage: 'Get verification code', |
|||
}); |
|||
}} |
|||
name="captcha" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.captcha.required" |
|||
defaultMessage="Please enter verification code!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
onGetCaptcha={async (mobile) => { |
|||
const result = await getFakeCaptcha(mobile); |
|||
if (result === false) { |
|||
return; |
|||
} |
|||
message.success( |
|||
'Get the verification code successfully! The verification code is: 1234', |
|||
); |
|||
}} |
|||
/> |
|||
</> |
|||
)} |
|||
<div |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
> |
|||
<ProFormCheckbox noStyle name="autoLogin"> |
|||
<FormattedMessage id="pages.login.rememberMe" defaultMessage="Auto login" /> |
|||
</ProFormCheckbox> |
|||
<a |
|||
style={{ |
|||
float: 'right', |
|||
}} |
|||
> |
|||
<FormattedMessage id="pages.login.forgotPassword" defaultMessage="Forget password" /> |
|||
</a> |
|||
</div> |
|||
</ProForm> |
|||
<Space className={styles.other}> |
|||
<FormattedMessage id="pages.login.loginWith" defaultMessage="Other login methods" /> |
|||
<AlipayCircleOutlined className={styles.icon} /> |
|||
<TaobaoCircleOutlined className={styles.icon} /> |
|||
<WeiboCircleOutlined className={styles.icon} /> |
|||
</Space> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default connect(({ login, loading }: ConnectState) => ({ |
|||
userLogin: login, |
|||
submitting: loading.effects['login/login'], |
|||
}))(Login); |
|||
@ -0,0 +1,318 @@ |
|||
import { |
|||
AlipayCircleOutlined, |
|||
LockOutlined, |
|||
MobileOutlined, |
|||
TaobaoCircleOutlined, |
|||
UserOutlined, |
|||
WeiboCircleOutlined, |
|||
} from '@ant-design/icons'; |
|||
import { Alert, Space, message, Tabs } from 'antd'; |
|||
import React, { useState } from 'react'; |
|||
import ProForm, { ProFormCaptcha, ProFormCheckbox, ProFormText } from '@ant-design/pro-form'; |
|||
import { useIntl, Link, history, FormattedMessage, SelectLang, useModel } from 'umi'; |
|||
import Footer from '@/components/Footer'; |
|||
import { login } from '@/services/ant-design-pro/api'; |
|||
import { getFakeCaptcha } from '@/services/ant-design-pro/login'; |
|||
|
|||
import styles from './index.less'; |
|||
|
|||
const LoginMessage: React.FC<{ |
|||
content: string; |
|||
}> = ({ content }) => ( |
|||
<Alert |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
message={content} |
|||
type="error" |
|||
showIcon |
|||
/> |
|||
); |
|||
|
|||
/** 此方法会跳转到 redirect 参数所在的位置 */ |
|||
const goto = () => { |
|||
if (!history) return; |
|||
setTimeout(() => { |
|||
const { query } = history.location; |
|||
const { redirect } = query as { redirect: string }; |
|||
history.push(redirect || '/'); |
|||
}, 10); |
|||
}; |
|||
|
|||
const Login: React.FC = () => { |
|||
const [submitting, setSubmitting] = useState(false); |
|||
const [userLoginState, setUserLoginState] = useState<API.LoginResult>({}); |
|||
const [type, setType] = useState<string>('account'); |
|||
const { initialState, setInitialState } = useModel('@@initialState'); |
|||
|
|||
const intl = useIntl(); |
|||
|
|||
const fetchUserInfo = async () => { |
|||
const userInfo = await initialState?.fetchUserInfo?.(); |
|||
if (userInfo) { |
|||
setInitialState({ |
|||
...initialState, |
|||
currentUser: userInfo, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
const handleSubmit = async (values: API.LoginParams) => { |
|||
setSubmitting(true); |
|||
try { |
|||
// 登录
|
|||
const msg = await login({ ...values, type }); |
|||
if (msg.status === 'ok') { |
|||
const defaultloginSuccessMessage = intl.formatMessage({ |
|||
id: 'pages.login.success', |
|||
defaultMessage: '登录成功!', |
|||
}); |
|||
message.success(defaultloginSuccessMessage); |
|||
await fetchUserInfo(); |
|||
goto(); |
|||
return; |
|||
} |
|||
// 如果失败去设置用户错误信息
|
|||
setUserLoginState(msg); |
|||
} catch (error) { |
|||
const defaultloginFailureMessage = intl.formatMessage({ |
|||
id: 'pages.login.failure', |
|||
defaultMessage: '登录失败,请重试!', |
|||
}); |
|||
|
|||
message.error(defaultloginFailureMessage); |
|||
} |
|||
setSubmitting(false); |
|||
}; |
|||
const { status, type: loginType } = userLoginState; |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
<div className={styles.lang} data-lang> |
|||
{SelectLang && <SelectLang />} |
|||
</div> |
|||
<div className={styles.content}> |
|||
<div className={styles.top}> |
|||
<div className={styles.header}> |
|||
<Link to="/"> |
|||
<img alt="logo" className={styles.logo} src="/logo.svg" /> |
|||
<span className={styles.title}>Ant Design</span> |
|||
</Link> |
|||
</div> |
|||
<div className={styles.desc}> |
|||
{intl.formatMessage({ id: 'pages.layouts.userLayout.title' })} |
|||
</div> |
|||
</div> |
|||
|
|||
<div className={styles.main}> |
|||
<ProForm |
|||
initialValues={{ |
|||
autoLogin: true, |
|||
}} |
|||
submitter={{ |
|||
searchConfig: { |
|||
submitText: intl.formatMessage({ |
|||
id: 'pages.login.submit', |
|||
defaultMessage: '登录', |
|||
}), |
|||
}, |
|||
render: (_, dom) => dom.pop(), |
|||
submitButtonProps: { |
|||
loading: submitting, |
|||
size: 'large', |
|||
style: { |
|||
width: '100%', |
|||
}, |
|||
}, |
|||
}} |
|||
onFinish={async (values) => { |
|||
handleSubmit(values as API.LoginParams); |
|||
}} |
|||
> |
|||
<Tabs activeKey={type} onChange={setType}> |
|||
<Tabs.TabPane |
|||
key="account" |
|||
tab={intl.formatMessage({ |
|||
id: 'pages.login.accountLogin.tab', |
|||
defaultMessage: '账户密码登录', |
|||
})} |
|||
/> |
|||
<Tabs.TabPane |
|||
key="mobile" |
|||
tab={intl.formatMessage({ |
|||
id: 'pages.login.phoneLogin.tab', |
|||
defaultMessage: '手机号登录', |
|||
})} |
|||
/> |
|||
</Tabs> |
|||
|
|||
{status === 'error' && loginType === 'account' && ( |
|||
<LoginMessage |
|||
content={intl.formatMessage({ |
|||
id: 'pages.login.accountLogin.errorMessage', |
|||
defaultMessage: '账户或密码错误(admin/ant.design)', |
|||
})} |
|||
/> |
|||
)} |
|||
{type === 'account' && ( |
|||
<> |
|||
<ProFormText |
|||
name="username" |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <UserOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.username.placeholder', |
|||
defaultMessage: '用户名: admin or user', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.username.required" |
|||
defaultMessage="请输入用户名!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormText.Password |
|||
name="password" |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <LockOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.password.placeholder', |
|||
defaultMessage: '密码: ant.design', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.password.required" |
|||
defaultMessage="请输入密码!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
</> |
|||
)} |
|||
|
|||
{status === 'error' && loginType === 'mobile' && <LoginMessage content="验证码错误" />} |
|||
{type === 'mobile' && ( |
|||
<> |
|||
<ProFormText |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <MobileOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
name="mobile" |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.phoneNumber.placeholder', |
|||
defaultMessage: '手机号', |
|||
})} |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.phoneNumber.required" |
|||
defaultMessage="请输入手机号!" |
|||
/> |
|||
), |
|||
}, |
|||
{ |
|||
pattern: /^1\d{10}$/, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.phoneNumber.invalid" |
|||
defaultMessage="手机号格式错误!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
/> |
|||
<ProFormCaptcha |
|||
fieldProps={{ |
|||
size: 'large', |
|||
prefix: <LockOutlined className={styles.prefixIcon} />, |
|||
}} |
|||
captchaProps={{ |
|||
size: 'large', |
|||
}} |
|||
placeholder={intl.formatMessage({ |
|||
id: 'pages.login.captcha.placeholder', |
|||
defaultMessage: '请输入验证码', |
|||
})} |
|||
captchaTextRender={(timing, count) => { |
|||
if (timing) { |
|||
return `${count} ${intl.formatMessage({ |
|||
id: 'pages.getCaptchaSecondText', |
|||
defaultMessage: '获取验证码', |
|||
})}`;
|
|||
} |
|||
return intl.formatMessage({ |
|||
id: 'pages.login.phoneLogin.getVerificationCode', |
|||
defaultMessage: '获取验证码', |
|||
}); |
|||
}} |
|||
name="captcha" |
|||
rules={[ |
|||
{ |
|||
required: true, |
|||
message: ( |
|||
<FormattedMessage |
|||
id="pages.login.captcha.required" |
|||
defaultMessage="请输入验证码!" |
|||
/> |
|||
), |
|||
}, |
|||
]} |
|||
onGetCaptcha={async (phone) => { |
|||
const result = await getFakeCaptcha({ |
|||
phone, |
|||
}); |
|||
if (result === false) { |
|||
return; |
|||
} |
|||
message.success('获取验证码成功!验证码为:1234'); |
|||
}} |
|||
/> |
|||
</> |
|||
)} |
|||
<div |
|||
style={{ |
|||
marginBottom: 24, |
|||
}} |
|||
> |
|||
<ProFormCheckbox noStyle name="autoLogin"> |
|||
<FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" /> |
|||
</ProFormCheckbox> |
|||
<a |
|||
style={{ |
|||
float: 'right', |
|||
}} |
|||
> |
|||
<FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" /> |
|||
</a> |
|||
</div> |
|||
</ProForm> |
|||
<Space className={styles.other}> |
|||
<FormattedMessage id="pages.login.loginWith" defaultMessage="其他登录方式" /> |
|||
<AlipayCircleOutlined className={styles.icon} /> |
|||
<TaobaoCircleOutlined className={styles.icon} /> |
|||
<WeiboCircleOutlined className={styles.icon} /> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<Footer /> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default Login; |
|||
@ -0,0 +1,83 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
import { request } from 'umi'; |
|||
|
|||
/** 获取当前的用户 GET /api/currentUser */ |
|||
export async function currentUser(options?: { [key: string]: any }) { |
|||
return request<API.CurrentUser>('/api/currentUser', { |
|||
method: 'GET', |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** 退出登录接口 POST /api/login/outLogin */ |
|||
export async function outLogin(options?: { [key: string]: any }) { |
|||
return request<Record<string, any>>('/api/login/outLogin', { |
|||
method: 'POST', |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** 登录接口 POST /api/login/account */ |
|||
export async function login(body: API.LoginParams, options?: { [key: string]: any }) { |
|||
return request<API.LoginResult>('/api/login/account', { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
data: body, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** 此处后端没有提供注释 GET /api/notices */ |
|||
export async function getNotices(options?: { [key: string]: any }) { |
|||
return request<API.NoticeIconList>('/api/notices', { |
|||
method: 'GET', |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** 获取规则列表 GET /api/rule */ |
|||
export async function rule( |
|||
params: { |
|||
// query
|
|||
/** 当前的页码 */ |
|||
current?: number; |
|||
/** 页面的容量 */ |
|||
pageSize?: number; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
return request<API.RuleList>('/api/rule', { |
|||
method: 'GET', |
|||
params: { |
|||
...params, |
|||
}, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** 新建规则 PUT /api/rule */ |
|||
export async function updateRule(options?: { [key: string]: any }) { |
|||
return request<API.RuleListItem>('/api/rule', { |
|||
method: 'PUT', |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** 新建规则 POST /api/rule */ |
|||
export async function addRule(options?: { [key: string]: any }) { |
|||
return request<API.RuleListItem>('/api/rule', { |
|||
method: 'POST', |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** 删除规则 DELETE /api/rule */ |
|||
export async function removeRule(options?: { [key: string]: any }) { |
|||
return request<Record<string, any>>('/api/rule', { |
|||
method: 'DELETE', |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
// API 更新时间:
|
|||
// API 唯一标识:
|
|||
import * as api from './api'; |
|||
import * as login from './login'; |
|||
export default { |
|||
api, |
|||
login, |
|||
}; |
|||
@ -0,0 +1,21 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
import { request } from 'umi'; |
|||
|
|||
/** 发送验证码 POST /api/login/captcha */ |
|||
export async function getFakeCaptcha( |
|||
params: { |
|||
// query
|
|||
/** 手机号 */ |
|||
phone?: string; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
return request<API.FakeCaptcha>('/api/login/captcha', { |
|||
method: 'POST', |
|||
params: { |
|||
...params, |
|||
}, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
|
|||
declare namespace API { |
|||
type CurrentUser = { |
|||
name?: string; |
|||
avatar?: string; |
|||
userid?: string; |
|||
email?: string; |
|||
signature?: string; |
|||
title?: string; |
|||
group?: string; |
|||
tags?: { key?: string; label?: string }[]; |
|||
notifyCount?: number; |
|||
unreadCount?: number; |
|||
country?: string; |
|||
access?: string; |
|||
geographic?: { |
|||
province?: { label?: string; key?: string }; |
|||
city?: { label?: string; key?: string }; |
|||
}; |
|||
address?: string; |
|||
phone?: string; |
|||
}; |
|||
|
|||
type LoginResult = { |
|||
status?: string; |
|||
type?: string; |
|||
currentAuthority?: string; |
|||
}; |
|||
|
|||
type PageParams = { |
|||
current?: number; |
|||
pageSize?: number; |
|||
}; |
|||
|
|||
type RuleListItem = { |
|||
key?: number; |
|||
disabled?: boolean; |
|||
href?: string; |
|||
avatar?: string; |
|||
name?: string; |
|||
owner?: string; |
|||
desc?: string; |
|||
callNo?: number; |
|||
status?: number; |
|||
updatedAt?: string; |
|||
createdAt?: string; |
|||
progress?: number; |
|||
}; |
|||
|
|||
type RuleList = { |
|||
data?: RuleListItem[]; |
|||
/** 列表的内容总数 */ |
|||
total?: number; |
|||
success?: boolean; |
|||
}; |
|||
|
|||
type FakeCaptcha = { |
|||
code?: number; |
|||
status?: string; |
|||
}; |
|||
|
|||
type LoginParams = { |
|||
username?: string; |
|||
password?: string; |
|||
autoLogin?: boolean; |
|||
type?: string; |
|||
}; |
|||
|
|||
type ErrorResponse = { |
|||
/** 业务约定的错误码 */ |
|||
errorCode: string; |
|||
/** 业务上的错误信息 */ |
|||
errorMessage?: string; |
|||
/** 业务上的请求是否成功 */ |
|||
success?: boolean; |
|||
}; |
|||
|
|||
type NoticeIconList = { |
|||
data?: NoticeIconItem[]; |
|||
/** 列表的内容总数 */ |
|||
total?: number; |
|||
success?: boolean; |
|||
}; |
|||
|
|||
type NoticeIconItemType = 'notification' | 'message' | 'event'; |
|||
|
|||
type NoticeIconItem = { |
|||
id?: string; |
|||
extra?: string; |
|||
key?: string; |
|||
read?: boolean; |
|||
avatar?: string; |
|||
title?: string; |
|||
status?: string; |
|||
datetime?: string; |
|||
description?: string; |
|||
type?: NoticeIconItemType; |
|||
}; |
|||
} |
|||
@ -1,19 +0,0 @@ |
|||
import request from '@/utils/request'; |
|||
|
|||
export type LoginParamsType = { |
|||
userName: string; |
|||
password: string; |
|||
mobile: string; |
|||
captcha: string; |
|||
}; |
|||
|
|||
export async function fakeAccountLogin(params: LoginParamsType) { |
|||
return request('/api/login/account', { |
|||
method: 'POST', |
|||
data: params, |
|||
}); |
|||
} |
|||
|
|||
export async function getFakeCaptcha(mobile: string) { |
|||
return request(`/api/login/captcha?mobile=${mobile}`); |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
// @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, |
|||
}; |
|||
@ -0,0 +1,166 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
import { request } from 'umi'; |
|||
|
|||
/** Update an existing pet PUT /pet */ |
|||
export async function updatePet(body: API.Pet, options?: { [key: string]: any }) { |
|||
return request<any>('/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<any>('/pet', { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
}, |
|||
data: body, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */ |
|||
export async function findPetsByStatus( |
|||
params: { |
|||
// query
|
|||
/** Status values that need to be considered for filter */ |
|||
status: 'available' | 'pending' | 'sold'[]; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
return request<API.Pet[]>('/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( |
|||
params: { |
|||
// query
|
|||
/** Tags to filter by */ |
|||
tags: string[]; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
return request<API.Pet[]>('/pet/findByTags', { |
|||
method: 'GET', |
|||
params: { |
|||
...params, |
|||
}, |
|||
|
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Find pet by ID Returns a single pet GET /pet/${param0} */ |
|||
export async function getPetById( |
|||
params: { |
|||
// path
|
|||
/** ID of pet to return */ |
|||
petId: number; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { petId: param0 } = params; |
|||
return request<API.Pet>(`/pet/${param0}`, { |
|||
method: 'GET', |
|||
params: { ...params }, |
|||
|
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Updates a pet in the store with form data POST /pet/${param0} */ |
|||
export async function updatePetWithForm( |
|||
params: { |
|||
// path
|
|||
/** ID of pet that needs to be updated */ |
|||
petId: number; |
|||
}, |
|||
body: { name?: string; status?: string }, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { petId: param0 } = 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' ? JSON.stringify(item) : item); |
|||
} |
|||
}); |
|||
|
|||
return request<any>(`/pet/${param0}`, { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/x-www-form-urlencoded', |
|||
}, |
|||
params: { ...params }, |
|||
data: formData, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Deletes a pet DELETE /pet/${param0} */ |
|||
export async function deletePet( |
|||
params: { |
|||
// header
|
|||
api_key?: string; |
|||
// path
|
|||
/** Pet id to delete */ |
|||
petId: number; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { petId: param0 } = params; |
|||
return request<any>(`/pet/${param0}`, { |
|||
method: 'DELETE', |
|||
params: { ...params }, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** uploads an image POST /pet/${param0}/uploadImage */ |
|||
export async function uploadFile( |
|||
params: { |
|||
// path
|
|||
/** ID of pet to update */ |
|||
petId: number; |
|||
}, |
|||
body: { additionalMetadata?: string; file?: string }, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { petId: param0 } = 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' ? JSON.stringify(item) : item); |
|||
} |
|||
}); |
|||
|
|||
return request<API.ApiResponse>(`/pet/${param0}/uploadImage`, { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'multipart/form-data', |
|||
}, |
|||
params: { ...params }, |
|||
data: formData, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
import { request } from 'umi'; |
|||
|
|||
/** 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<Record<string, any>>('/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<API.Order>('/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( |
|||
params: { |
|||
// path
|
|||
/** ID of pet that needs to be fetched */ |
|||
orderId: number; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { orderId: param0 } = params; |
|||
return request<API.Order>(`/store/order/${param0}`, { |
|||
method: 'GET', |
|||
params: { ...params }, |
|||
...(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( |
|||
params: { |
|||
// path
|
|||
/** ID of the order that needs to be deleted */ |
|||
orderId: number; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { orderId: param0 } = params; |
|||
return request<any>(`/store/order/${param0}`, { |
|||
method: 'DELETE', |
|||
params: { ...params }, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
|
|||
declare namespace API { |
|||
type Order = { |
|||
id?: number; |
|||
petId?: number; |
|||
quantity?: number; |
|||
shipDate?: string; |
|||
/** Order Status */ |
|||
status?: 'placed' | 'approved' | 'delivered'; |
|||
complete?: boolean; |
|||
}; |
|||
|
|||
type Category = { |
|||
id?: number; |
|||
name?: string; |
|||
}; |
|||
|
|||
type User = { |
|||
id?: number; |
|||
username?: string; |
|||
firstName?: string; |
|||
lastName?: string; |
|||
email?: string; |
|||
password?: string; |
|||
phone?: string; |
|||
/** User Status */ |
|||
userStatus?: number; |
|||
}; |
|||
|
|||
type Tag = { |
|||
id?: number; |
|||
name?: string; |
|||
}; |
|||
|
|||
type Pet = { |
|||
id?: number; |
|||
category?: Category; |
|||
name: string; |
|||
photoUrls: string[]; |
|||
tags?: Tag[]; |
|||
/** pet status in the store */ |
|||
status?: 'available' | 'pending' | 'sold'; |
|||
}; |
|||
|
|||
type ApiResponse = { |
|||
code?: number; |
|||
type?: string; |
|||
message?: string; |
|||
}; |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
// @ts-ignore
|
|||
/* eslint-disable */ |
|||
import { request } from 'umi'; |
|||
|
|||
/** 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<any>('/user', { |
|||
method: 'POST', |
|||
data: body, |
|||
...(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<any>('/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<any>('/user/createWithList', { |
|||
method: 'POST', |
|||
data: body, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Logs user into the system GET /user/login */ |
|||
export async function loginUser( |
|||
params: { |
|||
// query
|
|||
/** The user name for login */ |
|||
username: string; |
|||
/** The password for login in clear text */ |
|||
password: string; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
return request<string>('/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<any>('/user/logout', { |
|||
method: 'GET', |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Get user by user name GET /user/${param0} */ |
|||
export async function getUserByName( |
|||
params: { |
|||
// path
|
|||
/** The name that needs to be fetched. Use user1 for testing. */ |
|||
username: string; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { username: param0 } = params; |
|||
return request<API.User>(`/user/${param0}`, { |
|||
method: 'GET', |
|||
params: { ...params }, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Updated user This can only be done by the logged in user. PUT /user/${param0} */ |
|||
export async function updateUser( |
|||
params: { |
|||
// path
|
|||
/** name that need to be updated */ |
|||
username: string; |
|||
}, |
|||
body: API.User, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { username: param0 } = params; |
|||
return request<any>(`/user/${param0}`, { |
|||
method: 'PUT', |
|||
params: { ...params }, |
|||
data: body, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
|
|||
/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */ |
|||
export async function deleteUser( |
|||
params: { |
|||
// path
|
|||
/** The name that needs to be deleted */ |
|||
username: string; |
|||
}, |
|||
options?: { [key: string]: any }, |
|||
) { |
|||
const { username: param0 } = params; |
|||
return request<any>(`/user/${param0}`, { |
|||
method: 'DELETE', |
|||
params: { ...params }, |
|||
...(options || {}), |
|||
}); |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
import request from '@/utils/request'; |
|||
|
|||
export async function query(): Promise<any> { |
|||
return request('/api/users'); |
|||
} |
|||
|
|||
export async function queryCurrent(): Promise<any> { |
|||
return request('/api/currentUser'); |
|||
} |
|||
|
|||
export async function queryNotices(): Promise<any> { |
|||
return request('/api/notices'); |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
import RenderAuthorize from '@/components/Authorized'; |
|||
import { getAuthority } from './authority'; |
|||
/* eslint-disable eslint-comments/disable-enable-pair */ |
|||
/* eslint-disable import/no-mutable-exports */ |
|||
let Authorized = RenderAuthorize(getAuthority()); |
|||
|
|||
// Reload the rights component
|
|||
const reloadAuthorized = (): void => { |
|||
Authorized = RenderAuthorize(getAuthority()); |
|||
}; |
|||
|
|||
/** Hard code block need it。 */ |
|||
window.reloadAuthorized = reloadAuthorized; |
|||
|
|||
export { reloadAuthorized }; |
|||
export default Authorized; |
|||
@ -1,32 +0,0 @@ |
|||
import { reloadAuthorized } from './Authorized'; |
|||
|
|||
// use localStorage to store the authority info, which might be sent from server in actual project.
|
|||
export function getAuthority(str?: string): string | string[] { |
|||
const authorityString = |
|||
typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str; |
|||
// authorityString could be admin, "admin", ["admin"]
|
|||
let authority; |
|||
try { |
|||
if (authorityString) { |
|||
authority = JSON.parse(authorityString); |
|||
} |
|||
} catch (e) { |
|||
authority = authorityString; |
|||
} |
|||
if (typeof authority === 'string') { |
|||
return [authority]; |
|||
} |
|||
// preview.pro.ant.design only do not use in your production.
|
|||
// preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
|
|||
if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { |
|||
return ['admin']; |
|||
} |
|||
return authority; |
|||
} |
|||
|
|||
export function setAuthority(authority: string | string[]): void { |
|||
const proAuthority = typeof authority === 'string' ? [authority] : authority; |
|||
localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority)); |
|||
// auto reload
|
|||
reloadAuthorized(); |
|||
} |
|||
@ -1,55 +0,0 @@ |
|||
/** Request 网络请求工具 更详细的 api 文档: https://github.com/umijs/umi-request */ |
|||
import { extend } from 'umi-request'; |
|||
import { notification } from 'antd'; |
|||
|
|||
const codeMessage: Record<number, string> = { |
|||
200: '服务器成功返回请求的数据。', |
|||
201: '新建或修改数据成功。', |
|||
202: '一个请求已经进入后台排队(异步任务)。', |
|||
204: '删除数据成功。', |
|||
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。', |
|||
401: '用户没有权限(令牌、用户名、密码错误)。', |
|||
403: '用户得到授权,但是访问是被禁止的。', |
|||
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。', |
|||
406: '请求的格式不可得。', |
|||
410: '请求的资源被永久删除,且不会再得到的。', |
|||
422: '当创建一个对象时,发生一个验证错误。', |
|||
500: '服务器发生错误,请检查服务器。', |
|||
502: '网关错误。', |
|||
503: '服务不可用,服务器暂时过载或维护。', |
|||
504: '网关超时。', |
|||
}; |
|||
|
|||
/** |
|||
* @zh-CN 异常处理程序 |
|||
* @en-US Exception handler |
|||
*/ |
|||
const errorHandler = (error: { response: Response }): Response => { |
|||
const { response } = error; |
|||
if (response && response.status) { |
|||
const errorText = codeMessage[response.status] || response.statusText; |
|||
const { status, url } = response; |
|||
|
|||
notification.error({ |
|||
message: `Request error ${status}: ${url}`, |
|||
description: errorText, |
|||
}); |
|||
} else if (!response) { |
|||
notification.error({ |
|||
description: 'Your network is abnormal and cannot connect to the server', |
|||
message: 'Network anomaly', |
|||
}); |
|||
} |
|||
return response; |
|||
}; |
|||
|
|||
/** |
|||
* @en-US Configure the default parameters for request |
|||
* @zh-CN 配置request请求时的默认参数 |
|||
*/ |
|||
const request = extend({ |
|||
errorHandler, // default error handling
|
|||
credentials: 'include', // Does the default request bring cookies
|
|||
}); |
|||
|
|||
export default request; |
|||
@ -1,16 +0,0 @@ |
|||
// mixins for clearfix |
|||
// ------------------------ |
|||
.clearfix() { |
|||
zoom: 1; |
|||
&::before, |
|||
&::after { |
|||
display: table; |
|||
content: ' '; |
|||
} |
|||
&::after { |
|||
clear: both; |
|||
height: 0; |
|||
font-size: 0; |
|||
visibility: hidden; |
|||
} |
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
import { isUrl } from './utils'; |
|||
|
|||
describe('isUrl tests', (): void => { |
|||
it('should return false for invalid and corner case inputs', (): void => { |
|||
expect(isUrl([] as any)).toBeFalsy(); |
|||
expect(isUrl({} as any)).toBeFalsy(); |
|||
expect(isUrl(false as any)).toBeFalsy(); |
|||
expect(isUrl(true as any)).toBeFalsy(); |
|||
expect(isUrl(NaN as any)).toBeFalsy(); |
|||
expect(isUrl(null as any)).toBeFalsy(); |
|||
expect(isUrl(undefined as any)).toBeFalsy(); |
|||
expect(isUrl('')).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return false for invalid URLs', (): void => { |
|||
expect(isUrl('foo')).toBeFalsy(); |
|||
expect(isUrl('bar')).toBeFalsy(); |
|||
expect(isUrl('bar/test')).toBeFalsy(); |
|||
expect(isUrl('http:/example.com/')).toBeFalsy(); |
|||
expect(isUrl('ttp://example.com/')).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return true for valid URLs', (): void => { |
|||
expect(isUrl('http://example.com/')).toBeTruthy(); |
|||
expect(isUrl('https://example.com/')).toBeTruthy(); |
|||
expect(isUrl('http://example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('https://example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy(); |
|||
expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy(); |
|||
expect(isUrl('http://www.example.com/')).toBeTruthy(); |
|||
expect(isUrl('https://www.example.com/')).toBeTruthy(); |
|||
expect(isUrl('http://www.example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('https://www.example.com/test/123')).toBeTruthy(); |
|||
expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy(); |
|||
expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -1,24 +0,0 @@ |
|||
import { parse } from 'querystring'; |
|||
|
|||
/* eslint no-useless-escape:0 import/prefer-default-export:0 */ |
|||
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; |
|||
|
|||
export const isUrl = (path: string): boolean => reg.test(path); |
|||
|
|||
export const isAntDesignPro = (): boolean => { |
|||
if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { |
|||
return true; |
|||
} |
|||
return window.location.hostname === 'preview.pro.ant.design'; |
|||
}; |
|||
|
|||
// For the official demo site, it is used to turn off features that are not needed in the real development environment
|
|||
export const isAntDesignProOrDev = (): boolean => { |
|||
const { NODE_ENV } = process.env; |
|||
if (NODE_ENV === 'development') { |
|||
return true; |
|||
} |
|||
return isAntDesignPro(); |
|||
}; |
|||
|
|||
export const getPageQuery = () => parse(window.location.href.split('?')[1]); |
|||
@ -0,0 +1,10 @@ |
|||
// do some test init
|
|||
|
|||
const localStorageMock = { |
|||
getItem: jest.fn(), |
|||
setItem: jest.fn(), |
|||
removeItem: jest.fn(), |
|||
clear: jest.fn(), |
|||
}; |
|||
|
|||
global.localStorage = localStorageMock; |
|||
Loading…
Reference in new issue