This commit is contained in:
sx1989827 2021-07-30 22:43:29 +08:00
commit 05b4e21f58
96 changed files with 6420 additions and 0 deletions

16
code/app/.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

8
code/app/.eslintignore Normal file
View File

@ -0,0 +1,8 @@
/lambda/
/scripts
/config
.history
public
dist
.umi
mock

8
code/app/.eslintrc.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
page: true,
REACT_APP_ENV: true,
},
};

40
code/app/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
# roadhog-api-doc ignore
/src/utils/request-temp.js
_roadhog-api-doc
# production
/dist
/.vscode
# misc
.DS_Store
npm-debug.log*
yarn-error.log
/coverage
.idea
yarn.lock
package-lock.json
*bak
.vscode
# visual studio code
.history
*.log
functions/*
.temp/**
# umi
.umi
.umi-production
# screenshot
screenshot
.firebase
.eslintcache
build

23
code/app/.prettierignore Normal file
View File

@ -0,0 +1,23 @@
**/*.svg
package.json
.umi
.umi-production
/dist
.dockerignore
.DS_Store
.eslintignore
*.png
*.toml
docker
.editorconfig
Dockerfile*
.gitignore
.prettierignore
LICENSE
.eslintcache
*.lock
yarn-error.log
.history
CNAME
/build
/public

5
code/app/.prettierrc.js Normal file
View File

@ -0,0 +1,5 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
};

5
code/app/.stylelintrc.js Normal file
View File

@ -0,0 +1,5 @@
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.stylelint,
};

57
code/app/README.md Normal file
View File

@ -0,0 +1,57 @@
# Ant Design Pro
This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
## Environment Prepare
Install `node_modules`:
```bash
npm install
```
or
```bash
yarn
```
## Provided Scripts
Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
Scripts provided in `package.json`. It's safe to modify or add additional script:
### Start project
```bash
npm start
```
### Build project
```bash
npm run build
```
### Check code style
```bash
npm run lint
```
You can also use script to auto fix some lint error:
```bash
npm run lint:fix
```
### Test code
```bash
npm test
```
## More
You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).

View File

@ -0,0 +1,15 @@
// https://umijs.org/config/
import { defineConfig } from 'umi';
export default defineConfig({
plugins: [
// https://github.com/zthxxx/react-dev-inspector
'react-dev-inspector/plugins/umi/react-inspector',
],
// https://github.com/zthxxx/react-dev-inspector#inspector-loader-props
inspectorConfig: {
exclude: [],
babelPlugins: [],
babelOptions: {},
},
});

71
code/app/config/config.ts Normal file
View File

@ -0,0 +1,71 @@
// https://umijs.org/config/
import { defineConfig } from 'umi';
import { join } from 'path';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
import routes from './routes';
const { REACT_APP_ENV } = process.env;
export default defineConfig({
hash: true,
antd: {},
dva: {
hmr: true,
},
layout: {
// https://umijs.org/zh-CN/plugins/plugin-layout
locale: true,
siderWidth: 208,
...defaultSettings,
},
// 国际化 https://umijs.org/zh-CN/plugins/plugin-locale
locale: {
default: 'en-US',
antd: true,
// default true, when it is true, will use `navigator.language` overwrite default
baseNavigator: true,
},
dynamicImport: {
loading: '@ant-design/pro-layout/es/PageLoading',
},
targets: {
ie: 11,
},
// umi routes: https://umijs.org/docs/routing
routes,
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
theme: {
'primary-color': defaultSettings.primaryColor,
},
// esbuild is father build tools
// https://umijs.org/plugins/plugin-esbuild
esbuild: {},
title: false,
ignoreMomentLocale: true,
proxy: proxy[REACT_APP_ENV || 'dev'],
manifest: {
basePath: '/',
},
// Fast Refresh 热更新
fastRefresh: {},
openAPI: [
{
requestLibPath: "import { request } from 'umi'",
// 或者使用在线的版本
// schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
schemaPath: join(__dirname, 'oneapi.json'),
mock: false,
},
{
requestLibPath: "import { request } from 'umi'",
schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
projectName: 'swagger',
},
],
nodeModulesTransform: { type: 'none' },
mfsu: {},
webpack5: {},
exportStatic: {},
});

View File

@ -0,0 +1,22 @@
import { Settings as LayoutSettings } from '@ant-design/pro-layout';
const Settings: LayoutSettings & {
pwa?: boolean;
logo?: string;
} = {
navTheme: 'light',
headerTheme: 'light',
// TeamLinker 主题
primaryColor: '#0052CC',
layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: true,
colorWeak: false,
title: false,
pwa: false,
iconfontUrl: '',
splitMenus: true,
};
export default Settings;

593
code/app/config/oneapi.json Normal file
View File

@ -0,0 +1,593 @@
{
"openapi": "3.0.1",
"info": {
"title": "Teamlinker",
"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"
}
}
}
}
}
}

31
code/app/config/proxy.ts Normal file
View File

@ -0,0 +1,31 @@
/**
*
* -------------------------------
* The agent cannot take effect in the production environment
* so there is no configuration of the production environment
* For details, please see
* https://pro.ant.design/docs/deploy
*/
export default {
dev: {
'/api/': {
target: 'https://preview.pro.ant.design',
changeOrigin: true,
pathRewrite: { '^': '' },
},
},
test: {
'/api/': {
target: 'https://preview.pro.ant.design',
changeOrigin: true,
pathRewrite: { '^': '' },
},
},
pre: {
'/api/': {
target: 'your pre url',
changeOrigin: true,
pathRewrite: { '^': '' },
},
},
};

52
code/app/config/routes.ts Normal file
View File

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

10
code/app/jest.config.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
testURL: 'http://localhost:8000',
testEnvironment: './tests/PuppeteerEnvironment',
verbose: false,
extraSetupFiles: ['./tests/setupTests.js'],
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
localStorage: null,
},
};

10
code/app/jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View File

@ -0,0 +1,175 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { Request, Response } from 'express';
import moment from 'moment';
import { parse } from 'url';
// mock tableListDataSource
const genList = (current: number, pageSize: number) => {
const tableListDataSource: API.RuleListItem[] = [];
for (let i = 0; i < pageSize; i += 1) {
const index = (current - 1) * 10 + i;
tableListDataSource.push({
key: index,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${index}`,
owner: '曲丽丽',
desc: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: moment().format('YYYY-MM-DD'),
createdAt: moment().format('YYYY-MM-DD'),
progress: Math.ceil(Math.random() * 100),
});
}
tableListDataSource.reverse();
return tableListDataSource;
};
let tableListDataSource = genList(1, 100);
function getRule(req: Request, res: Response, u: string) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
const params = parse(realUrl, true).query as unknown as API.PageParams &
API.RuleListItem & {
sorter: any;
filter: any;
};
let dataSource = [...tableListDataSource].slice(
((current as number) - 1) * (pageSize as number),
(current as number) * (pageSize as number),
);
if (params.sorter) {
const sorter = JSON.parse(params.sorter);
dataSource = dataSource.sort((prev, next) => {
let sortNumber = 0;
Object.keys(sorter).forEach((key) => {
if (sorter[key] === 'descend') {
if (prev[key] - next[key] > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (prev[key] - next[key] > 0) {
sortNumber += 1;
} else {
sortNumber += -1;
}
});
return sortNumber;
});
}
if (params.filter) {
const filter = JSON.parse(params.filter as any) as {
[key: string]: string[];
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return Object.keys(filter).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}
if (params.name) {
dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
}
const result = {
data: dataSource,
total: tableListDataSource.length,
success: true,
pageSize,
current: parseInt(`${params.current}`, 10) || 1,
};
return res.json(result);
}
function postRule(req: Request, res: Response, u: string, b: Request) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const body = (b && b.body) || req.body;
const { method, name, desc, key } = body;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {
const i = Math.ceil(Math.random() * 10000);
const newRule: API.RuleListItem = {
key: tableListDataSource.length,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name,
owner: '曲丽丽',
desc,
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 2,
updatedAt: moment().format('YYYY-MM-DD'),
createdAt: moment().format('YYYY-MM-DD'),
progress: Math.ceil(Math.random() * 100),
};
tableListDataSource.unshift(newRule);
return res.json(newRule);
})();
return;
case 'update':
(() => {
let newRule = {};
tableListDataSource = tableListDataSource.map((item) => {
if (item.key === key) {
newRule = { ...item, desc, name };
return { ...item, desc, name };
}
return item;
});
return res.json(newRule);
})();
return;
default:
break;
}
const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};
res.json(result);
}
export default {
'GET /api/rule': getRule,
'POST /api/rule': postRule,
};

107
code/app/mock/notices.ts Normal file
View File

@ -0,0 +1,107 @@
import { Request, Response } from 'express';
const getNotices = (req: Request, res: Response) => {
res.json({
data: [
{
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',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: 'notification',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: 'event',
},
{
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: 'event',
},
{
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: 'event',
},
{
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: 'event',
},
],
});
};
export default {
'GET /api/notices': getNotices,
};

5
code/app/mock/route.ts Normal file
View File

@ -0,0 +1,5 @@
export default {
'/api/auth_routes': {
'/form/advanced-form': { authority: ['admin', 'user'] },
},
};

203
code/app/mock/user.ts Normal file
View File

@ -0,0 +1,203 @@
import { Request, Response } from 'express';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
async function getFakeCaptcha(req: Request, res: Response) {
await waitTime(2000);
return res.json('captcha-xxx');
}
const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
/**
*
* current user access if is '', user need login
* pro
*/
let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
const getAccess = () => {
return access;
};
// 代码中会兼容本地 service mock 以及部署站点的静态数据
export default {
// 支持值为 Object 和 Array
'GET /api/currentUser': (req: Request, res: Response) => {
if (!getAccess()) {
res.status(401).send({
data: {
isLogin: false,
},
errorCode: '401',
errorMessage: '请先登录!',
success: true,
});
return;
}
res.send({
success: true,
data: {
name: 'Geminy',
avatar: 'https://avatars.githubusercontent.com/u/14195576?s=60&v=4',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服某某某事业群某某平台部某某技术部UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
access: getAccess(),
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
});
},
// GET POST 可省略
'GET /api/users': [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
],
'POST /api/login/account': async (req: Request, res: Response) => {
const { password, username, type } = req.body;
await waitTime(2000);
if (password === '123' && username === 'admin') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
access = 'admin';
return;
}
if (password === '123' && username === 'user') {
res.send({
status: 'ok',
type,
currentAuthority: 'user',
});
access = 'user';
return;
}
if (type === 'mobile') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
access = 'admin';
return;
}
res.send({
status: 'error',
type,
currentAuthority: 'guest',
});
access = 'guest';
},
'POST /api/login/outLogin': (req: Request, res: Response) => {
access = '';
res.send({ data: {}, success: true });
},
'POST /api/register': (req: Request, res: Response) => {
res.send({ status: 'ok', currentAuthority: 'user', success: true });
},
'GET /api/500': (req: Request, res: Response) => {
res.status(500).send({
timestamp: 1513932555104,
status: 500,
error: 'error',
message: 'error',
path: '/base/category/list',
});
},
'GET /api/404': (req: Request, res: Response) => {
res.status(404).send({
timestamp: 1513932643431,
status: 404,
error: 'Not Found',
message: 'No message available',
path: '/base/category/list/2121212',
});
},
'GET /api/403': (req: Request, res: Response) => {
res.status(403).send({
timestamp: 1513932555104,
status: 403,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/401': (req: Request, res: Response) => {
res.status(401).send({
timestamp: 1513932555104,
status: 401,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/login/captcha': getFakeCaptcha,
};

110
code/app/package.json Normal file
View File

@ -0,0 +1,110 @@
{
"name": "ant-design-pro",
"version": "5.0.0",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
"build": "umi build",
"deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "umi g tmp",
"lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto",
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
"openapi": "umi openapi",
"precommit": "lint-staged",
"prettier": "prettier -c --write \"src/**/*\"",
"start": "cross-env UMI_ENV=dev umi dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev umi dev",
"start:no-ui": "cross-env UMI_UI=none UMI_ENV=dev umi dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev umi dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev umi dev",
"pretest": "node ./tests/beforeTest",
"test": "umi test",
"test:all": "node ./tests/run-tests.js",
"test:component": "umi test ./src/components",
"serve": "umi-serve",
"tsc": "tsc --noEmit",
"pkgup:taobao": "npm install --registry=https://registry.npm.taobao.org"
},
"lint-staged": {
"**/*.less": "stylelint --syntax less",
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"dependencies": {
"@ant-design/icons": "^4.5.0",
"@ant-design/pro-descriptions": "^1.6.8",
"@ant-design/pro-form": "^1.18.3",
"@ant-design/pro-layout": "^6.15.3",
"@ant-design/pro-table": "^2.30.8",
"@umijs/route-utils": "^1.0.36",
"antd": "^4.14.0",
"classnames": "^2.2.6",
"lodash": "^4.17.11",
"moment": "^2.25.3",
"omit.js": "^2.0.2",
"react": "^17.0.0",
"react-dev-inspector": "^1.1.1",
"react-dom": "^17.0.0",
"react-helmet-async": "^1.0.4",
"umi": "^3.5.0",
"umi-serve": "^1.9.10"
},
"devDependencies": {
"@ant-design/pro-cli": "^2.0.2",
"@types/express": "^4.17.0",
"@types/history": "^4.7.2",
"@types/jest": "^26.0.0",
"@types/lodash": "^4.14.144",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.0",
"@umijs/fabric": "^2.6.2",
"@umijs/openapi": "^1.1.14",
"@umijs/plugin-blocks": "^2.0.5",
"@umijs/plugin-esbuild": "^1.0.1",
"@umijs/plugin-openapi": "^1.2.0",
"@umijs/preset-ant-design-pro": "^1.2.0",
"@umijs/preset-dumi": "^1.1.7",
"@umijs/preset-react": "^1.8.17",
"@umijs/preset-ui": "^2.2.9",
"@umijs/yorkie": "^2.0.3",
"carlo": "^0.9.46",
"cross-env": "^7.0.0",
"cross-port-killer": "^1.1.1",
"detect-installer": "^1.0.1",
"enzyme": "^3.11.0",
"eslint": "^7.1.0",
"express": "^4.17.1",
"gh-pages": "^3.0.0",
"jsdom-global": "^3.0.2",
"lint-staged": "^10.0.0",
"mockjs": "^1.0.1-beta3",
"prettier": "^2.3.2",
"puppeteer-core": "^8.0.0",
"stylelint": "^13.0.0",
"typescript": "^4.2.2"
},
"engines": {
"node": ">=10.0.0"
},
"gitHooks": {
"commit-msg": "fabric verify-commit"
}
}

1
code/app/public/CNAME Normal file
View File

@ -0,0 +1 @@
preview.pro.ant.design

BIN
code/app/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
code/app/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

9
code/app/public/logo.svg Normal file
View File

@ -0,0 +1,9 @@
<svg width="202" height="31" viewBox="0 0 202 31" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="202" height="31" fill="url(#pattern0)" />
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0" transform="scale(0.0049505 0.0322581)" />
</pattern>
<image id="image0" width="202" height="31" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMoAAAAfCAYAAACmllI0AAAN/0lEQVR4nO1cCbRVVRn+7vCmi4DEDKaBA7BQyxBIJU2TQMgiLTE1yyaWRYtIo1AWZYNJgxKaVCZUamlimk3GuFJSQwkwTSLiBaLEkO/x3vPN977W9n47Nv/5977DGxh631pnvfXO2Wefvfc/7v//943hkmfXAhgKIIaOQRLAfwB8BMCGDurz/wkJAGcDuAnAaACvAlgA4EEArynr8A4+PwFAFYDfAPgigHSkZedhCoAvcQzVAB7hGI4aGKYez79tHTSpGAWvf+RJN/KBWbsHAPQFUAqgH4C7KTCPivfPBXA7gFP5/wC2T3cho74PwCIAx/H/gQCuocB/PtL6CEUcQAuHHuugy6LlaFmkLkQKwHQAgykkII3MNQPAyWIoFzhCAtLPCMqHAVR0wbB70pocJ+4P4DyOibxxhCLeSQzd1sWm/2iBsezDPXMxQtJH3ItHWmWRitzpHPSh5dNQcbQJSjcOHxgF0+QZTbOifJ4EsE3cawTwM/7tbGR4+RB6dkQh2UnaJ0YftRuFo9XzRqvCeKsBfBbAR+nu1AF4GsCdHbjnDCGT4zuhZ0cUkrw6A6VHxxJ1OXzRx5jyzFiZ3wP4CwWlHsC/AlapG0XCCMl3AbxBvJ6hW3YuQ34aTKjytwpREtRs/1TeAb/Zj75tOYnfAKAWwG6lv3wQZ5StDy1kKbVZC10QM549HeSOmL4HkTHjXIddjEppGMyrjG0NI9co7YqFsTQv82ov4lzDQZynoctOjvtwsQ4mcDCE4ywhjWtI311UFsUiybkPZJh7u7XwSZruMtGxNfFXApgH4ETx3AzoNgA/IPO5mi5GTSfdhBTzAhMAjKWg9CABGjiwnfS7n+DEcxGngv29jWPsz0hMGd9t5sLVkJH+DGANBTIEI8DjAJxFgvwIwFaGbt8P4HwKe4IC/iKAXwD4k9OnEaRLAJwD4HiO1SqQZQBWKGtUDAxhL+Y39gNYB+BxpZ93AXgr1ybDv1sAPMa1Ppb9XMg+yykglQAeJk3aa6n6kVaj2b+df5I5IEObTZG3sngzgIv4902ktRWUKvLkX7muz3hyTmDeaSzpESd/mPn9A8A0AFeT5lVcS5MTet66Xb4F+DEJcJNzr4kdfyvS2o9eDBd+DsBIb6ssPgngVwAW0qXwRc+MQFwFYC6AN0ae6jBMfRfH/m+lhQ2vmlDsJ9hvjMK7lfmBr0beAiYCmMzn62mhb+T/cg9ohOw85jl+GempMBiG+QqADzlvGYLPokvm4hsUFBdGgXwNwBIAM6kUNVd8Cp8vb0eU1DDm9QCuJT9IVFJRSkGJU3jnUXFp47O4mGu+kHPaJ56/k/QfJu73o5AZmo1w7l9Er+qa0EctmsX/1ew0XxjNcR2TT/nE9o2VuYIWYjaApyItsm0M4W6OPAmjJ4W1HxdUs3omw/xxYWWH08JcGOj9FAAPkUijAFyuWGoLE+qdA2AHgGcjT/PH5UJIwL6/B+AkMT8twtmLGvR8MoUPRiB/SIHxaXwN7jfnBISkkRb5YXG/lAnNpQXkhYybO58u2myhaK/zKNVZVIgl4n6SgrJAWzwJmWFPeT7mg9EiNxSRABvPCY+KPMkybKFC4sIQ/NPiXpIMM0Nh8Pl0USZEejoYxmW5hQk/n5BYjKewF4ty+tIa+iqWrFZpByYsQ0JiMYSWsDzyxA/rOp/C9daExOAOT9mNee/eInjHKlu5vr5IbKkiJBaGjpPyEZQeSqcy8eXDOfTTNctlTPhL1Kq+jbBxZ94r7hlCnR5peQA17Hcb9yXSIoIWZaLS7wyP5u3Dy7fQxeIE7mWKQZtiES1eUyJkvmhaIZhWgJLMcO9j+OUnSsDIYhnD2RIDqHQ03mmlq7aSrq62ge9LJe0q+rrAmoXQoDGFi6SiGUsCmkHCuDBnRO5mheQP1GTWX98faZWFMb1nind9bc3+6VajAQCMoc+quW7gAg5y/o+ROCGGslE0377JRYbjCRGmXx57Nh9Cyb62IqJUGSqV0HunF6AkQSVrgh9v8SiZjawTq4w8ya7L1MjdLFbz2UQq4+9HWmTRp8B6M0szdx9mhHBtLkEpU/Ih8TxLE0YEmGANmfgFhkuXcO+gaYZxop7JMOmvWTjYzIhZE/8a3/ybjEJVs3rZaLNXIr1mNdyp4p4v2eeO+84892gb6Vv/PcDQFQUyXmdiKyuU/xb4Rt8C3KA+ZOh7Pe6acQW/zGiaxABl7+W+9wXSGKT9d8gPEj1oBfNFJcf7EPlyB/dOn9HMmotyRVCQJ3EvUKILINP+XNxLU1jmchMqMZKa3mq7Ska8hrL9MBJxEQXGxXCP2S9X7ocsxQIStpGa7C5PjsmMcTMjLEZYezNkOTbSMru2PSN3ux6/A3Ap53Y8GVymBCzyrd8qoZLz4VOMbmroTU9DwxJGx1y8QiU2XWnfl/srTVm62M6Ag41EJpwQueqPuyhXXC9wIpopdTGCroU2oBWRu1ms8+wpTiQBXbRS4tcwjP1tR0iMIL+HWmamR6MV4p7soT9sE5ZPUftqyFCgbBx/P8OqdUrb+GFQ6mPW8D5nbvuYI/OhI8a7lZcPZQq9LV7yJI5rPaHrhAj5+rBMhOvTbi4ml0Wp8AhKCTVhdeTJAQz2LKoJX36dWte1Vkkmo7QxDaTQbRf3LbPHuBeawFj72dxHVQSiGaHNsMR6EsiiwcP4tt/1wo3bxiCD1MaFjKGzsI2JWItmz57BHXN7cRLd3qc9/Rzr4TvQRZ9KGsQdHjjZQ+uEI3Shscvi0oOgMaWLCo82TpARQ4KiuTugKbyMBJEZ/ZTHyvXyRN/Gs3pgLDfmKaeEpSOxX2ixmGecIAFrBFGK2Vx3Fep4ItUda8gF7SiYvcNaKkyJkGs/hIrTXc8QPeLOUQAfDXKWAOWzR9EkO0n3KwQfs8YogIXExgeIcw9DGPq7kpatsw8ppRXN71v0FqVtPEc07VAi43FZOhtTuVHWBEWzDC40T8WHBBOmIewVyiKCYl2vJM1jCB25+E3O3qWMFml2pFUWm5gc3Enhmu5JWhYCjcm1e/BU+R7OkGHmrhz/RO7fZDmRr06rGKQZwUJgXrW5vplLUFIe1ysfQamK3DmAeoYFHw8M3sJ8/znWW4H7kDmRVlli3wNgMbVUHTXTSpaVnBl5oxs4xEJ9BSNsS8V9XwLaYhV/P2CP43JZCy/d+SanWNU3V5k7iaA9FiXkR4LhuIziO7Yx6jVT0STuBjjhTMzVeqcxUOAiTcG7kd+1i5bmIr3cLSiHBK2MDm5iGZNMVMcZfLlfhPXrqei0UPRuhup9kdNikM61L/NtgCx8gpJQJi3xIn0/iXpaiJ1cSPcayauUA28VJ/t6eXIzxnR+TAiJxXSWZneja5FhQnkOGVvmPiymMvnsoj6Q+NytRD/BfNk00lrj2RBCVQ6vIx9B0VyvhBKFkljuiZWnOBnXKsSZoFvIzPfdLE2Qm7YeniBCGxdXCsloVobm2sx1o+NRywSyFZAHPAfWTGDm7eLe3kCeagST2W4AZxSPPyzmNTdQMKohZ1QyH9dLi14lPGbRhYlLP0+GdxFj9e9trNFJ00rcwFi4Fd7RPAu+0Xm33rPYx1C4HnQs1TBWrMozGCG4eZlioS24dg/tDBuH3vXdzxcdsW9pFGdi7mBJv+aJnEdFuYr/1/H07M1KBKyM1dxDmfjtT2/CCtsgKuLeSsCn6HUpNjycj0Ux+CnLGGRhZIqRq0nMBGtlK2eQyV1B2e9JDJWwdH6KszcalGN+Wuy9xMMkMhIUV94N9VHqaa9l5n3ElIKRCMwvoYzBh7jSj6/fQpBwTgqCG/QNtO5yzqexeHKVc28nD5vNV745mAe5WjxrmOKRCYmkZ10SSh8HQSOei3JFopGnRQEjVcs8xI8xcqYJiYUmpBs8PqrFEB7ascTWciDgYrrmOeZxM0FGDy6kgwplvknPWieUOfqYVPYRsiipwDOJVlE21OYpTi0GkncWiwoHF2OEQq3l7zm8EJhLSYAu2qndlEdQQorvfw1C0PYDIOMMjdzVsYil7xqzhrCdpSAST7JaNB/sY0Rli9K2hzhb0RogSpWIyjR7MrkZupuyvGWzJ+RZx7PeFiFhLRdC1Mw6LYk2amPJ7L6k7BbBVGlWPPt+V8AdQy5NLJlytZivi7F0tV1UM4S8ucC83HbSXaLSUyG+IzDf15FPmT0o3e7VUsCBozoWCc5jOLiOg5UlHq1kxlcpIJexSFKihVbqdiaJmpy+bD+1FJJZrDK+n9+tIdPXkxncH4No4G8DPMd3q+jq7eYPDMhQ9gpWQdeToNU8r36tUti5km13cQz7OZ7lPDJg0co+qvl9ezXyvtyfPcqC0Gb2V8t5Xa8oplr2b9s1c193a2SFs3O9mtrfjLXWEX5ZytPo9G+ver6nKccnuFlvcNrbeY0U+bkMjzRM4vrtYNtG8kGrYxHruG7PcL+rFXbeQkVWw/YNDF3fp5yvPwjm1+wjNx1MZvgu6cSZ4xzYeib4CsEYEmAcN2HWfbO/wlJJP/UehSkkerE0/CpahhTHtYfFdku5KGlWH0+mFbTh5nUsLz94PbJ7m0tZhBnnOe5NHqIPpm89kIyxJscZ+Mks2EzwhzMeUzLCfXngzVqAGNf/Ec+PbRhX84OM/Oxln3+MfDmreM5yLNYmCpqv/DzB3NO7ybzGzTECZITLuk/2TLthZGvB7HEIY9XMj3jI8Zo9iqGZKVR0FYqZr7Ho5viC79iyCQKZTb/Z0xj+Meti6GL3rqZ2zCilUEm9CRh9gN833zHz0cpoDgDAfwG0VmQaaHaGxwAAAABJRU5ErkJggg==" />
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="#070707" d="m6.717392,13.773912l5.6,0c2.8,0 4.7,1.9 4.7,4.7c0,2.8 -2,4.7 -4.9,4.7l-2.5,0l0,4.3l-2.9,0l0,-13.7zm2.9,2.2l0,4.9l1.9,0c1.6,0 2.6,-0.9 2.6,-2.4c0,-1.6 -0.9,-2.4 -2.6,-2.4l-1.9,0l0,-0.1zm8.9,11.5l2.7,0l0,-5.7c0,-1.4 0.8,-2.3 2.2,-2.3c0.4,0 0.8,0.1 1,0.2l0,-2.4c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-1.2,0 -2.1,0.7 -2.4,2l-0.1,0l0,-1.9l-2.7,0l0,10.2l0.1,0zm11.7,0.1c-3.1,0 -5,-2 -5,-5.3c0,-3.3 2,-5.3 5,-5.3s5,2 5,5.3c0,3.4 -1.9,5.3 -5,5.3zm0,-2.1c1.4,0 2.2,-1.1 2.2,-3.2c0,-2 -0.8,-3.2 -2.2,-3.2c-1.4,0 -2.2,1.2 -2.2,3.2c0,2.1 0.8,3.2 2.2,3.2z" class="st0" id="Ant-Design-Pro"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 677 B

9
code/app/src/access.ts Normal file
View File

@ -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',
};
}

17
code/app/src/animate.css vendored Normal file
View File

@ -0,0 +1,17 @@
.zoom-enter {
opacity: 0;
transform: scale(0.9);
}
.zoom-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.zoom-exit {
opacity: 1;
}
.zoom-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}

138
code/app/src/app.tsx Normal file
View File

@ -0,0 +1,138 @@
import type { Settings as LayoutSettings } from '@ant-design/pro-layout';
import { PageLoading } from '@ant-design/pro-layout';
import { notification, Image } from 'antd';
import type { RequestConfig, RunTimeLayoutConfig } from 'umi';
import { history, Link } from 'umi';
import RightContent from '@/components/RightContent';
import Footer from '@/components/Footer';
import { UserService } from './services/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 msg = await UserService.currentUser();
return msg.data;
} 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,
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>,
logo: () => (
<div>
<Image preview={false} src="./logo.png" width={160} height={24} />
</div>
),
...initialState?.settings,
};
};

View File

@ -0,0 +1,24 @@
import { useIntl } from 'umi';
import { DefaultFooter } from '@ant-design/pro-layout';
export default () => {
const intl = useIntl();
const defaultMessage = intl.formatMessage({
id: 'app.copyright.produced',
});
// TODO: 官网
return (
<DefaultFooter
copyright={`${new Date().getFullYear()} ${defaultMessage}`}
links={[
{
key: 'Teamlinker',
title: 'Teamlinker',
href: 'https://pro.ant.design',
blankTarget: true,
},
]}
/>
);
};

View File

@ -0,0 +1,16 @@
@import '~antd/es/style/themes/default.less';
.container > * {
background-color: @popover-bg;
border-radius: 4px;
box-shadow: @shadow-1-down;
}
@media screen and (max-width: @screen-xs) {
.container {
width: 100% !important;
}
.container > * {
border-radius: 0 !important;
}
}

View File

@ -0,0 +1,17 @@
import type { DropDownProps } from 'antd/es/dropdown';
import { Dropdown } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
export type HeaderDropdownProps = {
overlayClassName?: string;
overlay: React.ReactNode | (() => React.ReactNode) | any;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
);
export default HeaderDropdown;

View File

@ -0,0 +1,25 @@
@import '~antd/es/style/themes/default.less';
.headerSearch {
display: inline-flex;
align-items: center;
.input {
width: 0;
min-width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
transition: width 0.3s, margin-left 0.3s;
:global(.ant-select-selection) {
background: transparent;
}
input {
box-shadow: none !important;
}
&.show {
width: 210px;
margin-left: 8px;
}
}
}

View File

@ -0,0 +1,101 @@
import { SearchOutlined } from '@ant-design/icons';
import { AutoComplete, Input } from 'antd';
import useMergedState from 'rc-util/es/hooks/useMergedState';
import type { AutoCompleteProps } from 'antd/es/auto-complete';
import React, { useRef } from 'react';
import classNames from 'classnames';
import styles from './index.less';
export type HeaderSearchProps = {
onSearch?: (value?: string) => void;
onChange?: (value?: string) => void;
onVisibleChange?: (b: boolean) => void;
className?: string;
placeholder?: string;
options: AutoCompleteProps['options'];
defaultVisible?: boolean;
visible?: boolean;
defaultValue?: string;
value?: string;
};
const HeaderSearch: React.FC<HeaderSearchProps> = (props) => {
const {
className,
defaultValue,
onVisibleChange,
placeholder,
visible,
defaultVisible,
...restProps
} = props;
const inputRef = useRef<Input | null>(null);
const [value, setValue] = useMergedState<string | undefined>(defaultValue, {
value: props.value,
onChange: props.onChange,
});
const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
value: props.visible,
onChange: onVisibleChange,
});
const inputClass = classNames(styles.input, {
[styles.show]: searchMode,
});
return (
<div
className={classNames(className, styles.headerSearch)}
onClick={() => {
setSearchMode(true);
if (searchMode && inputRef.current) {
inputRef.current.focus();
}
}}
onTransitionEnd={({ propertyName }) => {
if (propertyName === 'width' && !searchMode) {
if (onVisibleChange) {
onVisibleChange(searchMode);
}
}
}}
>
<SearchOutlined
key="Icon"
style={{
cursor: 'pointer',
}}
/>
<AutoComplete
key="AutoComplete"
className={inputClass}
value={value}
options={restProps.options}
onChange={setValue}
>
<Input
size="small"
ref={inputRef}
defaultValue={defaultValue}
aria-label={placeholder}
placeholder={placeholder}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (restProps.onSearch) {
restProps.onSearch(value);
}
}
}}
onBlur={() => {
setSearchMode(false);
}}
/>
</AutoComplete>
</div>
);
};
export default HeaderSearch;

View File

@ -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;

View File

@ -0,0 +1,103 @@
@import '~antd/es/style/themes/default.less';
.list {
max-height: 400px;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
.item {
padding-right: 24px;
padding-left: 24px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
.meta {
width: 100%;
}
.avatar {
margin-top: 4px;
background: @component-background;
}
.iconElement {
font-size: 32px;
}
&.read {
opacity: 0.4;
}
&:last-child {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.title {
margin-bottom: 8px;
font-weight: normal;
}
.description {
font-size: 12px;
line-height: @line-height-base;
}
.datetime {
margin-top: 4px;
font-size: 12px;
line-height: @line-height-base;
}
.extra {
float: right;
margin-top: -1.5px;
margin-right: 0;
color: @text-color-secondary;
font-weight: normal;
}
}
.loadMore {
padding: 8px 0;
color: @primary-6;
text-align: center;
cursor: pointer;
&.loadedAll {
color: rgba(0, 0, 0, 0.25);
cursor: unset;
}
}
}
.notFound {
padding: 73px 0 88px;
color: @text-color-secondary;
text-align: center;
img {
display: inline-block;
height: 76px;
margin-bottom: 16px;
}
}
.bottomBar {
height: 46px;
color: @text-color;
line-height: 46px;
text-align: center;
border-top: 1px solid @border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
transition: all 0.3s;
div {
display: inline-block;
width: 50%;
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:only-child {
width: 100%;
}
&:not(:only-child):last-child {
border-left: 1px solid @border-color-split;
}
}
}

View File

@ -0,0 +1,113 @@
import { Avatar, List } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './NoticeList.less';
export type NoticeIconTabProps = {
count?: number;
showClear?: boolean;
showViewMore?: boolean;
style?: React.CSSProperties;
title: string;
tabKey: API.NoticeIconItemType;
onClick?: (item: API.NoticeIconItem) => void;
onClear?: () => void;
emptyText?: string;
clearText?: string;
viewMoreText?: string;
list: API.NoticeIconItem[];
onViewMore?: (e: any) => void;
};
const NoticeList: React.FC<NoticeIconTabProps> = ({
list = [],
onClick,
onClear,
title,
onViewMore,
emptyText,
showClear = true,
clearText,
viewMoreText,
showViewMore = false,
}) => {
if (!list || list.length === 0) {
return (
<div className={styles.notFound}>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
alt="not found"
/>
<div>{emptyText}</div>
</div>
);
}
return (
<div>
<List<API.NoticeIconItem>
className={styles.list}
dataSource={list}
renderItem={(item, i) => {
const itemCls = classNames(styles.item, {
[styles.read]: item.read,
});
// eslint-disable-next-line no-nested-ternary
const leftIcon = item.avatar ? (
typeof item.avatar === 'string' ? (
<Avatar className={styles.avatar} src={item.avatar} />
) : (
<span className={styles.iconElement}>{item.avatar}</span>
)
) : null;
return (
<List.Item
className={itemCls}
key={item.key || i}
onClick={() => {
onClick?.(item);
}}
>
<List.Item.Meta
className={styles.meta}
avatar={leftIcon}
title={
<div className={styles.title}>
{item.title}
<div className={styles.extra}>{item.extra}</div>
</div>
}
description={
<div>
<div className={styles.description}>{item.description}</div>
<div className={styles.datetime}>{item.datetime}</div>
</div>
}
/>
</List.Item>
);
}}
/>
<div className={styles.bottomBar}>
{showClear ? (
<div onClick={onClear}>
{clearText} {title}
</div>
) : null}
{showViewMore ? (
<div
onClick={(e) => {
if (onViewMore) {
onViewMore(e);
}
}}
>
{viewMoreText}
</div>
) : null}
</div>
</div>
);
};
export default NoticeList;

View File

@ -0,0 +1,35 @@
@import '~antd/es/style/themes/default.less';
.popover {
position: relative;
width: 336px;
}
.noticeButton {
display: inline-block;
cursor: pointer;
transition: all 0.3s;
}
.icon {
padding: 4px;
vertical-align: middle;
}
.badge {
font-size: 16px;
}
.tabs {
:global {
.ant-tabs-nav-list {
margin: auto;
}
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-bar {
margin-bottom: 0;
}
}
}

View File

@ -0,0 +1,153 @@
import { useEffect, useState } from 'react';
import { Tag, message } from 'antd';
import { groupBy } from 'lodash';
import moment from 'moment';
import { useModel, useRequest } from 'umi';
import { getNotices } from '@/services/api/api';
import NoticeIcon from './NoticeIcon';
import styles from './index.less';
export type GlobalHeaderRightProps = {
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onNoticeClear?: (tabName?: string) => void;
};
const getNoticeData = (notices: API.NoticeIconItem[]): Record<string, API.NoticeIconItem[]> => {
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>
) as any;
}
return newNotice;
});
return groupBy(newNotices, 'type');
};
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;
}
if (Array.isArray(value)) {
unreadMsg[key] = value.filter((item) => !item.read).length;
}
});
return unreadMsg;
};
const NoticeIconView = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const [notices, setNotices] = useState<API.NoticeIconItem[]>([]);
const { data } = useRequest(getNotices);
useEffect(() => {
setNotices(data || []);
}, [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;
}),
);
};
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 (
<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
>
<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>
);
};
export default NoticeIconView;

View File

@ -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/api/login';
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') {
setInitialState((s) => ({ ...s, currentUser: undefined }));
loginOut();
return;
}
history.push(`/account/${key}`);
},
[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;

View File

@ -0,0 +1,84 @@
@import '~antd/es/style/themes/default.less';
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.right {
display: flex;
float: right;
height: 48px;
margin-left: auto;
overflow: hidden;
.action {
display: flex;
align-items: center;
height: 48px;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
> span {
vertical-align: middle;
}
&:hover {
background: @pro-header-hover-bg;
}
&:global(.opened) {
background: @pro-header-hover-bg;
}
}
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.account {
.avatar {
margin-right: 8px;
color: @primary-color;
vertical-align: top;
background: rgba(255, 255, 255, 0.85);
}
}
}
.dark {
.action {
&:hover {
background: #252a3d;
}
&:global(.opened) {
background: #252a3d;
}
}
}
@media only screen and (max-width: @screen-md) {
:global(.ant-divider-vertical) {
vertical-align: unset;
}
.name {
display: none;
}
.right {
position: absolute;
top: 0;
right: 12px;
.account {
.avatar {
margin-right: 0;
}
}
.search {
display: none;
}
}
}

View File

@ -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;

View File

@ -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>
```

View File

@ -0,0 +1,61 @@
const { uniq } = require('lodash');
const RouterConfig = require('../../config/config').default.routes;
const BASE_URL = `http://localhost:${process.env.PORT || 8001}`;
function formatter(routes, parentPath = '') {
const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
let result = [];
routes.forEach((item) => {
if (item.path && !item.path.startsWith('/')) {
result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
}
if (item.path && item.path.startsWith('/')) {
result.push(`${item.path}`.replace(/\/{1,}/g, '/'));
}
if (item.routes) {
result = result.concat(
formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
);
}
});
return uniq(result.filter((item) => !!item));
}
beforeEach(async () => {
await page.goto(`${BASE_URL}`);
await page.evaluate(() => {
localStorage.setItem('antd-pro-authority', '["admin"]');
});
});
describe('Teamlinker E2E test', () => {
const testPage = (path) => async () => {
await page.goto(`${BASE_URL}${path}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0,
);
expect(haveFooter).toBeTruthy();
};
const routers = formatter(RouterConfig);
routers.forEach((route) => {
it(`test pages ${route}`, testPage(route));
});
it('topmenu should have footer', async () => {
const params = '?navTheme=light&layout=topmenu';
await page.goto(`${BASE_URL}${params}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0,
);
expect(haveFooter).toBeTruthy();
});
});

57
code/app/src/global.less Normal file
View File

@ -0,0 +1,57 @@
@import '~antd/es/style/themes/default.less';
html,
body,
#root {
height: 100%;
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
left: unset;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
// Compatible with IE11
@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
body .ant-design-pro > .ant-layout {
min-height: 100vh;
}
}

83
code/app/src/global.tsx Normal file
View File

@ -0,0 +1,83 @@
import { Button, message, notification } from 'antd';
import { useIntl } from 'umi';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
const isHttps = document.location.protocol === 'https:';
// if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
});
// Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', (event: Event) => {
const e = event as CustomEvent;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail && e.detail.waiting;
if (!worker) {
return true;
}
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (msgEvent) => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
});
// Refresh current page to use the updated HTML and other assets after SW has skiped waiting
window.location.reload(true);
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.close(key);
reloadSW();
}}
>
{useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
</Button>
);
notification.open({
message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
btn,
key,
onClose: async () => null,
});
});
} else if ('serviceWorker' in navigator && isHttps) {
// unregister service worker
const { serviceWorker } = navigator;
if (serviceWorker.getRegistrations) {
serviceWorker.getRegistrations().then((sws) => {
sws.forEach((sw) => {
sw.unregister();
});
});
}
serviceWorker.getRegistration().then((sw) => {
if (sw) sw.unregister();
});
// remove all caches
if (window.caches && window.caches.keys()) {
caches.keys().then((keys) => {
keys.forEach((key) => {
caches.delete(key);
});
});
}
}

View File

@ -0,0 +1,25 @@
import component from './en-US/component';
import globalHeader from './en-US/globalHeader';
import menu from './en-US/menu';
import pages from './en-US/pages';
import pwa from './en-US/pwa';
import settingDrawer from './en-US/settingDrawer';
import settings from './en-US/settings';
export default {
'navBar.lang': 'Languages',
'layout.user.link.help': 'Help',
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'app.copyright.produced': 'Produced by Ant Financial Experience Department',
'app.preview.down.block': 'Download this page to your local project',
'app.welcome.link.fetch-blocks': 'Get all block',
'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
...pages,
};

View File

@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': 'Expand',
'component.tagSelect.collapse': 'Collapse',
'component.tagSelect.all': 'All',
};

View File

@ -0,0 +1,17 @@
export default {
'component.globalHeader.search': 'Search',
'component.globalHeader.search.example1': 'Search example 1',
'component.globalHeader.search.example2': 'Search example 2',
'component.globalHeader.search.example3': 'Search example 3',
'component.globalHeader.help': 'Help',
'component.globalHeader.notification': 'Notification',
'component.globalHeader.notification.empty': 'You have viewed all notifications.',
'component.globalHeader.message': 'Message',
'component.globalHeader.message.empty': 'You have viewed all messsages.',
'component.globalHeader.event': 'Event',
'component.globalHeader.event.empty': 'You have viewed all events.',
'component.noticeIcon.clear': 'Clear',
'component.noticeIcon.cleared': 'Cleared',
'component.noticeIcon.empty': 'No notifications',
'component.noticeIcon.view-more': 'View more',
};

View File

@ -0,0 +1,52 @@
export default {
'menu.welcome': 'Welcome',
'menu.more-blocks': 'More Blocks',
'menu.home': 'Home',
'menu.admin': 'Admin',
'menu.admin.sub-page': 'Sub-Page',
'menu.login': 'Login',
'menu.register': 'Register',
'menu.register-result': 'Register Result',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Analysis',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Workplace',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Form',
'menu.form.basic-form': 'Basic Form',
'menu.form.step-form': 'Step Form',
'menu.form.step-form.info': 'Step Form(write transfer information)',
'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
'menu.form.step-form.result': 'Step Form(finished)',
'menu.form.advanced-form': 'Advanced Form',
'menu.list': 'List',
'menu.list.table-list': 'Search Table',
'menu.list.basic-list': 'Basic List',
'menu.list.card-list': 'Card List',
'menu.list.search-list': 'Search List',
'menu.list.search-list.articles': 'Search List(articles)',
'menu.list.search-list.projects': 'Search List(projects)',
'menu.list.search-list.applications': 'Search List(applications)',
'menu.profile': 'Profile',
'menu.profile.basic': 'Basic Profile',
'menu.profile.advanced': 'Advanced Profile',
'menu.result': 'Result',
'menu.result.success': 'Success',
'menu.result.fail': 'Fail',
'menu.exception': 'Exception',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Trigger',
'menu.account': 'Account',
'menu.account.center': 'Account Center',
'menu.account.settings': 'Account Settings',
'menu.account.trigger': 'Trigger Error',
'menu.account.logout': 'Logout',
'menu.editor': 'Graphic Editor',
'menu.editor.flow': 'Flow Editor',
'menu.editor.mind': 'Mind Editor',
'menu.editor.koni': 'Koni Editor',
};

View File

@ -0,0 +1,70 @@
export default {
'pages.layouts.userLayout.title':
'Ant Design is the most influential web design specification in Xihu district',
'pages.login.accountLogin.tab': 'Account Login',
'pages.login.accountLogin.errorMessage': 'Incorrect username/password',
'pages.login.failure': 'Login failed, please try again!',
'pages.login.success': 'Login successful!',
'pages.login.username.placeholder': 'Username: admin or user',
'pages.login.username.required': 'Please input your username!',
'pages.login.password.placeholder': 'Password: ant.design',
'pages.login.password.required': 'Please input your password!',
'pages.login.phoneLogin.tab': 'Phone Login',
'pages.login.phoneLogin.errorMessage': 'Verification Code Error',
'pages.login.phoneNumber.placeholder': 'Phone Number',
'pages.login.phoneNumber.required': 'Please input your phone number!',
'pages.login.phoneNumber.invalid': 'Phone number is invalid!',
'pages.login.captcha.placeholder': 'Verification Code',
'pages.login.captcha.required': 'Please input verification code!',
'pages.login.phoneLogin.getVerificationCode': 'Get Code',
'pages.getCaptchaSecondText': 'sec(s)',
'pages.login.rememberMe': 'Remember me',
'pages.login.forgotPassword': 'Forgot Password ?',
'pages.login.submit': 'Login',
'pages.login.loginWith': 'Login with :',
'pages.login.registerAccount': 'Register Account',
'pages.welcome.advancedComponent': 'Advanced Component',
'pages.welcome.link': 'Welcome',
'pages.welcome.advancedLayout': 'Advanced Layout',
'pages.welcome.alertMessage': 'Faster and stronger heavy-duty components have been released.',
'pages.admin.subPage.title': 'This page can only be viewed by Admin',
'pages.admin.subPage.alertMessage':
'Umi ui is now released, welcome to use npm run ui to start the experience.',
'pages.searchTable.createForm.newRule': 'New Rule',
'pages.searchTable.updateForm.ruleConfig': 'Rule configuration',
'pages.searchTable.updateForm.basicConfig': 'Basic Information',
'pages.searchTable.updateForm.ruleName.nameLabel': 'Rule Name',
'pages.searchTable.updateForm.ruleName.nameRules': 'Please enter the rule name!',
'pages.searchTable.updateForm.ruleDesc.descLabel': 'Rule Description',
'pages.searchTable.updateForm.ruleDesc.descPlaceholder': 'Please enter at least five characters',
'pages.searchTable.updateForm.ruleDesc.descRules':
'Please enter a rule description of at least five characters!',
'pages.searchTable.updateForm.ruleProps.title': 'Configure Properties',
'pages.searchTable.updateForm.object': 'Monitoring Object',
'pages.searchTable.updateForm.ruleProps.templateLabel': 'Rule Template',
'pages.searchTable.updateForm.ruleProps.typeLabel': 'Rule Type',
'pages.searchTable.updateForm.schedulingPeriod.title': 'Set Scheduling Period',
'pages.searchTable.updateForm.schedulingPeriod.timeLabel': 'Starting Time',
'pages.searchTable.updateForm.schedulingPeriod.timeRules': 'Please choose a start time!',
'pages.searchTable.titleDesc': 'Description',
'pages.searchTable.ruleName': 'Rule name is required',
'pages.searchTable.titleCallNo': 'Number of Service Calls',
'pages.searchTable.titleStatus': 'Status',
'pages.searchTable.nameStatus.default': 'default',
'pages.searchTable.nameStatus.running': 'running',
'pages.searchTable.nameStatus.online': 'online',
'pages.searchTable.nameStatus.abnormal': 'abnormal',
'pages.searchTable.titleUpdatedAt': 'Last Scheduled at',
'pages.searchTable.exception': 'Please enter the reason for the exception!',
'pages.searchTable.titleOption': 'Option',
'pages.searchTable.config': 'Configuration',
'pages.searchTable.subscribeAlert': 'Subscribe to alerts',
'pages.searchTable.title': 'Enquiry Form',
'pages.searchTable.new': 'New',
'pages.searchTable.chosen': 'chosen',
'pages.searchTable.item': 'item',
'pages.searchTable.totalServiceCalls': 'Total Number of Service Calls',
'pages.searchTable.tenThousand': '0000',
'pages.searchTable.batchDeletion': 'bacth deletion',
'pages.searchTable.batchApproval': 'batch approval',
};

View File

@ -0,0 +1,6 @@
export default {
'app.pwa.offline': 'You are offline now',
'app.pwa.serviceworker.updated': 'New content is available',
'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page',
'app.pwa.serviceworker.updated.ok': 'Refresh',
};

View File

@ -0,0 +1,31 @@
export default {
'app.setting.pagestyle': 'Page style setting',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.light': 'Light style',
'app.setting.content-width': 'Content Width',
'app.setting.content-width.fixed': 'Fixed',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Navigation Mode',
'app.setting.sidemenu': 'Side Menu Layout',
'app.setting.topmenu': 'Top Menu Layout',
'app.setting.fixedheader': 'Fixed Header',
'app.setting.fixedsidebar': 'Fixed Sidebar',
'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
'app.setting.hideheader': 'Hidden Header when scrolling',
'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
'app.setting.othersettings': 'Other Settings',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copy Setting',
'app.setting.copyinfo': 'copy successplease replace defaultSettings in src/models/setting.js',
'app.setting.production.hint':
'Setting panel shows in development environment only, please manually modify',
};

View File

@ -0,0 +1,60 @@
export default {
'app.settings.menuMap.basic': 'Basic Settings',
'app.settings.menuMap.security': 'Security Settings',
'app.settings.menuMap.binding': 'Account Binding',
'app.settings.menuMap.notification': 'New Message Notification',
'app.settings.basic.avatar': 'Avatar',
'app.settings.basic.change-avatar': 'Change avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Please input your email!',
'app.settings.basic.nickname': 'Nickname',
'app.settings.basic.nickname-message': 'Please input your Nickname!',
'app.settings.basic.profile': 'Personal profile',
'app.settings.basic.profile-message': 'Please input your personal profile!',
'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
'app.settings.basic.country': 'Country/Region',
'app.settings.basic.country-message': 'Please input your country!',
'app.settings.basic.geographic': 'Province or city',
'app.settings.basic.geographic-message': 'Please input your geographic info!',
'app.settings.basic.address': 'Street Address',
'app.settings.basic.address-message': 'Please input your address!',
'app.settings.basic.phone': 'Phone Number',
'app.settings.basic.phone-message': 'Please input your phone!',
'app.settings.basic.update': 'Update Information',
'app.settings.security.strong': 'Strong',
'app.settings.security.medium': 'Medium',
'app.settings.security.weak': 'Weak',
'app.settings.security.password': 'Account Password',
'app.settings.security.password-description': 'Current password strength',
'app.settings.security.phone': 'Security Phone',
'app.settings.security.phone-description': 'Bound phone',
'app.settings.security.question': 'Security Question',
'app.settings.security.question-description':
'The security question is not set, and the security policy can effectively protect the account security',
'app.settings.security.email': 'Backup Email',
'app.settings.security.email-description': 'Bound Email',
'app.settings.security.mfa': 'MFA Device',
'app.settings.security.mfa-description':
'Unbound MFA device, after binding, can be confirmed twice',
'app.settings.security.modify': 'Modify',
'app.settings.security.set': 'Set',
'app.settings.security.bind': 'Bind',
'app.settings.binding.taobao': 'Binding Taobao',
'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
'app.settings.binding.alipay': 'Binding Alipay',
'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
'app.settings.binding.dingding': 'Binding DingTalk',
'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
'app.settings.binding.bind': 'Bind',
'app.settings.notification.password': 'Account Password',
'app.settings.notification.password-description':
'Messages from other users will be notified in the form of a station letter',
'app.settings.notification.messages': 'System Messages',
'app.settings.notification.messages-description':
'System messages will be notified in the form of a station letter',
'app.settings.notification.todo': 'To-do Notification',
'app.settings.notification.todo-description':
'The to-do list will be notified in the form of a letter from the station',
'app.settings.open': 'Open',
'app.settings.close': 'Close',
};

View File

@ -0,0 +1,25 @@
import component from './zh-CN/component';
import globalHeader from './zh-CN/globalHeader';
import menu from './zh-CN/menu';
import pwa from './zh-CN/pwa';
import settingDrawer from './zh-CN/settingDrawer';
import settings from './zh-CN/settings';
import pages from './zh-CN/pages';
export default {
'navBar.lang': '语言',
'layout.user.link.help': '帮助',
'layout.user.link.privacy': '隐私',
'layout.user.link.terms': '条款',
'app.copyright.produced': 'Copyright Teamlinker',
'app.preview.down.block': '下载此页面到本地项目',
'app.welcome.link.fetch-blocks': '获取全部区块',
'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
...pages,
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
};

View File

@ -0,0 +1,5 @@
export default {
'component.tagSelect.expand': '展开',
'component.tagSelect.collapse': '收起',
'component.tagSelect.all': '全部',
};

View File

@ -0,0 +1,17 @@
export default {
'component.globalHeader.search': '站内搜索',
'component.globalHeader.search.example1': '搜索提示一',
'component.globalHeader.search.example2': '搜索提示二',
'component.globalHeader.search.example3': '搜索提示三',
'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': '查看更多',
};

View File

@ -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': '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': '拓扑编辑器',
};

View File

@ -0,0 +1,67 @@
export default {
'pages.layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范',
'pages.login.accountLogin.tab': '账户密码登录',
'pages.login.accountLogin.errorMessage': '错误的用户名和密码',
'pages.login.failure': '登录失败,请重试!',
'pages.login.success': '登录成功!',
'pages.login.username.placeholder': '用户名: admin or user',
'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': ' 这个页面只有 admin 权限才能查看',
'pages.admin.subPage.alertMessage': 'umi ui 现已发布,欢迎使用 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': '万',
'pages.searchTable.batchDeletion': '批量删除',
'pages.searchTable.batchApproval': '批量审批',
};

View File

@ -0,0 +1,6 @@
export default {
'app.pwa.offline': '当前处于离线状态',
'app.pwa.serviceworker.updated': '有新内容',
'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
'app.pwa.serviceworker.updated.ok': '刷新',
};

View File

@ -0,0 +1,31 @@
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': '固定 Header',
'app.setting.fixedsidebar': '固定侧边菜单',
'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
'app.setting.hideheader': '下滑时隐藏 Header',
'app.setting.hideheader.hint': '固定 Header 时可配置',
'app.setting.othersettings': '其他设置',
'app.setting.weakmode': '色弱模式',
'app.setting.copy': '拷贝设置',
'app.setting.copyinfo': '拷贝成功,请到 config/defaultSettings.js 中替换默认配置',
'app.setting.production.hint':
'配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
};

View File

@ -0,0 +1,55 @@
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': '绑定淘宝',
'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
'app.settings.binding.alipay': '绑定支付宝',
'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
'app.settings.binding.dingding': '绑定钉钉',
'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
'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': '关',
};

View File

@ -0,0 +1,22 @@
{
"name": "Teamlinker",
"short_name": "Teamlinker",
"display": "standalone",
"start_url": "./?utm_source=homescreen",
"theme_color": "#002140",
"background_color": "#001529",
"icons": [
{
"src": "icons/icon-192x192.png",
"sizes": "192x192"
},
{
"src": "icons/icon-128x128.png",
"sizes": "128x128"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512"
}
]
}

View File

@ -0,0 +1,18 @@
import { Button, Result } from 'antd';
import React from 'react';
import { history } from 'umi';
const NoFoundPage: React.FC = () => (
<Result
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Button type="primary" onClick={() => history.push('/')}>
Back Home
</Button>
}
/>
);
export default NoFoundPage;

View File

@ -0,0 +1,43 @@
import React from 'react';
import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
import { Card, Typography, Alert } from 'antd';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import { useIntl } from 'umi';
export default (): React.ReactNode => {
const intl = useIntl();
return (
<PageHeaderWrapper
content={intl.formatMessage({
id: 'pages.admin.subPage.title',
defaultMessage: 'This page can only be viewed by admin',
})}
>
<Card>
<Alert
message={intl.formatMessage({
id: 'pages.welcome.alertMessage',
defaultMessage: 'Faster and stronger heavy-duty components have been released.',
})}
type="success"
showIcon
banner
style={{
margin: -12,
marginBottom: 48,
}}
/>
<Typography.Title level={2} style={{ textAlign: 'center' }}>
<SmileTwoTone /> Teamlinker <HeartTwoTone twoToneColor="#eb2f96" /> You
</Typography.Title>
</Card>
<p style={{ textAlign: 'center', marginTop: 24 }}>
Want to add more pages? Please refer to{' '}
<a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
use block
</a>
</p>
</PageHeaderWrapper>
);
};

View File

@ -0,0 +1,209 @@
import React from 'react';
import { Modal } from 'antd';
import {
ProFormSelect,
ProFormText,
ProFormTextArea,
StepsForm,
ProFormRadio,
ProFormDateTimePicker,
} from '@ant-design/pro-form';
import { useIntl, FormattedMessage } from 'umi';
export type FormValueType = {
target?: string;
template?: string;
type?: string;
time?: string;
frequency?: string;
} & Partial<API.RuleListItem>;
export type UpdateFormProps = {
onCancel: (flag?: boolean, formVals?: FormValueType) => void;
onSubmit: (values: FormValueType) => Promise<void>;
updateModalVisible: boolean;
values: Partial<API.RuleListItem>;
};
const UpdateForm: React.FC<UpdateFormProps> = (props) => {
const intl = useIntl();
return (
<StepsForm
stepsProps={{
size: 'small',
}}
stepsFormRender={(dom, submitter) => {
return (
<Modal
width={640}
bodyStyle={{ padding: '32px 40px 48px' }}
destroyOnClose
title={intl.formatMessage({
id: 'pages.searchTable.updateForm.ruleConfig',
defaultMessage: '规则配置',
})}
visible={props.updateModalVisible}
footer={submitter}
onCancel={() => {
props.onCancel();
}}
>
{dom}
</Modal>
);
}}
onFinish={props.onSubmit}
>
<StepsForm.StepForm
initialValues={{
name: props.values.name,
desc: props.values.desc,
}}
title={intl.formatMessage({
id: 'pages.searchTable.updateForm.basicConfig',
defaultMessage: '基本信息',
})}
>
<ProFormText
name="name"
label={intl.formatMessage({
id: 'pages.searchTable.updateForm.ruleName.nameLabel',
defaultMessage: '规则名称',
})}
width="md"
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.searchTable.updateForm.ruleName.nameRules"
defaultMessage="请输入规则名称!"
/>
),
},
]}
/>
<ProFormTextArea
name="desc"
width="md"
label={intl.formatMessage({
id: 'pages.searchTable.updateForm.ruleDesc.descLabel',
defaultMessage: '规则描述',
})}
placeholder={intl.formatMessage({
id: 'pages.searchTable.updateForm.ruleDesc.descPlaceholder',
defaultMessage: '请输入至少五个字符',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.searchTable.updateForm.ruleDesc.descRules"
defaultMessage="请输入至少五个字符的规则描述!"
/>
),
min: 5,
},
]}
/>
</StepsForm.StepForm>
<StepsForm.StepForm
initialValues={{
target: '0',
template: '0',
}}
title={intl.formatMessage({
id: 'pages.searchTable.updateForm.ruleProps.title',
defaultMessage: '配置规则属性',
})}
>
<ProFormSelect
name="target"
width="md"
label={intl.formatMessage({
id: 'pages.searchTable.updateForm.object',
defaultMessage: '监控对象',
})}
valueEnum={{
0: '表一',
1: '表二',
}}
/>
<ProFormSelect
name="template"
width="md"
label={intl.formatMessage({
id: 'pages.searchTable.updateForm.ruleProps.templateLabel',
defaultMessage: '规则模板',
})}
valueEnum={{
0: '规则模板一',
1: '规则模板二',
}}
/>
<ProFormRadio.Group
name="type"
label={intl.formatMessage({
id: 'pages.searchTable.updateForm.ruleProps.typeLabel',
defaultMessage: '规则类型',
})}
options={[
{
value: '0',
label: '强',
},
{
value: '1',
label: '弱',
},
]}
/>
</StepsForm.StepForm>
<StepsForm.StepForm
initialValues={{
type: '1',
frequency: 'month',
}}
title={intl.formatMessage({
id: 'pages.searchTable.updateForm.schedulingPeriod.title',
defaultMessage: '设定调度周期',
})}
>
<ProFormDateTimePicker
name="time"
width="md"
label={intl.formatMessage({
id: 'pages.searchTable.updateForm.schedulingPeriod.timeLabel',
defaultMessage: '开始时间',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.searchTable.updateForm.schedulingPeriod.timeRules"
defaultMessage="请选择开始时间!"
/>
),
},
]}
/>
<ProFormSelect
name="frequency"
label={intl.formatMessage({
id: 'pages.searchTable.updateForm.object',
defaultMessage: '监控对象',
})}
width="md"
valueEnum={{
month: '月',
week: '周',
}}
/>
</StepsForm.StepForm>
</StepsForm>
);
};
export default UpdateForm;

View File

@ -0,0 +1,391 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, message, Input, Drawer } from 'antd';
import React, { useState, useRef } from 'react';
import { useIntl, FormattedMessage } from 'umi';
import { PageContainer, FooterToolbar } from '@ant-design/pro-layout';
import type { ProColumns, ActionType } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
import type { ProDescriptionsItemProps } from '@ant-design/pro-descriptions';
import ProDescriptions from '@ant-design/pro-descriptions';
import type { FormValueType } from './components/UpdateForm';
import UpdateForm from './components/UpdateForm';
import { rule, addRule, updateRule, removeRule } from '@/services/api/rule';
/**
* @en-US Add node
* @zh-CN
* @param fields
*/
const handleAdd = async (fields: API.RuleListItem) => {
const hide = message.loading('正在添加');
try {
await addRule({ ...fields });
hide();
message.success('Added successfully');
return true;
} catch (error) {
hide();
message.error('Adding failed, please try again!');
return false;
}
};
/**
* @en-US Update node
* @zh-CN
*
* @param fields
*/
const handleUpdate = async (fields: FormValueType) => {
const hide = message.loading('Configuring');
try {
await updateRule({
name: fields.name,
desc: fields.desc,
key: fields.key,
});
hide();
message.success('Configuration is successful');
return true;
} catch (error) {
hide();
message.error('Configuration failed, please try again!');
return false;
}
};
/**
* Delete node
* @zh-CN
*
* @param selectedRows
*/
const handleRemove = async (selectedRows: API.RuleListItem[]) => {
const hide = message.loading('正在删除');
if (!selectedRows) return true;
try {
await removeRule({
key: selectedRows.map((row) => row.key),
});
hide();
message.success('Deleted successfully and will refresh soon');
return true;
} catch (error) {
hide();
message.error('Delete failed, please try again');
return false;
}
};
const TableList: React.FC = () => {
/**
* @en-US Pop-up window of new window
* @zh-CN
* */
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
/**
* @en-US The pop-up window of the distribution update window
* @zh-CN
* */
const [updateModalVisible, handleUpdateModalVisible] = useState<boolean>(false);
const [showDetail, setShowDetail] = useState<boolean>(false);
const actionRef = useRef<ActionType>();
const [currentRow, setCurrentRow] = useState<API.RuleListItem>();
const [selectedRowsState, setSelectedRows] = useState<API.RuleListItem[]>([]);
/**
* @en-US International configuration
* @zh-CN
* */
const intl = useIntl();
const columns: ProColumns<API.RuleListItem>[] = [
{
title: (
<FormattedMessage
id="pages.searchTable.updateForm.ruleName.nameLabel"
defaultMessage="Rule name"
/>
),
dataIndex: 'name',
tip: 'The rule name is the unique key',
render: (dom, entity) => {
return (
<a
onClick={() => {
setCurrentRow(entity);
setShowDetail(true);
}}
>
{dom}
</a>
);
},
},
{
title: <FormattedMessage id="pages.searchTable.titleDesc" defaultMessage="Description" />,
dataIndex: 'desc',
valueType: 'textarea',
},
{
title: (
<FormattedMessage
id="pages.searchTable.titleCallNo"
defaultMessage="Number of service calls"
/>
),
dataIndex: 'callNo',
sorter: true,
hideInForm: true,
renderText: (val: string) =>
`${val}${intl.formatMessage({
id: 'pages.searchTable.tenThousand',
defaultMessage: ' 万 ',
})}`,
},
{
title: <FormattedMessage id="pages.searchTable.titleStatus" defaultMessage="Status" />,
dataIndex: 'status',
hideInForm: true,
valueEnum: {
0: {
text: (
<FormattedMessage
id="pages.searchTable.nameStatus.default"
defaultMessage="Shut down"
/>
),
status: 'Default',
},
1: {
text: (
<FormattedMessage id="pages.searchTable.nameStatus.running" defaultMessage="Running" />
),
status: 'Processing',
},
2: {
text: (
<FormattedMessage id="pages.searchTable.nameStatus.online" defaultMessage="Online" />
),
status: 'Success',
},
3: {
text: (
<FormattedMessage
id="pages.searchTable.nameStatus.abnormal"
defaultMessage="Abnormal"
/>
),
status: 'Error',
},
},
},
{
title: (
<FormattedMessage
id="pages.searchTable.titleUpdatedAt"
defaultMessage="Last scheduled time"
/>
),
sorter: true,
dataIndex: 'updatedAt',
valueType: 'dateTime',
renderFormItem: (item, { defaultRender, ...rest }, form) => {
const status = form.getFieldValue('status');
if (`${status}` === '0') {
return false;
}
if (`${status}` === '3') {
return (
<Input
{...rest}
placeholder={intl.formatMessage({
id: 'pages.searchTable.exception',
defaultMessage: 'Please enter the reason for the exception!',
})}
/>
);
}
return defaultRender(item);
},
},
{
title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="Operating" />,
dataIndex: 'option',
valueType: 'option',
render: (_, record) => [
<a
key="config"
onClick={() => {
handleUpdateModalVisible(true);
setCurrentRow(record);
}}
>
<FormattedMessage id="pages.searchTable.config" defaultMessage="Configuration" />
</a>,
<a key="subscribeAlert" href="https://procomponents.ant.design/">
<FormattedMessage
id="pages.searchTable.subscribeAlert"
defaultMessage="Subscribe to alerts"
/>
</a>,
],
},
];
return (
<PageContainer>
<ProTable<API.RuleListItem, API.PageParams>
headerTitle={intl.formatMessage({
id: 'pages.searchTable.title',
defaultMessage: 'Enquiry form',
})}
actionRef={actionRef}
rowKey="key"
search={{
labelWidth: 120,
}}
toolBarRender={() => [
<Button
type="primary"
key="primary"
onClick={() => {
handleModalVisible(true);
}}
>
<PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" />
</Button>,
]}
request={rule}
columns={columns}
rowSelection={{
onChange: (_, selectedRows) => {
setSelectedRows(selectedRows);
},
}}
/>
{selectedRowsState?.length > 0 && (
<FooterToolbar
extra={
<div>
<FormattedMessage id="pages.searchTable.chosen" defaultMessage="Chosen" />{' '}
<a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
<FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
&nbsp;&nbsp;
<span>
<FormattedMessage
id="pages.searchTable.totalServiceCalls"
defaultMessage="Total number of service calls"
/>{' '}
{selectedRowsState.reduce((pre, item) => pre + item.callNo!, 0)}{' '}
<FormattedMessage id="pages.searchTable.tenThousand" defaultMessage="万" />
</span>
</div>
}
>
<Button
onClick={async () => {
await handleRemove(selectedRowsState);
setSelectedRows([]);
actionRef.current?.reloadAndRest?.();
}}
>
<FormattedMessage
id="pages.searchTable.batchDeletion"
defaultMessage="Batch deletion"
/>
</Button>
<Button type="primary">
<FormattedMessage
id="pages.searchTable.batchApproval"
defaultMessage="Batch approval"
/>
</Button>
</FooterToolbar>
)}
<ModalForm
title={intl.formatMessage({
id: 'pages.searchTable.createForm.newRule',
defaultMessage: 'New rule',
})}
width="400px"
visible={createModalVisible}
onVisibleChange={handleModalVisible}
onFinish={async (value) => {
const success = await handleAdd(value as API.RuleListItem);
if (success) {
handleModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
>
<ProFormText
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.searchTable.ruleName"
defaultMessage="Rule name is required"
/>
),
},
]}
width="md"
name="name"
/>
<ProFormTextArea width="md" name="desc" />
</ModalForm>
<UpdateForm
onSubmit={async (value) => {
const success = await handleUpdate(value);
if (success) {
handleUpdateModalVisible(false);
setCurrentRow(undefined);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
onCancel={() => {
handleUpdateModalVisible(false);
setCurrentRow(undefined);
}}
updateModalVisible={updateModalVisible}
values={currentRow || {}}
/>
<Drawer
width={600}
visible={showDetail}
onClose={() => {
setCurrentRow(undefined);
setShowDetail(false);
}}
closable={false}
>
{currentRow?.name && (
<ProDescriptions<API.RuleListItem>
column={2}
title={currentRow?.name}
request={async () => ({
data: currentRow || {},
})}
params={{
id: currentRow?.name,
}}
columns={columns as ProDescriptionsItemProps<API.RuleListItem>[]}
/>
)}
</Drawer>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,8 @@
@import '~antd/lib/style/themes/default.less';
.pre {
margin: 12px 0;
padding: 12px 20px;
background: @input-bg;
box-shadow: @card-shadow;
}

View File

@ -0,0 +1,63 @@
import React from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Card, Alert, Typography } from 'antd';
import { useIntl, FormattedMessage } from 'umi';
import styles from './Welcome.less';
const CodePreview: React.FC = ({ children }) => (
<pre className={styles.pre}>
<code>
<Typography.Text copyable>{children}</Typography.Text>
</code>
</pre>
);
export default (): React.ReactNode => {
const intl = useIntl();
return (
<PageContainer>
<Card>
<Alert
message={intl.formatMessage({
id: 'pages.welcome.alertMessage',
defaultMessage: 'Faster and stronger heavy-duty components have been released.',
})}
type="success"
showIcon
banner
style={{
margin: -12,
marginBottom: 24,
}}
/>
<Typography.Text strong>
<FormattedMessage id="pages.welcome.advancedComponent" defaultMessage="Advanced Form" />{' '}
<a
href="https://procomponents.ant.design/components/table"
rel="noopener noreferrer"
target="__blank"
>
<FormattedMessage id="pages.welcome.link" defaultMessage="Welcome" />
</a>
</Typography.Text>
<CodePreview>yarn add @ant-design/pro-table</CodePreview>
<Typography.Text
strong
style={{
marginBottom: 12,
}}
>
<FormattedMessage id="pages.welcome.advancedLayout" defaultMessage="Advanced layout" />{' '}
<a
href="https://procomponents.ant.design/components/layout"
rel="noopener noreferrer"
target="__blank"
>
<FormattedMessage id="pages.welcome.link" defaultMessage="Welcome" />
</a>
</Typography.Text>
<CodePreview>yarn add @ant-design/pro-layout</CodePreview>
</Card>
</PageContainer>
);
};

View File

@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- TODO: Finish SEO -->
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="keywords" content="teamlinker, Team linker" />
<meta name="description" content="Project manager." />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<title>Teamlinker</title>
<link rel="icon" href="<%= context.config.publicPath +'favicon.ico'%>" type="image/x-icon" />
</head>
<body>
<noscript>Project manager! Contact with us: !</noscript>
<div id="root">
<style>
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
}
#root {
background-repeat: no-repeat;
background-size: 100% auto;
}
.page-loading-warp {
display: flex;
align-items: center;
justify-content: center;
padding: 98px;
}
.ant-spin {
position: absolute;
display: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
color: #1890ff;
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
text-align: center;
list-style: none;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
-webkit-font-feature-settings: 'tnum';
font-feature-settings: 'tnum';
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-dot {
position: relative;
display: inline-block;
width: 20px;
height: 20px;
font-size: 20px;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
-ms-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antspinmove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antrotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-lg .ant-spin-dot {
width: 32px;
height: 32px;
font-size: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
</style>
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 420px;
"
>
<img src="<%= context.config.publicPath +'logo.png'%>" alt="logo" width="256" />
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin"
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
></span>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,114 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 20vh 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 20vh 0 24px;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
width: 202px;
height: 32px;
vertical-align: top;
}
.title {
position: relative;
top: 2px;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.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;
}
}

View File

@ -0,0 +1,292 @@
import { LockOutlined, MobileOutlined, UserOutlined } from '@ant-design/icons';
import { Alert, 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 { LoginService } from '@/services/api';
import styles from './index.less';
const LoginMessage: React.FC<{
content: string;
}> = ({ content }) => (
<Alert
style={{
marginBottom: 24,
}}
message={content}
type="error"
showIcon
/>
);
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) {
await setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
}
};
const handleSubmit = async (values: API.LoginParams) => {
setSubmitting(true);
try {
// 登录
const msg = await LoginService.login({ ...values, type });
if (msg.status === 'ok') {
const defaultloginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultloginSuccessMessage);
await fetchUserInfo();
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const { query } = history.location;
const { redirect } = query as { redirect: string };
history.push(redirect || '/');
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.png" />
</Link>
</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: '账户或密码错误',
})}
/>
)}
{type === 'account' && (
<>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon} />,
}}
// TODO: 去除用户名和密码显示
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={'密码: 123'}
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 LoginService.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>
</div>
</div>
<Footer />
</div>
);
};
export default Login;

View File

@ -0,0 +1,114 @@
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 20vh 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 20vh 0 24px;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
width: 202px;
height: 32px;
vertical-align: top;
}
.title {
position: relative;
top: 2px;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.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;
}
}

View File

@ -0,0 +1,292 @@
import { LockOutlined, MobileOutlined, UserOutlined } from '@ant-design/icons';
import { Alert, 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 { LoginService } from '@/services/api';
import styles from './index.less';
const LoginMessage: React.FC<{
content: string;
}> = ({ content }) => (
<Alert
style={{
marginBottom: 24,
}}
message={content}
type="error"
showIcon
/>
);
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) {
await setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
}
};
const handleSubmit = async (values: API.LoginParams) => {
setSubmitting(true);
try {
// 登录
const msg = await LoginService.login({ ...values, type });
if (msg.status === 'ok') {
const defaultloginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultloginSuccessMessage);
await fetchUserInfo();
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const { query } = history.location;
const { redirect } = query as { redirect: string };
history.push(redirect || '/');
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.png" />
</Link>
</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: '账户或密码错误',
})}
/>
)}
{type === 'account' && (
<>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon} />,
}}
// TODO: 去除用户名和密码显示
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={'密码: 123'}
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 LoginService.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>
</div>
</div>
<Footer />
</div>
);
};
export default Login;

View File

@ -0,0 +1,66 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-restricted-globals */
/* eslint-disable no-underscore-dangle */
/* globals workbox */
workbox.core.setCacheNameDetails({
prefix: 'antd-pro',
suffix: 'v5',
});
// Control all opened tabs ASAP
workbox.clientsClaim();
/**
* Use precaching list generated by workbox in build process.
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
*/
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
/**
* Register a navigation route.
* https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
*/
workbox.routing.registerNavigationRoute('/index.html');
/**
* Use runtime cache:
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
*
* Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
*/
/** Handle API requests */
workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
/** Handle third party requests */
workbox.routing.registerRoute(
/^https:\/\/gw\.alipayobjects\.com\//,
workbox.strategies.networkFirst(),
);
workbox.routing.registerRoute(
/^https:\/\/cdnjs\.cloudflare\.com\//,
workbox.strategies.networkFirst(),
);
workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
/** Response to client after skipping waiting with MessageChannel */
addEventListener('message', (event) => {
const replyPort = event.ports[0];
const message = event.data;
if (replyPort && message && message.type === 'skip-waiting') {
event.waitUntil(
self.skipWaiting().then(
() => {
replyPort.postMessage({
error: null,
});
},
(error) => {
replyPort.postMessage({
error,
});
},
),
);
}
});

View File

@ -0,0 +1,7 @@
// @ts-ignore
/* eslint-disable */
export * as LoginService from './login';
export * as NoticeService from './notice';
export * as RuleService from './rule';
export * as UserService from './user';

View File

@ -0,0 +1,41 @@
// @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: 'GET',
params: {
...params,
},
...(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 || {}),
});
}

View File

@ -0,0 +1,11 @@
// @ts-ignore
/* eslint-disable */
import { request } from 'umi';
/** 此处后端没有提供注释 GET /api/notices */
export async function getNotices(options?: { [key: string]: any }) {
return request<API.NoticeIconList>('/api/notices', {
method: 'GET',
...(options || {}),
});
}

View File

@ -0,0 +1,47 @@
// @ts-ignore
/* eslint-disable */
import { request } from 'umi';
/** 获取规则列表 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 || {}),
});
}

103
code/app/src/services/api/typings.d.ts vendored Normal file
View File

@ -0,0 +1,103 @@
// @ts-ignore
/* eslint-disable */
// API 命名空间
// 声明各接口参数类型
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;
};
}

View File

@ -0,0 +1,13 @@
// @ts-ignore
/* eslint-disable */
import { request } from 'umi';
/** 获取当前的用户 GET /api/currentUser */
export async function currentUser(options?: { [key: string]: any }) {
return request<{
data: API.CurrentUser;
}>('/api/currentUser', {
method: 'GET',
...(options || {}),
});
}

View File

@ -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,
};

View File

@ -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 || {}),
});
}

View File

@ -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 || {}),
});
}

View File

@ -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;
};
}

View File

@ -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 || {}),
});
}

24
code/app/src/typings.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
declare module 'slash2';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module 'omit.js';
declare module 'numeral';
declare module '@antv/data-set';
declare module 'mockjs';
declare module 'react-fittext';
declare module 'bizcharts-plugin-slider';
// 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.
declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

View File

@ -0,0 +1,41 @@
// eslint-disable-next-line
const NodeEnvironment = require('jest-environment-node');
const getBrowser = require('./getBrowser');
class PuppeteerEnvironment extends NodeEnvironment {
// Jest is not available here, so we have to reverse engineer
// the setTimeout function, see https://github.com/facebook/jest/blob/v23.1.0/packages/jest-runtime/src/index.js#L823
setTimeout(timeout) {
if (this.global.jasmine) {
// eslint-disable-next-line no-underscore-dangle
this.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout;
} else {
this.global[Symbol.for('TEST_TIMEOUT_SYMBOL')] = timeout;
}
}
async setup() {
const browser = await getBrowser();
const page = await browser.newPage();
this.global.browser = browser;
this.global.page = page;
}
async teardown() {
const { page, browser } = this.global;
if (page) {
await page.close();
}
if (browser) {
await browser.disconnect();
}
if (browser) {
await browser.close();
}
}
}
module.exports = PuppeteerEnvironment;

View File

@ -0,0 +1,39 @@
/* eslint-disable global-require */
/* eslint-disable import/no-extraneous-dependencies */
const { execSync } = require('child_process');
const { join } = require('path');
const findChrome = require('carlo/lib/find_chrome');
const detectInstaller = require('detect-installer');
const installPuppeteer = () => {
// find can use package manager
const packages = detectInstaller(join(__dirname, '../'));
// get installed package manager
const packageName = packages.find(detectInstaller.hasPackageCommand) || 'npm';
console.log(`🤖 will use ${packageName} install puppeteer`);
const command = `${packageName} ${packageName.includes('yarn') ? 'add' : 'i'} puppeteer`;
execSync(command, {
stdio: 'inherit',
});
};
const initPuppeteer = async () => {
try {
// eslint-disable-next-line import/no-unresolved
const findChromePath = await findChrome({});
const { executablePath } = findChromePath;
console.log(`🧲 find you browser in ${executablePath}`);
return;
} catch (error) {
console.log('🧲 no find chrome');
}
try {
require.resolve('puppeteer');
} catch (error) {
// need install puppeteer
await installPuppeteer();
}
};
initPuppeteer();

View File

@ -0,0 +1,45 @@
/* eslint-disable global-require */
/* eslint-disable import/no-extraneous-dependencies */
const findChrome = require('carlo/lib/find_chrome');
const getBrowser = async () => {
try {
// eslint-disable-next-line import/no-unresolved
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--no-first-run',
'--no-zygote',
'--no-sandbox',
],
});
return browser;
} catch (error) {
// console.log(error)
}
try {
// eslint-disable-next-line import/no-unresolved
const puppeteer = require('puppeteer-core');
const findChromePath = await findChrome({});
const { executablePath } = findChromePath;
const browser = await puppeteer.launch({
executablePath,
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--no-first-run',
'--no-zygote',
'--no-sandbox',
],
});
return browser;
} catch (error) {
console.log('🧲 no find chrome');
}
throw new Error('no find puppeteer');
};
module.exports = getBrowser;

View File

@ -0,0 +1,50 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable eslint-comments/no-unlimited-disable */
const { spawn } = require('child_process');
// eslint-disable-next-line import/no-extraneous-dependencies
const { kill } = require('cross-port-killer');
const env = Object.create(process.env);
env.BROWSER = 'none';
env.TEST = true;
env.UMI_UI = 'none';
env.PROGRESS = 'none';
// flag to prevent multiple test
let once = false;
const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['run', 'serve'], {
env,
});
startServer.stderr.on('data', (data) => {
// eslint-disable-next-line
console.log(data.toString());
});
startServer.on('exit', () => {
kill(process.env.PORT || 8000);
});
console.log('Starting development server for e2e tests...');
startServer.stdout.on('data', (data) => {
console.log(data.toString());
// hack code , wait umi
if (!once && data.toString().indexOf('Serving your umi project!') >= 0) {
// eslint-disable-next-line
once = true;
console.log('Development server is started, ready to run tests.');
const testCmd = spawn(
/^win/.test(process.platform) ? 'npm.cmd' : 'npm',
['test', '--', '--maxWorkers=1', '--runInBand'],
{
stdio: 'inherit',
},
);
testCmd.on('exit', (code) => {
console.log(code);
startServer.kill();
process.exit(code);
});
}
});

View File

@ -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;

57
code/app/tsconfig.json Normal file
View File

@ -0,0 +1,57 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "esnext",
"lib": [
"esnext",
"dom"
],
"sourceMap": true,
"baseUrl": ".",
"jsx": "react-jsx",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true,
"allowJs": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"strict": true,
"paths": {
"@/*": [
"./src/*"
],
"@@/*": [
"./src/.umi/*"
]
}
},
"include": [
"mock/**/*",
"src/**/*",
"tests/**/*",
"test/**/*",
"__test__/**/*",
"typings/**/*",
"config/**/*",
".eslintrc.js",
".stylelintrc.js",
".prettierrc.js",
"jest.config.js",
"mock/*"
],
"exclude": [
"node_modules",
"build",
"dist",
"scripts",
"src/.umi/*",
"webpack",
"jest",
"sh/*"
]
}