From 1add80ee83c2f42ff8dd903b28afbcef62e6b319 Mon Sep 17 00:00:00 2001 From: gary <1032230992@qq.com> Date: Tue, 7 Jun 2022 13:56:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 16 + .eslintignore | 8 + .eslintrc.js | 21 + .gitignore | 40 + .prettierignore | 23 + .prettierrc.js | 5 + .stylelintrc.js | 5 + .vscode/extensions.json | 8 + .vscode/settings.json | 5 + README.md | 68 +- build.js | 53 + config/config.dev.ts | 15 + config/config.ts | 77 + config/defaultSettings.ts | 21 + config/proxy.ts | 20 + config/theme/index.ts | 18 + jsconfig.json | 11 + package.json | 116 + public/CNAME | 1 + public/favicon.ico | Bin 0 -> 4286 bytes public/icons/icon-128x128.png | Bin 0 -> 1329 bytes public/icons/icon-192x192.png | Bin 0 -> 1856 bytes public/icons/icon-512x512.png | Bin 0 -> 5082 bytes public/logo.svg | 1 + public/pro_icon.svg | 5 + src/access.ts | 9 + src/app.tsx | 29 + src/components/Breadcrumb/defaultSettings.ts | 75 + .../Breadcrumb/getBreadcrumbProps.tsx | 137 + src/components/Breadcrumb/getMenuData.ts | 43 + src/components/Breadcrumb/index.tsx | 111 + src/components/Breadcrumb/pathTools.ts | 9 + src/components/Breadcrumb/typings.ts | 61 + src/components/DetailPageContainer/index.less | 4 + src/components/DetailPageContainer/index.tsx | 8 + src/components/Editor/EditorPreview.tsx | 66 + src/components/Editor/index.tsx | 133 + src/components/HeaderDropdown/index.less | 16 + src/components/HeaderDropdown/index.tsx | 17 + src/components/Modal/index.tsx | 7 + src/components/NoticeIcon/NoticeIcon.tsx | 126 + src/components/NoticeIcon/NoticeList.less | 103 + src/components/NoticeIcon/NoticeList.tsx | 113 + src/components/NoticeIcon/index.less | 35 + src/components/NoticeIcon/index.tsx | 153 + .../RightContent/AvatarDropdown.tsx | 97 + src/components/RightContent/index.less | 84 + src/components/RightContent/index.tsx | 25 + src/components/Table/DeleteButton/index.less | 13 + src/components/Table/DeleteButton/index.tsx | 23 + src/components/Table/index.tsx | 261 + src/components/Typography/TimeText.tsx | 23 + src/components/Upload/index.less | 14 + src/components/Upload/index.tsx | 98 + src/constants/cacheKey.ts | 1 + src/constants/enum/fileType.ts | 5 + src/constants/enum/uploadBizType.ts | 11 + src/constants/enum/work.ts | 13 + src/global.less | 61 + src/global.tsx | 91 + src/manifest.json | 22 + src/pages/404.tsx | 18 + src/pages/Login/index.less | 55 + src/pages/Login/index.tsx | 87 + .../Work/Edit/components/AuthorSelector.tsx | 60 + src/pages/Work/Edit/index.tsx | 289 + src/pages/Work/List/index.tsx | 97 + src/pages/document.ejs | 235 + src/routes/index.ts | 44 + src/routes/routePath.ts | 11 + src/service-worker.js | 65 + src/services/login.ts | 9 + src/services/work.ts | 41 + src/styles/antdGlobal.less | 324 + src/typings.d.ts | 28 + src/utils/getFileType.ts | 17 + src/utils/request.ts | 67 + src/utils/table.ts | 24 + src/widget/Work/WorkSelectModal.tsx | 93 + tsconfig.json | 42 + yarn.lock | 16094 ++++++++++++++++ 81 files changed, 20333 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 .stylelintrc.js create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 build.js create mode 100644 config/config.dev.ts create mode 100644 config/config.ts create mode 100644 config/defaultSettings.ts create mode 100644 config/proxy.ts create mode 100644 config/theme/index.ts create mode 100644 jsconfig.json create mode 100644 package.json create mode 100644 public/CNAME create mode 100644 public/favicon.ico create mode 100644 public/icons/icon-128x128.png create mode 100644 public/icons/icon-192x192.png create mode 100644 public/icons/icon-512x512.png create mode 100644 public/logo.svg create mode 100644 public/pro_icon.svg create mode 100644 src/access.ts create mode 100644 src/app.tsx create mode 100644 src/components/Breadcrumb/defaultSettings.ts create mode 100644 src/components/Breadcrumb/getBreadcrumbProps.tsx create mode 100644 src/components/Breadcrumb/getMenuData.ts create mode 100644 src/components/Breadcrumb/index.tsx create mode 100644 src/components/Breadcrumb/pathTools.ts create mode 100644 src/components/Breadcrumb/typings.ts create mode 100644 src/components/DetailPageContainer/index.less create mode 100644 src/components/DetailPageContainer/index.tsx create mode 100644 src/components/Editor/EditorPreview.tsx create mode 100644 src/components/Editor/index.tsx create mode 100644 src/components/HeaderDropdown/index.less create mode 100644 src/components/HeaderDropdown/index.tsx create mode 100644 src/components/Modal/index.tsx create mode 100644 src/components/NoticeIcon/NoticeIcon.tsx create mode 100755 src/components/NoticeIcon/NoticeList.less create mode 100644 src/components/NoticeIcon/NoticeList.tsx create mode 100644 src/components/NoticeIcon/index.less create mode 100644 src/components/NoticeIcon/index.tsx create mode 100644 src/components/RightContent/AvatarDropdown.tsx create mode 100644 src/components/RightContent/index.less create mode 100644 src/components/RightContent/index.tsx create mode 100644 src/components/Table/DeleteButton/index.less create mode 100644 src/components/Table/DeleteButton/index.tsx create mode 100644 src/components/Table/index.tsx create mode 100644 src/components/Typography/TimeText.tsx create mode 100644 src/components/Upload/index.less create mode 100644 src/components/Upload/index.tsx create mode 100644 src/constants/cacheKey.ts create mode 100644 src/constants/enum/fileType.ts create mode 100644 src/constants/enum/uploadBizType.ts create mode 100644 src/constants/enum/work.ts create mode 100644 src/global.less create mode 100644 src/global.tsx create mode 100644 src/manifest.json create mode 100644 src/pages/404.tsx create mode 100644 src/pages/Login/index.less create mode 100644 src/pages/Login/index.tsx create mode 100644 src/pages/Work/Edit/components/AuthorSelector.tsx create mode 100644 src/pages/Work/Edit/index.tsx create mode 100644 src/pages/Work/List/index.tsx create mode 100644 src/pages/document.ejs create mode 100644 src/routes/index.ts create mode 100644 src/routes/routePath.ts create mode 100644 src/service-worker.js create mode 100644 src/services/login.ts create mode 100644 src/services/work.ts create mode 100644 src/styles/antdGlobal.less create mode 100644 src/typings.d.ts create mode 100644 src/utils/getFileType.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/table.ts create mode 100644 src/widget/Work/WorkSelectModal.tsx create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7e3649a --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..8336e93 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +/lambda/ +/scripts +/config +.history +public +dist +.umi +mock \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..6991ad1 --- /dev/null +++ b/.eslintrc.js @@ -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, + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d504cd8 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d17efb4 --- /dev/null +++ b/.prettierignore @@ -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 \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..7b597d7 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,5 @@ +const fabric = require('@umijs/fabric'); + +module.exports = { + ...fabric.prettier, +}; diff --git a/.stylelintrc.js b/.stylelintrc.js new file mode 100644 index 0000000..c203078 --- /dev/null +++ b/.stylelintrc.js @@ -0,0 +1,5 @@ +const fabric = require('@umijs/fabric'); + +module.exports = { + ...fabric.stylelint, +}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..33f300d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint", + "wangzy.sneak-mark" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a5d9d03 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.formatOnSave": true, + "prettier.requireConfig": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" +} diff --git a/README.md b/README.md index 986cf4c..747cea4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,68 @@ -# frontend-recharge-system +# 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 +yarn 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). + +### 参考文档 + +1. [ts 入门](https://ts.xcatliu.com/) +1. [react hook 入门](https://www.ruanyifeng.com/blog/2019/09/react-hooks.html) +1. [antd4.0](https://ant-design.gitee.io/components/overview-cn/) +1. [@ant-design/pro-layout](https://procomponents.ant.design/components/layout) +1. [@ant-design/icons](https://ant.design/components/icon-cn/#API) +1. [@ant-design/pro-table](https://procomponents.ant.design/components/table) +1. [@formily/antd-components](https://formilyjs.org/#/zoi8i0/ZrsYs6hytQ) +1. [@formily](https://formilyjs.org) diff --git a/build.js b/build.js new file mode 100644 index 0000000..ac6100a --- /dev/null +++ b/build.js @@ -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); +} diff --git a/config/config.dev.ts b/config/config.dev.ts new file mode 100644 index 0000000..ab0e590 --- /dev/null +++ b/config/config.dev.ts @@ -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: {}, + }, +}); diff --git a/config/config.ts b/config/config.ts new file mode 100644 index 0000000..a4cbaed --- /dev/null +++ b/config/config.ts @@ -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: {}, +}); diff --git a/config/defaultSettings.ts b/config/defaultSettings.ts new file mode 100644 index 0000000..1077b94 --- /dev/null +++ b/config/defaultSettings.ts @@ -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; diff --git a/config/proxy.ts b/config/proxy.ts new file mode 100644 index 0000000..4f407c4 --- /dev/null +++ b/config/proxy.ts @@ -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 代理到 https + // 依赖 origin 的功能可能需要这个,比如 cookie + changeOrigin: true, + }, + }, +}; diff --git a/config/theme/index.ts b/config/theme/index.ts new file mode 100644 index 0000000..8b2f0c9 --- /dev/null +++ b/config/theme/index.ts @@ -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', +}; diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..197bee5 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e4df34 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/CNAME b/public/CNAME new file mode 100644 index 0000000..30c2d4d --- /dev/null +++ b/public/CNAME @@ -0,0 +1 @@ +preview.pro.ant.design \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e2e93252988b5bed120ddeb168eb577bf0caa881 GIT binary patch literal 4286 zcmeHLO(?8k6#ga#Gs=ufvobTd=q@y~nOLdYlqL%nQY4WqC`)z}3y~cqOAAF4VkQd~ z^3$wrC}~D1l%!eMp!|gA&U5sN?~b{2>)yNg-s#Lc-+9k_p7Y<=XqqYcDJs(Vt%dBG z=F~Lpdy*pd!Y zozZT}udJ-#| zqiAVq0sT@kAMMobbULxOw+F6{j*egO8CQ?TgTuo^+}_@zrlv;l$*-)e#PRVl?(Xgo zi9`?zg>ZFsh0DuJ)Ya9!von7LPN@OfP9IfH_4tet&&I~a#O{TK1<3*LO-xJ( zZ*Fc5%v&m4#Z&mZySs8{XJ-fV^Yh@{p`ju8d_J)w6+U%`!(qAC+uJK~qg_o+O|QC` zTVg0Y{jSHqzP^^&G0t4^cwF+$`egj?@9$AtTMLuPB>7!lUKY8&zP?xd=H_PM(U;HS ztNi!(_oJes0<5X6tu4{BxVR{FO)_x3UauFkv$I%RTSHe@7dAIHF+4mhc?|>t(!XbCXVQZk z8yje7Xh2R*&WCwdw&?N4$H!4$Uyq)i9_;V$3;y)^-LKhmkv{Z#(CP^-7Wn# zI5-H>z`y`nTU%u=f7Tbh-mg5fPT0G8`Wx6^vD1(>-r0;AFlyjWYC!!D@UQlt)%}4^ literal 0 HcmV?d00001 diff --git a/public/icons/icon-128x128.png b/public/icons/icon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..48d0e2339a60a637b94319c65e8654289b4f4b6c GIT binary patch literal 1329 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

*C7T$?5!b= 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 28 Copy 5Created with Sketch. \ No newline at end of file diff --git a/public/pro_icon.svg b/public/pro_icon.svg new file mode 100644 index 0000000..e075b78 --- /dev/null +++ b/public/pro_icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/access.ts b/src/access.ts new file mode 100644 index 0000000..2ec89bd --- /dev/null +++ b/src/access.ts @@ -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', + }; +} diff --git a/src/app.tsx b/src/app.tsx new file mode 100644 index 0000000..c667cc9 --- /dev/null +++ b/src/app.tsx @@ -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: () => , + disableContentMargin: false, + onPageChange: () => { + if (!localStorage.getItem(CACHE_TOKEN)) { + history.push(RoutePath.LOGIN); + } + }, + + menuHeaderRender: undefined, + // 自定义 403 页面 + // unAccessible:

unAccessible
, + // 增加一个 loading 的状态 + childrenRender: (children) => { + const pathname = location.hash.replace('#', ''); + return pathname === RoutePath.LOGIN ? children : {children}; + }, + ...initialState?.settings, + }; +}; diff --git a/src/components/Breadcrumb/defaultSettings.ts b/src/components/Breadcrumb/defaultSettings.ts new file mode 100644 index 0000000..3a9a56f --- /dev/null +++ b/src/components/Breadcrumb/defaultSettings.ts @@ -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; diff --git a/src/components/Breadcrumb/getBreadcrumbProps.tsx b/src/components/Breadcrumb/getBreadcrumbProps.tsx new file mode 100644 index 0000000..531e84b --- /dev/null +++ b/src/components/Breadcrumb/getBreadcrumbProps.tsx @@ -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; + 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 }) => ( + {breadcrumbName} +); + +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, + 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, + 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 +>; + +/** 将参数转化为面包屑 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, + }; +}; diff --git a/src/components/Breadcrumb/getMenuData.ts b/src/components/Breadcrumb/getMenuData.ts new file mode 100644 index 0000000..eada06b --- /dev/null +++ b/src/components/Breadcrumb/getMenuData.ts @@ -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, [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, + }; +}; diff --git a/src/components/Breadcrumb/index.tsx b/src/components/Breadcrumb/index.tsx new file mode 100644 index 0000000..376137b --- /dev/null +++ b/src/components/Breadcrumb/index.tsx @@ -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([]); +const PageComponent = (props: any) => { + const [menuInfoData, setMenuInfoData] = useMergedState<{ + breadcrumb?: Record; + breadcrumbMap?: Map; + 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 ? ( + {route.breadcrumbName} + ) : ( + { + breadClick(route); + }} + > + {route.breadcrumbName} + + ); + }; + + 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 ( + <> + + + + + + ); +}; + +export default PageComponent; diff --git a/src/components/Breadcrumb/pathTools.ts b/src/components/Breadcrumb/pathTools.ts new file mode 100644 index 0000000..76c9ede --- /dev/null +++ b/src/components/Breadcrumb/pathTools.ts @@ -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('/')}`); +} diff --git a/src/components/Breadcrumb/typings.ts b/src/components/Breadcrumb/typings.ts new file mode 100644 index 0000000..6eb3c9b --- /dev/null +++ b/src/components/Breadcrumb/typings.ts @@ -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; +} & React.AnchorHTMLAttributes; + +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 | false; + +export type RouterTypes

= { + computedMatch?: match

; + route?: Route; + location: BasicRouteProps['location'] | { pathname?: string }; +} & Omit; + +export type MessageDescriptor = { + id: any; + description?: string; + defaultMessage?: string; +}; diff --git a/src/components/DetailPageContainer/index.less b/src/components/DetailPageContainer/index.less new file mode 100644 index 0000000..98d8898 --- /dev/null +++ b/src/components/DetailPageContainer/index.less @@ -0,0 +1,4 @@ +.container { + padding: 40px 20px; + background: #fff; +} diff --git a/src/components/DetailPageContainer/index.tsx b/src/components/DetailPageContainer/index.tsx new file mode 100644 index 0000000..0ab35e5 --- /dev/null +++ b/src/components/DetailPageContainer/index.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import styles from './index.less'; + +const DetailPageContainer: React.FC = ({ children }) => { + return

{children}
; +}; + +export default DetailPageContainer; diff --git a/src/components/Editor/EditorPreview.tsx b/src/components/Editor/EditorPreview.tsx new file mode 100644 index 0000000..15cc5e2 --- /dev/null +++ b/src/components/Editor/EditorPreview.tsx @@ -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 = (props) => { + const { + value, + visible, + title, + width, + renderHeader, + renderFooter, + onCancel, + prefixCls, + className, + style, + } = props; + const previewRef = useRef(null); + setTimeout(() => { + if (previewRef.current) { + previewRef.current.innerHTML = value; + } + }, 0); + return ( + + {typeof renderHeader === 'function' ? renderHeader() : renderHeader} +
+ {typeof renderFooter === 'function' ? renderFooter() : renderFooter} + + ); +}; + +EditorPreview.defaultProps = { + visible: false, + title: '图文详情预览', + value: '', + width: 375, + onCancel: () => {}, + className: '', + style: {}, +}; + +export default EditorPreview; diff --git a/src/components/Editor/index.tsx b/src/components/Editor/index.tsx new file mode 100644 index 0000000..43c1cf8 --- /dev/null +++ b/src/components/Editor/index.tsx @@ -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 = (props) => { + const { value, request, action, data, prefixCls, className, style, onChange, ...rest } = props; + // 富文本数据是否初始化 + const [isInit, setIsInit] = useState(false); + // 富文本 + const [editorState, setEditorState] = useState(BraftEditor.createEditorState(value)); + // 富文本预览 + const [previewVisible, setPreviewVisible] = useState(false); + // onChange + const handleEditorChange = (newEditorState: EditorState) => { + if (isInit || (!isInit && value === '

')) { + setIsInit(true); + setEditorState(newEditorState); + const changeValue = newEditorState.toHTML() === '

' ? '' : 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 !== '

') { + setEditorState(BraftEditor.createEditorState(value)); + setIsInit(true); + } + }, [value]); + return ( +
+ + setPreviewVisible(false)} + /> +
+ ); +}; + +Editor.defaultProps = { + value: '

', + data: {}, + className: '', + style: {}, +}; + +export default Editor; diff --git a/src/components/HeaderDropdown/index.less b/src/components/HeaderDropdown/index.less new file mode 100644 index 0000000..004b53e --- /dev/null +++ b/src/components/HeaderDropdown/index.less @@ -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; + } +} diff --git a/src/components/HeaderDropdown/index.tsx b/src/components/HeaderDropdown/index.tsx new file mode 100644 index 0000000..45af90a --- /dev/null +++ b/src/components/HeaderDropdown/index.tsx @@ -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; + +const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( + +); + +export default HeaderDropdown; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx new file mode 100644 index 0000000..175bae9 --- /dev/null +++ b/src/components/Modal/index.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { Modal as AntModal, ModalProps } from 'antd'; +const Modal: React.FC = ({ okText = '确认', ...rest }) => { + return ; +}; +export type { ModalProps }; +export default Modal; diff --git a/src/components/NoticeIcon/NoticeIcon.tsx b/src/components/NoticeIcon/NoticeIcon.tsx new file mode 100644 index 0000000..7a530c0 --- /dev/null +++ b/src/components/NoticeIcon/NoticeIcon.tsx @@ -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[]; +}; + +const NoticeIcon: React.FC & { + 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): 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( + + 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} + /> + , + ); + }); + return ( + <> + + + {panes} + + + + ); + }; + + const { className, count, bell } = props; + + const [visible, setVisible] = useMergedState(false, { + value: props.popupVisible, + onChange: props.onPopupVisibleChange, + }); + const noticeButtonClass = classNames(className, styles.noticeButton); + const notificationBox = getNotificationBox(); + const NoticeBellIcon = bell || ; + const trigger = ( + + + {NoticeBellIcon} + + + ); + if (!notificationBox) { + return trigger; + } + + return ( + + {trigger} + + ); +}; + +NoticeIcon.defaultProps = { + emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', +}; + +NoticeIcon.Tab = NoticeList; + +export default NoticeIcon; diff --git a/src/components/NoticeIcon/NoticeList.less b/src/components/NoticeIcon/NoticeList.less new file mode 100755 index 0000000..65e0c40 --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.less @@ -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; + } + } +} diff --git a/src/components/NoticeIcon/NoticeList.tsx b/src/components/NoticeIcon/NoticeList.tsx new file mode 100644 index 0000000..5face39 --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.tsx @@ -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 = ({ + list = [], + onClick, + onClear, + title, + onViewMore, + emptyText, + showClear = true, + clearText, + viewMoreText, + showViewMore = false, +}) => { + if (!list || list.length === 0) { + return ( +
+ not found +
{emptyText}
+
+ ); + } + return ( +
+ + 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' ? ( + + ) : ( + {item.avatar} + ) + ) : null; + + return ( + { + onClick?.(item); + }} + > + + {item.title} +
{item.extra}
+
+ } + description={ +
+
{item.description}
+
{item.datetime}
+
+ } + /> + + ); + }} + /> +
+ {showClear ? ( +
+ {clearText} {title} +
+ ) : null} + {showViewMore ? ( +
{ + if (onViewMore) { + onViewMore(e); + } + }} + > + {viewMoreText} +
+ ) : null} +
+
+ ); +}; + +export default NoticeList; diff --git a/src/components/NoticeIcon/index.less b/src/components/NoticeIcon/index.less new file mode 100644 index 0000000..45251cd --- /dev/null +++ b/src/components/NoticeIcon/index.less @@ -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; + } + } +} diff --git a/src/components/NoticeIcon/index.tsx b/src/components/NoticeIcon/index.tsx new file mode 100644 index 0000000..326a363 --- /dev/null +++ b/src/components/NoticeIcon/index.tsx @@ -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 => { + 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 = ( + + {newNotice.extra} + + ) as any; + } + + return newNotice; + }); + return groupBy(newNotices, 'type'); +}; + +const getUnreadData = (noticeData: Record) => { + const unreadMsg: Record = {}; + 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([]); + 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 ( + { + changeReadState(item.id!); + }} + onClear={(title: string, key: string) => clearReadState(title, key)} + loading={false} + clearText="清空" + viewMoreText="查看更多" + onViewMore={() => message.info('Click on view more')} + clearClose + > + + + + + ); +}; + +export default NoticeIconView; diff --git a/src/components/RightContent/AvatarDropdown.tsx b/src/components/RightContent/AvatarDropdown.tsx new file mode 100644 index 0000000..150cbce --- /dev/null +++ b/src/components/RightContent/AvatarDropdown.tsx @@ -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 = ({ menu }) => { + const onMenuClick = useCallback((event: MenuInfo) => { + const { key } = event; + if (key === 'logout') { + loginOut(); + return; + } + history.push(`/account/${key}`); + }, []); + + const loading = ( + + + + ); + + const currentUser = { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png', + name: 'admin', + }; + + if (!currentUser || !currentUser.name) { + return loading; + } + + const menuHeaderDropdown = ( + + {menu && ( + + + 个人中心 + + )} + {menu && ( + + + 个人设置 + + )} + {menu && } + + + + 退出登录 + + + ); + return ( + + + + {currentUser.name} + + + ); +}; + +export default AvatarDropdown; diff --git a/src/components/RightContent/index.less b/src/components/RightContent/index.less new file mode 100644 index 0000000..486e80c --- /dev/null +++ b/src/components/RightContent/index.less @@ -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; + } + } +} diff --git a/src/components/RightContent/index.tsx b/src/components/RightContent/index.tsx new file mode 100644 index 0000000..b4d9c7c --- /dev/null +++ b/src/components/RightContent/index.tsx @@ -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 ( + + + + ); +}; +export default GlobalHeaderRight; diff --git a/src/components/Table/DeleteButton/index.less b/src/components/Table/DeleteButton/index.less new file mode 100644 index 0000000..61897f7 --- /dev/null +++ b/src/components/Table/DeleteButton/index.less @@ -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; + } +} diff --git a/src/components/Table/DeleteButton/index.tsx b/src/components/Table/DeleteButton/index.tsx new file mode 100644 index 0000000..55f29e5 --- /dev/null +++ b/src/components/Table/DeleteButton/index.tsx @@ -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 = ({ onDelete, disabled = false }) => { + return ( + + 删除 + + ); +}; +export default DeleteButton; diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx new file mode 100644 index 0000000..1873c46 --- /dev/null +++ b/src/components/Table/index.tsx @@ -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 { + toolBarActions?: toolBarActionsItem[]; + alertProps?: AlertProps; + indexColumn?: boolean | ProColumns; +} + +type Record = { + [P in K]: T; +}; + +const Table = >(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(); + const actionRef: any = actionRefProps || defaultRef; + const [selectedRows, setSelectedRows] = useState([]); + + // 暂时有批量删除的话,替换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 ( + }> + {(item.render && (item.render(_, val, ...rest) as []))?.map((item2) => item2)} + + ); + }; + 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 }) => [ + , + , + ], + }; + }, [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( + , + ); + } else if (item.type === 'batchDelete') { + buttonList.push( + , + ); + } + }); + 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: , + }; + } + return toolbarProps; + }, [alertProps, toolbarProps]); + + return ( +
+ + {...{ + ...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, + }); + } + }, + }} + /> +
+ ); +}; + +export type { ProColumns, ActionType, ProTableProps }; +export default Table; diff --git a/src/components/Typography/TimeText.tsx b/src/components/Typography/TimeText.tsx new file mode 100644 index 0000000..9ba4eec --- /dev/null +++ b/src/components/Typography/TimeText.tsx @@ -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 = (props) => { + const { value, format } = props; + const txtFormat = format ? format : DEFAULT_FORMAT; + const txt = value ? moment(value).format(txtFormat) : '-'; + return {txt}; +}; + +export default TimeText; diff --git a/src/components/Upload/index.less b/src/components/Upload/index.less new file mode 100644 index 0000000..9c2ebaa --- /dev/null +++ b/src/components/Upload/index.less @@ -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; + } + } + } +} diff --git a/src/components/Upload/index.tsx b/src/components/Upload/index.tsx new file mode 100644 index 0000000..725de93 --- /dev/null +++ b/src/components/Upload/index.tsx @@ -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[]; + bizType: UploadBizType; +} +const Upload: React.FC = ({ + listType = 'picture-card', + accept = 'image/*', + value = [], + maxCount = 1, + bizType, + onChange, + ...rest +}) => { + // 图片预览 + const [previewVisible, setPreviewVisible] = useState(false); + const [previewImage, setPreviewImage] = useState(''); + const [previewTitle, setPreviewTitle] = useState('预览'); + 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 ( +
+ + + setPreviewVisible(false)} + > + {getFileType(previewImage) === FileType.VIDEO ? ( + +
+ ); +}; + +export default Upload; diff --git a/src/constants/cacheKey.ts b/src/constants/cacheKey.ts new file mode 100644 index 0000000..aa73703 --- /dev/null +++ b/src/constants/cacheKey.ts @@ -0,0 +1 @@ +export const CACHE_TOKEN = 'token'; diff --git a/src/constants/enum/fileType.ts b/src/constants/enum/fileType.ts new file mode 100644 index 0000000..dfec55b --- /dev/null +++ b/src/constants/enum/fileType.ts @@ -0,0 +1,5 @@ +export enum FileType { + IMAGE = 'image', + VIDEO = 'video', +} +export default FileType; diff --git a/src/constants/enum/uploadBizType.ts b/src/constants/enum/uploadBizType.ts new file mode 100644 index 0000000..904a32b --- /dev/null +++ b/src/constants/enum/uploadBizType.ts @@ -0,0 +1,11 @@ +/** + * 上传类型 + * @author gary + */ +export enum UploadBizType { + WORK = 'work', // 应用信息相关 + AVATAR = 'avatar', // 头像 + AUTHOR = 'author', // 作者 + BLIND_BOX = 'blind_box', // 盲盒 + COLLECT = 'collect', // 集卡 +} diff --git a/src/constants/enum/work.ts b/src/constants/enum/work.ts new file mode 100644 index 0000000..7012a01 --- /dev/null +++ b/src/constants/enum/work.ts @@ -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', +} diff --git a/src/global.less b/src/global.less new file mode 100644 index 0000000..1e3ace5 --- /dev/null +++ b/src/global.less @@ -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; + } +} diff --git a/src/global.tsx b/src/global.tsx new file mode 100644 index 0000000..29f47a0 --- /dev/null +++ b/src/global.tsx @@ -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 = ( + + ); + 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(); +} diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..839bc5b --- /dev/null +++ b/src/manifest.json @@ -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" + } + ] +} diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..301e173 --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,18 @@ +import { Button, Result } from 'antd'; +import React from 'react'; +import { history } from 'umi'; + +const NoFoundPage: React.FC = () => ( + history.push('/')}> + Back Home + + } + /> +); + +export default NoFoundPage; diff --git a/src/pages/Login/index.less b/src/pages/Login/index.less new file mode 100644 index 0000000..88b73ab --- /dev/null +++ b/src/pages/Login/index.less @@ -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; + } +} diff --git a/src/pages/Login/index.tsx b/src/pages/Login/index.tsx new file mode 100644 index 0000000..a06865a --- /dev/null +++ b/src/pages/Login/index.tsx @@ -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 ( +
+
+ } + title={defaultSettings.title as string} + initialValues={{ + autoLogin: true, + }} + onFinish={async (values) => { + await handleSubmit(values as API.LoginParams); + }} + > + , + }} + placeholder={intl.formatMessage({ + id: 'pages.login.username.placeholder', + defaultMessage: '用户名', + })} + rules={[ + { + required: true, + message: ( + + ), + }, + ]} + /> + , + }} + placeholder={intl.formatMessage({ + id: 'pages.login.password.placeholder', + defaultMessage: '密码', + })} + rules={[ + { + required: true, + message: ( + + ), + }, + ]} + /> + +
+
+ ); +}; + +export default Login; diff --git a/src/pages/Work/Edit/components/AuthorSelector.tsx b/src/pages/Work/Edit/components/AuthorSelector.tsx new file mode 100644 index 0000000..26bcbd7 --- /dev/null +++ b/src/pages/Work/Edit/components/AuthorSelector.tsx @@ -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 = ({ + params = {}, + value, + defaultOptions, + ...rest +}) => { + const [options, setOptions] = useState([]); + 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 ( +