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 ca535c3..4c89a72 100644 --- a/README.md +++ b/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). 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..f32d47c --- /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://106.55.157.116:8086', + // 配置了这个可以从 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 0000000..e2e9325 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/icons/icon-128x128.png b/public/icons/icon-128x128.png new file mode 100644 index 0000000..48d0e23 Binary files /dev/null and b/public/icons/icon-128x128.png differ diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png new file mode 100644 index 0000000..938e9b5 Binary files /dev/null and b/public/icons/icon-192x192.png differ diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png new file mode 100644 index 0000000..21fc108 Binary files /dev/null and b/public/icons/icon-512x512.png differ diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..239bf69 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1 @@ +Group 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/blindbox.ts b/src/constants/enum/blindbox.ts new file mode 100644 index 0000000..eda2eee --- /dev/null +++ b/src/constants/enum/blindbox.ts @@ -0,0 +1,4 @@ +export enum BlindboxStatusType { + PUBLISHED = 'published', + NO_PUBLISH = 'no_publish', +} 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 ( +