chore: init

This commit is contained in:
Kuizuo 2024-04-26 02:12:44 +08:00
commit 5231e5f565
298 changed files with 44435 additions and 0 deletions

33
.env.example Normal file
View File

@ -0,0 +1,33 @@
# app
APP_NAME=nest-trpc-prisma-starter
APP_PORT=5001
APP_BASE_URL=http://localhost:5001
# logger
LOGGER_LEVEL=debug
# security
JWT_SECRET=mySecretKey123
JWT_EXPIRE=7d
# db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=
POSTGRES_DB=demo
DB_HOST=127.0.0.1
DB_PORT=5432
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${POSTGRES_DB}?schema=public
# redis
REDIS_PORT=6379
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=
REDIS_DB=0
# smtp
SMTP_HOST=smtp.qq.com
SMTP_PORT=465
SMTP_USER=
SMTP_PASS=

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# compiled output
dist
node_modules
# testing
coverage
# OS
.DS_Store
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# local env files
.env
.env.*
!.env.example
# log files
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# turbo
.turbo

16
.npmrc Normal file
View File

@ -0,0 +1,16 @@
public-hoist-pattern[]=*fastify*
public-hoist-pattern[]=fastify
public-hoist-pattern[]=*@typescript-eslint*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*socket.io*
public-hoist-pattern[]=*jsonwebtoken*
public-hoist-pattern[]=@nestjs*
public-hoist-pattern[]=*prisma*
registry=https://registry.npmjs.org
node-linker=hoisted
shamefully-hoist=true
enable-pre-post-scripts=true
strict-peer-dependencies=false

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
20.11

27
.vscode/project.code-workspace vendored Normal file
View File

@ -0,0 +1,27 @@
{
"folders": [
{
"name": "ROOT",
"path": "../"
},
{
"name": "server",
"path": "../apps/server"
},
{
"name": "admin",
"path": "../apps/admin"
},
{
"name": "web",
"path": "../apps/web"
},
{
"name": "database",
"path": "../packages/database"
},
],
"settings": {
"testing.automaticallyOpenPeekView": "never"
},
}

44
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,44 @@
{
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "format/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml"
],
"typescript.tsdk": "node_modules/typescript/lib"
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 kuizuo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

37
README.md Normal file
View File

@ -0,0 +1,37 @@
# Nest + Prisma + tRPC + Zod
这是一个使用 Nest.js、Prisma、tRPC 和 Zod 构建的现代化全栈应用程序的示例项目。
## 🔧 技术栈
- Server
- NestJS
- tRPC
- Prisma
- Zod
- Web
- Next.js
- Admin
- Ant Design Pro
## 📄 使用说明
撰写中...
## TODO
- [ ] 升级到 Trpc 11
- [ ] 升级 Tanstack Query 5
- [ ] Nest.js 集成 Auth.js
## 参考
http://blog.innei.ren/nestjs-with-trpc-and-dependency-injection
https://www.tomray.dev/nestjs-nextjs-trpc
## 📝License
[MIT](./LICENSE)
Copyright (c) 2024 Kuizuo

16
apps/admin/.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
apps/admin/.eslintignore Normal file
View File

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

7
apps/admin/.eslintrc.js Normal file
View File

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

41
apps/admin/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# 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
# 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
.umi-test
# screenshot
screenshot
.firebase
.eslintcache
build

View File

@ -0,0 +1,22 @@
**/*.svg
.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

21
apps/admin/.prettierrc.js Normal file
View File

@ -0,0 +1,21 @@
module.exports = {
singleQuote: true,
trailingComma: 'all',
printWidth: 100,
proseWrap: 'never',
endOfLine: 'lf',
overrides: [
{
files: '.prettierrc',
options: {
parser: 'json',
},
},
{
files: 'document.ejs',
options: {
parser: 'html',
},
},
],
};

57
apps/admin/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).

151
apps/admin/config/config.ts Normal file
View File

@ -0,0 +1,151 @@
// https://umijs.org/config/
import { defineConfig } from '@umijs/max';
import { join } from 'path';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
import routes from './routes';
const { REACT_APP_ENV = 'dev' } = process.env;
export default defineConfig({
/**
* @name hash
* @description build hash
* @doc https://umijs.org/docs/api/config#hash
*/
hash: true,
/**
* @name
* @description ie11 使
* @doc https://umijs.org/docs/api/config#targets
*/
// targets: {
// ie: 11,
// },
/**
* @name
* @description pathcomponentroutesredirectwrapperstitle
* @doc https://umijs.org/docs/guides/routes
*/
// umi routes: https://umijs.org/docs/routing
routes,
/**
* @name
* @description less
* @doc antd的主题设置 https://ant.design/docs/react/customize-theme-cn
* @doc umi theme https://umijs.org/docs/api/config#theme
*/
theme: {
// 如果不想要 configProvide 动态设置主题需要把这个设置为 default
// 只有设置为 variable 才能使用 configProvide 动态设置主色调
'root-entry-name': 'variable',
},
/**
* @name moment
* @description js的包大小
* @doc https://umijs.org/docs/api/config#ignoremomentlocale
*/
ignoreMomentLocale: true,
/**
* @name
* @description 访
* @see 使build 使
* @doc https://umijs.org/docs/guides/proxy
* @doc https://umijs.org/docs/api/config#proxy
*/
proxy: proxy[REACT_APP_ENV as keyof typeof proxy],
/**
* @name
* @description state
*/
fastRefresh: true,
//============== 以下都是max的插件配置 ===============
/**
* @name
* @@doc https://umijs.org/docs/max/data-flow
*/
model: {},
/**
*
* @description Umi
* @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81
*/
initialState: {},
/**
* @name layout
* @doc https://umijs.org/docs/max/layout-menu
*/
title: 'Admin',
layout: {
locale: true,
...defaultSettings,
},
/**
* @name moment2dayjs
* @description moment dayjs
* @doc https://umijs.org/docs/max/moment2dayjs
*/
moment2dayjs: {
preset: 'antd',
plugins: ['duration'],
},
/**
* @name
* @doc https://umijs.org/docs/max/i18n
*/
locale: {
// default zh-CN
default: 'zh-CN',
antd: true,
// default true, when it is true, will use `navigator.language` overwrite default
baseNavigator: true,
},
/**
* @name antd
* @description babel import
* @doc https://umijs.org/docs/max/antd#antd
*/
antd: {},
/**
* @name
* @description axios ahooks useRequest
* @doc https://umijs.org/docs/max/request
*/
request: {
dataField: 'data',
},
/**
* @name
* @description initialState initialState
* @doc https://umijs.org/docs/max/access
*/
access: {},
/**
* @name <head> script
* @description <head> script
*/
headScripts: [
// 解决首次加载时白屏的问题
{ src: '/scripts/loading.js', async: true },
],
//================ pro 插件配置 =================
presets: ['umi-presets-pro'],
/**
* @name openAPI
* @description openapi serve mock
* @doc https://pro.ant.design/zh-cn/docs/openapi/
*/
// openAPI: [
// {
// requestLibPath: "import { request } from '@umijs/max'",
// schemaPath: 'http://localhost:6001/api-docs-json',
// projectName: 'swagger',
// mock: false,
// },
// ],
mfsu: {
strategy: 'normal',
},
requestRecord: {},
});

View File

@ -0,0 +1,28 @@
import { ProLayoutProps } from '@ant-design/pro-components';
/**
* @name
*/
const Settings: ProLayoutProps & {
pwa?: boolean;
logo?: string;
} = {
navTheme: 'light',
// 拂晓蓝
colorPrimary: '#1890ff',
layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: true,
colorWeak: false,
title: 'Admin',
pwa: true,
logo: '/logo.svg',
iconfontUrl: '',
token: {
// 参见ts声明demo 见文档通过token 修改样式
//https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F
},
};
export default Settings;

View File

@ -0,0 +1,593 @@
{
"openapi": "3.0.1",
"info": {
"title": "Ant Design Pro",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8000/"
},
{
"url": "https://localhost:8000/"
}
],
"paths": {
"/api/currentUser": {
"get": {
"tags": ["api"],
"description": "获取当前的用户",
"operationId": "currentUser",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentUser"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/captcha": {
"post": {
"description": "发送验证码",
"operationId": "getFakeCaptcha",
"tags": ["login"],
"parameters": [
{
"name": "phone",
"in": "query",
"description": "手机号",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FakeCaptcha"
}
}
}
}
}
}
},
"/api/login/outLogin": {
"post": {
"description": "登录接口",
"operationId": "outLogin",
"tags": ["login"],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/account": {
"post": {
"tags": ["login"],
"description": "登录接口",
"operationId": "login",
"requestBody": {
"description": "登录系统",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginParams"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginResult"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"x-codegen-request-body-name": "body"
},
"x-swagger-router-controller": "api"
},
"/api/notices": {
"summary": "getNotices",
"description": "NoticeIconItem",
"get": {
"tags": ["api"],
"operationId": "getNotices",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NoticeIconList"
}
}
}
}
}
}
},
"/api/rule": {
"get": {
"tags": ["rule"],
"description": "获取规则列表",
"operationId": "rule",
"parameters": [
{
"name": "current",
"in": "query",
"description": "当前的页码",
"schema": {
"type": "number"
}
},
{
"name": "pageSize",
"in": "query",
"description": "页面的容量",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleList"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"post": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "addRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"put": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "updateRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"delete": {
"tags": ["rule"],
"description": "删除规则",
"operationId": "removeRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/swagger": {
"x-swagger-pipe": "swagger_raw"
}
},
"components": {
"schemas": {
"CurrentUser": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"avatar": {
"type": "string"
},
"userid": {
"type": "string"
},
"email": {
"type": "string"
},
"signature": {
"type": "string"
},
"title": {
"type": "string"
},
"group": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
}
}
}
},
"notifyCount": {
"type": "integer",
"format": "int32"
},
"unreadCount": {
"type": "integer",
"format": "int32"
},
"country": {
"type": "string"
},
"access": {
"type": "string"
},
"geographic": {
"type": "object",
"properties": {
"province": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"city": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
}
}
},
"address": {
"type": "string"
},
"phone": {
"type": "string"
}
}
},
"LoginResult": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"type": {
"type": "string"
},
"currentAuthority": {
"type": "string"
}
}
},
"PageParams": {
"type": "object",
"properties": {
"current": {
"type": "number"
},
"pageSize": {
"type": "number"
}
}
},
"RuleListItem": {
"type": "object",
"properties": {
"key": {
"type": "integer",
"format": "int32"
},
"disabled": {
"type": "boolean"
},
"href": {
"type": "string"
},
"avatar": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"desc": {
"type": "string"
},
"callNo": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "integer",
"format": "int32"
},
"updatedAt": {
"type": "string",
"format": "datetime"
},
"createdAt": {
"type": "string",
"format": "datetime"
},
"progress": {
"type": "integer",
"format": "int32"
}
}
},
"RuleList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RuleListItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"FakeCaptcha": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "string"
}
}
},
"LoginParams": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"autoLogin": {
"type": "boolean"
},
"type": {
"type": "string"
}
}
},
"ErrorResponse": {
"required": ["errorCode"],
"type": "object",
"properties": {
"errorCode": {
"type": "string",
"description": "业务约定的错误码"
},
"errorMessage": {
"type": "string",
"description": "业务上的错误信息"
},
"success": {
"type": "boolean",
"description": "业务上的请求是否成功"
}
}
},
"NoticeIconList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NoticeIconItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"NoticeIconItemType": {
"title": "NoticeIconItemType",
"description": "已读未读列表的枚举",
"type": "string",
"properties": {},
"enum": ["notification", "message", "event"]
},
"NoticeIconItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"extra": {
"type": "string",
"format": "any"
},
"key": { "type": "string" },
"read": {
"type": "boolean"
},
"avatar": {
"type": "string"
},
"title": {
"type": "string"
},
"status": {
"type": "string"
},
"datetime": {
"type": "string",
"format": "date"
},
"description": {
"type": "string"
},
"type": {
"extensions": {
"x-is-enum": true
},
"$ref": "#/components/schemas/NoticeIconItemType"
}
}
}
}
}
}

View File

@ -0,0 +1,42 @@
/**
* @name
* @see
* -------------------------------
* 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
*
* @doc https://umijs.org/docs/guides/proxy
*/
export default {
// 如果需要自定义本地开发服务器 请取消注释按需调整
dev: {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/api/': {
// 要代理的地址
target: 'http://localhost:5001',
changeOrigin: true,
},
},
/**
* @name
* @doc https://github.com/chimurai/http-proxy-middleware
*/
// test: {
// // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
// '/api/': {
// target: 'https://proapi.azurewebsites.net',
// changeOrigin: true,
// pathRewrite: { '^': '' },
// },
// },
// pre: {
// '/api/': {
// target: 'your pre url',
// changeOrigin: true,
// pathRewrite: { '^': '' },
// },
// },
};

View File

@ -0,0 +1,95 @@
/**
* @name umi
* @description path,component,routes,redirect,wrappers,name,icon
* @param path path 第一种是动态参数 :id *
* @param component location path React src/pages
* @param routes layout 使
* @param redirect
* @param wrappers
* @param name menu.ts menu.xxxx name login menu.ts menu.login
* @param icon https://ant.design/components/icon-cn 注意去除风格后缀和大小写,如想要配置图标为 <StepBackwardOutlined /> 则取值应为 stepBackward 或 StepBackward如想要配置图标为 <UserOutlined /> 则取值应为 user 或者 User
* @doc https://umijs.org/docs/guides/routes
*/
export default [
{
layout: false,
name: 'login',
path: '/login',
component: './login',
},
{
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
routes: [
{
path: '/dashboard',
redirect: '/dashboard/analysis',
},
{
name: 'analysis',
icon: 'smile',
path: '/dashboard/analysis',
component: './dashboard/analysis',
},
{
name: 'monitor',
icon: 'smile',
path: '/dashboard/monitor',
component: './dashboard/monitor',
},
// {
// name: 'workplace',
// icon: 'smile',
// path: '/dashboard/workplace',
// component: './dashboard/workplace',
// },
],
},
{
name: 'account',
icon: 'user',
path: '/account',
routes: [
{
path: '/account',
redirect: '/account/center',
},
{
name: 'center',
icon: 'smile',
path: '/account/center',
component: './account/center',
},
{
name: 'settings',
icon: 'smile',
path: '/account/settings',
component: './account/settings',
},
],
},
{
name: 'Todo管理',
icon: 'user',
path: '/todo',
component: './todo',
access: 'canAdmin',
},
{
name: '用户管理',
icon: 'user',
path: '/user',
component: './user',
access: 'canAdmin',
},
{
path: '/',
redirect: '/dashboard/analysis',
},
{
path: '*',
layout: false,
component: './404',
},
];

23
apps/admin/jest.config.ts Normal file
View File

@ -0,0 +1,23 @@
import { configUmiAlias, createConfig } from '@umijs/max/test';
export default async () => {
const config = await configUmiAlias({
...createConfig({
target: 'browser',
}),
});
console.log();
return {
...config,
testEnvironmentOptions: {
...(config?.testEnvironmentOptions || {}),
url: 'http://localhost:8000',
},
setupFiles: [...(config.setupFiles || []), './tests/setupTests.jsx'],
globals: {
...config.globals,
localStorage: null,
},
};
};

11
apps/admin/jsconfig.json Normal file
View File

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

View File

@ -0,0 +1,176 @@
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) as Array<keyof API.RuleListItem>).forEach((key) => {
let nextSort = next?.[key] as number;
let preSort = prev?.[key] as number;
if (sorter[key] === 'descend') {
if (preSort - nextSort > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (preSort - nextSort > 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) as Array<keyof API.RuleListItem>).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}
if (params.name) {
dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
}
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,
};

115
apps/admin/mock/notices.ts Normal file
View File

@ -0,0 +1,115 @@
import { Request, Response } from 'express';
const getNotices = (req: Request, res: Response) => {
res.json({
data: [
{
id: '000000001',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/MSbDR4FR2MUAAAAAAAAAAAAAFl94AQBr',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: 'notification',
},
{
id: '000000002',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/hX-PTavYIq4AAAAAAAAAAAAAFl94AQBr',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: 'notification',
},
{
id: '000000003',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/jHX5R5l3QjQAAAAAAAAAAAAAFl94AQBr',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: 'notification',
},
{
id: '000000004',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Wr4mQqx6jfwAAAAAAAAAAAAAFl94AQBr',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000005',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Mzj_TbcWUj4AAAAAAAAAAAAAFl94AQBr',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000006',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/eXLzRbPqQE4AAAAAAAAAAAAAFl94AQBr',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000007',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/w5mRQY2AmEEAAAAAAAAAAAAAFl94AQBr',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000008',
avatar:
'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/wPadR5M9918AAAAAAAAAAAAAFl94AQBr',
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,
};

View File

@ -0,0 +1,324 @@
module.exports = {
'GET /api/currentUser': {
data: {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服某某某事业群某某平台部某某技术部UED',
tags: [
{ key: '0', label: '很有想法的' },
{ key: '1', label: '专注设计' },
{ key: '2', label: '辣~' },
{ key: '3', label: '大长腿' },
{ key: '4', label: '川妹子' },
{ key: '5', label: '海纳百川' },
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: { label: '浙江省', key: '330000' },
city: { label: '杭州市', key: '330100' },
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
},
'GET /api/rule': {
data: [
{
key: 99,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 99',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 503,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 98,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 98',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 164,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 97,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 97',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 174,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 96,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 96',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 914,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 7,
},
{
key: 95,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 95',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 698,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 94,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 94',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 488,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 14,
},
{
key: 93,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 93',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 580,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 77,
},
{
key: 92,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 92',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 244,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 58,
},
{
key: 91,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 91',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 959,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 66,
},
{
key: 90,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 90',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 958,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 72,
},
{
key: 89,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 89',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 301,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 2,
},
{
key: 88,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 88',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 277,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 87,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 87',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 810,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 86,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 86',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 780,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 22,
},
{
key: 85,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 85',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 705,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 84,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 84',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 203,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 79,
},
{
key: 83,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 83',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 491,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 59,
},
{
key: 82,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 82',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 73,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 100,
},
{
key: 81,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 81',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 406,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 61,
},
{
key: 80,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 80',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 112,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 20,
},
],
total: 100,
success: true,
pageSize: 20,
current: 1,
},
'POST /api/login/outLogin': { data: {}, success: true },
'POST /api/login/account': {
status: 'ok',
type: 'account',
currentAuthority: 'admin',
},
};

5
apps/admin/mock/route.ts Normal file
View File

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

203
apps/admin/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: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服某某某事业群某某平台部某某技术部UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
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 === 'ant.design' && username === 'admin') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
access = 'admin';
return;
}
if (password === 'ant.design' && 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: 'Forbidden',
message: 'Forbidden',
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,
};

95
apps/admin/package.json Normal file
View File

@ -0,0 +1,95 @@
{
"name": "admin",
"version": "0.1.0",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
"analyze": "cross-env ANALYZE=1 max build",
"build": "max build",
"deploy": "npm run build",
"dev": "npm run start:dev",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
"jest": "jest",
"lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
"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 ",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
"openapi": "max openapi",
"prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
"preview": "npm run build && max preview --port 8000",
"record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
"serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"test": "jest",
"test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u",
"tsc": "tsc --noEmit"
},
"lint-staged": {
"**/*.{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.8.0",
"@ant-design/pro-components": "^2.3.57",
"@ant-design/use-emotion-css": "1.0.4",
"@umijs/route-utils": "^2.2.2",
"antd": "^5.2.2",
"classnames": "^2.3.2",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"omit.js": "^2.0.2",
"rc-menu": "^9.8.2",
"rc-util": "^5.27.2",
"react": "^18.2.0",
"react-dev-inspector": "^1.8.4",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0"
},
"devDependencies": {
"@ant-design/pro-cli": "^2.1.5",
"@testing-library/react": "^13.4.0",
"@types/express": "^4.17.17",
"@types/history": "^4.7.11",
"@types/jest": "^29.4.0",
"@types/lodash": "^4.14.191",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-helmet": "^6.1.6",
"@umijs/fabric": "^2.14.1",
"@umijs/lint": "^4.0.52",
"@umijs/max": "^4.0.52",
"cross-env": "^7.0.3",
"eslint": "^8.34.0",
"express": "^4.18.2",
"gh-pages": "^3.2.3",
"husky": "^7.0.4",
"jest": "^29.4.3",
"jest-environment-jsdom": "^29.4.3",
"lint-staged": "^10.5.4",
"mockjs": "^1.1.0",
"prettier": "^2.8.4",
"swagger-ui-dist": "^4.15.5",
"ts-node": "^10.9.1",
"typescript": "^5.3.0",
"umi-presets-pro": "^2.0.2"
},
"engines": {
"node": ">=18.0.0"
}
}

1
apps/admin/public/CNAME Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 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

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 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

View File

@ -0,0 +1,202 @@
/**
* loading 占位
* 解决首次加载时白屏的问题
*/
(function () {
const _root = document.querySelector('#root');
if (_root && _root.innerHTML === '') {
_root.innerHTML = `
<style>
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
}
#root {
background-repeat: no-repeat;
background-size: 100% auto;
}
.loading-title {
font-size: 1.1rem;
}
.loading-sub-title {
margin-top: 20px;
font-size: 1rem;
color: #888;
}
.page-loading-warp {
display: flex;
align-items: center;
justify-content: center;
padding: 26px;
}
.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: 362px;
">
<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 class="loading-title">
正在加载资源
</div>
<div class="loading-sub-title">
初次加载资源可能需要较多时间 请耐心等待
</div>
</div>
`;
}
})();

14
apps/admin/src/access.ts Normal file
View File

@ -0,0 +1,14 @@
const Role = {
Admin: 'Admin',
User: 'User'
}
/**
* @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.role === Role.Admin,
};
}

136
apps/admin/src/app.tsx Normal file
View File

@ -0,0 +1,136 @@
import Footer from '@/components/Footer';
import { Question, SelectLang } from '@/components/RightContent';
import { LinkOutlined } from '@ant-design/icons';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { SettingDrawer } from '@ant-design/pro-components';
import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
import { history, Link } from '@umijs/max';
import defaultSettings from '../config/defaultSettings';
import { errorConfig } from './requestErrorConfig';
import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
import React from 'react';
import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown';
const isDev = process.env.NODE_ENV === 'development';
const loginPath = '/login';
/**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
const fetchUserInfo = async () => {
try {
const response = await queryCurrentUser({
skipErrorHandler: true,
});
return response.data;
} catch (error) {
history.push(loginPath);
}
return undefined;
};
// 如果不是登录页面,执行
const { location } = history;
if (location.pathname !== loginPath) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
return {
fetchUserInfo,
settings: defaultSettings as Partial<LayoutSettings>,
};
}
// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
return {
actionsRender: () => [<Question key="doc" />, <SelectLang key="SelectLang" />],
avatarProps: {
src: initialState?.currentUser?.avatar,
title: <AvatarName />,
render: (_, avatarChildren) => {
return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
},
},
// waterMarkProps: {
// content: initialState?.currentUser?.username,
// },
// footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
}
},
layoutBgImgList: [
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
left: 85,
bottom: 100,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
bottom: -68,
right: -45,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
bottom: 0,
left: 0,
width: '331px',
},
],
links: isDev
? [
<Link key="openapi" to="/umi/plugin/openapi" target="_blank">
<LinkOutlined />
<span>OpenAPI </span>
</Link>,
]
: [],
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: (children) => {
// if (initialState?.loading) return <PageLoading />;
return (
<>
{children}
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
</>
);
},
...initialState?.settings,
};
};
/**
* @name request
* axios ahooks useRequest
* @doc https://umijs.org/docs/max/request#配置
*/
export const request: RequestConfig = {
...errorConfig,
};

View File

@ -0,0 +1,45 @@
import { GithubOutlined } from '@ant-design/icons';
import { DefaultFooter } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import React from 'react';
const Footer: React.FC = () => {
const intl = useIntl();
const defaultMessage = intl.formatMessage({
id: 'app.copyright.produced',
defaultMessage: '蚂蚁集团体验技术部出品',
});
const currentYear = new Date().getFullYear();
return (
<DefaultFooter
style={{
background: 'none',
}}
copyright={`${currentYear} ${defaultMessage}`}
links={[
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
blankTarget: true,
},
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
/>
);
};
export default Footer;

View File

@ -0,0 +1,23 @@
import { Dropdown } from 'antd';
import type { DropDownProps } from 'antd/es/dropdown';
import React from 'react';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import classNames from 'classnames';
export type HeaderDropdownProps = {
overlayClassName?: string;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} & Omit<DropDownProps, 'overlay'>;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => {
const className = useEmotionCss(({ token }) => {
return {
[`@media screen and (max-width: ${token.screenXS})`]: {
width: '100%',
},
};
});
return <Dropdown overlayClassName={classNames(className, cls)} {...restProps} />;
};
export default HeaderDropdown;

View File

@ -0,0 +1,132 @@
import { outLogin } from '@/services/ant-design-pro/api';
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { history, useModel } from '@umijs/max';
import { Spin } from 'antd';
import { stringify } from 'querystring';
import type { MenuInfo } from 'rc-menu/lib/interface';
import React, { useCallback } from 'react';
import { flushSync } from 'react-dom';
import HeaderDropdown from '../HeaderDropdown';
export type GlobalHeaderRightProps = {
menu?: boolean;
children?: React.ReactNode;
};
export const AvatarName = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
return <span className="anticon">{currentUser?.username}</span>;
};
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu, children }) => {
/**
* 退 url
*/
const loginOut = async () => {
await outLogin();
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
/** 此方法会跳转到 redirect 参数所在的位置 */
const redirect = urlParams.get('redirect');
// Note: There may be security issues, please note
if (window.location.pathname !== '/login' && !redirect) {
history.replace({
pathname: '/login',
search: stringify({
redirect: pathname + search,
}),
});
}
};
const actionClassName = useEmotionCss(({ token }) => {
return {
display: 'flex',
height: '48px',
marginLeft: 'auto',
overflow: 'hidden',
alignItems: 'center',
padding: '0 8px',
cursor: 'pointer',
borderRadius: token.borderRadius,
'&:hover': {
backgroundColor: token.colorBgTextHover,
},
};
});
const { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick = useCallback(
(event: MenuInfo) => {
const { key } = event;
if (key === 'logout') {
flushSync(() => {
setInitialState((s) => ({ ...s, currentUser: undefined }));
});
loginOut();
return;
}
history.push(`/account/${key}`);
},
[setInitialState],
);
const loading = (
<span className={actionClassName}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
if (!initialState) {
return loading;
}
const { currentUser } = initialState;
if (!currentUser || !currentUser.username) {
return loading;
}
const menuItems = [
...(menu
? [
{
key: 'center',
icon: <UserOutlined />,
label: '个人中心',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '个人设置',
},
{
type: 'divider' as const,
},
]
: []),
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
},
];
return (
<HeaderDropdown
menu={{
selectedKeys: [],
onClick: onMenuClick,
items: menuItems,
}}
>
{children}
</HeaderDropdown>
);
};

View File

@ -0,0 +1,31 @@
import { QuestionCircleOutlined } from '@ant-design/icons';
import { SelectLang as UmiSelectLang } from '@umijs/max';
import React from 'react';
export type SiderTheme = 'light' | 'dark';
export const SelectLang = () => {
return (
<UmiSelectLang
style={{
padding: 4,
}}
/>
);
};
export const Question = () => {
return (
<div
style={{
display: 'flex',
height: 26,
}}
onClick={() => {
window.open('https://pro.ant.design/docs/getting-started');
}}
>
<QuestionCircleOutlined />
</div>
);
};

View File

@ -0,0 +1,53 @@
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
}
.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: 768px) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}

91
apps/admin/src/global.tsx Normal file
View File

@ -0,0 +1,91 @@
import { useIntl } from '@umijs/max';
import { Button, message, notification } from 'antd';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
const isHttps = document.location.protocol === 'https:';
const clearCache = () => {
// remove all caches
if (window.caches) {
caches
.keys()
.then((keys) => {
keys.forEach((key) => {
caches.delete(key);
});
})
.catch((e) => console.log(e));
}
};
// 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]);
});
clearCache();
window.location.reload();
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.destroy(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();
});
clearCache();
}

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,68 @@
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(admin/ant.design)',
'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.link': 'Welcome',
'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': 'batch 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 pages from './zh-CN/pages';
import pwa from './zh-CN/pwa';
import settingDrawer from './zh-CN/settingDrawer';
import settings from './zh-CN/settings';
export default {
'navBar.lang': '语言',
'layout.user.link.help': '帮助',
'layout.user.link.privacy': '隐私',
'layout.user.link.terms': '条款',
'app.copyright.produced': '蚂蚁集团体验技术部出品',
'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,65 @@
export default {
'pages.layouts.userLayout.title': 'Ant Design 是西湖区最具影响力的 Web 设计规范',
'pages.login.accountLogin.tab': '账户密码登录',
'pages.login.accountLogin.errorMessage': '错误的用户名和密码(admin/ant.design)',
'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.link': '欢迎使用',
'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": "Ant Design Pro",
"short_name": "Ant Design Pro",
"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 { history } from '@umijs/max';
import { Button, Result } from 'antd';
import React from 'react';
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,45 @@
import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useIntl } from '@umijs/max';
import { Alert, Card, Typography } from 'antd';
import React from 'react';
const Admin: React.FC = () => {
const intl = useIntl();
return (
<PageContainer
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 /> Ant Design Pro <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>
</PageContainer>
);
};
export default Admin;

View File

@ -0,0 +1,164 @@
import { PageContainer } from '@ant-design/pro-components';
import { useModel } from '@umijs/max';
import { Card, theme } from 'antd';
import React from 'react';
/**
*
* @param param0
* @returns
*/
const InfoCard: React.FC<{
title: string;
index: number;
desc: string;
href: string;
}> = ({ title, href, index, desc }) => {
const { useToken } = theme;
const { token } = useToken();
return (
<div
style={{
backgroundColor: token.colorBgContainer,
boxShadow: token.boxShadow,
borderRadius: '8px',
fontSize: '14px',
color: token.colorTextSecondary,
lineHeight: '22px',
padding: '16px 19px',
minWidth: '220px',
flex: 1,
}}
>
<div
style={{
display: 'flex',
gap: '4px',
alignItems: 'center',
}}
>
<div
style={{
width: 48,
height: 48,
lineHeight: '22px',
backgroundSize: '100%',
textAlign: 'center',
padding: '8px 16px 16px 12px',
color: '#FFF',
fontWeight: 'bold',
backgroundImage:
"url('https://gw.alipayobjects.com/zos/bmw-prod/daaf8d50-8e6d-4251-905d-676a24ddfa12.svg')",
}}
>
{index}
</div>
<div
style={{
fontSize: '16px',
color: token.colorText,
paddingBottom: 8,
}}
>
{title}
</div>
</div>
<div
style={{
fontSize: '14px',
color: token.colorTextSecondary,
textAlign: 'justify',
lineHeight: '22px',
marginBottom: 8,
}}
>
{desc}
</div>
<a href={href} target="_blank" rel="noreferrer">
{'>'}
</a>
</div>
);
};
const Welcome: React.FC = () => {
const { token } = theme.useToken();
const { initialState } = useModel('@@initialState');
return (
<PageContainer>
<Card
style={{
borderRadius: 8,
}}
bodyStyle={{
backgroundImage:
initialState?.settings?.navTheme === 'realDark'
? 'background-image: linear-gradient(75deg, #1A1B1F 0%, #191C1F 100%)'
: 'background-image: linear-gradient(75deg, #FBFDFF 0%, #F5F7FF 100%)',
}}
>
<div
style={{
backgroundPosition: '100% -30%',
backgroundRepeat: 'no-repeat',
backgroundSize: '274px auto',
backgroundImage:
"url('https://gw.alipayobjects.com/mdn/rms_a9745b/afts/img/A*BuFmQqsB2iAAAAAAAAAAAAAAARQnAQ')",
}}
>
<div
style={{
fontSize: '20px',
color: token.colorTextHeading,
}}
>
使 Ant Design Pro
</div>
<p
style={{
fontSize: '14px',
color: token.colorTextSecondary,
lineHeight: '22px',
marginTop: 16,
marginBottom: 32,
width: '65%',
}}
>
Ant Design Pro umiAnt Design ProComponents
//
</p>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: 16,
}}
>
<InfoCard
index={1}
href="https://umijs.org/docs/introduce/introduce"
title="了解 umi"
desc="umi 是一个可扩展的企业级前端应用框架,umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。"
/>
<InfoCard
index={2}
title="了解 ant design"
href="https://ant.design"
desc="antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。"
/>
<InfoCard
index={3}
title="了解 Pro Components"
href="https://procomponents.ant.design"
desc="ProComponents 是一个基于 Ant Design 做了更高抽象的模板组件,以 一个组件就是一个页面为开发理念,为中后台开发带来更好的体验。"
/>
</div>
</div>
</Card>
</PageContainer>
);
};
export default Welcome;

View File

@ -0,0 +1,3 @@
export default () => {
return <div>New Page</div>;
};

View File

@ -0,0 +1,3 @@
export default () => {
return <div>New Page</div>;
};

View File

@ -0,0 +1,3 @@
export default () => {
return <div>New Page</div>;
};

View File

@ -0,0 +1,3 @@
export default () => {
return <div>New Page</div>;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,344 @@
import Footer from '@/components/Footer';
import { login } from '@/services/ant-design-pro/api';
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
import {
AlipayCircleOutlined,
LockOutlined,
MobileOutlined,
TaobaoCircleOutlined,
UserOutlined,
WeiboCircleOutlined,
} from '@ant-design/icons';
import {
LoginForm,
ProFormCaptcha,
ProFormCheckbox,
ProFormText,
} from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
import { Alert, message, Tabs } from 'antd';
import Settings from '../../../config/defaultSettings';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
import { LoginType } from '@server/modules/auth/auth.constant';
import { LoginDto } from '@server/modules/auth/auth.dto';
import { setToken } from '@/utils/auth';
const ActionIcons = () => {
const langClassName = useEmotionCss(({ token }) => {
return {
marginLeft: '8px',
color: 'rgba(0, 0, 0, 0.2)',
fontSize: '24px',
verticalAlign: 'middle',
cursor: 'pointer',
transition: 'color 0.3s',
'&:hover': {
color: token.colorPrimaryActive,
},
};
});
return (
<>
<AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
<TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
<WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
</>
);
};
const Lang = () => {
const langClassName = useEmotionCss(({ token }) => {
return {
width: 42,
height: 42,
lineHeight: '42px',
position: 'fixed',
right: 16,
borderRadius: token.borderRadius,
':hover': {
backgroundColor: token.colorBgTextHover,
},
};
});
return (
<div className={langClassName} data-lang>
{SelectLang && <SelectLang />}
</div>
);
};
const Login: React.FC = () => {
const [type, setType] = useState<LoginType>('account');
const { initialState, setInitialState } = useModel('@@initialState');
const containerClassName = useEmotionCss(() => {
return {
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
backgroundImage:
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
backgroundSize: '100% 100%',
};
});
const intl = useIntl();
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
flushSync(() => {
setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
});
}
};
const handleSubmit = async (values: LoginDto) => {
try {
// 登录
const response = await login({ ...values, type });
if (response.ok) {
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultLoginSuccessMessage);
setToken(response.data?.authToken!)
await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
}
} catch (error) {
// ...
}
};
return (
<div className={containerClassName}>
<Helmet>
<title>
{intl.formatMessage({
id: 'menu.login',
defaultMessage: '登录页',
})}
- {Settings.title}
</title>
</Helmet>
<Lang />
<div
style={{
flex: '1',
padding: '32px 0',
}}
>
<LoginForm
contentStyle={{
minWidth: 280,
maxWidth: '75vw',
}}
logo={<img alt="logo" src="/logo.svg" />}
title="Admin"
subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
initialValues={{
autoLogin: true,
}}
actions={[
<FormattedMessage
key="loginWith"
id="pages.login.loginWith"
defaultMessage="其他登录方式"
/>,
<ActionIcons key="icons" />,
]}
onFinish={async (values) => {
await handleSubmit(values as LoginDto);
}}
>
<Tabs
activeKey={type}
onChange={setType as any}
centered
items={[
{
key: 'account',
label: intl.formatMessage({
id: 'pages.login.accountLogin.tab',
defaultMessage: '账户密码登录',
}),
},
{
key: 'mobile',
label: intl.formatMessage({
id: 'pages.login.phoneLogin.tab',
defaultMessage: '手机号登录',
}),
},
]}
/>
{type === 'account' && (
<>
<ProFormText
name="username"
fieldProps={{
size: 'large',
prefix: <UserOutlined />,
}}
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 />,
}}
placeholder={intl.formatMessage({
id: 'pages.login.password.placeholder',
defaultMessage: '密码: ant.design',
})}
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.password.required"
defaultMessage="请输入密码!"
/>
),
},
]}
/>
</>
)}
{type === 'mobile' && (
<>
<ProFormText
fieldProps={{
size: 'large',
prefix: <MobileOutlined />,
}}
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 />,
}}
captchaProps={{
size: 'large',
}}
placeholder={intl.formatMessage({
id: 'pages.login.captcha.placeholder',
defaultMessage: '请输入验证码',
})}
captchaTextRender={(timing, count) => {
if (timing) {
return `${count} ${intl.formatMessage({
id: 'pages.getCaptchaSecondText',
defaultMessage: '获取验证码',
})}`;
}
return intl.formatMessage({
id: 'pages.login.phoneLogin.getVerificationCode',
defaultMessage: '获取验证码',
});
}}
name="captcha"
rules={[
{
required: true,
message: (
<FormattedMessage
id="pages.login.captcha.required"
defaultMessage="请输入验证码!"
/>
),
},
]}
onGetCaptcha={async (phone) => {
const result = await getFakeCaptcha({
phone,
});
if (!result) {
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>
</LoginForm>
</div>
<Footer />
</div>
);
};
export default Login;

View File

@ -0,0 +1,96 @@
import { render, fireEvent, act } from '@testing-library/react';
import React from 'react';
import { TestBrowser } from '@@/testBrowser';
// @ts-ignore
import { startMock } from '@@/requestRecordMock';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
let server: {
close: () => void;
};
describe('Login Page', () => {
beforeAll(async () => {
server = await startMock({
port: 8000,
scene: 'login',
});
});
afterAll(() => {
server?.close();
});
it('should show login form', async () => {
const historyRef = React.createRef<any>();
const rootContainer = render(
<TestBrowser
historyRef={historyRef}
location={{
pathname: '/login',
}}
/>,
);
await rootContainer.findAllByText('Ant Design');
act(() => {
historyRef.current?.push('/login');
});
expect(rootContainer.baseElement?.querySelector('.ant-pro-form-login-desc')?.textContent).toBe(
'Ant Design is the most influential web design specification in Xihu district',
);
expect(rootContainer.asFragment()).toMatchSnapshot();
rootContainer.unmount();
});
it('should login success', async () => {
const historyRef = React.createRef<any>();
const rootContainer = render(
<TestBrowser
historyRef={historyRef}
location={{
pathname: '/login',
}}
/>,
);
await rootContainer.findAllByText('Ant Design');
const userNameInput = await rootContainer.findByPlaceholderText('Username: admin or user');
act(() => {
fireEvent.change(userNameInput, { target: { value: 'admin' } });
});
const passwordInput = await rootContainer.findByPlaceholderText('Password: ant.design');
act(() => {
fireEvent.change(passwordInput, { target: { value: 'ant.design' } });
});
await (await rootContainer.findByText('Login')).click();
// 等待接口返回结果
await waitTime(5000);
await rootContainer.findAllByText('Ant Design Pro');
expect(rootContainer.asFragment()).toMatchSnapshot();
await waitTime(2000);
rootContainer.unmount();
});
});

View File

@ -0,0 +1,338 @@
import { addTodo, removeTodo, queryTodo, updateTodo } from '@/services/todo/api';
import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
import {
FooterToolbar,
ModalForm,
PageContainer,
ProDescriptions,
ProFormSegmented,
ProTable,
} from '@ant-design/pro-components';
import { FormattedMessage } from '@umijs/max';
import { Button, Drawer, message, Tag, Input, Space, InputRef } from 'antd';
import React, { useRef, useState } from 'react';
const statusValueEnum = {
false: {
text: '待完成',
status: 'Default',
},
true: {
text: '已完成',
status: 'Success',
},
}
const TagList: React.FC<{
value?: {
key: string;
name: string;
}[];
onChange?: (
value: {
key: string;
name: string;
}[],
) => void;
}> = ({ value, onChange }) => {
const ref = useRef<InputRef | null>(null);
const [newTags, setNewTags] = useState<
{
key: string;
name: string;
}[]
>([]);
const [inputValue, setInputValue] = useState<string>('');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
let tempsTags = [...(value || [])];
if (
inputValue &&
tempsTags.filter((tag) => tag.name === inputValue).length === 0
) {
tempsTags = [
...tempsTags,
{ key: `new-${tempsTags.length}`, name: inputValue },
];
}
onChange?.(tempsTags);
setNewTags([]);
setInputValue('');
};
return (
<Space>
{(value || []).concat(newTags).map((item) => (
<Tag key={item.key}>{item.name}</Tag>
))}
<Input
ref={ref}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
</Space>
);
};
const handleAdd = async (fields: API.TodoItem) => {
const hide = message.loading('正在添加');
try {
await addTodo({ ...fields });
hide();
message.success('添加成功');
return true;
} catch (error) {
hide();
message.error('添加失败!');
return false;
}
};
const handleUpdate = async ({ id, ...fields }: API.TodoItem) => {
const hide = message.loading('Configuring');
try {
await updateTodo(id, {
...fields
});
hide();
message.success('更新成功');
return true;
} catch (error) {
hide();
message.error('更新失败');
return false;
}
};
const handleRemove = async (selectedRows: API.TodoItem[]) => {
console.log("🚀 ~ handleRemove ~ selectedRows:", selectedRows)
const hide = message.loading('正在删除');
if (!selectedRows) return true;
try {
await removeTodo(selectedRows.map((row) => row.id));
hide();
message.success('删除成功');
return true;
} catch (error) {
hide();
return false;
}
};
const TableList: React.FC = () => {
const [createModalOpen, handleModalOpen] = useState<boolean>(false);
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [showDetail, setShowDetail] = useState<boolean>(false);
const actionRef = useRef<ActionType>();
const [currentRow, setCurrentRow] = useState<API.TodoItem>();
const [selectedRowsState, setSelectedRows] = useState<API.TodoItem[]>([]);
const columns: ProColumns<API.TodoItem>[] = [
{
title: '代办事项',
dataIndex: 'value',
width: 180,
render: (dom, entity) => {
return (
<a
onClick={() => {
setCurrentRow(entity);
setShowDetail(true);
}}
>
{dom}
</a>
);
},
},
{
title: '状态',
dataIndex: 'status',
hideInForm: true,
width: 100,
valueEnum: statusValueEnum,
},
{
title: '创建时间',
key: 'showTime',
dataIndex: 'createdAt',
valueType: 'dateTime',
sorter: true,
hideInSearch: true,
width: 180
},
{
title: '创建时间',
dataIndex: 'createdAt',
valueType: 'dateRange',
hideInTable: true,
hideInDescriptions: true,
search: {
transform: (value) => {
return {
startTime: value[0],
endTime: value[1],
};
},
},
},
// {
// title: '备注',
// dataIndex: 'remark',
// width: 150,
// },
{
title: '创建者',
dataIndex: 'user.username',
width: 150,
render: (dom, entity) => {
return (<span>{entity.user.username}</span>)
},
},
{
title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="Operating" />,
dataIndex: 'option',
valueType: 'option',
width: 80,
render: (_, record) => [
<a
key="config"
onClick={() => {
handleUpdateModalOpen(true);
setCurrentRow(record);
}}
>
</a>
],
},
];
return (
<PageContainer header={{ title: 'Todo列表' }}>
<ProTable<API.TodoItem, API.PageParams>
actionRef={actionRef}
rowKey="id"
search={{
labelWidth: 120,
}}
cardBordered
// toolBarRender={() => [
// <Button
// type="primary"
// key="primary"
// onClick={() => {
// handleModalOpen(true);
// }}
// >
// <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" />
// </Button>,
// ]}
request={queryTodo}
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="项" />
</div>
}
>
<Button
onClick={async () => {
await handleRemove(selectedRowsState);
setSelectedRows([]);
actionRef.current?.reloadAndRest?.();
}}
>
<FormattedMessage
id="pages.searchTable.batchDeletion"
defaultMessage="Batch deletion"
/>
</Button>
</FooterToolbar>
)}
<ModalForm
title={'更新todo'}
width="400px"
layout="horizontal"
grid
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
modalProps={{ destroyOnClose: true }}
initialValues={{
value: currentRow?.value,
status: currentRow?.status,
}}
onFinish={async (value) => {
const success = await handleUpdate({ id: currentRow?.id, ...value });
if (success) {
handleUpdateModalOpen(false);
setCurrentRow(undefined);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
>
<ProFormSegmented
label={"状态"}
colProps={{
span: 24,
}}
name="status"
valueEnum={statusValueEnum}
/>
</ModalForm>
<Drawer
width={600}
open={showDetail}
onClose={() => {
setCurrentRow(undefined);
setShowDetail(false);
}}
closable={false}
>
{currentRow?.value && (
<ProDescriptions<API.TodoItem>
column={1}
title={currentRow?.value}
request={async () => ({
data: currentRow || {},
})}
params={{
id: currentRow?.id,
}}
columns={columns as ProDescriptionsItemProps<API.TodoItem>[]}
/>
)}
</Drawer>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,336 @@
import { addUser, removeUser, queryUser, updateUser } from '@/services/user/api';
import { PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns, ProDescriptionsItemProps } from '@ant-design/pro-components';
import {
FooterToolbar,
ModalForm,
PageContainer,
ProDescriptions,
ProFormSegmented,
ProFormText,
ProTable,
} from '@ant-design/pro-components';
import { FormattedMessage } from '@umijs/max';
import { Button, Drawer, Image, message, Tag } from 'antd';
import React, { useRef, useState } from 'react';
const statusValueEnum = {
0: {
text: '禁用',
status: 'Default',
},
1: {
text: '正常',
status: 'Processing',
},
}
const handleAdd = async (fields: API.UserItem) => {
const hide = message.loading('正在添加...');
try {
await addUser({ ...fields });
hide();
message.success('添加成功');
return true;
} catch (error) {
hide();
return false;
}
};
const handleUpdate = async ({ id, ...fields }: API.UserItem) => {
const hide = message.loading('更新中...');
try {
await updateUser(id, {
...fields
});
hide();
message.success('更新成功');
return true;
} catch (error) {
hide();
return false;
}
};
const handleRemove = async (selectedRows: API.UserItem[]) => {
console.log("🚀 ~ handleRemove ~ selectedRows:", selectedRows)
const hide = message.loading('正在删除...');
if (!selectedRows) return true;
try {
await removeUser(selectedRows.map((row) => row.id));
hide();
message.success('删除成功');
return true;
} catch (error) {
hide();
return false;
}
};
const TableList: React.FC = () => {
const [createModalOpen, handleModalOpen] = useState<boolean>(false);
const [updateModalOpen, handleUpdateModalOpen] = useState<boolean>(false);
const [showDetail, setShowDetail] = useState<boolean>(false);
const actionRef = useRef<ActionType>();
const [currentRow, setCurrentRow] = useState<API.UserItem>();
const [selectedRowsState, setSelectedRows] = useState<API.UserItem[]>([]);
const columns: ProColumns<API.UserItem>[] = [
{
title: '头像',
dataIndex: 'avatar',
hideInSearch: true,
width: 80,
align: 'center',
render: (dom, entity) => {
return (
<Image
width={80}
src={entity.avatar}
/>
);
},
},
{
title: '用户名',
dataIndex: 'username',
width: 180,
align: 'center',
render: (dom, entity) => {
return (
<a
onClick={() => {
setCurrentRow(entity);
setShowDetail(true);
}}
>
{dom}
</a>
);
},
},
{
title: '状态',
dataIndex: 'status',
width: 100,
align: 'center',
render: (dom, entity) => {
const text = entity.status === 1 ? '正常' : '禁用'
const status = entity.status === 1 ? 'green' : 'red'
return <Tag color={status}>{text}</Tag>
},
},
{
title: '创建时间',
sorter: true,
align: 'center',
dataIndex: 'createdAt',
valueType: 'dateTime',
width: 180,
},
{
title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="Operating" />,
dataIndex: 'option',
valueType: 'option',
width: 100,
render: (_, record) => [
<a
key="update"
onClick={() => {
setCurrentRow(record);
handleUpdateModalOpen(true);
}}
>
</a>
],
},
];
return (
<PageContainer header={{ title: '用户管理' }} >
<ProTable<API.UserItem, API.PageParams>
actionRef={actionRef}
rowKey="id"
search={{
labelWidth: 120,
}}
toolBarRender={() => [
<Button
type="primary"
key="primary"
onClick={() => {
handleModalOpen(true);
}}
>
<PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="New" />
</Button>,
]}
request={queryUser}
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="项" />
</div>
}
>
<Button
onClick={async () => {
await handleRemove(selectedRowsState);
setSelectedRows([]);
actionRef.current?.reloadAndRest?.();
}}
>
<FormattedMessage
id="pages.searchTable.batchDeletion"
defaultMessage="Batch deletion"
/>
</Button>
</FooterToolbar>
)}
<ModalForm
title={'新建用户'}
width="400px"
layout="horizontal"
grid
open={createModalOpen}
onOpenChange={handleModalOpen}
onFinish={async (value) => {
const success = await handleAdd(value as API.UserItem);
if (success) {
handleModalOpen(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
>
<ProFormText
rules={[
{
required: true,
message: "用户名不能为空",
},
]}
label={"用户名"}
colProps={{
span: 24,
}}
name="username"
/>
<ProFormText
rules={[
{
required: true,
message: "密码不能为空",
},
]}
label={"密码"}
colProps={{
span: 24,
}}
name="password"
/>
<ProFormText
label={"描述"}
colProps={{
span: 24,
}}
name="desc"
/>
</ModalForm>
<ModalForm
title={'更新用户'}
width="400px"
layout="horizontal"
grid
initialValues={currentRow}
open={updateModalOpen}
onOpenChange={handleUpdateModalOpen}
modalProps={{ destroyOnClose: true }}
onFinish={async (value) => {
console.log(value)
const success = await handleUpdate({ id: currentRow?.id, ...value });
if (success) {
handleUpdateModalOpen(false);
setCurrentRow(undefined);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
>
<ProFormText
rules={[
{
required: true,
message: "昵称不能为空",
},
]}
label={"昵称"}
colProps={{
span: 24,
}}
name="username"
/>
<ProFormText
label={"描述"}
colProps={{
span: 24,
}}
name="desc"
/>
<ProFormSegmented
label={"状态"}
colProps={{
span: 24,
}}
name="status"
valueEnum={statusValueEnum}
/>
</ModalForm>
<Drawer
width={600}
open={showDetail}
onClose={() => {
setCurrentRow(undefined);
setShowDetail(false);
}}
closable={false}
>
{currentRow?.username && (
<ProDescriptions<API.UserItem>
column={1}
title={currentRow?.username}
request={async () => ({
data: currentRow || {},
})}
params={{
id: currentRow?.username,
}}
columns={columns as ProDescriptionsItemProps<API.UserItem>[]}
/>
)}
</Drawer>
</PageContainer>
);
};
export default TableList;

View File

@ -0,0 +1,115 @@
import type { RequestOptions } from '@@/plugin-request/request';
import type { RequestConfig } from '@umijs/max';
import { message, notification } from 'antd';
import { getToken } from './utils/auth';
// 错误处理方案: 错误类型
enum ErrorShowType {
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3,
REDIRECT = 9,
}
// 与后端约定的响应数据格式
interface ResponseStructure {
ok: boolean;
data: any;
code?: number;
message?: string;
showType?: ErrorShowType;
}
/**
* @name
* pro
* @doc https://umijs.org/docs/max/request#配置
*/
export const errorConfig: RequestConfig = {
// 错误处理: umi@3 的错误处理方案。
errorConfig: {
// 错误抛出
errorThrower: (res) => {
const { ok, data, code, message, showType } = res as unknown as ResponseStructure;
if (!ok) {
const error: any = new Error(message);
error.name = 'BizError';
error.info = { code, message, showType, data };
throw error; // 抛出自制的错误
}
},
// 错误接收及处理
errorHandler: (error: any, opts: any) => {
if (opts?.skipErrorHandler) throw error;
// 我们的 errorThrower 抛出的错误。
if (error.name === 'BizError') {
const errorInfo: ResponseStructure | undefined = error.info;
if (errorInfo) {
const { message: msg, code } = errorInfo;
switch (errorInfo.showType) {
case ErrorShowType.SILENT:
// do nothing
break;
case ErrorShowType.WARN_MESSAGE:
message.warning(msg);
break;
case ErrorShowType.ERROR_MESSAGE:
message.error(msg);
break;
case ErrorShowType.NOTIFICATION:
notification.open({
description: msg,
message: code,
});
break;
case ErrorShowType.REDIRECT:
// TODO: redirect
break;
default:
message.error(msg);
}
}
} else if (error.response) {
// Axios 的错误
// 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
// message.error(`Response status:${error.response.status}`);
message.error(`${error.response.data.message}`);
} else if (error.request) {
// 请求已经成功发起,但没有收到响应
// \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
// 而在node.js中是 http.ClientRequest 的实例
message.error('None response! Please retry.');
} else {
// 发送请求时出了点问题
message.error('Request error, please retry.');
}
},
},
// 请求拦截器
requestInterceptors: [
(config: RequestOptions) => {
const token = getToken();
if (token) {
config.headers!.Authorization = token;
}
return config;
},
],
// 响应拦截器
responseInterceptors: [
(response) => {
// 拦截响应数据,进行个性化处理
const { data } = response as unknown as ResponseStructure;
if (data?.ok === false && errorConfig?.errorConfig?.errorThrower) {
errorConfig.errorConfig.errorThrower(data);
}
return response;
},
],
};

View File

@ -0,0 +1,65 @@
/* 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,42 @@
import { request } from '@umijs/max';
import { LoginResult } from '@server/modules/auth/auth.model';
import { IBaseResponse } from '@server/common/model/response.model';
import { LoginDto } from '@server/modules/auth/auth.dto';
/** 获取当前的用户 GET /api/currentUser */
export async function currentUser(options?: { [key: string]: any }) {
return request<{
data: API.CurrentUser;
}>('/api/account/profile', {
method: 'GET',
...(options || {}),
});
}
/** 退出登录接口 POST /api/account/logout */
export async function outLogin(options?: { [key: string]: any }) {
return request<Record<string, any>>('/api/account/logout', {
method: 'GET',
...(options || {}),
});
}
/** 登录接口 POST /api/auth/admin/login/ */
export async function login(body: LoginDto, options?: { [key: string]: any }) {
return request<IBaseResponse<LoginResult>>('/api/auth/admin/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /api/notices */
export async function getNotices(options?: { [key: string]: any }) {
return request<API.NoticeIconList>('/api/notices', {
method: 'GET',
...(options || {}),
});
}

View File

@ -0,0 +1,10 @@
// @ts-ignore
/* eslint-disable */
// API 更新时间:
// API 唯一标识:
import * as api from './api';
import * as login from './login';
export default {
api,
login,
};

View File

@ -0,0 +1,21 @@
// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';
/** 发送验证码 POST /api/auth/captcha */
export async function getFakeCaptcha(
params: {
// query
/** 手机号 */
phone?: string;
},
options?: { [key: string]: any },
) {
return request<API.FakeCaptcha>('/api/auth/captcha', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}

View File

@ -0,0 +1,45 @@
declare namespace API {
type CurrentUser = import('@server/modules/user/user').UserProfile;
type PageParams = {
current?: number;
pageSize?: number;
};
type FakeCaptcha = {
code?: number;
status?: 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,56 @@
import { request } from '@umijs/max';
import { IBaseResponse } from '@server/common/model/response.model'
import { TodoDto, TodoUpdateDto } from '@server/modules/todo/todo.dto';
/** 获取待办事项列表 GET /api/todos */
export async function queryTodo(
params: {
// query
/** 当前的页码 */
current?: number;
/** 页面的容量 */
pageSize?: number;
},
options?: { [key: string]: any },
) {
const result = await request<IBaseResponse<API.TodoList>>('/api/todos/page', {
method: 'GET',
params: {
...params,
page: params.current,
limit: params.pageSize
},
...(options || {}),
});
return {
success: result.ok,
data: result.data?.items,
total: result.data?.meta.totalCount,
}
}
/** 新建待办事项 POST /api/todos */
export async function addTodo(data: TodoDto) {
return request<IBaseResponse<API.TodoItem>>(`/api/todos`, {
method: 'POST',
data,
});
}
/** 更新待办事项 PUT /api/todos */
export async function updateTodo(id: string, data: TodoUpdateDto) {
return request<IBaseResponse<API.TodoItem>>(`/api/todos/${id}`, {
method: 'PUT',
data,
});
}
/** 删除待办事项 DELETE /api/todos */
export async function removeTodo(ids: string[]) {
return request('/api/todos', {
method: 'DELETE',
data: { ids },
});
}

View File

@ -0,0 +1,6 @@
declare namespace API {
type TodoList = import('@server/modules/todo/todo').TodoList
type TodoItem = import('@server/modules/todo/todo').TodoItem
}

View File

@ -0,0 +1,49 @@
import { request } from '@umijs/max';
import { IBaseResponse } from '@server/common/model/response.model'
import { UserDto, UserUpdateDto } from '@server/modules/user/dto/user.dto';
export async function queryUser(
params: {
current?: number;
pageSize?: number;
},
options?: { [key: string]: any },
) {
const result = await request<IBaseResponse<API.UserList>>('/api/users/page', {
method: 'GET',
params: {
...params,
page: params.current,
limit: params.pageSize
},
...(options || {}),
});
return {
success: result.ok,
data: result.data?.items,
total: result.data?.meta.totalCount,
}
}
export async function addUser(data: UserDto) {
return request<IBaseResponse<API.UserItem>>(`/api/users`, {
method: 'POST',
data,
});
}
export async function updateUser(id: string, data: UserUpdateDto) {
return request<IBaseResponse<API.UserItem>>(`/api/users/${id}`, {
method: 'PUT',
data,
});
}
export async function removeUser(ids: string[]) {
return request('/api/users', {
method: 'DELETE',
data: { ids },
});
}

View File

@ -0,0 +1,6 @@
declare namespace API {
type UserList = import('@server/modules/user/user').UserList
type UserItem = import('@server/modules/user/user').UserItem
}

21
apps/admin/src/typings.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
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';
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

View File

@ -0,0 +1,21 @@
import Cookies from 'js-cookie';
export const TOKEN_KEY = 'TOKEN__';
export function getToken() {
const token = Cookies.get(TOKEN_KEY);
return token ? `Bearer ${token}` : '';
}
export function setToken(token: string) {
if (typeof token !== 'string') {
return;
}
return Cookies.set(TOKEN_KEY, token, {
expires: 14,
});
}
export function removeToken() {
return Cookies.remove(TOKEN_KEY);
}

View File

@ -0,0 +1,64 @@
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
global.localStorage = localStorageMock;
Object.defineProperty(URL, 'createObjectURL', {
writable: true,
value: jest.fn(),
});
class Worker {
constructor(stringUrl) {
this.url = stringUrl;
this.onmessage = () => {};
}
postMessage(msg) {
this.onmessage(msg);
}
}
window.Worker = Worker;
/* eslint-disable global-require */
if (typeof window !== 'undefined') {
// ref: https://github.com/ant-design/ant-design/issues/18774
if (!window.matchMedia) {
Object.defineProperty(global.window, 'matchMedia', {
writable: true,
configurable: true,
value: jest.fn(() => ({
matches: false,
addListener: jest.fn(),
removeListener: jest.fn(),
})),
});
}
if (!window.matchMedia) {
Object.defineProperty(global.window, 'matchMedia', {
writable: true,
configurable: true,
value: jest.fn((query) => ({
matches: query.includes('max-width'),
addListener: jest.fn(),
removeListener: jest.fn(),
})),
});
}
}
const errorLog = console.error;
Object.defineProperty(global.window.console, 'error', {
writable: true,
configurable: true,
value: (...rest) => {
const logStr = rest.join('');
if (logStr.includes('Warning: An update to %s inside a test was not wrapped in act(...)')) {
return;
}
errorLog(...rest);
},
});

30
apps/admin/tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"removeComments": true,
"noImplicitAny": false,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"paths": {
"@/*": ["./src/*"],
"@@/*": ["./src/.umi/*"],
"@server/*": ["../server/src/*"],
"@model": ["../../packages/database/client/index.d.ts"]
}
}
}

1
apps/admin/types/cache/cache.json vendored Normal file
View File

@ -0,0 +1 @@
{}

386
apps/admin/types/cache/login.cache.json vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,324 @@
module.exports = {
'GET /api/currentUser': {
data: {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服某某某事业群某某平台部某某技术部UED',
tags: [
{ key: '0', label: '很有想法的' },
{ key: '1', label: '专注设计' },
{ key: '2', label: '辣~' },
{ key: '3', label: '大长腿' },
{ key: '4', label: '川妹子' },
{ key: '5', label: '海纳百川' },
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: { label: '浙江省', key: '330000' },
city: { label: '杭州市', key: '330100' },
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
},
'GET /api/rule': {
data: [
{
key: 99,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 99',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 503,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 98,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 98',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 164,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 97,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 97',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 174,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 81,
},
{
key: 96,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 96',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 914,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 7,
},
{
key: 95,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 95',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 698,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 94,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 94',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 488,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 14,
},
{
key: 93,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 93',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 580,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 77,
},
{
key: 92,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 92',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 244,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 58,
},
{
key: 91,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 91',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 959,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 66,
},
{
key: 90,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 90',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 958,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 72,
},
{
key: 89,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 89',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 301,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 2,
},
{
key: 88,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 88',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 277,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 87,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 87',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 810,
status: '1',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 82,
},
{
key: 86,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 86',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 780,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 22,
},
{
key: 85,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 85',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 705,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 12,
},
{
key: 84,
disabled: true,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 84',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 203,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 79,
},
{
key: 83,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 83',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 491,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 59,
},
{
key: 82,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 82',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 73,
status: '0',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 100,
},
{
key: 81,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
name: 'TradeCode 81',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 406,
status: '3',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 61,
},
{
key: 80,
disabled: false,
href: 'https://ant.design',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
name: 'TradeCode 80',
owner: '曲丽丽',
desc: '这是一段描述',
callNo: 112,
status: '2',
updatedAt: '2022-12-06T05:00:57.040Z',
createdAt: '2022-12-06T05:00:57.040Z',
progress: 20,
},
],
total: 100,
success: true,
pageSize: 20,
current: 1,
},
'POST /api/login/outLogin': { data: {}, success: true },
'POST /api/login/account': {
status: 'ok',
type: 'account',
currentAuthority: 'admin',
},
};

View File

120
apps/admin/types/index.d.ts vendored Normal file

File diff suppressed because one or more lines are too long

50
apps/server/.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# compiled output
dist
node_modules
out
documentation
# testing
coverage
# OS
.DS_Store
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# local env files
.env
# log files
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
tmp
public/static/
# turbo
.turbo
# prisma
prisma/client
prisma/schemas
prisma/zod
prisma/dist

View File

21
apps/server/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/tsconfig.json",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}

41
apps/server/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,41 @@
{
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}

View File

@ -0,0 +1,22 @@
const { cpus } = require('node:os')
const cpuLen = cpus().length
module.exports = {
apps: [
{
name: 'm-shop',
script: './dist/main.js',
autorestart: true,
exec_mode: 'cluster',
watch: false,
instances: cpuLen,
max_memory_restart: '520M',
args: '',
env: {
NODE_ENV: 'production',
PORT: process.env.APP_PORT,
},
},
],
}

View File

@ -0,0 +1,38 @@
const antfu = require('@antfu/eslint-config').default
module.exports = antfu({
stylistic: {
indent: 2,
quotes: 'single',
},
typescript: true,
}, {
rules: {
'no-console': 'off',
'unused-imports/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 2,
'ts/consistent-type-imports': 'off',
'ts/prefer-ts-expect-error': 'off',
'node/prefer-global/process': 'off',
'node/prefer-global/buffer': 'off',
'import/order': [
2,
{
'pathGroups': [
{
pattern: '~/**',
group: 'external',
position: 'after',
},
],
'alphabetize': { order: 'asc', caseInsensitive: false },
'newlines-between': 'always-and-inside-groups',
'warnOnUnassignedImports': true,
},
],
'eslint-comments/no-unlimited-disable': 'off',
},
})

View File

@ -0,0 +1,9 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"builder": "swc",
"typeCheck": true,
"plugins": ["@nestjs/swagger"]
}
}

159
apps/server/package.json Normal file
View File

@ -0,0 +1,159 @@
{
"name": "server",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@9.0.5",
"license": "MIT",
"exports": {
"./*": {
"types": "./src/*"
}
},
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build --webpack",
"dev": "npm run start",
"dev:debug": "npm run start:debug",
"repl": "npm run start -- --entryFile repl",
"bundle": "rimraf out && npm run build && ncc build dist/main.js -o out -m -t && chmod +x out/index.js",
"start": "cross-env NODE_ENV=development nest start -w --path tsconfig.json",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
"prod": "cross-env NODE_ENV=production pm2-runtime start ecosystem.config.js",
"prod:pm2": "cross-env NODE_ENV=production pm2 restart ecosystem.config.js",
"prod:stop": "pm2 stop ecosystem.config.js",
"prod:debug": "cross-env NODE_ENV=production nest start --debug --watch",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "dotenv -e ../../.env.test vitest",
"doc": "compodoc -p tsconfig.json -s"
},
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/prisma": "^1.4.1",
"@fastify/cookie": "^9.1.0",
"@fastify/multipart": "^8.0.0",
"@fastify/static": "^6.12.0",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/axios": "^3.0.1",
"@nestjs/bull": "^10.0.1",
"@nestjs/cache-manager": "^2.1.1",
"@nestjs/common": "^10.3.1",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.1",
"@nestjs/event-emitter": "^2.0.3",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-fastify": "^10.3.1",
"@nestjs/platform-socket.io": "^10.3.1",
"@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.15",
"@nestjs/terminus": "^10.1.1",
"@nestjs/throttler": "^5.0.1",
"@nestjs/websockets": "^10.3.1",
"@socket.io/redis-adapter": "^8.2.1",
"@socket.io/redis-emitter": "^5.1.0",
"@trpc/server": "10.45.1",
"@types/lodash": "^4.14.201",
"database": "workspace:*",
"axios": "^1.6.1",
"bcrypt": "^5.1.1",
"bull": "^4.11.4",
"cache-manager": "^5.2.4",
"cache-manager-ioredis-yet": "^1.2.2",
"chalk": "^5.3.0",
"cron": "^3.1.6",
"cron-parser": "^4.9.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"dotenv": "16.3.1",
"dotenv-expand": "^10.0.0",
"fastify": "^4.24.3",
"fastify-multer": "^2.0.3",
"handlebars": "^4.7.8",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"lodash": "^4.17.21",
"mysql": "^2.18.1",
"nanoid": "^3.3.6",
"nestjs-prisma": "^0.22.0",
"nestjs-zod": "3.0.0",
"nodemailer": "^6.9.7",
"passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pluralize": "^8.0.0",
"prisma-extension-pagination": "^0.6.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"snakecase-keys": "^5.5.0",
"socket.io": "^4.7.2",
"stacktrace-js": "^2.0.2",
"svg-captcha": "^1.4.0",
"systeminformation": "^5.21.16",
"trpc-playground": "^1.0.4",
"ua-parser-js": "^1.0.37",
"unplugin-swc": "^1.4.4",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
"zod": "3.22.4",
"zx-cjs": "7.0.7-0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.1.0",
"@compodoc/compodoc": "^1.1.22",
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.3.1",
"@swc/cli": "^0.1.63",
"@swc/core": "^1.3.102",
"@types/cache-manager": "^4.0.5",
"@types/jest": "29.5.8",
"@types/multer": "^1.4.10",
"@types/node": "^20.9.0",
"@types/pluralize": "^0.0.33",
"@types/ua-parser-js": "^0.7.39",
"cross-env": "^7.0.3",
"dotenv-cli": "^7.3.0",
"eslint": "^8.53.0",
"lint-staged": "^15.1.0",
"simple-git-hooks": "^2.9.0",
"source-map-support": "^0.5.21",
"ts-jest": "^29.1.1",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.0",
"vite": "^5.0.11",
"vite-tsconfig-paths": "^4.2.3",
"vitest": "^1.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/$1"
},
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
}
}

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