feat: 初始化项目
This commit is contained in:
parent
d08c850f29
commit
b9898ba85c
|
|
@ -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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/lambda/
|
||||
/scripts
|
||||
/config
|
||||
.history
|
||||
public
|
||||
dist
|
||||
.umi
|
||||
mock
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
module.exports = {
|
||||
extends: [require.resolve('@umijs/fabric/dist/eslint')],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
'no-underscore-dangle': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 0,
|
||||
'@typescript-eslint/no-shadow': 1,
|
||||
'no-param-reassign': 1,
|
||||
'array-callback-return': 1,
|
||||
'no-plusplus': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 1,
|
||||
'@typescript-eslint/consistent-type-imports': 0,
|
||||
'for-direction': 0,
|
||||
'@typescript-eslint/no-unused-expressions': 0,
|
||||
},
|
||||
globals: {
|
||||
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
|
||||
page: true,
|
||||
REACT_APP_ENV: true,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
**/node_modules
|
||||
# roadhog-api-doc ignore
|
||||
/src/utils/request-temp.js
|
||||
_roadhog-api-doc
|
||||
|
||||
# production
|
||||
/dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
|
||||
/coverage
|
||||
.idea
|
||||
# yarn.lock
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
*bak
|
||||
|
||||
|
||||
# visual studio code
|
||||
.history
|
||||
*.log
|
||||
functions/*
|
||||
.temp/**
|
||||
|
||||
# umi
|
||||
.umi
|
||||
.umi-production
|
||||
|
||||
# screenshot
|
||||
screenshot
|
||||
.firebase
|
||||
.eslintcache
|
||||
|
||||
build
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
**/*.svg
|
||||
package.json
|
||||
.umi
|
||||
.umi-production
|
||||
/dist
|
||||
.dockerignore
|
||||
.DS_Store
|
||||
.eslintignore
|
||||
*.png
|
||||
*.toml
|
||||
docker
|
||||
.editorconfig
|
||||
Dockerfile*
|
||||
.gitignore
|
||||
.prettierignore
|
||||
LICENSE
|
||||
.eslintcache
|
||||
*.lock
|
||||
yarn-error.log
|
||||
.history
|
||||
CNAME
|
||||
/build
|
||||
/public
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const fabric = require('@umijs/fabric');
|
||||
|
||||
module.exports = {
|
||||
...fabric.prettier,
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const fabric = require('@umijs/fabric');
|
||||
|
||||
module.exports = {
|
||||
...fabric.stylelint,
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"wangzy.sneak-mark"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"prettier.requireConfig": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
57
README.md
57
README.md
|
|
@ -1,2 +1,57 @@
|
|||
# frontend-template
|
||||
# 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).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
const shell = require('child_process').execSync;
|
||||
const outputRoot = process.env.npm_config_outputRoot || './dist';
|
||||
// const apptype = process.env.npm_config_apptype
|
||||
const branch = 'master'; //process.env.npm_config_branch
|
||||
// const env = process.env.npm_config_env
|
||||
// if (!env) {
|
||||
// console.error('请配置项目环境env,如:npm config set env=test')
|
||||
// return
|
||||
// }
|
||||
|
||||
// if (!branch) {
|
||||
// console.error(
|
||||
// '请配置发布目录的分支名称branch,如:npm config set branch=BRANCH_V1.0_FEATURE_20201106_HUISHAN'
|
||||
// )
|
||||
// return
|
||||
// }
|
||||
if (!outputRoot) {
|
||||
console.log(
|
||||
`未配置h5导出目录outputRoot,会使用默认导出相对路径,如:npm config set outputRoot=${outputRoot}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
// 更新分支
|
||||
// console.log('---1 更新项目代码 git pull start---')
|
||||
// shell(`git pull`)
|
||||
// console.log('---git pull success---')
|
||||
// console.log('---2 更新依赖:npm i')
|
||||
// console.log(shell(`npm i`).toString())
|
||||
// console.log(`---3 切分支 git checkout -q ${branch}`)
|
||||
shell(`cd ${outputRoot} && git checkout -q ${branch}`);
|
||||
// console.log(`---4 更新发布目录代码 git pull ${outputRoot}---`)
|
||||
shell(`cd ${outputRoot}&&git pull`);
|
||||
console.log(`---git pull success ${outputRoot}---`);
|
||||
console.log(`--- 开始编译`);
|
||||
console.log(shell(`npm run build`).toString());
|
||||
console.log('---npm run build success---');
|
||||
console.log(`---6 提交代码---`);
|
||||
shell(`cd ${outputRoot} && git add .`);
|
||||
shell(`cd ${outputRoot} && git commit -m "feat: 编译发布 ${new Date()}"`);
|
||||
shell(`cd ${outputRoot} && git push origin ${branch}:${branch}`);
|
||||
// console.log(`---7 提交代码成功!---`)
|
||||
// console.log(
|
||||
// `---发布正式测试环境请点击:https://jenkins-dev.yzone01.com/jenkins/job/saas-deploy-dtest-pageframework/build?delay=0sec`
|
||||
// )
|
||||
// console.log(
|
||||
// `---发布备用测试环境请点击:http://172.20.208.10:29010/jenkins/job/saas-dtest-deploy/build?delay=0sec`
|
||||
// )
|
||||
// console.log(
|
||||
// `---发布远程测试环境请点击:https://jenkins-dev.yzone01.com/jenkins/job/saas-deploy-devweb/build?delay=0sec`
|
||||
// )
|
||||
} catch (e) {
|
||||
console.log('build error ---', e);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// https://umijs.org/config/
|
||||
import { defineConfig } from 'umi';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
// https://github.com/zthxxx/react-dev-inspector
|
||||
'react-dev-inspector/plugins/umi/react-inspector',
|
||||
],
|
||||
// https://github.com/zthxxx/react-dev-inspector#inspector-loader-props
|
||||
inspectorConfig: {
|
||||
exclude: [],
|
||||
babelPlugins: [],
|
||||
babelOptions: {},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// https://umijs.org/config/
|
||||
import { defineConfig } from 'umi';
|
||||
import { join } from 'path';
|
||||
|
||||
import defaultSettings from './defaultSettings';
|
||||
import theme from './theme';
|
||||
import proxy from './proxy';
|
||||
import routes from '../src/routes';
|
||||
|
||||
const { REACT_APP_ENV } = process.env;
|
||||
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
antd: {},
|
||||
dva: {
|
||||
hmr: true,
|
||||
},
|
||||
devServer: {
|
||||
port: 8003,
|
||||
},
|
||||
history: {
|
||||
type: 'hash',
|
||||
},
|
||||
layout: {
|
||||
// https://umijs.org/zh-CN/plugins/plugin-layout
|
||||
// locale: true,
|
||||
siderWidth: 208,
|
||||
...defaultSettings,
|
||||
},
|
||||
// https://umijs.org/zh-CN/plugins/plugin-locale
|
||||
locale: {
|
||||
// default zh-CN
|
||||
default: 'zh-CN',
|
||||
antd: true,
|
||||
// default true, when it is true, will use `navigator.language` overwrite default
|
||||
baseNavigator: true,
|
||||
},
|
||||
dynamicImport: {
|
||||
loading: '@ant-design/pro-layout/es/PageLoading',
|
||||
},
|
||||
targets: {
|
||||
ie: 11,
|
||||
},
|
||||
// umi routes: https://umijs.org/docs/routing
|
||||
routes,
|
||||
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
|
||||
theme: {
|
||||
...theme,
|
||||
'root-entry-name': 'default',
|
||||
},
|
||||
// esbuild is father build tools
|
||||
// https://umijs.org/plugins/plugin-esbuild
|
||||
esbuild: {},
|
||||
title: false,
|
||||
ignoreMomentLocale: true,
|
||||
proxy: proxy[REACT_APP_ENV || 'dev'],
|
||||
manifest: {
|
||||
basePath: '/',
|
||||
},
|
||||
|
||||
extraBabelPlugins: [
|
||||
['babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'],
|
||||
[
|
||||
'babel-plugin-import',
|
||||
{ libraryName: '@formily/antd', libraryDirectory: 'esm', style: true },
|
||||
'@formily/antd',
|
||||
],
|
||||
],
|
||||
cssLoader: {
|
||||
localsConvention: 'camelCase',
|
||||
},
|
||||
outputPath: './dist',
|
||||
nodeModulesTransform: { type: 'none' },
|
||||
mfsu: {},
|
||||
webpack5: {},
|
||||
exportStatic: {},
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Settings as LayoutSettings } from '@ant-design/pro-layout';
|
||||
|
||||
const Settings: LayoutSettings & {
|
||||
pwa?: boolean;
|
||||
logo?: string;
|
||||
} = {
|
||||
navTheme: 'light',
|
||||
// 拂晓蓝
|
||||
primaryColor: '#1890ff',
|
||||
layout: 'mix',
|
||||
contentWidth: 'Fluid',
|
||||
fixedHeader: false,
|
||||
fixSiderbar: true,
|
||||
colorWeak: false,
|
||||
title: 'Pro',
|
||||
pwa: false,
|
||||
logo: '/pro_icon.svg',
|
||||
iconfontUrl: '',
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
|
||||
* -------------------------------
|
||||
* The agent cannot take effect in the production environment
|
||||
* so there is no configuration of the production environment
|
||||
* For details, please see
|
||||
* https://pro.ant.design/docs/deploy
|
||||
*/
|
||||
export default {
|
||||
dev: {
|
||||
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
|
||||
'/admin/': {
|
||||
// 要代理的地址
|
||||
target: 'http://106.55.157.116:8086',
|
||||
// 配置了这个可以从 http 代理到 https
|
||||
// 依赖 origin 的功能可能需要这个,比如 cookie
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
export default {
|
||||
'primary-color': '#04A17E',
|
||||
// 'secondary-color': '#87dba2',
|
||||
// 'primary-1': 'rgba(@primary-color,0.2)',
|
||||
'success-color': '#04A17E',
|
||||
'error-color': '#FD6353',
|
||||
'warning-color': '#F8991D',
|
||||
'disabled-color': 'rgba(#000,0.25)',
|
||||
'text-color': '#334458',
|
||||
// 'text-color-secondary': 'rgba(@text-color,0.6)',
|
||||
// 'text-color-third': 'rgba(@text-color,0.8)',
|
||||
// 'border-color-base': '#D5DEE7',
|
||||
// 'border-color-split': '#ECF1F5',
|
||||
// 'background-color-base': '#F8FAFB',
|
||||
// 'background-color-dark': '#FAFBFC',
|
||||
// 'steps-icon-size': '28px',
|
||||
// 'steps-title-line-height': '@steps-icon-size',
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"name": "ant-design-pro",
|
||||
"version": "5.0.0",
|
||||
"private": true,
|
||||
"description": "An out-of-box UI solution for enterprise applications",
|
||||
"scripts": {
|
||||
"analyze": "cross-env ANALYZE=1 umi build",
|
||||
"build": "umi build",
|
||||
"b": "node build",
|
||||
"deploy": "npm run build && npm run gh-pages",
|
||||
"dev": "npm run start:dev",
|
||||
"gh-pages": "gh-pages -d dist",
|
||||
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
|
||||
"postinstall": "umi g tmp",
|
||||
"lint": "umi g tmp && npm run lint:js && npm run lint:style && npm run lint:prettier && 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 && npm run lint:style",
|
||||
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
|
||||
"lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto",
|
||||
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
|
||||
"openapi": "umi openapi",
|
||||
"precommit": "lint-staged",
|
||||
"prettier": "prettier -c --write \"src/**/*\"",
|
||||
"start": "cross-env UMI_ENV=dev umi dev",
|
||||
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev umi dev",
|
||||
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev umi dev",
|
||||
"start:no-ui": "cross-env UMI_UI=none UMI_ENV=dev umi dev",
|
||||
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev umi dev",
|
||||
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev umi dev",
|
||||
"pretest": "node ./tests/beforeTest",
|
||||
"test": "umi test",
|
||||
"test:all": "node ./tests/run-tests.js",
|
||||
"test:component": "umi test ./src/components",
|
||||
"serve": "umi-serve",
|
||||
"tsc": "tsc --noEmit"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.less": "stylelint --syntax less",
|
||||
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
|
||||
"**/*.{js,jsx,tsx,ts,less,md,json}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 10"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.5.0",
|
||||
"@ant-design/pro-descriptions": "^1.9.0",
|
||||
"@ant-design/pro-form": "^1.43.0",
|
||||
"@ant-design/pro-layout": "^6.26.0",
|
||||
"@ant-design/pro-table": "^2.56.0",
|
||||
"@formily/antd": "^2.0.7",
|
||||
"@formily/core": "^2.0.7",
|
||||
"@formily/react": "^2.0.7",
|
||||
"@umijs/route-utils": "^2.0.3",
|
||||
"ahooks": "^2.10.14",
|
||||
"antd": "^4.17.2",
|
||||
"axios": "^0.24.0",
|
||||
"braft-editor": "^2.3.9",
|
||||
"classnames": "^2.2.6",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.25.3",
|
||||
"omit.js": "^2.0.2",
|
||||
"rc-menu": "^9.0.13",
|
||||
"rc-util": "^5.14.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dev-inspector": "^1.1.1",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-helmet-async": "^1.0.4",
|
||||
"umi": "^3.5.0",
|
||||
"umi-serve": "^1.9.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/pro-cli": "^2.0.2",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/history": "^4.7.2",
|
||||
"@types/jest": "^26.0.0",
|
||||
"@types/lodash": "^4.14.144",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
"@umijs/fabric": "^2.6.2",
|
||||
"@umijs/openapi": "^1.3.0",
|
||||
"@umijs/plugin-blocks": "^2.0.5",
|
||||
"@umijs/plugin-esbuild": "^1.0.1",
|
||||
"@umijs/plugin-openapi": "^1.2.0",
|
||||
"@umijs/preset-ant-design-pro": "^1.2.0",
|
||||
"@umijs/preset-dumi": "^1.1.7",
|
||||
"@umijs/preset-react": "^1.8.17",
|
||||
"@umijs/yorkie": "^2.0.3",
|
||||
"babel-plugin-import": "^1.13.3",
|
||||
"carlo": "^0.9.46",
|
||||
"cross-env": "^7.0.0",
|
||||
"cross-port-killer": "^1.1.1",
|
||||
"detect-installer": "^1.0.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"eslint": "^7.1.0",
|
||||
"express": "^4.17.1",
|
||||
"gh-pages": "^3.0.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"lint-staged": "^10.0.0",
|
||||
"mockjs": "^1.0.1-beta3",
|
||||
"prettier": "^2.3.2",
|
||||
"puppeteer-core": "^8.0.0",
|
||||
"stylelint": "^13.0.0",
|
||||
"swagger-ui-react": "^3.52.3",
|
||||
"typescript": "^4.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* @see https://umijs.org/zh-CN/plugins/plugin-access
|
||||
* */
|
||||
export default function access(initialState: { currentUser?: API.CurrentUser | undefined }) {
|
||||
const { currentUser } = initialState || {};
|
||||
return {
|
||||
canAdmin: currentUser && currentUser.access === 'admin',
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import type { RunTimeLayoutConfig } from 'umi';
|
||||
import { history } from 'umi';
|
||||
import RightContent from '@/components/RightContent';
|
||||
import RoutePath from '@/routes/routePath';
|
||||
import { CACHE_TOKEN } from '@/constants/cacheKey';
|
||||
|
||||
// ProLayout 支持的api https://procomponents.ant.design/components/layout
|
||||
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
||||
return {
|
||||
rightContentRender: () => <RightContent />,
|
||||
disableContentMargin: false,
|
||||
onPageChange: () => {
|
||||
if (!localStorage.getItem(CACHE_TOKEN)) {
|
||||
history.push(RoutePath.LOGIN);
|
||||
}
|
||||
},
|
||||
|
||||
menuHeaderRender: undefined,
|
||||
// 自定义 403 页面
|
||||
// unAccessible: <div>unAccessible</div>,
|
||||
// 增加一个 loading 的状态
|
||||
childrenRender: (children) => {
|
||||
const pathname = location.hash.replace('#', '');
|
||||
return pathname === RoutePath.LOGIN ? children : <PageContainer>{children}</PageContainer>;
|
||||
},
|
||||
...initialState?.settings,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import type { MenuTheme } from 'antd/lib/menu/MenuContext';
|
||||
|
||||
export type ContentWidth = 'Fluid' | 'Fixed';
|
||||
|
||||
export type RenderSetting = {
|
||||
headerRender?: false;
|
||||
footerRender?: false;
|
||||
menuRender?: false;
|
||||
menuHeaderRender?: false;
|
||||
};
|
||||
export type PureSettings = {
|
||||
/** @name theme for nav menu */
|
||||
navTheme?: MenuTheme | 'realDark' | undefined;
|
||||
|
||||
/** @name 顶部菜单的颜色,mix 模式下生效 */
|
||||
headerTheme?: MenuTheme;
|
||||
/** @name nav menu position: `side` or `top` */
|
||||
headerHeight?: number;
|
||||
/** @name customize header height */
|
||||
layout?: 'side' | 'top' | 'mix';
|
||||
/** @name layout of content: `Fluid` or `Fixed`, only works when layout is top */
|
||||
contentWidth?: ContentWidth;
|
||||
/** @name sticky header */
|
||||
fixedHeader?: boolean;
|
||||
/** @name sticky siderbar */
|
||||
fixSiderbar?: boolean;
|
||||
/** @name menu 相关的一些配置 */
|
||||
menu?: {
|
||||
locale?: boolean;
|
||||
defaultOpenAll?: boolean;
|
||||
loading?: boolean;
|
||||
type?: 'sub' | 'group';
|
||||
autoClose?: false;
|
||||
};
|
||||
/**
|
||||
* 设置为 false,在 layout 中只展示 pageName,而不是 pageName - title
|
||||
*
|
||||
* @name Layout 的 title,也会显示在浏览器标签上
|
||||
*/
|
||||
title: string | false;
|
||||
/**
|
||||
* Your custom iconfont Symbol script Url eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
|
||||
* 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理 Usage: https://github.com/ant-design/ant-design-pro/pull/3517
|
||||
*/
|
||||
iconfontUrl?: string;
|
||||
/** @name 主色,需要配合 umi 使用 */
|
||||
primaryColor?: string;
|
||||
/** @name 全局增加滤镜 */
|
||||
colorWeak?: boolean;
|
||||
/**
|
||||
* 只在 mix 模式下生效
|
||||
*
|
||||
* @name 切割菜单
|
||||
*/
|
||||
splitMenus?: boolean;
|
||||
};
|
||||
|
||||
export type ProSettings = PureSettings & RenderSetting;
|
||||
|
||||
const defaultSettings: ProSettings = {
|
||||
navTheme: 'dark',
|
||||
layout: 'side',
|
||||
contentWidth: 'Fluid',
|
||||
fixedHeader: false,
|
||||
fixSiderbar: false,
|
||||
menu: {
|
||||
locale: true,
|
||||
},
|
||||
headerHeight: 48,
|
||||
title: 'pro',
|
||||
iconfontUrl: '',
|
||||
primaryColor: 'daybreak',
|
||||
splitMenus: false,
|
||||
};
|
||||
export default defaultSettings;
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import type H from 'history';
|
||||
import type { BreadcrumbProps as AntdBreadcrumbProps } from 'antd';
|
||||
import React from 'react';
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import { isBrowser } from '@ant-design/pro-utils';
|
||||
|
||||
import type { ProSettings } from './defaultSettings';
|
||||
import type { MenuDataItem, MessageDescriptor, WithFalse } from './typings';
|
||||
import { urlToList } from './pathTools';
|
||||
|
||||
export type BreadcrumbProps = {
|
||||
breadcrumbList?: { title: string; href: string }[];
|
||||
home?: string;
|
||||
location?:
|
||||
| H.Location
|
||||
| {
|
||||
pathname?: string;
|
||||
};
|
||||
menu?: ProSettings['menu'];
|
||||
breadcrumbMap?: Map<string, MenuDataItem>;
|
||||
formatMessage?: (message: MessageDescriptor) => string;
|
||||
breadcrumbRender?: WithFalse<
|
||||
(routers: AntdBreadcrumbProps['routes']) => AntdBreadcrumbProps['routes']
|
||||
>;
|
||||
itemRender?: AntdBreadcrumbProps['itemRender'];
|
||||
};
|
||||
|
||||
// 渲染Breadcrumb 子节点
|
||||
// Render the Breadcrumb child node
|
||||
const defaultItemRender: AntdBreadcrumbProps['itemRender'] = ({ breadcrumbName, path }) => (
|
||||
<a href={path}>{breadcrumbName}</a>
|
||||
);
|
||||
|
||||
const renderItemLocal = (item: MenuDataItem, props: BreadcrumbProps): string => {
|
||||
const { formatMessage, menu } = props;
|
||||
if (item.locale && formatMessage && menu?.locale !== false) {
|
||||
return formatMessage({ id: item.locale, defaultMessage: item.name });
|
||||
}
|
||||
return item.name as string;
|
||||
};
|
||||
|
||||
export const getBreadcrumb = (
|
||||
breadcrumbMap: Map<string, MenuDataItem>,
|
||||
url: string,
|
||||
): MenuDataItem => {
|
||||
let breadcrumbItem = breadcrumbMap.get(url);
|
||||
if (!breadcrumbItem) {
|
||||
// Find the first matching path in the order defined by route config
|
||||
// 按照 route config 定义的顺序找到第一个匹配的路径
|
||||
const targetPath = [...breadcrumbMap.keys()].find((path) =>
|
||||
// remove ? ,不然会重复
|
||||
pathToRegexp(path.replace('?', '')).test(url),
|
||||
);
|
||||
if (targetPath) breadcrumbItem = breadcrumbMap.get(targetPath);
|
||||
}
|
||||
return breadcrumbItem || { path: '' };
|
||||
};
|
||||
|
||||
export const getBreadcrumbFromProps = (
|
||||
props: BreadcrumbProps,
|
||||
): {
|
||||
location: BreadcrumbProps['location'];
|
||||
breadcrumbMap: BreadcrumbProps['breadcrumbMap'];
|
||||
} => {
|
||||
const { location, breadcrumbMap } = props;
|
||||
return {
|
||||
location,
|
||||
breadcrumbMap,
|
||||
};
|
||||
};
|
||||
|
||||
export const conversionFromLocation = (
|
||||
routerLocation: BreadcrumbProps['location'],
|
||||
breadcrumbMap: Map<string, MenuDataItem>,
|
||||
props: BreadcrumbProps,
|
||||
): AntdBreadcrumbProps['routes'] => {
|
||||
// Convertor the url to an array
|
||||
const pathSnippets = urlToList(routerLocation?.pathname);
|
||||
// Loop data mosaic routing
|
||||
const extraBreadcrumbItems: AntdBreadcrumbProps['routes'] = pathSnippets
|
||||
.map((url) => {
|
||||
// For application that has configured router base
|
||||
// @ts-ignore
|
||||
const { routerBase = '/' } = isBrowser() ? window : {};
|
||||
const realPath = routerBase === '/' ? url : `${routerBase}${url}`;
|
||||
const currentBreadcrumb = getBreadcrumb(breadcrumbMap, url);
|
||||
const name = renderItemLocal(currentBreadcrumb, props);
|
||||
const { hideInBreadcrumb } = currentBreadcrumb;
|
||||
return name && !hideInBreadcrumb
|
||||
? {
|
||||
path: realPath,
|
||||
breadcrumbName: name,
|
||||
component: currentBreadcrumb.component,
|
||||
meta: currentBreadcrumb.meta || {},
|
||||
}
|
||||
: { path: '', breadcrumbName: '' };
|
||||
})
|
||||
.filter((item) => item && item.path);
|
||||
|
||||
return extraBreadcrumbItems;
|
||||
};
|
||||
|
||||
export type BreadcrumbListReturn = Pick<
|
||||
AntdBreadcrumbProps,
|
||||
Extract<keyof AntdBreadcrumbProps, 'routes' | 'itemRender'>
|
||||
>;
|
||||
|
||||
/** 将参数转化为面包屑 Convert parameters into breadcrumbs */
|
||||
export const genBreadcrumbProps = (props: BreadcrumbProps): AntdBreadcrumbProps['routes'] => {
|
||||
const { location, breadcrumbMap } = getBreadcrumbFromProps(props);
|
||||
|
||||
// 根据 location 生成 面包屑
|
||||
// Generate breadcrumbs based on location
|
||||
if (location && location.pathname && breadcrumbMap) {
|
||||
return conversionFromLocation(location, breadcrumbMap, props);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// use breadcrumbRender to change routes
|
||||
export const getBreadcrumbProps = (props: BreadcrumbProps): BreadcrumbListReturn => {
|
||||
const { breadcrumbRender, itemRender: propsItemRender } = props;
|
||||
const routesArray = genBreadcrumbProps(props);
|
||||
const itemRender = propsItemRender || defaultItemRender;
|
||||
let routes = routesArray;
|
||||
// if routes.length =1, don't show it
|
||||
if (breadcrumbRender) {
|
||||
routes = breadcrumbRender(routes) || [];
|
||||
}
|
||||
if ((routes && routes.length < 1) || breadcrumbRender === false) {
|
||||
routes = undefined;
|
||||
}
|
||||
return {
|
||||
routes,
|
||||
itemRender,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { transformRoute } from '@umijs/route-utils';
|
||||
|
||||
import type { MenuDataItem, Route, MessageDescriptor } from './typings';
|
||||
|
||||
function fromEntries(iterable: any) {
|
||||
return [...iterable].reduce((obj: Record<string, MenuDataItem>, [key, val]) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[key] = val;
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export default (
|
||||
routes: Route[],
|
||||
menu?: { locale?: boolean },
|
||||
formatMessage?: (message: MessageDescriptor) => string,
|
||||
menuDataRender?: (menuData: MenuDataItem[]) => MenuDataItem[],
|
||||
) => {
|
||||
const { menuData, breadcrumb } = transformRoute(
|
||||
routes,
|
||||
menu?.locale || false,
|
||||
formatMessage,
|
||||
true,
|
||||
);
|
||||
if (!menuDataRender) {
|
||||
return {
|
||||
breadcrumb: fromEntries(breadcrumb),
|
||||
breadcrumbMap: breadcrumb,
|
||||
menuData,
|
||||
};
|
||||
}
|
||||
const renderData = transformRoute(
|
||||
menuDataRender(menuData),
|
||||
menu?.locale || false,
|
||||
formatMessage,
|
||||
true,
|
||||
);
|
||||
return {
|
||||
breadcrumb: fromEntries(renderData.breadcrumb),
|
||||
breadcrumbMap: renderData.breadcrumb,
|
||||
menuData: renderData.menuData,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Breadcrumb, Button, Row } from 'antd';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import { getBreadcrumbProps, conversionFromLocation } from './getBreadcrumbProps';
|
||||
import type { MenuDataItem } from './typings';
|
||||
import { getMenuData } from '@ant-design/pro-layout';
|
||||
import { Prompt, useLocation, history } from 'umi';
|
||||
import qs from 'qs';
|
||||
|
||||
export const BreadcrumbContext = React.createContext<any>([]);
|
||||
const PageComponent = (props: any) => {
|
||||
const [menuInfoData, setMenuInfoData] = useMergedState<{
|
||||
breadcrumb?: Record<string, MenuDataItem>;
|
||||
breadcrumbMap?: Map<string, MenuDataItem>;
|
||||
menuData?: MenuDataItem[];
|
||||
}>(() => getMenuData(props.route?.routes || []));
|
||||
const location = useLocation();
|
||||
const { breadcrumbMap } = menuInfoData;
|
||||
|
||||
const itemRender = (route, params, routes, paths) => {
|
||||
const last = routes.indexOf(route) === routes.length - 1;
|
||||
return last || !route.component ? (
|
||||
<span>{route.breadcrumbName}</span>
|
||||
) : (
|
||||
<a
|
||||
onClick={() => {
|
||||
breadClick(route);
|
||||
}}
|
||||
>
|
||||
{route.breadcrumbName}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const breadcrumbProps = getBreadcrumbProps({ ...props, breadcrumbMap, itemRender });
|
||||
useEffect(() => {
|
||||
const infoData = getMenuData(props.route?.routes || []);
|
||||
// 稍微慢一点 render,不然会造成性能问题,看起来像是菜单的卡顿
|
||||
const animationFrameId = requestAnimationFrame(() => {
|
||||
setMenuInfoData(infoData);
|
||||
});
|
||||
return () => window.cancelAnimationFrame && window.cancelAnimationFrame(animationFrameId);
|
||||
}, [props.route, props.location?.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.onChange) {
|
||||
props.onChange(breadcrumbProps.routes);
|
||||
}
|
||||
}, [props.location.pathname]);
|
||||
|
||||
const handleBeforeLeave = (e, action) => {
|
||||
// 当前 history 跳转的 action,有 PUSH、REPLACE 和 POP 三种类型
|
||||
if (action !== 'PUSH' || e.pass) {
|
||||
// @ts-ignore
|
||||
document.getElementById('pdmRightZone').scrollTop = 0;
|
||||
return true;
|
||||
}
|
||||
handleLink(e, location.query);
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleLink = (value: any, query = {}, actionType = 'push') => {
|
||||
const valueParams = { ...value };
|
||||
const beforeQuery = getBackQueryParams(value.pathname, query);
|
||||
valueParams.query = { ...beforeQuery, ...valueParams.query };
|
||||
valueParams.search = `?${qs.stringify(valueParams.query)}`;
|
||||
// @ts-ignore
|
||||
history[actionType]({
|
||||
...valueParams,
|
||||
pass: true, // 代表不拦截
|
||||
});
|
||||
};
|
||||
|
||||
const breadClick = (route) => {
|
||||
const routeValue = {
|
||||
pathname: route.path,
|
||||
};
|
||||
handleLink(routeValue, location.query || {}, 'push');
|
||||
};
|
||||
|
||||
// 获取回填的参数,所需的参数放在meta里面
|
||||
const getBackQueryParams = (pathname: string, beforeQuery = {}) => {
|
||||
const routeList = conversionFromLocation(
|
||||
{
|
||||
pathname,
|
||||
},
|
||||
breadcrumbMap,
|
||||
{},
|
||||
);
|
||||
let metaParams: string[] = [];
|
||||
routeList?.map((item: any) => {
|
||||
item.meta.params && (metaParams = metaParams.concat(item.meta.params));
|
||||
});
|
||||
const routeQuery = {};
|
||||
[...new Set(metaParams)].map((item) => {
|
||||
routeQuery[item] = beforeQuery[item];
|
||||
});
|
||||
return routeQuery;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Prompt when={true} message={handleBeforeLeave} />
|
||||
<Row align="middle" justify="space-between">
|
||||
<Breadcrumb {...breadcrumbProps}></Breadcrumb>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageComponent;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// /userInfo/2144/id => ['/userInfo','/userInfo/2144,'/userInfo/2144/id']
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function urlToList(url?: string): string[] {
|
||||
if (!url || url === '/') {
|
||||
return ['/'];
|
||||
}
|
||||
const urlList = url.split('/').filter((i) => i);
|
||||
return urlList.map((urlItem, index) => `/${urlList.slice(0, index + 1).join('/')}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import type * as H from 'history';
|
||||
|
||||
import type { RouteComponentProps as BasicRouteProps, match } from 'react-router-dom';
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
export type LinkProps = {
|
||||
to: H.LocationDescriptor;
|
||||
replace?: boolean;
|
||||
innerRef?: React.Ref<HTMLAnchorElement>;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||
|
||||
export type MenuDataItem = {
|
||||
/** @name 子菜单 */
|
||||
children?: MenuDataItem[];
|
||||
/** @name 在菜单中隐藏子节点 */
|
||||
hideChildrenInMenu?: boolean;
|
||||
/** @name 在菜单中隐藏自己和子节点 */
|
||||
hideInMenu?: boolean;
|
||||
/** @name 菜单的icon */
|
||||
icon?: React.ReactNode;
|
||||
/** @name 自定义菜单的国际化 key */
|
||||
locale?: string | false;
|
||||
/** @name 菜单的名字 */
|
||||
name?: string;
|
||||
/** @name 用于标定选中的值,默认是 path */
|
||||
key?: string;
|
||||
/** @name disable 菜单选项 */
|
||||
disabled?: boolean;
|
||||
/** @name 路径,可以设定为网页链接 */
|
||||
path?: string;
|
||||
/**
|
||||
* 当此节点被选中的时候也会选中 parentKeys 的节点
|
||||
*
|
||||
* @name 自定义父节点
|
||||
*/
|
||||
parentKeys?: string[];
|
||||
/** @name 隐藏自己,并且将子节点提升到与自己平级 */
|
||||
flatMenu?: boolean;
|
||||
/** @name 指定外链打开形式,同a标签 */
|
||||
target?: string;
|
||||
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
routes?: Route[];
|
||||
} & MenuDataItem;
|
||||
export type WithFalse<T> = T | false;
|
||||
|
||||
export type RouterTypes<P> = {
|
||||
computedMatch?: match<P>;
|
||||
route?: Route;
|
||||
location: BasicRouteProps['location'] | { pathname?: string };
|
||||
} & Omit<BasicRouteProps, 'location'>;
|
||||
|
||||
export type MessageDescriptor = {
|
||||
id: any;
|
||||
description?: string;
|
||||
defaultMessage?: string;
|
||||
};
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.container {
|
||||
padding: 40px 20px;
|
||||
background: #fff;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
const DetailPageContainer: React.FC = ({ children }) => {
|
||||
return <div className={styles.container}>{children}</div>;
|
||||
};
|
||||
|
||||
export default DetailPageContainer;
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import React, { useRef, ReactNode } from 'react';
|
||||
import { Modal } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export interface IProps {
|
||||
value: string; // 预览html代码段
|
||||
visible: boolean; // 是否可见
|
||||
title?: string; // 预览弹框名称
|
||||
width?: number; // 预览弹框高度
|
||||
renderHeader?: () => ReactNode | ReactNode; // 渲染头部
|
||||
renderFooter?: () => ReactNode | ReactNode; // 渲染底部
|
||||
onCancel?: () => void; // 预览弹框关闭
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const EditorPreview: React.FC<IProps> = (props) => {
|
||||
const {
|
||||
value,
|
||||
visible,
|
||||
title,
|
||||
width,
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
onCancel,
|
||||
prefixCls,
|
||||
className,
|
||||
style,
|
||||
} = props;
|
||||
const previewRef = useRef<HTMLInputElement>(null);
|
||||
setTimeout(() => {
|
||||
if (previewRef.current) {
|
||||
previewRef.current.innerHTML = value;
|
||||
}
|
||||
}, 0);
|
||||
return (
|
||||
<Modal
|
||||
className={classnames(className, `${prefixCls}-wrapper`)}
|
||||
style={style}
|
||||
destroyOnClose
|
||||
maskClosable={false}
|
||||
title={title}
|
||||
visible={visible}
|
||||
width={width}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
>
|
||||
{typeof renderHeader === 'function' ? renderHeader() : renderHeader}
|
||||
<div className="eidtor-content" ref={previewRef} />
|
||||
{typeof renderFooter === 'function' ? renderFooter() : renderFooter}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
EditorPreview.defaultProps = {
|
||||
visible: false,
|
||||
title: '图文详情预览',
|
||||
value: '',
|
||||
width: 375,
|
||||
onCancel: () => {},
|
||||
className: '',
|
||||
style: {},
|
||||
};
|
||||
|
||||
export default EditorPreview;
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import BraftEditor, { EditorState, ExtendControlType, MediaType } from 'braft-editor';
|
||||
import classnames from 'classnames';
|
||||
import EditorPreview from './EditorPreview';
|
||||
import 'braft-editor/dist/index.css';
|
||||
|
||||
export interface IEditorProps {
|
||||
/**
|
||||
* 富文本html代码段
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* 请求实例
|
||||
*/
|
||||
request?: any;
|
||||
/**
|
||||
* 上传接口
|
||||
*/
|
||||
action?: string;
|
||||
/**
|
||||
* 上传中除了 order 外的额外参数
|
||||
*/
|
||||
data?: {
|
||||
[propName: string]: any;
|
||||
};
|
||||
/**
|
||||
* 组件样式类名称前缀
|
||||
*/
|
||||
prefixCls?: string;
|
||||
/**
|
||||
* 容器节点样式类名称
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* 容器节点样式
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
[propName: string]: any;
|
||||
}
|
||||
// 上传到服务端的排序号
|
||||
let order = 0;
|
||||
const Editor: React.FC<IEditorProps> = (props) => {
|
||||
const { value, request, action, data, prefixCls, className, style, onChange, ...rest } = props;
|
||||
// 富文本数据是否初始化
|
||||
const [isInit, setIsInit] = useState(false);
|
||||
// 富文本
|
||||
const [editorState, setEditorState] = useState<EditorState>(BraftEditor.createEditorState(value));
|
||||
// 富文本预览
|
||||
const [previewVisible, setPreviewVisible] = useState<boolean>(false);
|
||||
// onChange
|
||||
const handleEditorChange = (newEditorState: EditorState) => {
|
||||
if (isInit || (!isInit && value === '<p></p>')) {
|
||||
setIsInit(true);
|
||||
setEditorState(newEditorState);
|
||||
const changeValue = newEditorState.toHTML() === '<p></p>' ? '' : newEditorState.toHTML();
|
||||
onChange(changeValue);
|
||||
}
|
||||
};
|
||||
// 富文本预览
|
||||
const extendControls: ExtendControlType[] = [
|
||||
{
|
||||
key: 'preview-button',
|
||||
type: 'button',
|
||||
text: '预览',
|
||||
onClick: () => setPreviewVisible(true),
|
||||
},
|
||||
];
|
||||
// 自定义媒体
|
||||
const media: MediaType = {
|
||||
uploadFn: async ({ file, progress, error, success }) => {
|
||||
// 自定义上传
|
||||
const baseRequestForm = request.requestForm.bind(request);
|
||||
order += 1;
|
||||
baseRequestForm(
|
||||
action,
|
||||
{ file, order, ...data },
|
||||
{
|
||||
onUploadProgress: ({ total, loaded }: { total: number; loaded: number }) => {
|
||||
progress((loaded / total) * 100);
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((res: any) => {
|
||||
const newRes = res.value || res.valueObj || {};
|
||||
const { imageUrl = '', attachUrl = '', attachId = '' } = newRes;
|
||||
res.url = imageUrl || attachUrl;
|
||||
res.uid = attachId;
|
||||
res.width = '100%';
|
||||
res.height = 'auto';
|
||||
success(res);
|
||||
})
|
||||
.catch(error);
|
||||
},
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isInit && value !== '<p></p>') {
|
||||
setEditorState(BraftEditor.createEditorState(value));
|
||||
setIsInit(true);
|
||||
}
|
||||
}, [value]);
|
||||
return (
|
||||
<div>
|
||||
<BraftEditor
|
||||
className={classnames(className, `${prefixCls}-wrapper`)}
|
||||
style={style}
|
||||
contentStyle={{ boxShadow: 'inset 0 1px 3px rgba(0,0,0,.1)' }}
|
||||
contentClassName="editor-content"
|
||||
extendControls={extendControls}
|
||||
// media={media}
|
||||
value={editorState}
|
||||
onChange={handleEditorChange}
|
||||
textBackgroundColor={false}
|
||||
stripPastedStyles
|
||||
{...rest}
|
||||
/>
|
||||
<EditorPreview
|
||||
width={600}
|
||||
value={editorState.toHTML()}
|
||||
visible={previewVisible}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Editor.defaultProps = {
|
||||
value: '<p></p>',
|
||||
data: {},
|
||||
className: '',
|
||||
style: {},
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.container > * {
|
||||
background-color: @popover-bg;
|
||||
border-radius: 4px;
|
||||
box-shadow: @shadow-1-down;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xs) {
|
||||
.container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.container > * {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type { DropDownProps } from 'antd/es/dropdown';
|
||||
import { Dropdown } from 'antd';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './index.less';
|
||||
|
||||
export type HeaderDropdownProps = {
|
||||
overlayClassName?: string;
|
||||
overlay: React.ReactNode | (() => React.ReactNode) | any;
|
||||
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
|
||||
} & Omit<DropDownProps, 'overlay'>;
|
||||
|
||||
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
|
||||
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
|
||||
);
|
||||
|
||||
export default HeaderDropdown;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Modal as AntModal, ModalProps } from 'antd';
|
||||
const Modal: React.FC<ModalProps> = ({ okText = '确认', ...rest }) => {
|
||||
return <AntModal {...{ ...rest, okText }}></AntModal>;
|
||||
};
|
||||
export type { ModalProps };
|
||||
export default Modal;
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { BellOutlined } from '@ant-design/icons';
|
||||
import { Badge, Spin, Tabs } from 'antd';
|
||||
import useMergedState from 'rc-util/es/hooks/useMergedState';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { NoticeIconTabProps } from './NoticeList';
|
||||
import NoticeList from './NoticeList';
|
||||
import HeaderDropdown from '../HeaderDropdown';
|
||||
import styles from './index.less';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
export type NoticeIconProps = {
|
||||
count?: number;
|
||||
bell?: React.ReactNode;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
onClear?: (tabName: string, tabKey: string) => void;
|
||||
onItemClick?: (item: API.NoticeIconItem, tabProps: NoticeIconTabProps) => void;
|
||||
onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
|
||||
onTabChange?: (tabTile: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
onPopupVisibleChange?: (visible: boolean) => void;
|
||||
popupVisible?: boolean;
|
||||
clearText?: string;
|
||||
viewMoreText?: string;
|
||||
clearClose?: boolean;
|
||||
emptyImage?: string;
|
||||
children?: React.ReactElement<NoticeIconTabProps>[];
|
||||
};
|
||||
|
||||
const NoticeIcon: React.FC<NoticeIconProps> & {
|
||||
Tab: typeof NoticeList;
|
||||
} = (props) => {
|
||||
const getNotificationBox = (): React.ReactNode => {
|
||||
const {
|
||||
children,
|
||||
loading,
|
||||
onClear,
|
||||
onTabChange,
|
||||
onItemClick,
|
||||
onViewMore,
|
||||
clearText,
|
||||
viewMoreText,
|
||||
} = props;
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
const panes: React.ReactNode[] = [];
|
||||
React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
|
||||
if (!child) {
|
||||
return;
|
||||
}
|
||||
const { list, title, count, tabKey, showClear, showViewMore } = child.props;
|
||||
const len = list && list.length ? list.length : 0;
|
||||
const msgCount = count || count === 0 ? count : len;
|
||||
const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
|
||||
panes.push(
|
||||
<TabPane tab={tabTitle} key={tabKey}>
|
||||
<NoticeList
|
||||
clearText={clearText}
|
||||
viewMoreText={viewMoreText}
|
||||
list={list}
|
||||
tabKey={tabKey}
|
||||
onClear={(): void => onClear && onClear(title, tabKey)}
|
||||
onClick={(item): void => onItemClick && onItemClick(item, child.props)}
|
||||
onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
|
||||
showClear={showClear}
|
||||
showViewMore={showViewMore}
|
||||
title={title}
|
||||
/>
|
||||
</TabPane>,
|
||||
);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Spin spinning={loading} delay={300}>
|
||||
<Tabs className={styles.tabs} onChange={onTabChange}>
|
||||
{panes}
|
||||
</Tabs>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const { className, count, bell } = props;
|
||||
|
||||
const [visible, setVisible] = useMergedState<boolean>(false, {
|
||||
value: props.popupVisible,
|
||||
onChange: props.onPopupVisibleChange,
|
||||
});
|
||||
const noticeButtonClass = classNames(className, styles.noticeButton);
|
||||
const notificationBox = getNotificationBox();
|
||||
const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
|
||||
const trigger = (
|
||||
<span className={classNames(noticeButtonClass, { opened: visible })}>
|
||||
<Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
|
||||
{NoticeBellIcon}
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
if (!notificationBox) {
|
||||
return trigger;
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderDropdown
|
||||
placement="bottomRight"
|
||||
overlay={notificationBox}
|
||||
overlayClassName={styles.popover}
|
||||
trigger={['click']}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
>
|
||||
{trigger}
|
||||
</HeaderDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
NoticeIcon.defaultProps = {
|
||||
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
|
||||
};
|
||||
|
||||
NoticeIcon.Tab = NoticeList;
|
||||
|
||||
export default NoticeIcon;
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.list {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.item {
|
||||
padding-right: 24px;
|
||||
padding-left: 24px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
.meta {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-top: 4px;
|
||||
background: @component-background;
|
||||
}
|
||||
.iconElement {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
&.read {
|
||||
opacity: 0.4;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: @primary-1;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.description {
|
||||
font-size: 12px;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
.datetime {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
.extra {
|
||||
float: right;
|
||||
margin-top: -1.5px;
|
||||
margin-right: 0;
|
||||
color: @text-color-secondary;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.loadMore {
|
||||
padding: 8px 0;
|
||||
color: @primary-6;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
&.loadedAll {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notFound {
|
||||
padding: 73px 0 88px;
|
||||
color: @text-color-secondary;
|
||||
text-align: center;
|
||||
img {
|
||||
display: inline-block;
|
||||
height: 76px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottomBar {
|
||||
height: 46px;
|
||||
color: @text-color;
|
||||
line-height: 46px;
|
||||
text-align: center;
|
||||
border-top: 1px solid @border-color-split;
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
transition: all 0.3s;
|
||||
div {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
|
||||
&:only-child {
|
||||
width: 100%;
|
||||
}
|
||||
&:not(:only-child):last-child {
|
||||
border-left: 1px solid @border-color-split;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { Avatar, List } from 'antd';
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './NoticeList.less';
|
||||
|
||||
export type NoticeIconTabProps = {
|
||||
count?: number;
|
||||
showClear?: boolean;
|
||||
showViewMore?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
title: string;
|
||||
tabKey: API.NoticeIconItemType;
|
||||
onClick?: (item: API.NoticeIconItem) => void;
|
||||
onClear?: () => void;
|
||||
emptyText?: string;
|
||||
clearText?: string;
|
||||
viewMoreText?: string;
|
||||
list: API.NoticeIconItem[];
|
||||
onViewMore?: (e: any) => void;
|
||||
};
|
||||
const NoticeList: React.FC<NoticeIconTabProps> = ({
|
||||
list = [],
|
||||
onClick,
|
||||
onClear,
|
||||
title,
|
||||
onViewMore,
|
||||
emptyText,
|
||||
showClear = true,
|
||||
clearText,
|
||||
viewMoreText,
|
||||
showViewMore = false,
|
||||
}) => {
|
||||
if (!list || list.length === 0) {
|
||||
return (
|
||||
<div className={styles.notFound}>
|
||||
<img
|
||||
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
|
||||
alt="not found"
|
||||
/>
|
||||
<div>{emptyText}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<List<API.NoticeIconItem>
|
||||
className={styles.list}
|
||||
dataSource={list}
|
||||
renderItem={(item, i) => {
|
||||
const itemCls = classNames(styles.item, {
|
||||
[styles.read]: item.read,
|
||||
});
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const leftIcon = item.avatar ? (
|
||||
typeof item.avatar === 'string' ? (
|
||||
<Avatar className={styles.avatar} src={item.avatar} />
|
||||
) : (
|
||||
<span className={styles.iconElement}>{item.avatar}</span>
|
||||
)
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
className={itemCls}
|
||||
key={item.key || i}
|
||||
onClick={() => {
|
||||
onClick?.(item);
|
||||
}}
|
||||
>
|
||||
<List.Item.Meta
|
||||
className={styles.meta}
|
||||
avatar={leftIcon}
|
||||
title={
|
||||
<div className={styles.title}>
|
||||
{item.title}
|
||||
<div className={styles.extra}>{item.extra}</div>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<div className={styles.description}>{item.description}</div>
|
||||
<div className={styles.datetime}>{item.datetime}</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.bottomBar}>
|
||||
{showClear ? (
|
||||
<div onClick={onClear}>
|
||||
{clearText} {title}
|
||||
</div>
|
||||
) : null}
|
||||
{showViewMore ? (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (onViewMore) {
|
||||
onViewMore(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{viewMoreText}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoticeList;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.popover {
|
||||
position: relative;
|
||||
width: 336px;
|
||||
}
|
||||
|
||||
.noticeButton {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.icon {
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
:global {
|
||||
.ant-tabs-nav-list {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.ant-tabs-nav-scroll {
|
||||
text-align: center;
|
||||
}
|
||||
.ant-tabs-bar {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Tag, message } from 'antd';
|
||||
import { groupBy } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { useModel, useRequest } from 'umi';
|
||||
import { getNotices } from '@/services/ant-design-pro/api';
|
||||
|
||||
import NoticeIcon from './NoticeIcon';
|
||||
import styles from './index.less';
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
fetchingNotices?: boolean;
|
||||
onNoticeVisibleChange?: (visible: boolean) => void;
|
||||
onNoticeClear?: (tabName?: string) => void;
|
||||
};
|
||||
|
||||
const getNoticeData = (notices: API.NoticeIconItem[]): Record<string, API.NoticeIconItem[]> => {
|
||||
if (!notices || notices.length === 0 || !Array.isArray(notices)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const newNotices = notices.map((notice) => {
|
||||
const newNotice = { ...notice };
|
||||
|
||||
if (newNotice.datetime) {
|
||||
newNotice.datetime = moment(notice.datetime as string).fromNow();
|
||||
}
|
||||
|
||||
if (newNotice.id) {
|
||||
newNotice.key = newNotice.id;
|
||||
}
|
||||
|
||||
if (newNotice.extra && newNotice.status) {
|
||||
const color = {
|
||||
todo: '',
|
||||
processing: 'blue',
|
||||
urgent: 'red',
|
||||
doing: 'gold',
|
||||
}[newNotice.status];
|
||||
newNotice.extra = (
|
||||
<Tag
|
||||
color={color}
|
||||
style={{
|
||||
marginRight: 0,
|
||||
}}
|
||||
>
|
||||
{newNotice.extra}
|
||||
</Tag>
|
||||
) as any;
|
||||
}
|
||||
|
||||
return newNotice;
|
||||
});
|
||||
return groupBy(newNotices, 'type');
|
||||
};
|
||||
|
||||
const getUnreadData = (noticeData: Record<string, API.NoticeIconItem[]>) => {
|
||||
const unreadMsg: Record<string, number> = {};
|
||||
Object.keys(noticeData).forEach((key) => {
|
||||
const value = noticeData[key];
|
||||
|
||||
if (!unreadMsg[key]) {
|
||||
unreadMsg[key] = 0;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
unreadMsg[key] = value.filter((item) => !item.read).length;
|
||||
}
|
||||
});
|
||||
return unreadMsg;
|
||||
};
|
||||
|
||||
const NoticeIconView: React.FC = () => {
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const { currentUser } = initialState || {};
|
||||
const [notices, setNotices] = useState<API.NoticeIconItem[]>([]);
|
||||
const { data } = useRequest(getNotices);
|
||||
|
||||
useEffect(() => {
|
||||
setNotices(data || []);
|
||||
}, [data]);
|
||||
|
||||
const noticeData = getNoticeData(notices);
|
||||
const unreadMsg = getUnreadData(noticeData || {});
|
||||
|
||||
const changeReadState = (id: string) => {
|
||||
setNotices(
|
||||
notices.map((item) => {
|
||||
const notice = { ...item };
|
||||
if (notice.id === id) {
|
||||
notice.read = true;
|
||||
}
|
||||
return notice;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const clearReadState = (title: string, key: string) => {
|
||||
setNotices(
|
||||
notices.map((item) => {
|
||||
const notice = { ...item };
|
||||
if (notice.type === key) {
|
||||
notice.read = true;
|
||||
}
|
||||
return notice;
|
||||
}),
|
||||
);
|
||||
message.success(`${'清空了'} ${title}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<NoticeIcon
|
||||
className={styles.action}
|
||||
count={currentUser && currentUser.unreadCount}
|
||||
onItemClick={(item) => {
|
||||
changeReadState(item.id!);
|
||||
}}
|
||||
onClear={(title: string, key: string) => clearReadState(title, key)}
|
||||
loading={false}
|
||||
clearText="清空"
|
||||
viewMoreText="查看更多"
|
||||
onViewMore={() => message.info('Click on view more')}
|
||||
clearClose
|
||||
>
|
||||
<NoticeIcon.Tab
|
||||
tabKey="notification"
|
||||
count={unreadMsg.notification}
|
||||
list={noticeData.notification}
|
||||
title="通知"
|
||||
emptyText="你已查看所有通知"
|
||||
showViewMore
|
||||
/>
|
||||
<NoticeIcon.Tab
|
||||
tabKey="message"
|
||||
count={unreadMsg.message}
|
||||
list={noticeData.message}
|
||||
title="消息"
|
||||
emptyText="您已读完所有消息"
|
||||
showViewMore
|
||||
/>
|
||||
<NoticeIcon.Tab
|
||||
tabKey="event"
|
||||
title="待办"
|
||||
emptyText="你已完成所有待办"
|
||||
count={unreadMsg.event}
|
||||
list={noticeData.event}
|
||||
showViewMore
|
||||
/>
|
||||
</NoticeIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoticeIconView;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Menu, Spin } from 'antd';
|
||||
import { history } from 'umi';
|
||||
import { stringify } from 'querystring';
|
||||
import HeaderDropdown from '../HeaderDropdown';
|
||||
import styles from './index.less';
|
||||
import type { MenuInfo } from 'rc-menu/lib/interface';
|
||||
import RoutePath from '@/routes/routePath';
|
||||
import { CACHE_TOKEN } from '@/constants/cacheKey';
|
||||
|
||||
export type GlobalHeaderRightProps = {
|
||||
menu?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出登录,并且将当前的 url 保存
|
||||
*/
|
||||
const loginOut = async () => {
|
||||
const { query = {}, search, pathname } = history.location;
|
||||
const { redirect } = query;
|
||||
// Note: There may be security issues, please note
|
||||
if (window.location.pathname !== RoutePath.LOGIN && !redirect) {
|
||||
localStorage.removeItem(CACHE_TOKEN);
|
||||
history.replace({
|
||||
pathname: RoutePath.LOGIN,
|
||||
search: stringify({
|
||||
redirect: pathname + search,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
|
||||
const onMenuClick = useCallback((event: MenuInfo) => {
|
||||
const { key } = event;
|
||||
if (key === 'logout') {
|
||||
loginOut();
|
||||
return;
|
||||
}
|
||||
history.push(`/account/${key}`);
|
||||
}, []);
|
||||
|
||||
const loading = (
|
||||
<span className={`${styles.action} ${styles.account}`}>
|
||||
<Spin
|
||||
size="small"
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
const currentUser = {
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
|
||||
name: 'admin',
|
||||
};
|
||||
|
||||
if (!currentUser || !currentUser.name) {
|
||||
return loading;
|
||||
}
|
||||
|
||||
const menuHeaderDropdown = (
|
||||
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
|
||||
{menu && (
|
||||
<Menu.Item key="center">
|
||||
<UserOutlined />
|
||||
个人中心
|
||||
</Menu.Item>
|
||||
)}
|
||||
{menu && (
|
||||
<Menu.Item key="settings">
|
||||
<SettingOutlined />
|
||||
个人设置
|
||||
</Menu.Item>
|
||||
)}
|
||||
{menu && <Menu.Divider />}
|
||||
|
||||
<Menu.Item key="logout">
|
||||
<LogoutOutlined />
|
||||
退出登录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<HeaderDropdown overlay={menuHeaderDropdown}>
|
||||
<span className={`${styles.action} ${styles.account}`}>
|
||||
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
|
||||
<span className={`${styles.name} anticon`}>{currentUser.name}</span>
|
||||
</span>
|
||||
</HeaderDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarDropdown;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
|
||||
|
||||
.menu {
|
||||
:global(.anticon) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
:global(.ant-dropdown-menu-item) {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
float: right;
|
||||
height: 48px;
|
||||
margin-left: auto;
|
||||
overflow: hidden;
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
> span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
&:hover {
|
||||
background: @pro-header-hover-bg;
|
||||
}
|
||||
&:global(.opened) {
|
||||
background: @pro-header-hover-bg;
|
||||
}
|
||||
}
|
||||
.search {
|
||||
padding: 0 12px;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.account {
|
||||
.avatar {
|
||||
margin-right: 8px;
|
||||
color: @primary-color;
|
||||
vertical-align: top;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.action {
|
||||
&:hover {
|
||||
background: #252a3d;
|
||||
}
|
||||
&:global(.opened) {
|
||||
background: #252a3d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @screen-md) {
|
||||
:global(.ant-divider-vertical) {
|
||||
vertical-align: unset;
|
||||
}
|
||||
.name {
|
||||
display: none;
|
||||
}
|
||||
.right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
.account {
|
||||
.avatar {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.search {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
import Avatar from './AvatarDropdown';
|
||||
import styles from './index.less';
|
||||
|
||||
export type SiderTheme = 'light' | 'dark';
|
||||
|
||||
const GlobalHeaderRight: React.FC = () => {
|
||||
// if (!initialState || !initialState.settings) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// const { navTheme, layout } = initialState.settings;
|
||||
const className = styles.right;
|
||||
|
||||
// if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
|
||||
// className = `${styles.right} ${styles.dark}`;
|
||||
// }
|
||||
return (
|
||||
<Space className={className}>
|
||||
<Avatar />
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
export default GlobalHeaderRight;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.delete-btn {
|
||||
color: @error-color;
|
||||
&:hover {
|
||||
color: @error-color !important;
|
||||
}
|
||||
}
|
||||
.disabled {
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
&:hover {
|
||||
color: @disabled-color !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { Popconfirm } from 'antd';
|
||||
import styles from './index.less';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface PropsType {
|
||||
onDelete: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const DeleteButton: React.FC<PropsType> = ({ onDelete, disabled = false }) => {
|
||||
return (
|
||||
<Popconfirm
|
||||
title="是否确认删除当前项?"
|
||||
disabled={disabled}
|
||||
onConfirm={onDelete}
|
||||
okText="是"
|
||||
cancelText="否"
|
||||
>
|
||||
<a className={classNames(styles.deleteBtn, disabled ? styles.disabled : '')}>删除</a>
|
||||
</Popconfirm>
|
||||
);
|
||||
};
|
||||
export default DeleteButton;
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
import React, { useMemo, useCallback, useState, useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
Divider,
|
||||
AlertProps,
|
||||
Alert,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import ProTable, { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-table';
|
||||
|
||||
export interface toolBarActionsItem {
|
||||
type: 'add' | 'batchDelete';
|
||||
text?: string;
|
||||
onConfirm: (val?: string[]) => void;
|
||||
}
|
||||
export interface PropsType extends ProTableProps<any, any> {
|
||||
toolBarActions?: toolBarActionsItem[];
|
||||
alertProps?: AlertProps;
|
||||
indexColumn?: boolean | ProColumns<any, 'text'>;
|
||||
}
|
||||
|
||||
type Record<K extends keyof any, T> = {
|
||||
[P in K]: T;
|
||||
};
|
||||
|
||||
const Table = <T extends Record<string, any>>(props: PropsType) => {
|
||||
const {
|
||||
columns: columnsProps = [],
|
||||
search: searchProps = {},
|
||||
pagination: paginationProps,
|
||||
options = false,
|
||||
toolBarRender: toolBarRenderProps,
|
||||
toolBarActions,
|
||||
actionRef: actionRefProps,
|
||||
rowSelection: rowSelectionProps,
|
||||
toolbar: toolbarProps,
|
||||
alertProps,
|
||||
indexColumn,
|
||||
} = props;
|
||||
const defaultRef = useRef<ActionType>();
|
||||
const actionRef: any = actionRefProps || defaultRef;
|
||||
const [selectedRows, setSelectedRows] = useState<React.Key[]>([]);
|
||||
|
||||
// 暂时有批量删除的话,替换rowSelection,后续有发现问题再改
|
||||
const rowSelection = useMemo(() => {
|
||||
if (toolBarActions && toolBarActions.findIndex((item) => item.type === 'batchDelete') > -1) {
|
||||
return {
|
||||
selectedRowKeys: selectedRows,
|
||||
onChange: setSelectedRows,
|
||||
preserveSelectedRowKeys: true,
|
||||
};
|
||||
}
|
||||
return rowSelectionProps
|
||||
? {
|
||||
preserveSelectedRowKeys: true,
|
||||
...rowSelectionProps,
|
||||
}
|
||||
: false;
|
||||
}, [rowSelectionProps, toolBarActions, setSelectedRows, selectedRows]);
|
||||
|
||||
const indexColumnValue = useMemo(() => {
|
||||
if (!indexColumn) return [];
|
||||
const rankItem = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
search: false,
|
||||
render: (_: any, item: any, index: number) => {
|
||||
// @ts-ignore
|
||||
const { current, pageSize } = actionRef.current?.pageInfo;
|
||||
return index + 1 + (current - 1) * pageSize;
|
||||
},
|
||||
...(typeof indexColumn === 'object' ? indexColumn : {}),
|
||||
},
|
||||
];
|
||||
return rankItem;
|
||||
}, [indexColumn, actionRef]);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const newColumns = columnsProps.map((item) => {
|
||||
const optionRender = (_: any, val: any, ...rest: any) => {
|
||||
return (
|
||||
<Space size={0} split={<Divider type="vertical" />}>
|
||||
{(item.render && (item.render(_, val, ...rest) as []))?.map((item2) => item2)}
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
let { render } = item;
|
||||
if (item.valueType === 'option') {
|
||||
render = optionRender;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
fieldProps:
|
||||
item.valueType === 'select'
|
||||
? {
|
||||
...item.fieldProps,
|
||||
showSearch: (item.fieldProps as any)?.options?.length > 8,
|
||||
}
|
||||
: item.fieldProps,
|
||||
render,
|
||||
};
|
||||
});
|
||||
|
||||
return indexColumnValue.concat(newColumns);
|
||||
}, [columnsProps, indexColumnValue]);
|
||||
|
||||
// search属性
|
||||
const search = useMemo(() => {
|
||||
if (searchProps === false) {
|
||||
return searchProps;
|
||||
}
|
||||
return {
|
||||
...searchProps,
|
||||
optionRender: ({ searchText, resetText }, { form }) => [
|
||||
<Button
|
||||
key="search"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
form?.submit();
|
||||
}}
|
||||
>
|
||||
{searchText}
|
||||
</Button>,
|
||||
<Button
|
||||
key="rest"
|
||||
onClick={() => {
|
||||
form?.resetFields();
|
||||
form?.submit();
|
||||
props.onReset && props.onReset();
|
||||
}}
|
||||
>
|
||||
{resetText}
|
||||
</Button>,
|
||||
],
|
||||
};
|
||||
}, [searchProps, columnsProps]);
|
||||
const handleBatchDelete = (action: any) => {
|
||||
if (selectedRows.length === 0) {
|
||||
message.warn('请选择要操作的项');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '是否确认删除?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
action(selectedRows);
|
||||
},
|
||||
});
|
||||
};
|
||||
// 渲染toolbarActions自定义按钮
|
||||
const toolBarActionsRender = useCallback(() => {
|
||||
const buttonList: any = [];
|
||||
toolBarActions &&
|
||||
toolBarActions.forEach((item) => {
|
||||
if (item.type === 'add') {
|
||||
buttonList.push(
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
item.onConfirm();
|
||||
}}
|
||||
>
|
||||
<PlusOutlined />
|
||||
{item.text || '新建'}
|
||||
</Button>,
|
||||
);
|
||||
} else if (item.type === 'batchDelete') {
|
||||
buttonList.push(
|
||||
<Button
|
||||
key="del"
|
||||
danger
|
||||
onClick={() => {
|
||||
handleBatchDelete(item.onConfirm);
|
||||
}}
|
||||
>
|
||||
{item.text || '批量删除'}
|
||||
</Button>,
|
||||
);
|
||||
}
|
||||
});
|
||||
return buttonList;
|
||||
}, [toolBarActions, selectedRows]);
|
||||
|
||||
// 渲染toolbar
|
||||
const toolBarRender = useCallback(
|
||||
(
|
||||
action,
|
||||
rows: {
|
||||
selectedRowKeys?: (string | number)[];
|
||||
selectedRows?: T[];
|
||||
},
|
||||
) => {
|
||||
let toolRender = toolBarRenderProps ? toolBarRenderProps(action, rows) : toolBarRenderProps;
|
||||
if (toolBarActions) {
|
||||
const buttonList: any = toolBarActionsRender();
|
||||
toolRender = toolRender ? toolRender.concat(buttonList) : buttonList;
|
||||
}
|
||||
return toolRender;
|
||||
},
|
||||
[toolBarRenderProps, toolBarActions, toolBarActionsRender],
|
||||
);
|
||||
// 分页统一为10条
|
||||
const pagination = useMemo(() => {
|
||||
if (paginationProps === undefined) {
|
||||
return { defaultCurrent: 1, defaultPageSize: 10, pageSize: 10, current: 1 };
|
||||
}
|
||||
return paginationProps;
|
||||
}, [paginationProps]);
|
||||
|
||||
// 渲染tip,用toolbar的filter
|
||||
const toolbar = useMemo(() => {
|
||||
if (alertProps) {
|
||||
return {
|
||||
...toolbarProps,
|
||||
multipleLine: true,
|
||||
filter: <Alert {...alertProps} />,
|
||||
};
|
||||
}
|
||||
return toolbarProps;
|
||||
}, [alertProps, toolbarProps]);
|
||||
|
||||
return (
|
||||
<div className="ls-table">
|
||||
<ProTable<T>
|
||||
{...{
|
||||
...props,
|
||||
search,
|
||||
pagination,
|
||||
columns,
|
||||
rowSelection,
|
||||
options,
|
||||
toolbar,
|
||||
toolBarRender: toolBarRenderProps || toolBarActions ? toolBarRender : toolBarRenderProps,
|
||||
actionRef,
|
||||
onLoad: (res) => {
|
||||
if (res.length === 0 && actionRef?.current.pageInfo.current !== 1) {
|
||||
actionRef?.current.setPageInfo({
|
||||
...actionRef?.current.pageInfo,
|
||||
current: actionRef?.current.pageInfo.current - 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type { ProColumns, ActionType, ProTableProps };
|
||||
export default Table;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import Text from 'antd/es/typography/Text';
|
||||
import moment, { Moment } from 'moment';
|
||||
|
||||
const DEFAULT_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
export interface TimeTextProps {
|
||||
value?: Moment;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间文本
|
||||
*
|
||||
*/
|
||||
const TimeText: React.FC<TimeTextProps> = (props) => {
|
||||
const { value, format } = props;
|
||||
const txtFormat = format ? format : DEFAULT_FORMAT;
|
||||
const txt = value ? moment(value).format(txtFormat) : '-';
|
||||
return <Text>{txt}</Text>;
|
||||
};
|
||||
|
||||
export default TimeText;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.upload {
|
||||
:global {
|
||||
.ant-upload-list-picture-card .ant-upload-list-item-info::before {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
&.hide-button {
|
||||
:global {
|
||||
.ant-upload {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Upload as AntdUpload } from '@formily/antd';
|
||||
import { UploadProps as AntdUploadProps, Modal } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { UploadChangeParam, UploadFile, UploadListType } from 'antd/lib/upload/interface';
|
||||
import { baseRequestForm as request } from '@/utils/request';
|
||||
import classNames from 'classnames';
|
||||
import { UploadBizType } from '@/constants/enum/uploadBizType';
|
||||
import { FileType } from '@/constants/enum/fileType';
|
||||
import { getFileType } from '@/utils/getFileType';
|
||||
import { CACHE_TOKEN } from '@/constants/cacheKey';
|
||||
import styles from './index.less';
|
||||
|
||||
const UPLOAD_URL = '/admin/common/upload';
|
||||
|
||||
interface UploadPropsType extends AntdUploadProps {
|
||||
value?: UploadFile<any>[];
|
||||
bizType: UploadBizType;
|
||||
}
|
||||
const Upload: React.FC<UploadPropsType> = ({
|
||||
listType = 'picture-card',
|
||||
accept = 'image/*',
|
||||
value = [],
|
||||
maxCount = 1,
|
||||
bizType,
|
||||
onChange,
|
||||
...rest
|
||||
}) => {
|
||||
// 图片预览
|
||||
const [previewVisible, setPreviewVisible] = useState<boolean>(false);
|
||||
const [previewImage, setPreviewImage] = useState<string | undefined>('');
|
||||
const [previewTitle, setPreviewTitle] = useState<string>('预览');
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
// // 文件上传失败时,没有url
|
||||
// if (
|
||||
// (file.url && !imageExpendRegExp.test(file.url)) ||
|
||||
// (!file.url && !imageExpendRegExp.test(file.name))
|
||||
// )
|
||||
// return;
|
||||
// !file.url &&
|
||||
// !file.preview &&
|
||||
// (file.preview = await getBase64(file.originFileObj));
|
||||
const { ...rest1 } = file;
|
||||
setPreviewImage(file.url || file.preview);
|
||||
setPreviewVisible(true);
|
||||
setPreviewTitle(file.name || '预览');
|
||||
};
|
||||
// onChange
|
||||
const handleChange = (fileList) => {
|
||||
fileList = fileList.slice(-maxCount);
|
||||
fileList.forEach((item) => {
|
||||
if (item.response && item.response.data) {
|
||||
const resData = item.response.data;
|
||||
resData.url && (item.url = resData.url);
|
||||
resData.path && (item.path = resData.path);
|
||||
}
|
||||
});
|
||||
onChange && onChange(fileList);
|
||||
};
|
||||
const props = {
|
||||
listType,
|
||||
accept,
|
||||
onPreview: handlePreview,
|
||||
maxCount,
|
||||
onChange: handleChange,
|
||||
// customRequest: handleCustomRequest,
|
||||
fileList: value,
|
||||
action: `${UPLOAD_URL}?type=${bizType}`,
|
||||
headers: { token: localStorage.getItem(CACHE_TOKEN) },
|
||||
...rest,
|
||||
};
|
||||
const className = {
|
||||
[props.className!]: true,
|
||||
[styles.upload]: true,
|
||||
[styles.hideButton]: value.length >= maxCount,
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<AntdUpload {...{ ...props, className: classNames(className) }} />
|
||||
|
||||
<Modal
|
||||
visible={previewVisible}
|
||||
title={previewTitle}
|
||||
footer={null}
|
||||
width={600}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
>
|
||||
{getFileType(previewImage) === FileType.VIDEO ? (
|
||||
<video controls style={{ width: '100%' }} src={previewImage} />
|
||||
) : (
|
||||
<img alt="预览" style={{ width: '100%' }} src={previewImage} />
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Upload;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const CACHE_TOKEN = 'token';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export enum BlindboxStatusType {
|
||||
PUBLISHED = 'published',
|
||||
NO_PUBLISH = 'no_publish',
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export enum FileType {
|
||||
IMAGE = 'image',
|
||||
VIDEO = 'video',
|
||||
}
|
||||
export default FileType;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* 上传类型
|
||||
* @author gary
|
||||
*/
|
||||
export enum UploadBizType {
|
||||
WORK = 'work', // 应用信息相关
|
||||
AVATAR = 'avatar', // 头像
|
||||
AUTHOR = 'author', // 作者
|
||||
BLIND_BOX = 'blind_box', // 盲盒
|
||||
COLLECT = 'collect', // 集卡
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
export enum WorkType {
|
||||
VIDEO = 'video',
|
||||
AUDIO = 'audio',
|
||||
PICTURE = 'picture',
|
||||
}
|
||||
|
||||
export enum WorkRarityType {
|
||||
UR = 'UR',
|
||||
SSR = 'SSR',
|
||||
SR = 'SR',
|
||||
R = 'R',
|
||||
N = 'N',
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
@import '~antd/es/style/themes/default.less';
|
||||
@import './styles/antdGlobal.less';
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.colorWeak {
|
||||
filter: invert(80%);
|
||||
}
|
||||
|
||||
.ant-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
|
||||
left: unset;
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media (max-width: @screen-xs) {
|
||||
.ant-table {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
&-thead > tr,
|
||||
&-tbody > tr {
|
||||
> th,
|
||||
> td {
|
||||
white-space: pre;
|
||||
> span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compatible with IE11
|
||||
@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
|
||||
body .ant-design-pro > .ant-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { Button, message, notification } from 'antd';
|
||||
import { useIntl } from 'umi';
|
||||
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.close(key);
|
||||
reloadSW();
|
||||
}}
|
||||
>
|
||||
{useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
|
||||
</Button>
|
||||
);
|
||||
notification.open({
|
||||
message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
|
||||
description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
|
||||
btn,
|
||||
key,
|
||||
onClose: async () => null,
|
||||
});
|
||||
});
|
||||
} else if ('serviceWorker' in navigator && isHttps) {
|
||||
// unregister service worker
|
||||
const { serviceWorker } = navigator;
|
||||
if (serviceWorker.getRegistrations) {
|
||||
serviceWorker.getRegistrations().then((sws) => {
|
||||
sws.forEach((sw) => {
|
||||
sw.unregister();
|
||||
});
|
||||
});
|
||||
}
|
||||
serviceWorker.getRegistration().then((sw) => {
|
||||
if (sw) sw.unregister();
|
||||
});
|
||||
|
||||
clearCache();
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Button, Result } from 'antd';
|
||||
import React from 'react';
|
||||
import { history } from 'umi';
|
||||
|
||||
const NoFoundPage: React.FC = () => (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="Sorry, the page you visited does not exist."
|
||||
extra={
|
||||
<Button type="primary" onClick={() => history.push('/')}>
|
||||
Back Home
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default NoFoundPage;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
background: @layout-body-background;
|
||||
:global {
|
||||
.ant-pro-form-login-header {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lang {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
line-height: 44px;
|
||||
text-align: right;
|
||||
:global(.ant-dropdown-trigger) {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-md-min) {
|
||||
.container {
|
||||
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 110px;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 100px 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 8px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import React, { useState } from 'react';
|
||||
import { ProFormText, LoginForm } from '@ant-design/pro-form';
|
||||
import { useIntl, history, FormattedMessage, useModel } from 'umi';
|
||||
import { login } from '@/services/login';
|
||||
import defaultSettings from '../../../config/defaultSettings';
|
||||
import styles from './index.less';
|
||||
import { CACHE_TOKEN } from '@/constants/cacheKey';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleSubmit = async (values: API.LoginParams) => {
|
||||
// 登录
|
||||
const res: any = await login({ ...values });
|
||||
localStorage.setItem(CACHE_TOKEN, res.token);
|
||||
/** 此方法会跳转到 redirect 参数所在的位置 */
|
||||
if (!history) return;
|
||||
const { query } = history.location;
|
||||
const { redirect } = query as { redirect: string };
|
||||
history.push(redirect || '/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<LoginForm
|
||||
logo={<img alt="logo" src="/logo.svg" />}
|
||||
title={defaultSettings.title as string}
|
||||
initialValues={{
|
||||
autoLogin: true,
|
||||
}}
|
||||
onFinish={async (values) => {
|
||||
await handleSubmit(values as API.LoginParams);
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="username"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <UserOutlined className={styles.prefixIcon} />,
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.login.username.placeholder',
|
||||
defaultMessage: '用户名',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.username.required"
|
||||
defaultMessage="请输入用户名!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name="password"
|
||||
fieldProps={{
|
||||
size: 'large',
|
||||
prefix: <LockOutlined className={styles.prefixIcon} />,
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.login.password.placeholder',
|
||||
defaultMessage: '密码',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="pages.login.password.required"
|
||||
defaultMessage="请输入密码!"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</LoginForm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
// import { queryAuthorList } from '@/services/author';
|
||||
import { Select } from 'antd';
|
||||
import _debounce from 'lodash/debounce';
|
||||
|
||||
export interface Options {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
interface AuthorSelectPropsType {
|
||||
params?: any;
|
||||
value?: string | number;
|
||||
defaultOptions: Options[];
|
||||
}
|
||||
const AuthorSelect: React.FC<AuthorSelectPropsType> = ({
|
||||
params = {},
|
||||
value,
|
||||
defaultOptions,
|
||||
...rest
|
||||
}) => {
|
||||
const [options, setOptions] = useState<Options[]>([]);
|
||||
const [isFirst, setIsFirst] = useState(true);
|
||||
useEffect(() => {
|
||||
if (isFirst) {
|
||||
if (defaultOptions.length > 0 && defaultOptions.findIndex((item) => !item.label) === -1) {
|
||||
setOptions(defaultOptions);
|
||||
setIsFirst(false);
|
||||
}
|
||||
}
|
||||
}, [defaultOptions, isFirst]);
|
||||
const handleSearch = async (name: string) => {
|
||||
// const { data } = await queryAuthorList({
|
||||
// page: 1,
|
||||
// num: 10,
|
||||
// name,
|
||||
// });
|
||||
// const optionsList = data?.map((item) => ({
|
||||
// label: `${item.first_name} ${item.last_name}`,
|
||||
// value: item.id,
|
||||
// }));
|
||||
// setOptions(optionsList);
|
||||
};
|
||||
const handleFocus = () => {
|
||||
handleSearch('');
|
||||
};
|
||||
return (
|
||||
<Select
|
||||
{...rest}
|
||||
options={options}
|
||||
value={value}
|
||||
allowClear
|
||||
showSearch
|
||||
onFocus={handleFocus}
|
||||
optionFilterProp="label"
|
||||
onSearch={_debounce(handleSearch, 500)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthorSelect;
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { createForm } from '@formily/core';
|
||||
import { createSchemaField } from '@formily/react';
|
||||
import {
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Select,
|
||||
Submit,
|
||||
FormGrid,
|
||||
ArrayItems,
|
||||
Editable,
|
||||
Switch,
|
||||
ArrayTable,
|
||||
FormButtonGroup,
|
||||
NumberPicker,
|
||||
} from '@formily/antd';
|
||||
import { observable } from '@formily/reactive';
|
||||
import Upload from '@/components/Upload';
|
||||
import { WorkType } from '@/constants/enum/work';
|
||||
import { message, Button, Spin } from 'antd';
|
||||
import Editor from '@/components/Editor';
|
||||
import DetailPageContainer from '@/components/DetailPageContainer';
|
||||
import { addWork, updateWork, queryWorkDetail } from '@/services/work';
|
||||
import { useLocation } from 'umi';
|
||||
import { UploadBizType } from '@/constants/enum/uploadBizType';
|
||||
import AuthorSelect, { Options } from './components/AuthorSelector';
|
||||
|
||||
const SchemaField = createSchemaField({
|
||||
components: {
|
||||
FormItem,
|
||||
FormGrid,
|
||||
Input,
|
||||
Select,
|
||||
ArrayTable,
|
||||
Upload,
|
||||
Switch,
|
||||
ArrayItems,
|
||||
NumberPicker,
|
||||
Editable,
|
||||
Editor,
|
||||
AuthorSelect,
|
||||
},
|
||||
});
|
||||
const obs: { authorDefaultOption: Options[] } = observable({
|
||||
authorDefaultOption: [],
|
||||
});
|
||||
const workTypeList = [
|
||||
{
|
||||
label: '视频',
|
||||
value: WorkType.VIDEO,
|
||||
},
|
||||
{
|
||||
label: '音频',
|
||||
value: WorkType.AUDIO,
|
||||
},
|
||||
{
|
||||
label: '图片',
|
||||
value: WorkType.PICTURE,
|
||||
},
|
||||
];
|
||||
const form = createForm({});
|
||||
const WorkEdit = () => {
|
||||
const { id } = useLocation().query;
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const getWorkDetail = async () => {
|
||||
const res: any = await queryWorkDetail({ id });
|
||||
const resourseList = res.resource_arr.map((item) => ({
|
||||
...item,
|
||||
img: [{ uid: item.path, path: item.path, url: item.url }],
|
||||
}));
|
||||
const newValue = { ...res, resource_arr: resourseList, author_id: res.author?.id };
|
||||
obs.authorDefaultOption = [
|
||||
{ label: `${res.author?.first_name} ${res.author?.last_name}`, value: res.author?.id },
|
||||
];
|
||||
form.setValues(newValue);
|
||||
};
|
||||
|
||||
const handleSubmit = async (val) => {
|
||||
const params = { ...val };
|
||||
|
||||
if (params.resource_arr.filter((item) => item.cover).length > 1) {
|
||||
message.warning('只能设置一张图片为封面');
|
||||
return;
|
||||
}
|
||||
if (params.resource_arr.filter((item) => item.cover).length === 0) {
|
||||
message.warning('请设置一张图片为封面');
|
||||
return;
|
||||
}
|
||||
if (params.resource_arr.filter((item) => item.avatar).length > 1) {
|
||||
message.warning('只能设置一个为nft资源');
|
||||
return;
|
||||
}
|
||||
if (params.resource_arr.filter((item) => item.avatar).length === 0) {
|
||||
message.warning('请设置一个为nft资源');
|
||||
return;
|
||||
}
|
||||
params.resource_arr.forEach((item, index) => {
|
||||
item.path = item.img[0].path;
|
||||
item.order_num = index;
|
||||
});
|
||||
setLoading(true);
|
||||
try {
|
||||
if (id) {
|
||||
await updateWork(params);
|
||||
} else {
|
||||
await addWork(params);
|
||||
}
|
||||
message.success('操作成功');
|
||||
setLoading(false);
|
||||
history.back();
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const handleAddResoruce = () => {
|
||||
const resourceList = form.getFieldState('resource_arr').value;
|
||||
if (resourceList.length >= 5) {
|
||||
message.warning('只能添加5个资源');
|
||||
return;
|
||||
}
|
||||
form.setFieldState('resource_arr', (state) => {
|
||||
state.value = [...resourceList, {}];
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
id && getWorkDetail();
|
||||
return () => {
|
||||
form.setValues({}, 'overwrite');
|
||||
obs.authorDefaultOption = [];
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<DetailPageContainer>
|
||||
<Spin spinning={loading}>
|
||||
<Form form={form} labelCol={4} wrapperCol={18} onAutoSubmit={handleSubmit}>
|
||||
<SchemaField>
|
||||
<SchemaField.String
|
||||
name="title"
|
||||
title="标题"
|
||||
required
|
||||
x-decorator="FormItem"
|
||||
x-component="Input"
|
||||
/>
|
||||
<SchemaField.String
|
||||
name="sub_title"
|
||||
title="副标题"
|
||||
required
|
||||
x-component-props={{ maxLength: 100, showCount: true }}
|
||||
x-decorator="FormItem"
|
||||
x-component="Input.TextArea"
|
||||
/>
|
||||
<SchemaField.String
|
||||
name="series"
|
||||
title="系列"
|
||||
required
|
||||
x-decorator="FormItem"
|
||||
x-component="Input"
|
||||
/>
|
||||
<SchemaField.Number
|
||||
name="rarity"
|
||||
title="等级"
|
||||
required
|
||||
description="rarity < 0 极度稀有 UR,
|
||||
rarity >= 0 && rarity < 100 较高级的超级稀有 SSR,
|
||||
rarity > =100 && rarity < 1000 超级稀有 SR,
|
||||
rarity >= 1000 && rarity < 10000 稀有 R,
|
||||
rarity >= 10000 普通 N"
|
||||
x-validator="number"
|
||||
x-decorator="FormItem"
|
||||
x-component="NumberPicker"
|
||||
/>
|
||||
<SchemaField.String
|
||||
name="author_id"
|
||||
title="作者"
|
||||
required
|
||||
x-component-props={{
|
||||
defaultOptions: obs.authorDefaultOption,
|
||||
}}
|
||||
x-reactions={(field) => {
|
||||
field.component[1].defaultOptions = obs.authorDefaultOption;
|
||||
}}
|
||||
x-decorator="FormItem"
|
||||
x-component="AuthorSelect"
|
||||
/>
|
||||
<SchemaField.String
|
||||
name="type"
|
||||
title="类型"
|
||||
required
|
||||
x-component-props={{
|
||||
options: workTypeList,
|
||||
}}
|
||||
x-decorator="FormItem"
|
||||
x-component="Select"
|
||||
/>
|
||||
<SchemaField.String
|
||||
name="introduction"
|
||||
title="描述"
|
||||
required
|
||||
x-decorator="FormItem"
|
||||
x-component="Editor"
|
||||
/>
|
||||
<SchemaField.Array
|
||||
name="resource_arr"
|
||||
title="资源"
|
||||
required
|
||||
// description="资源推荐尺寸:420x325"
|
||||
x-decorator="FormItem"
|
||||
x-component="ArrayTable"
|
||||
x-component-props={{
|
||||
pagination: { pageSize: 10 },
|
||||
}}
|
||||
>
|
||||
<SchemaField.Object>
|
||||
<SchemaField.Void
|
||||
x-component="ArrayTable.Column"
|
||||
x-component-props={{ width: 60, title: '拖动排序', align: 'center' }}
|
||||
>
|
||||
<SchemaField.Void
|
||||
x-decorator="FormItem"
|
||||
required
|
||||
x-component="ArrayTable.SortHandle"
|
||||
/>
|
||||
</SchemaField.Void>
|
||||
<SchemaField.Void
|
||||
x-component="ArrayTable.Column"
|
||||
x-component-props={{ width: 80, title: '序号', align: 'center' }}
|
||||
>
|
||||
<SchemaField.String x-decorator="FormItem" x-component="ArrayTable.Index" />
|
||||
</SchemaField.Void>
|
||||
<SchemaField.Void
|
||||
x-component="ArrayTable.Column"
|
||||
x-component-props={{ width: 200, title: '资源', align: 'center' }}
|
||||
>
|
||||
<SchemaField.String
|
||||
required
|
||||
x-decorator="FormItem"
|
||||
name="img"
|
||||
x-component-props={{ accept: 'image/*,video/mp4', bizType: UploadBizType.WORK }}
|
||||
x-component="Upload"
|
||||
/>
|
||||
</SchemaField.Void>
|
||||
|
||||
<SchemaField.Void
|
||||
x-component="ArrayTable.Column"
|
||||
x-component-props={{ width: 200, title: '设置为封面', align: 'center' }}
|
||||
>
|
||||
<SchemaField.String x-decorator="FormItem" name="cover" x-component="Switch" />
|
||||
</SchemaField.Void>
|
||||
<SchemaField.Void
|
||||
x-component="ArrayTable.Column"
|
||||
x-component-props={{ width: 200, title: '设置为NFT资源', align: 'center' }}
|
||||
>
|
||||
<SchemaField.String x-decorator="FormItem" name="avatar" x-component="Switch" />
|
||||
</SchemaField.Void>
|
||||
<SchemaField.Void
|
||||
x-component="ArrayTable.Column"
|
||||
x-component-props={{
|
||||
title: '操作',
|
||||
dataIndex: 'operations',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
}}
|
||||
>
|
||||
<SchemaField.Void x-component="FormItem">
|
||||
<SchemaField.Void x-component="ArrayTable.Remove" />
|
||||
</SchemaField.Void>
|
||||
</SchemaField.Void>
|
||||
</SchemaField.Object>
|
||||
</SchemaField.Array>
|
||||
</SchemaField>
|
||||
<FormButtonGroup.FormItem style={{ marginBottom: 20 }}>
|
||||
<Button type="dashed" onClick={handleAddResoruce} block>
|
||||
添加资源
|
||||
</Button>
|
||||
</FormButtonGroup.FormItem>
|
||||
<FormButtonGroup.FormItem>
|
||||
<Submit block size="large">
|
||||
提交
|
||||
</Submit>
|
||||
</FormButtonGroup.FormItem>
|
||||
</Form>
|
||||
</Spin>
|
||||
</DetailPageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkEdit;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import React, { useRef } from 'react';
|
||||
import Table, { ProColumns, ActionType } from '@/components/Table';
|
||||
import { Image } from 'antd';
|
||||
import DeleteButton from '@/components/Table/DeleteButton';
|
||||
import { history, Link } from 'umi';
|
||||
import RoutePath from '@/routes/routePath';
|
||||
import { queryWorkList, deleteWork } from '@/services/work';
|
||||
import { fetchTableData } from '@/utils/table';
|
||||
import TimeText from '@/components/Typography/TimeText';
|
||||
|
||||
const WorkList = () => {
|
||||
const tableRef = useRef<ActionType>();
|
||||
const handleDelete = async (id) => {
|
||||
await deleteWork({ id });
|
||||
tableRef.current?.reload();
|
||||
};
|
||||
const columns: ProColumns<any>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: '10%',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '封面',
|
||||
dataIndex: 'url',
|
||||
valueType: 'image',
|
||||
hideInSearch: true,
|
||||
width: '20%',
|
||||
render: (_, row) => {
|
||||
return <Image src={row.cover_resource?.url} height={100} alt="" />;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '系列',
|
||||
dataIndex: 'series',
|
||||
width: '10%',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
dataIndex: 'rarity_name',
|
||||
width: '10%',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
valueType: 'dateRange',
|
||||
width: '20%',
|
||||
hideInSearch: true,
|
||||
render: (_, row) => {
|
||||
return <TimeText value={row.created_at} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 150,
|
||||
render: (_, row) => [
|
||||
<Link to={`${RoutePath.WORK.EDIT}?id=${row.id}`}>详情</Link>,
|
||||
<DeleteButton
|
||||
onDelete={() => {
|
||||
handleDelete(row.id);
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
// indexColumn
|
||||
rowKey="id"
|
||||
actionRef={tableRef}
|
||||
toolBarActions={[
|
||||
{
|
||||
type: 'add',
|
||||
onConfirm: () => {
|
||||
history.push(RoutePath.WORK.EDIT);
|
||||
},
|
||||
},
|
||||
]}
|
||||
request={async (params) => {
|
||||
return fetchTableData(queryWorkList, params);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkList;
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta
|
||||
name="keywords"
|
||||
content="antd,umi,umijs,ant design,Scaffolding, layout, Ant Design, project, Pro, admin, console, homepage, out-of-the-box, middle and back office, solution, component library"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="
|
||||
An out-of-box UI solution for enterprise applications as a React boilerplate."
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="
|
||||
Out-of-the-box mid-stage front-end/design solution."
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<title>Pro</title>
|
||||
<link rel="icon" href="<%= context.config.publicPath +'logo.png'%>" type="image/png" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div class="noscript-container">
|
||||
Hi there! Please
|
||||
<div class="noscript-enableJS">
|
||||
<a href="https://www.enablejavascript.io/en" target="_blank" rel="noopener noreferrer">
|
||||
<b>enable Javascript</b>
|
||||
</a>
|
||||
</div>
|
||||
in your browser to use Ant Design, Out-of-the-box mid-stage front/design solution!
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="root">
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#root {
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
.noscript-container {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
margin-top: 90px;
|
||||
font-size: 20px;
|
||||
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode',
|
||||
Geneva, Verdana, sans-serif;
|
||||
}
|
||||
.noscript-enableJS {
|
||||
padding-right: 3px;
|
||||
padding-left: 3px;
|
||||
}
|
||||
.page-loading-warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 98px;
|
||||
}
|
||||
.ant-spin {
|
||||
position: absolute;
|
||||
display: none;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
|
||||
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
-webkit-font-feature-settings: 'tnum';
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.ant-spin-spinning {
|
||||
position: static;
|
||||
display: inline-block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ant-spin-dot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-color: #1890ff;
|
||||
border-radius: 100%;
|
||||
-webkit-transform: scale(0.75);
|
||||
-ms-transform: scale(0.75);
|
||||
transform: scale(0.75);
|
||||
-webkit-transform-origin: 50% 50%;
|
||||
-ms-transform-origin: 50% 50%;
|
||||
transform-origin: 50% 50%;
|
||||
opacity: 0.3;
|
||||
-webkit-animation: antspinmove 1s infinite linear alternate;
|
||||
animation: antSpinMove 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(1) {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(2) {
|
||||
top: 0;
|
||||
right: 0;
|
||||
-webkit-animation-delay: 0.4s;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(3) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
-webkit-animation-delay: 0.8s;
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-item:nth-child(4) {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
|
||||
.ant-spin-dot-spin {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-animation: antrotate 1.2s infinite linear;
|
||||
animation: antRotate 1.2s infinite linear;
|
||||
}
|
||||
|
||||
.ant-spin-lg .ant-spin-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.ant-spin-lg .ant-spin-dot i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
||||
.ant-spin-blur {
|
||||
background: #fff;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antSpinMove {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes antRotate {
|
||||
to {
|
||||
-webkit-transform: rotate(405deg);
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
"
|
||||
>
|
||||
<!-- <img src="<%= context.config.publicPath +'logo.svg'%>" alt="logo" width="256" /> -->
|
||||
<div class="page-loading-warp">
|
||||
<div class="ant-spin ant-spin-lg ant-spin-spinning">
|
||||
<span class="ant-spin-dot ant-spin-dot-spin"
|
||||
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
|
||||
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: center">
|
||||
<img
|
||||
src="<%= context.config.publicPath +'logo.svg'%>"
|
||||
width="32"
|
||||
style="margin-right: 8px"
|
||||
/>
|
||||
Pro
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import RoutePath from './routePath';
|
||||
|
||||
/*
|
||||
文档
|
||||
https://umijs.org/zh-CN/plugins/plugin-layout
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: '/login',
|
||||
layout: false,
|
||||
component: './Login',
|
||||
},
|
||||
{
|
||||
name: '设置',
|
||||
path: RoutePath.SETTING,
|
||||
routes: [
|
||||
{
|
||||
path: RoutePath.SETTING,
|
||||
redirect: RoutePath.WORK.LIST,
|
||||
hideInMenu: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: '作品',
|
||||
path: RoutePath.WORK.LIST,
|
||||
component: './Work/List',
|
||||
},
|
||||
{
|
||||
name: '作品详情',
|
||||
path: RoutePath.WORK.EDIT,
|
||||
hideInMenu: true,
|
||||
component: './Work/Edit',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: RoutePath.WORK.LIST,
|
||||
},
|
||||
|
||||
{
|
||||
component: './404',
|
||||
},
|
||||
];
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
const SETTING = '/setting';
|
||||
const RoutePath = {
|
||||
LOGIN: '/login',
|
||||
SETTING: SETTING,
|
||||
WORK: {
|
||||
LIST: `${SETTING}/work`,
|
||||
EDIT: `${SETTING}/work/edit`,
|
||||
},
|
||||
};
|
||||
|
||||
export default RoutePath;
|
||||
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import request from '@/utils/request';
|
||||
|
||||
export const login = (data) => {
|
||||
return request.request({
|
||||
url: '/admin/login',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import request from '@/utils/request';
|
||||
|
||||
export const queryWorkList = (params) => {
|
||||
return request.request({
|
||||
url: '/admin/work/page',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const addWork = (data) => {
|
||||
return request.request({
|
||||
url: '/admin/work',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const updateWork = (data) => {
|
||||
return request.request({
|
||||
url: '/admin/work',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteWork = (params) => {
|
||||
return request.request({
|
||||
url: '/admin/work',
|
||||
method: 'delete',
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const queryWorkDetail = (params: { id: string }) => {
|
||||
return request.request({
|
||||
url: '/admin/work',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
.table-btn() {
|
||||
.delete-btn {
|
||||
color: @error-color !important;
|
||||
}
|
||||
td:not(:last-child) {
|
||||
a {
|
||||
color: #035cc1;
|
||||
&:hover {
|
||||
color: #035cc1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-table() {
|
||||
.ant-table {
|
||||
.table-btn();
|
||||
&-tbody > tr:not(.ant-table-measure-row) > td {
|
||||
padding: 12px;
|
||||
color: @text-color;
|
||||
border-right: 0;
|
||||
}
|
||||
&-container {
|
||||
border: 0;
|
||||
}
|
||||
&-thead > tr > th {
|
||||
padding: 12px;
|
||||
color: @text-color;
|
||||
white-space: nowrap;
|
||||
background: #f5f7fa;
|
||||
border: 0;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
bottom: 10px;
|
||||
width: 1px;
|
||||
background: #e4ebf1;
|
||||
content: '';
|
||||
}
|
||||
&.ant-table-selection-column {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
&-thead > tr > th:nth-child(1),
|
||||
&-tbody > tr > td:nth-child(1) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
&-tbody > tr {
|
||||
&:nth-child(2n) {
|
||||
td {
|
||||
background: #fafbfc;
|
||||
}
|
||||
}
|
||||
td.ant-table-selection-column {
|
||||
text-align: left;
|
||||
}
|
||||
&:not(.ant-table-placeholder):hover {
|
||||
td {
|
||||
background: #effdf6;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-column-sorters {
|
||||
padding: 0;
|
||||
}
|
||||
&.ant-table-bordered
|
||||
> .ant-table-container
|
||||
> .ant-table-content
|
||||
> table
|
||||
> tbody
|
||||
> tr
|
||||
> .ant-table-cell-fix-right-first::after {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal() {
|
||||
.ant-modal {
|
||||
&-header {
|
||||
padding: 19px 20px;
|
||||
}
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
line-height: 18px;
|
||||
}
|
||||
&-close-x {
|
||||
height: 56px;
|
||||
color: @text-color;
|
||||
line-height: 56px;
|
||||
}
|
||||
&-content {
|
||||
max-width: 1200px;
|
||||
.ant-modal-body {
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
.ant-pro-table {
|
||||
.ant-pro-table-search {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.ls-pagination {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
&-body {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.at-pagegation () {
|
||||
.ant-pagination {
|
||||
&-item {
|
||||
margin-right: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
&-item-active {
|
||||
background: @primary-color;
|
||||
border-color: @primary-color;
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.ant-pagination-item-link,
|
||||
.ant-select-selector,
|
||||
&-options-quick-jumper input {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
.ant-pagination.mini {
|
||||
.ant-pagination-item {
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ant-select-selector,
|
||||
.ant-pagination-options-quick-jumper input {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-btn() {
|
||||
.ant-btn {
|
||||
&-lg {
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-pro-table() {
|
||||
.ant-pro-table {
|
||||
word-break: break-all;
|
||||
.table-btn();
|
||||
&.tool-bar-padding-top-0 {
|
||||
.ant-pro-table-list-toolbar-container {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
&.no-tool-bar {
|
||||
.ant-card {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
.ant-pro-table-list-toolbar {
|
||||
.ant-alert {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.ant-pro-table-list-toolbar-container {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
padding: 0 0 20px 0;
|
||||
.ant-pro-table-list-toolbar-left {
|
||||
.ant-space-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.ant-pro-table-list-toolbar-right {
|
||||
justify-content: start;
|
||||
}
|
||||
}
|
||||
.ant-pro-table-search {
|
||||
margin-bottom: 20px;
|
||||
padding-top: 20px;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
.ant-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
.ant-card {
|
||||
padding-top: 20px;
|
||||
&-body {
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
.ant-pro-table-alert {
|
||||
.ant-alert-info {
|
||||
padding: 8px 16px;
|
||||
a {
|
||||
color: @info-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-pro-form-collapse-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
span.anticon {
|
||||
position: relative;
|
||||
&::after {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-top: 6px;
|
||||
border-color: @primary-color transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 6px 5px;
|
||||
transform-origin: top;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-tabs() {
|
||||
.ant-tabs {
|
||||
&.ant-tabs-top {
|
||||
background: #fff;
|
||||
&.padding {
|
||||
.ant-tabs-content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
.ant-tabs-nav {
|
||||
&:hover {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
background: #f4f6f9;
|
||||
.ant-tabs-tab {
|
||||
margin: 0;
|
||||
padding: 10px 24px;
|
||||
border-right: 1px solid #e6edf2;
|
||||
&.ant-tabs-tab-active {
|
||||
color: @text-color;
|
||||
font-weight: bold;
|
||||
background: #fff;
|
||||
.ant-tabs-tab-btn {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ls-pagination {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.ant-pro-table {
|
||||
.ant-pro-table-search {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-pro-page-container() {
|
||||
.ant-pro-page-container {
|
||||
.ant-pro-page-container-warp {
|
||||
background: transparent;
|
||||
.ant-page-header {
|
||||
padding: 0 0 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-breadcrumb() {
|
||||
.ant-breadcrumb > span:last-child {
|
||||
color: @text-color;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
.ant-table() !important;
|
||||
.ant-modal();
|
||||
.at-pagegation () !important;
|
||||
.ant-btn();
|
||||
.ant-pro-table();
|
||||
.ant-pro-page-container-children-content {
|
||||
margin: 20px;
|
||||
}
|
||||
.ant-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.ant-form-item-label > label {
|
||||
color: rgba(@text-color, 0.8);
|
||||
}
|
||||
.ant-breadcrumb();
|
||||
.ant-formily-array-base-sort-handle {
|
||||
color: #888 !important;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { Location } from 'history';
|
||||
declare module 'slash2';
|
||||
declare module '*.css';
|
||||
declare module '*.less';
|
||||
declare module '*.scss';
|
||||
declare module '*.sass';
|
||||
declare module '*.svg';
|
||||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.gif';
|
||||
declare module '*.bmp';
|
||||
declare module '*.tiff';
|
||||
declare module 'omit.js';
|
||||
declare module 'numeral';
|
||||
declare module '@antv/data-set';
|
||||
declare module 'mockjs';
|
||||
declare module 'react-fittext';
|
||||
declare module 'bizcharts-plugin-slider';
|
||||
|
||||
// preview.pro.ant.design only do not use in your production ;
|
||||
// preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
|
||||
declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;
|
||||
|
||||
declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;
|
||||
export interface RouterLocation<S> extends Location<S> {
|
||||
query: Record<string, string>; /// nope
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { FileType } from '@/constants/enum/fileType';
|
||||
|
||||
const imgTypeList = ['png', 'jpg', 'gif', 'jpeg', 'nmp'];
|
||||
const videoTypeList = ['mp4'];
|
||||
export const getFileType = (url: string = '') => {
|
||||
// 获取最后一个.的位置
|
||||
const index = url.lastIndexOf('.');
|
||||
// 获取后缀
|
||||
const ext = url.substr(index + 1);
|
||||
if (imgTypeList.indexOf(ext) > -1) {
|
||||
return FileType.IMAGE;
|
||||
} else if (videoTypeList.indexOf(ext) > -1) {
|
||||
return FileType.VIDEO;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export default getFileType;
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import axios from 'axios';
|
||||
import { notification } from 'antd';
|
||||
import RoutePath from '@/routes/routePath';
|
||||
import { history } from 'umi';
|
||||
import { CACHE_TOKEN } from '@/constants/cacheKey';
|
||||
|
||||
// create an axios instance
|
||||
const request = axios.create({
|
||||
baseURL: '', //
|
||||
timeout: 10000, // request timeout
|
||||
});
|
||||
// request interceptor
|
||||
request.interceptors.request.use(
|
||||
(memo: any) => {
|
||||
memo.headers.token = localStorage.getItem(CACHE_TOKEN);
|
||||
return memo;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const res: any = response.data;
|
||||
if (res.code !== 200) {
|
||||
if (res.code === 401) {
|
||||
notification.error({
|
||||
message: '错误信息',
|
||||
description: '登录失效',
|
||||
});
|
||||
history.replace(RoutePath.LOGIN);
|
||||
localStorage.removeItem(CACHE_TOKEN);
|
||||
return Promise.reject(new Error('Login expiration'));
|
||||
}
|
||||
notification.error({
|
||||
message: '错误信息',
|
||||
description: res.msg,
|
||||
});
|
||||
return Promise.reject(res.msg || 'Error');
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export const baseRequestForm = (url: string, data: object, ...rest: any) => {
|
||||
const form = new FormData();
|
||||
Object.keys(data).map((key) => {
|
||||
form.append(key, data[key]);
|
||||
return key;
|
||||
});
|
||||
return request({
|
||||
url,
|
||||
method: 'post',
|
||||
body: form,
|
||||
...rest,
|
||||
});
|
||||
};
|
||||
|
||||
export const baseRequestGet = (url: string, params) => request({ url, method: 'get', params });
|
||||
|
||||
export const baseRequestPost = (url: string, data) => request({ url, method: 'post', data });
|
||||
|
||||
export default request;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// 格式化表格获取数据接口的返回
|
||||
export const fetchTableData = async (
|
||||
fetch: (params: any) => Promise<any>,
|
||||
params: any,
|
||||
formatObj: any = {},
|
||||
) => {
|
||||
params.page = params.current;
|
||||
params.num = params.pageSize;
|
||||
delete params.current;
|
||||
delete params.pageSize;
|
||||
const res = (await fetch(params)) || {};
|
||||
const data = res.data;
|
||||
|
||||
data?.forEach((n: any) => {
|
||||
for (const key in formatObj) {
|
||||
n[key] = n[formatObj[key]];
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
total: res.total,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// 选择作品弹窗
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Image } from 'antd';
|
||||
import Table, { ProColumns, ActionType } from '@/components/Table';
|
||||
import Modal, { ModalProps } from '@/components/Modal';
|
||||
import { queryWorkList } from '@/services/work';
|
||||
import { fetchTableData } from '@/utils/table';
|
||||
|
||||
interface WorkSelectModalPropsType extends ModalProps {
|
||||
value: any[];
|
||||
onOk: (val: any) => void;
|
||||
type?: 'checkbox' | 'radio';
|
||||
}
|
||||
const WorkSelectModal = ({
|
||||
value = [],
|
||||
onOk,
|
||||
type = 'checkbox',
|
||||
...rest
|
||||
}: WorkSelectModalPropsType) => {
|
||||
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
||||
const actionRef = useRef<ActionType>();
|
||||
const columns: ProColumns<any>[] = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'work_id',
|
||||
width: '10%',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '封面',
|
||||
dataIndex: 'url',
|
||||
valueType: 'image',
|
||||
hideInSearch: true,
|
||||
width: '30%',
|
||||
render: (_, row) => {
|
||||
return <Image src={row.cover_resource?.url} height={100} alt="" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
dataIndex: 'rarity_name',
|
||||
width: '10%',
|
||||
hideInSearch: true,
|
||||
},
|
||||
];
|
||||
const handleOk = () => {
|
||||
onOk && onOk(selectedRows);
|
||||
};
|
||||
const handleMerge = (keys, selectedInfos) => {
|
||||
const newInfo: any = [];
|
||||
const valueMap = {};
|
||||
const selectedInfosMap = {};
|
||||
value.forEach((item) => (valueMap[item.work_id] = item));
|
||||
selectedInfos.filter((item) => item).forEach((item) => (selectedInfosMap[item.work_id] = item));
|
||||
keys.forEach((item) => {
|
||||
newInfo.push(selectedInfosMap[item] || valueMap[item]);
|
||||
});
|
||||
setSelectedRows(newInfo);
|
||||
};
|
||||
useEffect(() => {
|
||||
setSelectedRows(value);
|
||||
}, [value]);
|
||||
return (
|
||||
<Modal title="选择作品" onOk={handleOk} width={800} {...rest}>
|
||||
<Table
|
||||
columns={columns}
|
||||
actionRef={actionRef}
|
||||
rowKey="work_id"
|
||||
rowSelection={{
|
||||
type,
|
||||
selectedRowKeys: selectedRows?.map((item) => item.work_id) || [],
|
||||
onChange: (keys, info) => {
|
||||
handleMerge(keys, info);
|
||||
},
|
||||
}}
|
||||
request={async (params) => {
|
||||
const res = await fetchTableData(queryWorkList, params);
|
||||
res.data?.forEach((item) => {
|
||||
item.work_id = item.id;
|
||||
delete item.id;
|
||||
});
|
||||
return res;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkSelectModal;
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "build/dist",
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"lib": ["esnext", "dom"],
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "node",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowJs": true,
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@@/*": ["./src/.umi/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"mock/**/*",
|
||||
"src/**/*",
|
||||
"tests/**/*",
|
||||
"test/**/*",
|
||||
"__test__/**/*",
|
||||
"typings/**/*",
|
||||
"config/**/*",
|
||||
".eslintrc.js",
|
||||
".stylelintrc.js",
|
||||
".prettierrc.js",
|
||||
"jest.config.js",
|
||||
"mock/*"
|
||||
],
|
||||
"exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"]
|
||||
}
|
||||
Loading…
Reference in New Issue