├── .stylelintignore ├── .eslintignore ├── .prettierignore ├── conf ├── dev │ ├── public │ │ └── readme.txt │ └── env.js ├── analyzer │ ├── public │ │ └── readme.txt │ └── env.js ├── prod │ ├── public │ │ └── readme.txt │ └── env.js └── env.base.js ├── mock ├── get--api-session.js ├── get--api-projectConfig.js ├── get--api-notices.js ├── delete--api-session.js ├── get--api-role-xxx@Z2V0LS1hcGktcm9sZS1cdys=.js ├── get--api-post-xxx@Z2V0LS1hcGktcG9zdC1cdys=.js ├── get--api-member-xxx@Z2V0LS1hcGktbWVtYmVyLVx3Kw==.js ├── delete--api-role.js ├── delete--api-post.js ├── delete--api-member.js ├── put--api-member.js ├── put--api-post.js ├── post--api-session.js ├── post--api-role.js ├── put--api-role-xxx@cHV0LS1hcGktcm9sZS1cdys=.js ├── post--api-post.js ├── put--api-post-xxx@cHV0LS1hcGktcG9zdC1cdys=.js ├── put--api-member-xxx@cHV0LS1hcGktbWVtYmVyLVx3Kw==.js ├── get--api-role$@Z2V0LS1hcGktcm9sZVwk.js ├── post--api-member.js ├── get--api-post$@Z2V0LS1hcGktcG9zdFwk.js └── get--api-member$@Z2V0LS1hcGktbWVtYmVyXCQ=.js ├── src ├── modules │ ├── admin │ │ ├── adminLayout │ │ │ ├── api.ts │ │ │ ├── views │ │ │ │ ├── Navs │ │ │ │ │ └── index.m.less │ │ │ │ ├── TabNavEditor │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Main │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Flag │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── index.m.less │ │ │ │ ├── Header │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ └── TabNavs │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ └── index.ts │ │ ├── adminMember │ │ │ ├── views │ │ │ │ ├── index.m.less │ │ │ │ ├── List.tsx │ │ │ │ ├── Selector.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── Detail.tsx │ │ │ │ └── SelectorTable.tsx │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── api.ts │ │ ├── adminPost │ │ │ ├── views │ │ │ │ ├── index.m.less │ │ │ │ ├── List.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── Detail.tsx │ │ │ │ └── Editor.tsx │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── api.ts │ │ ├── adminHome │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── views │ │ │ │ ├── summary.md │ │ │ │ ├── index.m.less │ │ │ │ ├── engineering.md │ │ │ │ └── Main.tsx │ │ └── adminRole │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── views │ │ │ ├── Selector.tsx │ │ │ ├── index.m.less │ │ │ ├── Search.tsx │ │ │ ├── List.tsx │ │ │ ├── Editor.tsx │ │ │ └── Detail.tsx │ │ │ └── api.ts │ ├── article │ │ ├── articleLayout │ │ │ ├── views │ │ │ │ ├── ConsultPop │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Footer │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Main.tsx │ │ │ │ └── Header │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ ├── index.ts │ │ │ └── model.ts │ │ ├── articleHome │ │ │ ├── views │ │ │ │ ├── imgs │ │ │ │ │ └── banner.jpg │ │ │ │ ├── Special │ │ │ │ │ ├── imgs │ │ │ │ │ │ ├── ad.jpg │ │ │ │ │ │ ├── ad.png │ │ │ │ │ │ ├── icon1.png │ │ │ │ │ │ ├── icon2.png │ │ │ │ │ │ ├── icon3.png │ │ │ │ │ │ └── icon4.png │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── index.m.less │ │ │ │ ├── Activities │ │ │ │ │ ├── imgs │ │ │ │ │ │ ├── ad.png │ │ │ │ │ │ ├── logo1.png │ │ │ │ │ │ ├── logo2.png │ │ │ │ │ │ ├── logo3.png │ │ │ │ │ │ └── logo4.png │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── Recommend │ │ │ │ │ ├── imgs │ │ │ │ │ │ └── ad.png │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ └── Main.tsx │ │ │ ├── index.ts │ │ │ └── model.ts │ │ ├── articleAbout │ │ │ ├── views │ │ │ │ ├── imgs │ │ │ │ │ └── banner.jpg │ │ │ │ ├── Activities │ │ │ │ │ ├── index.m.less │ │ │ │ │ └── index.tsx │ │ │ │ └── Main.tsx │ │ │ ├── index.ts │ │ │ └── model.ts │ │ └── articleService │ │ │ ├── views │ │ │ ├── imgs │ │ │ │ └── banner.jpg │ │ │ ├── Activities │ │ │ │ ├── index.m.less │ │ │ │ └── index.tsx │ │ │ └── Main.tsx │ │ │ ├── index.ts │ │ │ └── model.ts │ └── app │ │ ├── components │ │ └── FormLayout │ │ │ ├── imgs │ │ │ ├── bg.png │ │ │ ├── bg-icon1.png │ │ │ └── bg-icon2.png │ │ │ ├── index.tsx │ │ │ └── index.m.less │ │ ├── views │ │ ├── LoginPage │ │ │ └── index.tsx │ │ ├── RegisterPage │ │ │ └── index.tsx │ │ ├── LoginPop │ │ │ ├── index.m.less │ │ │ └── index.tsx │ │ ├── RegisterPop │ │ │ ├── index.m.less │ │ │ └── index.tsx │ │ ├── GlobalLoading │ │ │ ├── index.m.less │ │ │ └── index.tsx │ │ ├── LoginForm │ │ │ ├── index.m.less │ │ │ └── index.tsx │ │ ├── RegisterForm │ │ │ └── index.m.less │ │ ├── RegistrationAgreement │ │ │ └── index.tsx │ │ └── Main │ │ │ └── index.tsx │ │ ├── index.ts │ │ └── api.ts ├── assets │ ├── imgs │ │ ├── qq.png │ │ ├── logo.png │ │ ├── logo2.png │ │ ├── loading.gif │ │ └── loading48x48.gif │ └── css │ │ ├── vars.less │ │ ├── antd-vars.js │ │ └── override.less ├── tsconfig.json ├── .eslintrc.js ├── components │ ├── NotFound.tsx │ ├── DateTime.tsx │ ├── ResourceSimpleSelector │ │ └── index.m.less │ ├── Anchor │ │ ├── index.tsx │ │ └── index.m.less │ ├── EllipsisText.tsx │ ├── ResourceSelector │ │ └── index.m.less │ ├── ListKeyLink.tsx │ ├── ArticleBanner │ │ ├── index.tsx │ │ └── index.m.less │ ├── LoginLink.tsx │ ├── PurviewSelector.tsx │ ├── RangeDatePicker.tsx │ ├── PurviewEditor.tsx │ └── SearchForm.tsx ├── hooks │ ├── useConsult.ts │ ├── useEventCallback.ts │ ├── useDetail.ts │ ├── useLoginLink.ts │ ├── useAnchorPage.ts │ └── useSelector.ts ├── entity │ ├── adminLayout.ts │ ├── session.ts │ ├── index.ts │ ├── post.ts │ ├── member.ts │ └── role.ts ├── index.ts ├── Prepose.tsx ├── Global.ts └── common │ ├── errors.ts │ └── request.ts ├── public ├── client │ ├── logo.png │ └── imgs │ │ ├── qq.png │ │ └── u1.jpg ├── 50x.html ├── 404.html ├── pm2.json ├── package.json ├── start.js ├── nginx.conf ├── icon.html └── index.html ├── postcss.config.js ├── .gitignore ├── typings └── assets.d.ts ├── check-user.js ├── .editorconfig ├── .prettierrc.js ├── .eslintrc-src.js ├── .eslintrc.js ├── .stylelintrc.json ├── .vscode ├── settings.json └── extensions.json ├── tsconfig.json ├── tsconfig-src.json ├── babel.config.js ├── CHANGELOG.md ├── README.md └── package.json /.stylelintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /conf/dev/public/readme.txt: -------------------------------------------------------------------------------- 1 | 此目录发布后可覆盖:项目/publich目录 2 | -------------------------------------------------------------------------------- /conf/analyzer/public/readme.txt: -------------------------------------------------------------------------------- 1 | 此目录发布后可覆盖:项目/publich目录 2 | -------------------------------------------------------------------------------- /conf/prod/public/readme.txt: -------------------------------------------------------------------------------- 1 | 此目录发布后可覆盖:项目/publich目录 2 | -------------------------------------------------------------------------------- /mock/get--api-session.js: -------------------------------------------------------------------------------- 1 | return database.action.users.verifyToken(request.cookies.token); 2 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/api.ts: -------------------------------------------------------------------------------- 1 | export class API {} 2 | 3 | export default new API(); 4 | -------------------------------------------------------------------------------- /public/client/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/public/client/logo.png -------------------------------------------------------------------------------- /src/assets/imgs/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/assets/imgs/qq.png -------------------------------------------------------------------------------- /public/client/imgs/qq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/public/client/imgs/qq.png -------------------------------------------------------------------------------- /public/client/imgs/u1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/public/client/imgs/u1.jpg -------------------------------------------------------------------------------- /src/assets/css/vars.less: -------------------------------------------------------------------------------- 1 | @light-bg-color: #f4f8fa; 2 | @light-bd-color: #eee; 3 | @split-bd-color: #e9e9e9; 4 | -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /src/assets/imgs/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/assets/imgs/logo2.png -------------------------------------------------------------------------------- /src/assets/imgs/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/assets/imgs/loading.gif -------------------------------------------------------------------------------- /src/modules/admin/adminMember/views/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | display: block; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/views/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | display: block; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/css/antd-vars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '@primary-color': '#1978e6', 3 | '@border-color-base': '#ddd', 4 | }; 5 | -------------------------------------------------------------------------------- /src/assets/imgs/loading48x48.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/assets/imgs/loading48x48.gif -------------------------------------------------------------------------------- /src/modules/article/articleLayout/views/ConsultPop/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | padding: 0; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // eslint-disable-next-line global-require 3 | plugins: [require('autoprefixer')], 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | conf/local/ 4 | conf/locConf.js 5 | *.log 6 | .DS_Store 7 | .history 8 | .stylelintcache 9 | .eslintcache 10 | -------------------------------------------------------------------------------- /src/modules/app/components/FormLayout/imgs/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/app/components/FormLayout/imgs/bg.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/imgs/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/imgs/banner.jpg -------------------------------------------------------------------------------- /src/modules/app/components/FormLayout/imgs/bg-icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/app/components/FormLayout/imgs/bg-icon1.png -------------------------------------------------------------------------------- /src/modules/app/components/FormLayout/imgs/bg-icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/app/components/FormLayout/imgs/bg-icon2.png -------------------------------------------------------------------------------- /src/modules/article/articleAbout/views/imgs/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleAbout/views/imgs/banner.jpg -------------------------------------------------------------------------------- /src/modules/article/articleService/views/imgs/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleService/views/imgs/banner.jpg -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/imgs/ad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Special/imgs/ad.jpg -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/imgs/ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Special/imgs/ad.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Activities/imgs/ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Activities/imgs/ad.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Recommend/imgs/ad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Recommend/imgs/ad.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/imgs/icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Special/imgs/icon1.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/imgs/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Special/imgs/icon2.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/imgs/icon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Special/imgs/icon3.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/imgs/icon4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Special/imgs/icon4.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Activities/imgs/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Activities/imgs/logo1.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Activities/imgs/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Activities/imgs/logo2.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Activities/imgs/logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Activities/imgs/logo3.png -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Activities/imgs/logo4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wooline/medux-react-admin/HEAD/src/modules/article/articleHome/views/Activities/imgs/logo4.png -------------------------------------------------------------------------------- /typings/assets.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg'; 2 | declare module '*.png'; 3 | declare module '*.jpg'; 4 | declare module '*.gif'; 5 | declare module '*.less'; 6 | declare module '*.md'; 7 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig-src.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "rootDir": "./", 6 | }, 7 | "include": ["./"], 8 | "exclude": [] 9 | } 10 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['../.eslintrc-src.js'], 3 | parserOptions: { 4 | // @ts-ignore 5 | project: `${__dirname}/tsconfig.json`, 6 | }, 7 | ignorePatterns: [], 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Component: React.FC = () => ( 4 |
5 |

Not Found

6 |
7 | ); 8 | 9 | export default React.memo(Component); 10 | -------------------------------------------------------------------------------- /conf/dev/env.js: -------------------------------------------------------------------------------- 1 | const {clientGlobal, clientPublicPath, proxy, server, mock} = require('../env.base'); 2 | 3 | module.exports = { 4 | clientGlobal, 5 | clientPublicPath, 6 | proxy, 7 | server, 8 | mock, 9 | }; 10 | -------------------------------------------------------------------------------- /check-user.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const {execSync} = require('child_process'); 3 | 4 | const username = execSync('git config --get user.name', {encoding: 'utf8', silent: true}); 5 | console.log(username); 6 | 7 | // process.exit(1); 8 | -------------------------------------------------------------------------------- /conf/analyzer/env.js: -------------------------------------------------------------------------------- 1 | const {clientGlobal, clientPublicPath, proxy, server, mock} = require('../env.base'); 2 | 3 | module.exports = { 4 | clientGlobal, 5 | clientPublicPath, 6 | proxy, 7 | server, 8 | mock, 9 | }; 10 | -------------------------------------------------------------------------------- /conf/prod/env.js: -------------------------------------------------------------------------------- 1 | const {clientGlobal, clientPublicPath, proxy, server, mock} = require('../env.base'); 2 | 3 | module.exports = { 4 | clientGlobal, 5 | clientPublicPath, 6 | proxy, 7 | server, 8 | mock, 9 | }; 10 | -------------------------------------------------------------------------------- /src/hooks/useConsult.ts: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | 3 | export default function Hooks(dispatch: (action: any) => void) { 4 | return useCallback(() => { 5 | dispatch(actions.articleLayout.showConsult()); 6 | }, [dispatch]); 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/Navs/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | color: #fff; 4 | padding: '16px 0'; 5 | width: '100%'; 6 | .ant-menu-sub { 7 | background: transparent !important; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mock/get--api-projectConfig.js: -------------------------------------------------------------------------------- 1 | return { 2 | statusCode: 200, 3 | headers: { 4 | 'x-delay': 0, 5 | 'content-type': 'application/json; charset=utf-8', 6 | }, 7 | response: {tokenRenewalTime: database.data.config.tokenRenewalTime, noticeTimer: 15}, 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 200 11 | 12 | [**.bat] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /src/modules/admin/adminHome/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import main from './views/Main'; 5 | 6 | export default exportModule('adminHome', initModelState, ModelHandlers, {main}); 7 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import list from './views/List'; 5 | 6 | export default exportModule('adminPost', initModelState, ModelHandlers, {list}); 7 | -------------------------------------------------------------------------------- /src/assets/css/override.less: -------------------------------------------------------------------------------- 1 | /* 此文件用来覆盖默认 */ 2 | #doc { 3 | // .ant-input { 4 | // border-radius: 0; 5 | // } 6 | .ant-modal-title { 7 | font-weight: bold; 8 | } 9 | input:-internal-autofill-selected { 10 | background-color: transparent; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import main from './views/Main'; 5 | 6 | export default exportModule('adminLayout', initModelState, ModelHandlers, {main}); 7 | -------------------------------------------------------------------------------- /src/modules/article/articleAbout/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import main from './views/Main'; 5 | 6 | export default exportModule('articleAbout', initModelState, ModelHandlers, {main}); 7 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import main from './views/Main'; 5 | 6 | export default exportModule('articleHome', initModelState, ModelHandlers, {main}); 7 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import main from './views/Main'; 5 | 6 | export default exportModule('articleLayout', initModelState, ModelHandlers, {main}); 7 | -------------------------------------------------------------------------------- /src/modules/article/articleService/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import main from './views/Main'; 5 | 6 | export default exportModule('articleService', initModelState, ModelHandlers, {main}); 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | //endOfLine: 'lf', fork editorconfig 4 | //useTabs: true, fork editorconfig 5 | //tabWidth: 2, fork editorconfig 6 | // printWidth: 200, fork editorconfig 7 | trailingComma: 'es5', 8 | bracketSpacing: false, 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/article/articleAbout/model.ts: -------------------------------------------------------------------------------- 1 | import {BaseModelHandlers, BaseModelState} from '@medux/react-web-router'; 2 | 3 | export interface State extends BaseModelState {} 4 | 5 | export const initModelState: State = {}; 6 | 7 | export class ModelHandlers extends BaseModelHandlers {} 8 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/model.ts: -------------------------------------------------------------------------------- 1 | import {BaseModelHandlers, BaseModelState} from '@medux/react-web-router'; 2 | 3 | export interface State extends BaseModelState {} 4 | 5 | export const initModelState: State = {}; 6 | 7 | export class ModelHandlers extends BaseModelHandlers {} 8 | -------------------------------------------------------------------------------- /src/modules/article/articleService/model.ts: -------------------------------------------------------------------------------- 1 | import {BaseModelHandlers, BaseModelState} from '@medux/react-web-router'; 2 | 3 | export interface State extends BaseModelState {} 4 | 5 | export const initModelState: State = {}; 6 | 7 | export class ModelHandlers extends BaseModelHandlers {} 8 | -------------------------------------------------------------------------------- /mock/get--api-notices.js: -------------------------------------------------------------------------------- 1 | const verifyToken = database.action.users.verifyToken(request.cookies.token); 2 | 3 | if (verifyToken.statusCode === 200) { 4 | return { 5 | ...verifyToken, 6 | response: { 7 | count: Math.round(Math.random() * 100), 8 | }, 9 | }; 10 | } 11 | return verifyToken; 12 | -------------------------------------------------------------------------------- /mock/delete--api-session.js: -------------------------------------------------------------------------------- 1 | const result = { 2 | statusCode: 200, 3 | cookies: [['token', '', {expires: new Date(Date.now() - 10000), httpOnly: true}]], 4 | headers: { 5 | 'x-delay': 0, 6 | 'content-type': 'application/json; charset=utf-8', 7 | }, 8 | response: '', 9 | }; 10 | 11 | return result; 12 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import list from './views/List'; 5 | import selector from './views/Selector'; 6 | 7 | export default exportModule('adminRole', initModelState, ModelHandlers, {list, selector}); 8 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import list from './views/List'; 5 | import selector from './views/Selector'; 6 | 7 | export default exportModule('adminMember', initModelState, ModelHandlers, {list, selector}); 8 | -------------------------------------------------------------------------------- /.eslintrc-src.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:@medux/recommended/react'], 4 | env: { 5 | browser: true, 6 | node: false, 7 | }, 8 | parserOptions: { 9 | project: `${__dirname}/tsconfig.json`, 10 | }, 11 | rules: { 12 | //'global-require': 'off', 13 | }, 14 | ignorePatterns: [], 15 | }; 16 | -------------------------------------------------------------------------------- /public/50x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React App 9 | 10 | 11 | 12 | 500错误 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 | 404未找到 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/modules/admin/adminHome/model.ts: -------------------------------------------------------------------------------- 1 | import {BaseModelHandlers, BaseModelState} from '@medux/react-web-router'; 2 | // 定义本模块的State类型 3 | export interface State extends BaseModelState {} 4 | 5 | // 定义本模块State的初始值 6 | export const initModelState: State = {}; 7 | 8 | // 定义本模块的Handlers 9 | export class ModelHandlers extends BaseModelHandlers {} 10 | -------------------------------------------------------------------------------- /src/modules/app/views/LoginPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormLayout from '../../components/FormLayout'; 3 | import LoginForm from '../LoginForm'; 4 | 5 | const Component: React.FC = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default React.memo(Component); 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:@medux/recommended/common'], 4 | env: { 5 | browser: false, 6 | node: true, 7 | }, 8 | parserOptions: { 9 | project: `${__dirname}/tsconfig.json`, 10 | }, 11 | rules: { 12 | 'global-require': 'off', 13 | }, 14 | ignorePatterns: ['/src','/dist','/mock'], 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/DateTime.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | 4 | const dateFormat = 'YYYY-MM-DD HH:mm:ss'; 5 | 6 | interface Props { 7 | date: string | number; 8 | } 9 | const Component: React.FC = ({date}) => { 10 | return <>{date ? moment(date).format(dateFormat) : ''}; 11 | }; 12 | 13 | export default React.memo(Component); 14 | -------------------------------------------------------------------------------- /src/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@medux/react-web-router'; 2 | import {ModelHandlers, initModelState} from './model'; 3 | 4 | import loginPage from './views/LoginPage'; 5 | import main from './views/Main'; 6 | import registerPage from './views/RegisterPage'; 7 | 8 | export default exportModule('app', initModelState, ModelHandlers, {main, loginPage, registerPage}); 9 | -------------------------------------------------------------------------------- /public/pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "medux-react-admin", 5 | "cwd": "./", 6 | "script": "start.js", 7 | "error_file": "./logs/app-err.log", 8 | "out_file": "./logs/app-out.log", 9 | "merge_logs": true, 10 | "log_date_format": "YYYY-MM-DD HH:mm:ss", 11 | "autorestart": true, 12 | "instances": "max" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/admin/adminHome/views/summary.md: -------------------------------------------------------------------------------- 1 | 本项目主要用来展示如何将@medux 应用于 web 后台管理系统,你可能看不到丰富的后台 UI 控件及界面,因为这不是重点,网上这样的轮子已经很多了。而本项目想着重表达的是**通用化解题思路** 2 | 3 | ## 项目介绍 4 | 5 | - [语雀](https://www.yuque.com/medux/docs/medux-react-admin) 6 | 7 | ## 项目地址 8 | 9 | - [Github](https://github.com/wooline/medux-react-admin) 10 | 11 | ## QQ 群交流 12 | 13 | - QQ 群号: 929696953 14 | 15 | ![qq群](/client/imgs/qq.png) 16 | -------------------------------------------------------------------------------- /src/modules/app/views/RegisterPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FormLayout from '../../components/FormLayout'; 3 | import RegisterForm from '../RegisterForm'; 4 | 5 | interface StoreProps {} 6 | 7 | const Component: React.FC = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default React.memo(Component); 16 | -------------------------------------------------------------------------------- /src/modules/article/articleAbout/views/Activities/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | padding-top: 60px; 4 | padding-bottom: 60px; 5 | h2 { 6 | font-weight: 700; 7 | font-size: 30px; 8 | margin: 0 0 50px; 9 | text-align: center; 10 | } 11 | 12 | p { 13 | font-size: 16px; 14 | line-height: 2; 15 | text-indent: 2em; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/article/articleService/views/Activities/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | padding-top: 60px; 4 | padding-bottom: 60px; 5 | h2 { 6 | font-weight: 700; 7 | font-size: 30px; 8 | margin: 0 0 50px; 9 | text-align: center; 10 | } 11 | 12 | p { 13 | font-size: 16px; 14 | line-height: 2; 15 | text-indent: 2em; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /conf/env.base.js: -------------------------------------------------------------------------------- 1 | const {baseConf} = require('../package.json'); 2 | 3 | const {siteName, clientPublicPath, version, mock, server, proxy} = baseConf; 4 | 5 | const clientGlobal = { 6 | version, 7 | siteName, 8 | staticPath: `${clientPublicPath}client/`, 9 | apiServerPath: {'/api/': '/api/'}, 10 | }; 11 | module.exports = { 12 | proxy, 13 | server, 14 | mock, 15 | clientGlobal, 16 | clientPublicPath, 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/ResourceSimpleSelector/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.fetching) { 3 | background: @light-bg-color; 4 | &::before { 5 | content: ''; 6 | opacity: 0.5; 7 | display: block; 8 | width: 16px; 9 | height: 16px; 10 | background: url(~assets/imgs/loading.gif) no-repeat; 11 | position: absolute; 12 | right: 20px; 13 | top: 5px; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/admin/adminHome/views/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | .panel { 4 | background: transparent; 5 | border: none; 6 | display: flex; 7 | padding: 0; 8 | justify-content: space-between; 9 | > div { 10 | width: 49%; 11 | background: #fff; 12 | padding: 20px; 13 | border: 1px solid @light-bd-color; 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entity/adminLayout.ts: -------------------------------------------------------------------------------- 1 | export interface LayoutData { 2 | footer: FooterData; 3 | globalSearch: GlobalSearchData; 4 | } 5 | export interface GlobalSearchData { 6 | placeholder: string; 7 | dataSource: string[]; 8 | } 9 | export interface FooterData { 10 | links: { 11 | key: string; 12 | title: string; 13 | href: string; 14 | blankTarget: boolean; 15 | }[]; 16 | copyright: string; 17 | className?: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/app/views/LoginPop/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 340px; 4 | .hasLogin { 5 | padding-top: 20px; 6 | } 7 | .register { 8 | float: right; 9 | } 10 | .submit { 11 | width: 100%; 12 | } 13 | .title { 14 | font-weight: bold; 15 | text-align: center; 16 | font-size: 30px; 17 | padding-top: 20px; 18 | padding-bottom: 10px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/app/views/RegisterPop/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 340px; 4 | .hasLogin { 5 | padding-top: 20px; 6 | } 7 | .register { 8 | float: right; 9 | } 10 | .submit { 11 | width: 100%; 12 | } 13 | .title { 14 | font-weight: bold; 15 | text-align: center; 16 | font-size: 30px; 17 | padding-top: 20px; 18 | padding-bottom: 10px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /mock/get--api-role-xxx@Z2V0LS1hcGktcm9sZS1cdys=.js: -------------------------------------------------------------------------------- 1 | const id = request.path.split('/').pop(); 2 | const result = { 3 | statusCode: 422, 4 | headers: { 5 | 'x-delay': 0, 6 | 'content-type': 'application/json; charset=utf-8', 7 | }, 8 | response: '', 9 | }; 10 | const item = database.data.roles[id]; 11 | if (!item) { 12 | result.statusCode = 404; 13 | return result; 14 | } 15 | result.statusCode = 200; 16 | result.response = item; 17 | return result; 18 | -------------------------------------------------------------------------------- /mock/get--api-post-xxx@Z2V0LS1hcGktcG9zdC1cdys=.js: -------------------------------------------------------------------------------- 1 | const id = request.path.split('/').pop(); 2 | const result = { 3 | statusCode: 422, 4 | headers: { 5 | 'x-delay': 0, 6 | 'content-type': 'application/json; charset=utf-8', 7 | }, 8 | response: '', 9 | }; 10 | const item = database.data.posts[id]; 11 | if (!item) { 12 | result.statusCode = 404; 13 | return result; 14 | } 15 | result.statusCode = 200; 16 | result.response = {...item}; 17 | return result; 18 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/TabNavEditor/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | position: relative; 4 | width: 190px; 5 | height: 135px; 6 | padding-top: 5px; 7 | padding-bottom: 5px; 8 | 9 | .title { 10 | font-size: 13px; 11 | padding-left: 5px; 12 | color: @primary-color; 13 | } 14 | .g-btns { 15 | position: absolute; 16 | width: 100%; 17 | padding: 0 25px; 18 | bottom: 10px; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@medux/stylelint-config-recommended" 4 | ], 5 | "rules": { 6 | "at-rule-no-unknown": [ 7 | true, 8 | { 9 | "ignoreAtRules": [ 10 | "plugin", 11 | "mixin" 12 | ] 13 | } 14 | ], 15 | "selector-pseudo-class-no-unknown": [ 16 | true, 17 | { 18 | "ignorePseudoClasses": [ 19 | "global", 20 | "local" 21 | ] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/dist": true, 4 | "**/node_modules": true 5 | }, 6 | "files.eol": "\n", 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "typescript.updateImportsOnFileMove.enabled": "never", 9 | "editor.formatOnSave": true, 10 | "editor.codeActionsOnSave": { 11 | "source.organizeImports": false, 12 | "source.fixAll": true 13 | }, 14 | "css.validate": false, 15 | "less.validate": false, 16 | "scss.validate": false 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/Main/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | height: 100%; 4 | .ant-layout-content { 5 | padding: 10px; 6 | padding-top: 0; 7 | overflow: hidden auto; 8 | } 9 | .ant-layout-header { 10 | background: #fff; 11 | border-bottom: 1px solid @split-bd-color; 12 | height: 80px; 13 | padding: 0; 14 | margin-bottom: 10px; 15 | } 16 | .ant-layout-sider { 17 | overflow: hidden auto; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mock/get--api-member-xxx@Z2V0LS1hcGktbWVtYmVyLVx3Kw==.js: -------------------------------------------------------------------------------- 1 | const id = request.path.split('/').pop(); 2 | const result = { 3 | statusCode: 422, 4 | headers: { 5 | 'x-delay': 0, 6 | 'content-type': 'application/json; charset=utf-8', 7 | }, 8 | response: '', 9 | }; 10 | const item = database.data.users[id]; 11 | if (!item) { 12 | result.statusCode = 404; 13 | return result; 14 | } 15 | result.statusCode = 200; 16 | result.response = {...item, score: Math.round(Math.random() * 100), account: Math.round(Math.random() * 100)}; 17 | return result; 18 | -------------------------------------------------------------------------------- /src/components/Anchor/index.tsx: -------------------------------------------------------------------------------- 1 | import {Anchor} from 'antd'; 2 | import React from 'react'; 3 | import styles from './index.m.less'; 4 | 5 | const {Link} = Anchor; 6 | interface Props { 7 | navs: string[][]; 8 | } 9 | 10 | const Component: React.FC = (props) => { 11 | const {navs} = props; 12 | return ( 13 | 14 | {navs.map((item, idx) => ( 15 | 16 | ))} 17 | 18 | ); 19 | }; 20 | 21 | export default React.memo(Component); 22 | -------------------------------------------------------------------------------- /src/modules/app/views/GlobalLoading/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | position: fixed; 4 | left: 0; 5 | top: 0; 6 | width: 100%; 7 | height: 100%; 8 | background-color: rgba(0, 0, 0, 0); 9 | z-index: 100000; 10 | 11 | > .loadingIcon { 12 | position: absolute; 13 | width: 22px; 14 | height: 26px; 15 | left: 50%; 16 | top: 50%; 17 | margin-left: -11px; 18 | margin-top: -13px; 19 | } 20 | 21 | &.Depth { 22 | background-color: rgba(255, 255, 255, 0.3); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/model.ts: -------------------------------------------------------------------------------- 1 | import {CommonResourceHandlers, CommonResourceState} from 'common/resource'; 2 | import {Resource, defaultRouteParams} from 'entity/post'; 3 | 4 | import api from './api'; 5 | 6 | export interface State extends CommonResourceState {} 7 | 8 | export const initModelState: State = {routeParams: defaultRouteParams}; 9 | 10 | export class ModelHandlers extends CommonResourceHandlers { 11 | constructor(moduleName: string, store: any) { 12 | super({defaultRouteParams, api}, moduleName, store); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/model.ts: -------------------------------------------------------------------------------- 1 | import {CommonResourceHandlers, CommonResourceState} from 'common/resource'; 2 | import {Resource, defaultRouteParams} from 'entity/role'; 3 | 4 | import api from './api'; 5 | 6 | export interface State extends CommonResourceState {} 7 | 8 | export const initModelState: State = {routeParams: defaultRouteParams}; 9 | 10 | export class ModelHandlers extends CommonResourceHandlers { 11 | constructor(moduleName: string, store: any) { 12 | super({defaultRouteParams, api}, moduleName, store); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/entity/session.ts: -------------------------------------------------------------------------------- 1 | export const guest: CurUser = { 2 | id: '', 3 | username: '游客', 4 | hasLogin: false, 5 | avatar: `${initEnv.staticPath}imgs/u1.jpg`, 6 | }; 7 | export interface CurUser { 8 | id: string; 9 | username: string; 10 | hasLogin: boolean; 11 | avatar: string; 12 | expired?: number; 13 | } 14 | export interface LoginRequest { 15 | username: string; 16 | password: string; 17 | keep?: boolean; 18 | } 19 | 20 | export interface RegisterRequest { 21 | username: string; 22 | password: string; 23 | } 24 | export interface Notices { 25 | count: number; 26 | } 27 | -------------------------------------------------------------------------------- /public/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medux-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node start.js", 8 | "pm2": "pm2 start pm2.json" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@medux/dev-utils": "1.0.8", 14 | "express": "4.17.1", 15 | "body-parser": "1.19.0", 16 | "cookie-parser": "1.4.5", 17 | "chalk": "4.0.0", 18 | "axios": "0.19.2", 19 | "json-format": "1.0.1", 20 | "mockjs": "1.1.0", 21 | "http-proxy-middleware": "1.0.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Activities/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | padding-top: 60px; 4 | text-align: center; 5 | h2 { 6 | font-weight: 700; 7 | font-size: 30px; 8 | margin: 0 0 50px; 9 | } 10 | .g-activities { 11 | margin-bottom: 40px; 12 | } 13 | .logos { 14 | margin-bottom: 90px; 15 | padding: 0; 16 | li { 17 | list-style: none; 18 | float: left; 19 | margin-right: 30px; 20 | &:last-child { 21 | margin-right: 0; 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/EllipsisText.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | 3 | import {Popover} from 'antd'; 4 | 5 | interface Props {} 6 | const Component: React.FC = ({children}) => { 7 | const subDom = useRef(null); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | 15 | // componentDidMount() { 16 | // this.subDom.current && (this.subDom.current.parentNode! as HTMLDivElement).removeAttribute('title'); 17 | // } 18 | }; 19 | 20 | export default React.memo(Component); 21 | -------------------------------------------------------------------------------- /src/components/ResourceSelector/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | .g-search { 4 | padding-top: 0 !important; 5 | padding-bottom: 0 !important; 6 | } 7 | .ant-modal-footer { 8 | text-align: center; 9 | padding: 20px; 10 | } 11 | .ant-alert-info { 12 | flex: 1; 13 | } 14 | .ant-modal-body { 15 | padding-bottom: 0; 16 | } 17 | .ant-modal-title { 18 | span { 19 | font-size: 12px; 20 | font-weight: normal; 21 | display: inline-block; 22 | padding-left: 10px; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /mock/delete--api-role.js: -------------------------------------------------------------------------------- 1 | let {ids} = request.body; 2 | ids = Array.isArray(ids) ? ids : []; 3 | 4 | const roles = database.data.roles; 5 | const result = { 6 | statusCode: 422, 7 | headers: { 8 | 'x-delay': 0, 9 | 'content-type': 'application/json; charset=utf-8', 10 | }, 11 | }; 12 | 13 | if (ids.length === 0) { 14 | result.response = { 15 | message: '参数不合法!', 16 | }; 17 | return result; 18 | } 19 | 20 | ids.forEach(id => { 21 | if (roles[id] && !roles[id].fixed) { 22 | delete roles[id]; 23 | } 24 | }); 25 | 26 | result.statusCode = 200; 27 | result.response = ''; 28 | 29 | return result; 30 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/Flag/index.tsx: -------------------------------------------------------------------------------- 1 | import {Link} from '@medux/react-web-router'; 2 | import Logo from 'assets/imgs/logo.png'; 3 | import React from 'react'; 4 | import styles from './index.m.less'; 5 | 6 | const Component: React.FC = () => { 7 | return ( 8 |
9 | 10 | 通用管理后台 11 |

通用管理后台

12 | V1.0.0 13 | 14 |
15 | ); 16 | }; 17 | 18 | export default React.memo(Component); 19 | -------------------------------------------------------------------------------- /src/hooks/useEventCallback.ts: -------------------------------------------------------------------------------- 1 | import {useCallback, useEffect, useRef} from 'react'; 2 | 3 | export default function Hooks(fn: (...args: A) => void, dependencies: any[]) { 4 | const ref = useRef((...args: A) => { 5 | console.log(new Error('Cannot call an event handler while rendering.')); 6 | }); 7 | 8 | useEffect(() => { 9 | ref.current = fn; 10 | // eslint-disable-next-line react-hooks/exhaustive-deps 11 | }, [fn, ...dependencies]); 12 | 13 | return useCallback( 14 | (...args: A) => { 15 | const fun = ref.current; 16 | return fun(...args); 17 | }, 18 | [ref] 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/model.ts: -------------------------------------------------------------------------------- 1 | import {CommonResourceHandlers, CommonResourceState} from 'common/resource'; 2 | import {Resource, defaultRouteParams} from 'entity/member'; 3 | 4 | import api from './api'; 5 | 6 | export interface State extends CommonResourceState {} 7 | 8 | export const initModelState: State = {routeParams: defaultRouteParams}; 9 | 10 | export class ModelHandlers extends CommonResourceHandlers { 11 | constructor(moduleName: string, store: any) { 12 | super({defaultRouteParams, api, enableRoute: {list: true, detail: true, edit: true}}, moduleName, store); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": ["davidanson.vscode-markdownlint", "dbaeumer.vscode-eslint", "editorconfig.editorconfig", "amatiasq.sort-imports", "esbenp.prettier-vscode", "stylelint.vscode-stylelint"], 7 | 8 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 9 | "unwantedRecommendations": [] 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/app/views/LoginForm/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 100%; 4 | height: 310px; 5 | .hasLogin { 6 | padding-top: 20px; 7 | } 8 | .link { 9 | cursor: pointer; 10 | color: @primary-color; 11 | &:hover { 12 | opacity: 0.6; 13 | } 14 | } 15 | .register { 16 | float: right; 17 | } 18 | .submit { 19 | width: 100%; 20 | margin-top: 10px; 21 | clear: both; 22 | } 23 | .title { 24 | font-weight: bold; 25 | text-align: center; 26 | font-size: 30px; 27 | padding-bottom: 10px; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/app/views/RegisterForm/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 100%; 4 | height: 370px; 5 | .hasLogin { 6 | padding-top: 20px; 7 | } 8 | .title { 9 | font-weight: bold; 10 | text-align: center; 11 | font-size: 30px; 12 | padding-bottom: 10px; 13 | } 14 | .submit { 15 | width: 100%; 16 | margin-top: 10px; 17 | clear: both; 18 | } 19 | .login { 20 | float: right; 21 | } 22 | .link { 23 | cursor: pointer; 24 | color: @primary-color; 25 | &:hover { 26 | opacity: 0.6; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ListKeyLink.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react'; 2 | 3 | interface Props { 4 | children?: ReactNode; 5 | href: string; 6 | } 7 | const Component: React.FC = ({href, children}) => { 8 | const onClick = React.useCallback((event: React.MouseEvent) => { 9 | event.preventDefault(); 10 | const url = (event.target as HTMLAnchorElement).getAttribute('href')!.replace('${listVer}', `${Date.now()}`); 11 | historyActions.push(url); 12 | }, []); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | export default React.memo(Component); 22 | -------------------------------------------------------------------------------- /src/modules/article/articleAbout/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import ArticleBanner from 'components/ArticleBanner'; 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | import useConsult from 'hooks/useConsult'; 5 | import banner from './imgs/banner.jpg'; 6 | import Activities from './Activities'; 7 | 8 | const Component: React.FC = ({dispatch}) => { 9 | const onConsult = useConsult(dispatch); 10 | return ( 11 | <> 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default connect()(React.memo(Component)); 19 | -------------------------------------------------------------------------------- /mock/delete--api-post.js: -------------------------------------------------------------------------------- 1 | let {ids} = request.body; 2 | ids = Array.isArray(ids) ? ids : []; 3 | 4 | const users = database.data.users; 5 | const posts = database.data.posts; 6 | 7 | const result = { 8 | statusCode: 422, 9 | headers: { 10 | 'x-delay': 0, 11 | 'content-type': 'application/json; charset=utf-8', 12 | }, 13 | }; 14 | 15 | if (ids.length === 0) { 16 | result.response = { 17 | message: '参数不合法!', 18 | }; 19 | return result; 20 | } 21 | 22 | ids.forEach(id => { 23 | if (posts[id] && !posts[id].fixed) { 24 | const uid = posts[id].author.id; 25 | delete posts[id]; 26 | users[uid].post--; 27 | } 28 | }); 29 | 30 | result.statusCode = 200; 31 | result.response = ''; 32 | 33 | return result; 34 | -------------------------------------------------------------------------------- /mock/delete--api-member.js: -------------------------------------------------------------------------------- 1 | let {ids} = request.body; 2 | ids = Array.isArray(ids) ? ids : []; 3 | 4 | const users = database.data.users; 5 | const roles = database.data.roles; 6 | 7 | const result = { 8 | statusCode: 422, 9 | headers: { 10 | 'x-delay': 0, 11 | 'content-type': 'application/json; charset=utf-8', 12 | }, 13 | }; 14 | 15 | if (ids.length === 0) { 16 | result.response = { 17 | message: '参数不合法!', 18 | }; 19 | return result; 20 | } 21 | 22 | ids.forEach(id => { 23 | if (users[id] && !users[id].fixed) { 24 | const roleId = users[id].roleId; 25 | delete users[id]; 26 | roles[roleId].owner--; 27 | } 28 | }); 29 | 30 | result.statusCode = 200; 31 | result.response = ''; 32 | 33 | return result; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "CommonJS", 5 | "lib": ["es2018"], 6 | "moduleResolution": "node", 7 | "types": ["node"], 8 | "noEmit": true, 9 | "allowJs": true, 10 | "checkJs": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "noImplicitReturns": true, 16 | "noUnusedLocals": true, 17 | "resolveJsonModule": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "preserveConstEnums": false, 21 | "esModuleInterop": true 22 | }, 23 | "include": ["./"], 24 | "exclude": ["./src","./dist","./mock"] 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/admin/adminHome/views/engineering.md: -------------------------------------------------------------------------------- 1 | 本项目使用[@medux/react-web-router](https://github.com/wooline/medux/tree/master/packages/react-web-router) + [ANTD 4](https://ant.design/index-cn) 开发,全程使用 React Hooks,并配备了比较完善的脚手架。 2 | 3 | ## 安装及运行 4 | 5 | ``` 6 | // 注意一下,因为本项目风格检查要求以 LF 为换行符 7 | // 所以请先关闭 Git 配置中 autocrlf 8 | git config --global core.autocrlf false 9 | git clone https://github.com/wooline/medux-react-admin.git 10 | cd medux-react-admin 11 | yarn install 12 | ``` 13 | 14 | ### 以开发模式运行 15 | 16 | - 运行 `yarn start`,会自动启动一个开发服务器 17 | - 开发模式时 React 热更新使用最新的 React Fast Refresh 方案,需要安装最新的 React Developer Tools。 18 | 19 | ## 代码介绍 20 | 21 | - [语雀](https://www.yuque.com/medux/docs/medux-react-admin-2) 22 | 23 | **欢迎批评指正,觉得还不错的别忘了给个 Star >\_<,如有错误或 Bug 请反馈** 24 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/model.ts: -------------------------------------------------------------------------------- 1 | import {BaseModelHandlers, BaseModelState, effect, reducer} from '@medux/react-web-router'; 2 | 3 | import {UnauthorizedError} from 'common/errors'; 4 | 5 | export interface State extends BaseModelState { 6 | showConsult?: boolean; 7 | } 8 | 9 | export const initModelState: State = {}; 10 | 11 | export class ModelHandlers extends BaseModelHandlers { 12 | @effect(null) 13 | public async showConsult() { 14 | if (!this.rootState.app!.curUser.hasLogin) { 15 | throw new UnauthorizedError(false); 16 | } 17 | this.updateState({showConsult: true}, 'showConsult'); 18 | } 19 | 20 | @reducer 21 | public closeConsult(): State { 22 | return {...this.state, showConsult: false}; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ArticleBanner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {RightOutlined} from '@ant-design/icons'; 3 | import styles from './index.m.less'; 4 | 5 | interface Props { 6 | title: string; 7 | content: string; 8 | bg: string; 9 | onConsult: () => void; 10 | } 11 | 12 | const Component: React.FC = (props) => { 13 | const {title, content, onConsult, bg} = props; 14 | return ( 15 |
16 | {bg} 17 |
18 |

{title}

19 |

{content}

20 | 21 | 马上咨询 22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default React.memo(Component); 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'Global'; 2 | 3 | import {defaultRouteParams, moduleGetter, routeConfig} from 'modules'; 4 | import {buildApp} from '@medux/react-web-router'; 5 | 6 | const global: any = window; 7 | 8 | if (initEnv.production) { 9 | global.console = { 10 | log: () => undefined, 11 | info: () => undefined, 12 | error: () => undefined, 13 | warn: () => undefined, 14 | }; 15 | } 16 | 17 | buildApp({ 18 | moduleGetter, 19 | routeConfig, 20 | defaultRouteParams, 21 | beforeRender: ({store, historyActions}) => { 22 | global.historyActions = historyActions; 23 | return store; 24 | }, 25 | }).then(() => { 26 | const initLoading = document.getElementById('g-init-loading'); 27 | if (initLoading) { 28 | initLoading.parentNode!.removeChild(initLoading); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Anchor/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | border-bottom: #ddd 1px solid; 4 | .ant-anchor { 5 | width: 1200px; 6 | margin: auto; 7 | padding: 0; 8 | &::after { 9 | content: ' '; 10 | display: table; 11 | clear: both; 12 | } 13 | .ant-anchor-ink { 14 | display: none; 15 | } 16 | .ant-anchor-link-active { 17 | font-weight: bold; 18 | } 19 | .ant-anchor-link { 20 | float: left; 21 | padding: 18px 25px; 22 | 23 | &:nth-child(2) { 24 | padding-left: 0; 25 | } 26 | } 27 | } 28 | } 29 | .ant-affix { 30 | :local(.root) { 31 | background: #f7f7f7; 32 | box-shadow: rgba(0, 0, 0, 0.1) 0 2px 5px; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/LoginLink.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode, useCallback} from 'react'; 2 | 3 | import {Link} from '@medux/react-web-router'; 4 | 5 | interface Props { 6 | className?: string; 7 | register?: boolean; 8 | children?: ReactNode; 9 | } 10 | 11 | const Component: React.FC = ({className, register, children}) => { 12 | const onClick = useCallback(() => { 13 | // eslint-disable-next-line no-restricted-globals 14 | sessionStorage.setItem(metaKeys.LoginRedirectSessionStorageKey, location.pathname + location.search + location.hash); 15 | }, []); 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | export default React.memo(Component); 24 | -------------------------------------------------------------------------------- /src/modules/article/articleService/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import ArticleBanner from 'components/ArticleBanner'; 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | import useConsult from 'hooks/useConsult'; 5 | import banner from './imgs/banner.jpg'; 6 | import Activities from './Activities'; 7 | 8 | const Component: React.FC = ({dispatch}) => { 9 | const onConsult = useConsult(dispatch); 10 | return ( 11 | <> 12 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default connect()(React.memo(Component)); 24 | -------------------------------------------------------------------------------- /src/modules/admin/adminHome/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import {connect} from 'react-redux'; 4 | import engineeringArticle from './engineering.md'; 5 | import styles from './index.m.less'; 6 | import summaryArticle from './summary.md'; 7 | 8 | const Component: React.FC = () => { 9 | return ( 10 |
11 |

关于本项目

12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | ); 22 | }; 23 | 24 | export default connect()(React.memo(Component)); 25 | -------------------------------------------------------------------------------- /src/Prepose.tsx: -------------------------------------------------------------------------------- 1 | // 某些全局变量必须前置引入 2 | import React from 'react'; 3 | import {SyncOutlined} from '@ant-design/icons'; 4 | import {loadView as baseLoadView} from '@medux/react-web-router'; 5 | 6 | import {metaKeys} from './common/utils'; 7 | 8 | const DefLoading = () => ( 9 |
10 | 11 |
12 | ); 13 | const DefError = () =>
error
; 14 | const loadView = (moduleName: string, viewName: never, options?: any, loading: React.ComponentType = DefLoading, error: React.ComponentType = DefError) => 15 | baseLoadView(moduleName, viewName, options, loading, error); 16 | 17 | ((data: {[key: string]: any}) => { 18 | Object.keys(data).forEach((key) => { 19 | window[key] = data[key]; 20 | }); 21 | })({metaKeys, loadView}); 22 | -------------------------------------------------------------------------------- /mock/put--api-member.js: -------------------------------------------------------------------------------- 1 | let {ids, status = ''} = request.body; 2 | ids = Array.isArray(ids) ? ids : []; 3 | status = status.toString(); 4 | 5 | const users = database.data.users; 6 | 7 | const result = { 8 | statusCode: 422, 9 | headers: { 10 | 'x-delay': 0, 11 | 'content-type': 'application/json; charset=utf-8', 12 | }, 13 | }; 14 | 15 | if (ids.length === 0) { 16 | result.response = { 17 | message: '参数不合法!', 18 | }; 19 | return result; 20 | } 21 | 22 | if (!['enable', 'disable'].includes(status)) { 23 | result.response = { 24 | message: '状态不合法!', 25 | }; 26 | return result; 27 | } 28 | ids.forEach(id => { 29 | const curItem = users[id]; 30 | if (curItem && !curItem.fixed) { 31 | curItem.status = status; 32 | } 33 | }); 34 | 35 | result.statusCode = 200; 36 | result.response = ''; 37 | 38 | return result; 39 | -------------------------------------------------------------------------------- /mock/put--api-post.js: -------------------------------------------------------------------------------- 1 | let {ids, status = ''} = request.body; 2 | ids = Array.isArray(ids) ? ids : []; 3 | status = status.toString(); 4 | 5 | const posts = database.data.posts; 6 | 7 | const result = { 8 | statusCode: 422, 9 | headers: { 10 | 'x-delay': 0, 11 | 'content-type': 'application/json; charset=utf-8', 12 | }, 13 | }; 14 | 15 | if (ids.length === 0) { 16 | result.response = { 17 | message: '参数不合法!', 18 | }; 19 | return result; 20 | } 21 | 22 | if (!['resolved', 'rejected'].includes(status)) { 23 | result.response = { 24 | message: '状态不合法!', 25 | }; 26 | return result; 27 | } 28 | ids.forEach(id => { 29 | const curItem = posts[id]; 30 | if (curItem && !curItem.fixed) { 31 | curItem.status = status; 32 | } 33 | }); 34 | 35 | result.statusCode = 200; 36 | result.response = ''; 37 | 38 | return result; 39 | -------------------------------------------------------------------------------- /src/modules/app/views/LoginPop/index.tsx: -------------------------------------------------------------------------------- 1 | import {Modal} from 'antd'; 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | import LoginForm from '../LoginForm'; 5 | 6 | interface StoreProps { 7 | showPop: boolean; 8 | } 9 | 10 | const Component: React.FC = ({showPop, dispatch}) => { 11 | const onCancel = React.useCallback(() => { 12 | dispatch(actions.app.closesLoginOrRegisterPop()); 13 | }, [dispatch]); 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 23 | return { 24 | showPop: state.app!.showLoginOrRegisterPop === 'login', 25 | }; 26 | }; 27 | export default connect(mapStateToProps)(React.memo(Component)); 28 | -------------------------------------------------------------------------------- /src/modules/app/views/RegisterPop/index.tsx: -------------------------------------------------------------------------------- 1 | import {Modal} from 'antd'; 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | import RegisterForm from '../RegisterForm'; 5 | 6 | interface StoreProps { 7 | showPop: boolean; 8 | } 9 | 10 | const Component: React.FC = ({showPop, dispatch}) => { 11 | const onCancel = React.useCallback(() => { 12 | dispatch(actions.app.closesLoginOrRegisterPop()); 13 | }, [dispatch]); 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 23 | return { 24 | showPop: state.app!.showLoginOrRegisterPop === 'register', 25 | }; 26 | }; 27 | export default connect(mapStateToProps)(React.memo(Component)); 28 | -------------------------------------------------------------------------------- /src/modules/app/views/GlobalLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import {LoadingState} from '@medux/react-web-router'; 2 | import React from 'react'; 3 | import {Spin} from 'antd'; 4 | import {connect} from 'react-redux'; 5 | import styles from './index.m.less'; 6 | 7 | interface StoreProps { 8 | globalLoading: LoadingState; 9 | } 10 | 11 | const Component: React.FC = ({globalLoading}) => { 12 | return globalLoading === LoadingState.Start || globalLoading === LoadingState.Depth ? ( 13 |
14 |
15 | 16 |
17 |
18 | ) : null; 19 | }; 20 | 21 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 22 | return { 23 | globalLoading: state.app!.loading.global, 24 | }; 25 | }; 26 | export default connect(mapStateToProps)(React.memo(Component)); 27 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/views/Footer/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | background: #242a37; 4 | color: #fff; 5 | .g-doc { 6 | padding: 40px 0 20px; 7 | > dl { 8 | float: left; 9 | margin-right: 90px; 10 | margin-bottom: 20px; 11 | &:last-of-type { 12 | float: right; 13 | } 14 | } 15 | } 16 | a { 17 | color: #aaa; 18 | &:hover { 19 | color: #fff; 20 | } 21 | } 22 | dl, 23 | dd { 24 | line-height: 2.5; 25 | margin: 0; 26 | } 27 | dt { 28 | font-size: 16px; 29 | font-weight: 500; 30 | } 31 | .copyright { 32 | clear: both; 33 | font-size: 12px; 34 | text-align: center; 35 | color: #aaa; 36 | border-top: #393e49 1px solid; 37 | padding-top: 20px; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ArticleBanner/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | position: relative; 4 | background: #333; 5 | height: 500px; 6 | > img { 7 | width: 100%; 8 | height: 100%; 9 | position: absolute; 10 | left: 0; 11 | top: 0; 12 | } 13 | * { 14 | color: #fff; 15 | } 16 | .g-doc { 17 | transform: translateY(-50%); 18 | top: 50%; 19 | position: relative; 20 | } 21 | h2 { 22 | font-size: 44px; 23 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.24); 24 | line-height: 56px; 25 | } 26 | p { 27 | font-size: 18px; 28 | width: 70%; 29 | } 30 | .primaryBtn { 31 | background: @primary-color; 32 | display: inline-block; 33 | padding: 5px 20px; 34 | cursor: pointer; 35 | &:hover { 36 | opacity: 0.7; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/useDetail.ts: -------------------------------------------------------------------------------- 1 | import {BaseListItem} from 'entity'; 2 | import {CommonResourceActions} from 'common/resource'; 3 | import {useCallback} from 'react'; 4 | import useEventCallback from './useEventCallback'; 5 | 6 | export default function Hooks(dispatch: (action: any) => void, resourceActions: CommonResourceActions, currentItem: BaseListItem) { 7 | const {id} = currentItem; 8 | const onHide = useCallback(() => { 9 | dispatch(resourceActions.closeCurrentItem()); 10 | }, [dispatch, resourceActions]); 11 | const onEdit = useCallback(() => { 12 | dispatch(resourceActions.openCurrentItem(currentItem, 'edit')); 13 | }, [dispatch, resourceActions, currentItem]); 14 | const onDelete = useEventCallback(async () => { 15 | await dispatch(resourceActions.deleteList([id])); 16 | onHide(); 17 | }, [id, dispatch, onHide, resourceActions]); 18 | return {onHide, onEdit, onDelete}; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/Header/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | display: flex; 4 | height: 50px; 5 | line-height: 50px; 6 | justify-content: space-between; 7 | padding: 0 30px; 8 | 9 | .toggleSider { 10 | font-size: 20px; 11 | } 12 | > .main { 13 | display: flex; 14 | align-items: center; 15 | > * { 16 | margin-right: 20px; 17 | } 18 | } 19 | > .side { 20 | align-items: center; 21 | display: flex; 22 | > * { 23 | margin-left: 20px; 24 | } 25 | } 26 | .account { 27 | cursor: pointer; 28 | .avatar { 29 | margin-right: 5px; 30 | } 31 | } 32 | .noticeIcon { 33 | cursor: pointer; 34 | display: block; 35 | padding-right: 10px; 36 | margin-right: 18px; 37 | .anticon { 38 | font-size: 16px; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/views/Selector.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback} from 'react'; 2 | import ResourceSimpleSelector, {Props as ResourceSimpleSelectorProps} from 'components/ResourceSimpleSelector'; 3 | 4 | import {ListItem} from 'entity/role'; 5 | import {OmitSelf} from 'common/utils'; 6 | import api from '../api'; 7 | 8 | export interface Item { 9 | id: string; 10 | name: string; 11 | } 12 | type OwnProps = OmitSelf, 'fetch'>; 13 | 14 | const Component: React.FC = ({placeholder = '请选择角色', optionRender = 'roleName', ...props}) => { 15 | const fetch = useCallback((term: string, pageSize: number, pageCurrent: number) => { 16 | return api.searchList({term, pageSize, pageCurrent}); 17 | }, []); 18 | 19 | return {...props} placeholder={placeholder} optionRender={optionRender} fetch={fetch} />; 20 | }; 21 | 22 | export default React.memo(Component); 23 | -------------------------------------------------------------------------------- /tsconfig-src.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["es2018","DOM"], 6 | "moduleResolution": "node", 7 | "types": ["../typings/assets"], 8 | "noEmit": true, 9 | "allowJs": true, 10 | "checkJs": true, 11 | "strict": true, 12 | "jsx": "react", 13 | "declaration": false, 14 | "isolatedModules": true, 15 | "sourceMap": false, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "newLine": "LF", 19 | "removeComments": true, 20 | "resolveJsonModule": true, 21 | "noImplicitReturns": true, 22 | "noUnusedLocals": false, 23 | "importHelpers": false, 24 | "alwaysStrict": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "suppressImplicitAnyIndexErrors": true, 27 | "preserveConstEnums": false, 28 | "allowSyntheticDefaultImports": true, 29 | "noImplicitAny": false, 30 | "esModuleInterop": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/api.ts: -------------------------------------------------------------------------------- 1 | import {ListItem, ListSearch, ListSummary, UpdateItem} from 'entity/role'; 2 | 3 | import {CommonResourceAPI} from 'common/resource'; 4 | import ajax from 'common/request'; 5 | 6 | export class API extends CommonResourceAPI { 7 | public searchList(request: ListSearch): Promise<{list: ListItem[]; listSummary: ListSummary}> { 8 | return ajax('get', '/api/role', this._filterEmpty(request)); 9 | } 10 | 11 | public deleteList(ids: string[]): Promise { 12 | return ajax('delete', '/api/role', {}, {ids}); 13 | } 14 | 15 | public updateItem(item: UpdateItem): Promise { 16 | return ajax('put', '/api/role/:id', {id: item.id}, item); 17 | } 18 | 19 | public createItem(item: UpdateItem): Promise { 20 | return ajax('post', '/api/role', {}, item); 21 | } 22 | 23 | public getDetailItem(id: string): Promise { 24 | return ajax('get', '/api/role/:id', {id}); 25 | } 26 | } 27 | 28 | export default new API(); 29 | -------------------------------------------------------------------------------- /src/modules/app/components/FormLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import {DingdingOutlined} from '@ant-design/icons'; 2 | import {Link} from '@medux/react-web-router'; 3 | import React from 'react'; 4 | import styles from './index.m.less'; 5 | 6 | interface Props {} 7 | 8 | const Component: React.FC = ({children}) => { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 | 帮助中心 > 16 |
17 |

欢迎使用 Medux

18 |

本项目主要用来展示如何将 @medux 应用于 web 后台管理系统,你可能看不到丰富的后台 UI 控件及界面,因为这不是重点,网上这样的轮子已经很多了,而本项目想着重表达的是“通用化解题思路”

19 |
20 |
{children}
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default React.memo(Component); 28 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/views/ConsultPop/index.tsx: -------------------------------------------------------------------------------- 1 | import {Input, Modal} from 'antd'; 2 | import React, {useCallback} from 'react'; 3 | 4 | import {connect} from 'react-redux'; 5 | import styles from './index.m.less'; 6 | 7 | interface StoreProps { 8 | showPop?: boolean; 9 | } 10 | 11 | const Component: React.FC = ({showPop, dispatch}) => { 12 | const onCancel = useCallback(() => { 13 | dispatch(actions.articleLayout.closeConsult()); 14 | }, [dispatch]); 15 | 16 | return ( 17 | 18 |
19 |

请输入您要咨询的问题:

20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 27 | return { 28 | showPop: state.articleLayout!.showConsult, 29 | }; 30 | }; 31 | export default connect(mapStateToProps)(React.memo(Component)); 32 | -------------------------------------------------------------------------------- /mock/post--api-session.js: -------------------------------------------------------------------------------- 1 | let {username = '', password = '', keep} = request.body; 2 | username = username.toString(); 3 | password = password.toString(); 4 | keep = Boolean(keep); 5 | 6 | const result = { 7 | headers: { 8 | 'x-delay': 0, 9 | 'content-type': 'application/json; charset=utf-8', 10 | }, 11 | }; 12 | 13 | const year = Date.now() + 24 * 3600 * 365; 14 | const week = Date.now() + 24 * 3600 * 7; 15 | 16 | expired = keep ? week : year; 17 | 18 | const users = database.data.users; 19 | const curUser = users[username]; 20 | if (curUser && password === curUser.password) { 21 | curUser.loginTime = Date.now(); 22 | const token = database.action.users.createToken(username, expired); 23 | result.statusCode = 200; 24 | result.cookies = [['token', token, {expires: keep ? new Date(year) : undefined, httpOnly: true}]]; 25 | result.response = curUser; 26 | } else { 27 | result.statusCode = 422; 28 | result.response = { 29 | message: '用户名或密码错误!', 30 | }; 31 | } 32 | 33 | return result; 34 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/Flag/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | background: linear-gradient(to top, #001529, #103f6d, #001529); 4 | overflow: hidden; 5 | padding: 15px 0 0 10px; 6 | height: 80px; 7 | margin-bottom: 20px; 8 | .panel { 9 | display: block; 10 | width: 180px; 11 | } 12 | h1 { 13 | color: #fff; 14 | font-weight: bold; 15 | font-size: 20px; 16 | margin: 0; 17 | width: 178px; 18 | } 19 | .logo { 20 | float: left; 21 | padding: 6px 5px 5px 0; 22 | } 23 | .ver { 24 | font-family: 'Tahoma'; 25 | color: black; 26 | font-size: 12px; 27 | opacity: 0.5; 28 | background: #fff; 29 | display: inline-block; 30 | padding: 0 5px; 31 | border-radius: 10px; 32 | line-height: 14px; 33 | } 34 | } 35 | .ant-layout-sider-collapsed :local(.root) { 36 | h1, 37 | .ver { 38 | display: none; 39 | } 40 | .logo { 41 | margin-left: 10px; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/hooks/useLoginLink.ts: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | 3 | export default function Hooks(isPop: boolean, dispatch: (action: any) => void) { 4 | const handleUserHome = useCallback(() => { 5 | if (isPop) { 6 | dispatch(actions.app.closesLoginOrRegisterPop()); 7 | } 8 | historyActions.push(metaKeys.UserHomePathname); 9 | }, [dispatch, isPop]); 10 | 11 | const handleLogout = useCallback(() => { 12 | dispatch(actions.app.logout()); 13 | }, [dispatch]); 14 | 15 | const handleRegister = useCallback(() => { 16 | if (isPop) { 17 | dispatch(actions.app.openLoginOrRegisterPop('register')); 18 | } else { 19 | historyActions.push(metaKeys.RegisterPathname); 20 | } 21 | }, [dispatch, isPop]); 22 | 23 | const handleLogin = useCallback(() => { 24 | if (isPop) { 25 | dispatch(actions.app.openLoginOrRegisterPop('login')); 26 | } else { 27 | historyActions.push(metaKeys.LoginPathname); 28 | } 29 | }, [dispatch, isPop]); 30 | 31 | return {handleUserHome, handleLogout, handleRegister, handleLogin}; 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ImgAD from './imgs/ad.jpg'; 3 | import styles from './index.m.less'; 4 | 5 | const Component: React.FC = () => { 6 | return ( 7 |
8 |

技术优势

9 |
10 | logo 11 |
12 |
13 |
14 |

企业展示

15 |

通过入驻平台,联合开展市场营销,有利于进步扩大其他的宣传力度

16 |
17 |
18 |

赋能升级

19 |

通过提供专业的技术,帮助中小型产业赋能AI,实现企业升级

20 |
21 |
22 |

专业技术

23 |

专业的架构师团队支持合作伙伴构架解决方案,实现互联互通

24 |
25 |
26 |

国内领先

27 |

平台自身在技术上具有领先的优势,通过合作致力于打造全球生态

28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default React.memo(Component); 35 | -------------------------------------------------------------------------------- /mock/post--api-role.js: -------------------------------------------------------------------------------- 1 | let {roleName = '', remark = '', purviews} = request.body; 2 | roleName = roleName.toString(); 3 | remark = remark.toString(); 4 | purviews = Array.isArray(purviews) ? purviews : []; 5 | 6 | const result = { 7 | statusCode: 422, 8 | headers: { 9 | 'x-delay': 0, 10 | 'content-type': 'application/json; charset=utf-8', 11 | }, 12 | }; 13 | const roles = database.data.roles; 14 | 15 | if (roleName.length > 20 || roleName.length < 2) { 16 | result.response = { 17 | message: '角色名称必须为2-20个字符!', 18 | }; 19 | return result; 20 | } 21 | if (remark.length > 20) { 22 | result.response = { 23 | message: '备注不能超过20个字符!', 24 | }; 25 | return result; 26 | } 27 | if (purviews.length === 0 || purviews.some(purview => !database.data.config.purviews[purview])) { 28 | result.response = { 29 | message: '权限设置不合法!', 30 | }; 31 | return result; 32 | } 33 | const id = Date.now().toString(); 34 | roles[id] = { 35 | id, 36 | createdTime: Date.now(), 37 | owner: 0, 38 | roleName, 39 | remark, 40 | purviews: Array.from(new Set(purviews)), 41 | }; 42 | 43 | result.statusCode = 200; 44 | result.response = ''; 45 | 46 | return result; 47 | -------------------------------------------------------------------------------- /public/start.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const chalk = require('chalk'); 3 | const fs = require('fs'); 4 | const {createProxyMiddleware} = require('http-proxy-middleware'); 5 | const bodyParser = require('body-parser'); 6 | const cookieParser = require('cookie-parser'); 7 | const devMock = require('@medux/dev-utils/dist/express-middleware/dev-mock'); 8 | const prodServer = require('@medux/dev-utils/dist/express-middleware/prod-server'); 9 | 10 | const htmlTpl = fs.readFileSync('./index.html', 'utf8'); 11 | 12 | // @ts-ignore 13 | // eslint-disable-next-line import/no-unresolved 14 | const {proxy, server, mock} = require('./env.json'); 15 | 16 | const app = express(); 17 | const [, , port] = server.split(/:\/*/); 18 | 19 | app.use('/client', express.static('./client', {fallthrough: false})); 20 | app.use(bodyParser.urlencoded({extended: true})); 21 | app.use(bodyParser.json()); 22 | app.use(cookieParser()); 23 | app.use(devMock(mock, proxy, true)); 24 | app.use('/api', createProxyMiddleware(proxy['/api/**'])); 25 | app.use(prodServer(htmlTpl)); 26 | app.listen(port, () => console.info(chalk`.....${new Date().toLocaleString()} starting {red Demo Server} on {green ${server}/} \n`)); 27 | -------------------------------------------------------------------------------- /src/hooks/useAnchorPage.ts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | 3 | let routeFlag = ''; 4 | let routeTimer = 0; 5 | 6 | const scrollToAnchor = (flag: string) => { 7 | routeFlag = flag; 8 | if (routeTimer) { 9 | clearTimeout(routeTimer); 10 | routeTimer = 0; 11 | } 12 | routeTimer = setTimeout(() => { 13 | routeTimer = 0; 14 | const anchor = routeFlag.substr(1); 15 | const anchorTarget = anchor ? document.getElementById(anchor) : null; 16 | if (anchorTarget) { 17 | anchorTarget.scrollIntoView(); 18 | } else { 19 | document.body.scrollTop = 0; 20 | document.documentElement.scrollTop = 0; 21 | } 22 | }, 100); 23 | }; 24 | 25 | export default function Hooks(hash: string, history: {listen: (handler: (location: any) => void) => () => void}) { 26 | useEffect(() => { 27 | scrollToAnchor(hash); 28 | const unlisten = history.listen((location) => { 29 | scrollToAnchor(location.hash); 30 | }); 31 | return () => { 32 | unlisten(); 33 | if (routeTimer) { 34 | clearTimeout(routeTimer); 35 | routeTimer = 0; 36 | } 37 | }; 38 | // eslint-disable-next-line react-hooks/exhaustive-deps 39 | }, []); 40 | } 41 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const runtimeVersion = require('@babel/runtime/package.json').version; 2 | const pathsConfig = require('./build/path.conifg'); 3 | 4 | module.exports = { 5 | presets: [ 6 | [ 7 | '@babel/preset-env', 8 | { 9 | loose: true, 10 | modules: false, 11 | }, 12 | ], 13 | '@babel/preset-react', 14 | '@babel/preset-typescript', 15 | ].filter(Boolean), 16 | plugins: [ 17 | [ 18 | 'module-resolver', 19 | { 20 | root: pathsConfig.moduleSearch, 21 | alias: pathsConfig.alias, 22 | }, 23 | ], 24 | ['import', {libraryName: 'antd', libraryDirectory: 'es', style: true}], 25 | '@babel/plugin-syntax-dynamic-import', 26 | ['@babel/plugin-proposal-decorators', {legacy: false, decoratorsBeforeExport: true}], 27 | ['@babel/plugin-proposal-class-properties', {loose: true}], 28 | '@babel/plugin-proposal-nullish-coalescing-operator', 29 | '@babel/plugin-proposal-optional-chaining', 30 | '@babel/plugin-proposal-object-rest-spread', 31 | [ 32 | '@babel/plugin-transform-runtime', 33 | { 34 | useESModules: true, 35 | version: runtimeVersion, 36 | }, 37 | ], 38 | ].filter(Boolean), 39 | ignore: ['**/*.d.ts'], 40 | }; 41 | -------------------------------------------------------------------------------- /src/modules/app/api.ts: -------------------------------------------------------------------------------- 1 | import {CurUser, LoginRequest, Notices, RegisterRequest, guest} from 'entity/session'; 2 | 3 | import {ProjectConfig} from 'entity'; 4 | import ajax from 'common/request'; 5 | 6 | export function setCookie(name: string, value: string, expiredays: number) { 7 | const exdate = new Date(); 8 | exdate.setDate(exdate.getDate() + expiredays); 9 | document.cookie = `${name}=${escape(value)};expires=${exdate.toUTCString()};path=/`; 10 | } 11 | export class API { 12 | public getCurUser(): Promise { 13 | return ajax('get', '/api/session').catch((err) => { 14 | return guest; 15 | }); 16 | } 17 | 18 | public login(req: LoginRequest): Promise { 19 | return ajax('post', '/api/session', {}, req); 20 | } 21 | 22 | public register(req: RegisterRequest): Promise { 23 | return ajax('post', '/api/member', {}, req); 24 | } 25 | 26 | public logout(): Promise { 27 | return ajax('delete', '/api/session'); 28 | } 29 | 30 | public getNotices(): Promise { 31 | return ajax('get', '/api/notices'); 32 | } 33 | 34 | public getProjectConfig(): Promise { 35 | return ajax('get', '/api/projectConfig'); 36 | } 37 | } 38 | 39 | export default new API(); 40 | -------------------------------------------------------------------------------- /src/modules/article/articleService/views/Activities/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.m.less'; 3 | 4 | const Component: React.FC = () => { 5 | return ( 6 |
7 |

售后服务

8 |
9 |

10 | “土地征收问题的核心在于补偿。”10月27日,在第八届在鸣行政法治论坛上,多位学者聚焦新土地管理法动向,围绕“土地征收中的公共利益”和“征收新程序与土地利益分配”等议题展开探讨,有学者指出,政府因公共利益征地时,如果不解决补偿问题,土地征收问题就不可能有效解决。 11 |

12 | 13 |

14 | 上述论坛由北京在明律师事务所和《法律与生活》杂志社联合主办。澎湃新闻(www.thepaper.cn)注意到,今年8月26日,十三届全国人大常委会第十二次会议表决通过了关于修改土地管理法的决定,并于2020年1月1日起施行新法。 15 |

16 | 17 |

“补偿问题不解决,土地征收问题不可能有效解决”

18 | 19 |

20 | 有关征地补偿问题引发学界关注。澎湃新闻观察到,新土地管理法第四十五条首次针对土地征收的公共利益作出界定,明确了因军事和外交、政府组织实施的基础设施建设、公益事业、扶贫搬迁和保障性安居工程以及成片开发建设等六种情况确需要征地的,可以依法实施征收。 21 |

22 |

23 | 韩俊指出,乡村社会的治理要采取符合农村特点的方式,既要充分运用现代的治理理念和方式,也要充分发挥农村传统治理资源的作用,采取村规民约来推进乡风文明建设。村规民约是村民自我管理、自我教育、自我监督的行为规范,介于法律和道德之间,更多的是发挥道德的约束作用。 24 |

25 | 26 |

韩俊指出,一些地方村规民约的内容还是比较空泛,制定也不够规范,实施也存在流于形式的问题。特别是有一些村规民约的内容涉嫌违法违规,对亲情伦理缺乏关怀,容易伤害群众的感情。

27 |
28 |
29 | ); 30 | }; 31 | 32 | export default React.memo(Component); 33 | -------------------------------------------------------------------------------- /public/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name medux-react-admin.80zp.com; 5 | root /var/www/medux-react-admin; 6 | index index.html; 7 | access_log off; 8 | error_log /var/www/medux-react-admin/logs/nginx-error.log; 9 | error_page 404 /404.html; 10 | error_page 500 502 503 504 /50x.html; 11 | gzip on; 12 | # Load configuration files for the default server block. 13 | include /etc/nginx/default.d/*.conf; 14 | 15 | location = /404.html { 16 | } 17 | 18 | location = /50x.html { 19 | } 20 | 21 | location ^~ /client { 22 | etag off; 23 | add_header Last-Modified ""; 24 | if_modified_since off; 25 | expires max; 26 | } 27 | 28 | location ^~ /api { 29 | proxy_pass http://127.0.0.1:7445; 30 | } 31 | 32 | location ~* \.\w+$ { 33 | return 404; 34 | } 35 | 36 | location / { 37 | try_files /index.html /index.html; 38 | } 39 | 40 | #location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { 41 | # expires max; 42 | # access_log off; 43 | # log_not_found off; 44 | #} 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import NotFound from 'components/NotFound'; 2 | import React from 'react'; 3 | import {Switch} from '@medux/react-web-router'; 4 | import {connect} from 'react-redux'; 5 | import Header from './Header'; 6 | import Footer from './Footer'; 7 | import ConsultPop from './ConsultPop'; 8 | 9 | const ArticleHome = loadView('articleHome', 'main'); 10 | const ArticleAbout = loadView('articleAbout', 'main'); 11 | const ArticleService = loadView('articleService', 'main'); 12 | 13 | interface StoreProps { 14 | routeViews: RouteViews; 15 | } 16 | 17 | const Component: React.FC = ({routeViews}) => { 18 | return ( 19 |
20 |
21 |
22 | }> 23 | {routeViews.articleHome?.main && } 24 | {routeViews.articleAbout?.main && } 25 | {routeViews.articleService?.main && } 26 | 27 |
28 |
29 | 30 |
31 | ); 32 | }; 33 | 34 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 35 | return { 36 | routeViews: state.route.data.views, 37 | }; 38 | }; 39 | 40 | export default connect(mapStateToProps)(React.memo(Component)); 41 | -------------------------------------------------------------------------------- /mock/put--api-role-xxx@cHV0LS1hcGktcm9sZS1cdys=.js: -------------------------------------------------------------------------------- 1 | let {id = '', roleName = '', remark = '', purviews} = request.body; 2 | id = id.toString(); 3 | roleName = roleName.toString(); 4 | remark = remark.toString(); 5 | purviews = Array.isArray(purviews) ? purviews : []; 6 | 7 | const result = { 8 | statusCode: 422, 9 | headers: { 10 | 'x-delay': 0, 11 | 'content-type': 'application/json; charset=utf-8', 12 | }, 13 | }; 14 | 15 | const roles = database.data.roles; 16 | const curItem = roles[id]; 17 | 18 | if (!curItem) { 19 | result.response = { 20 | message: '目标不存在!', 21 | }; 22 | return result; 23 | } 24 | if (curItem.fixed) { 25 | result.response = { 26 | message: '目标不允许修改!', 27 | }; 28 | return result; 29 | } 30 | if (roleName.length > 20 || roleName.length < 2) { 31 | result.response = { 32 | message: '角色名称必须为2-20个字符!', 33 | }; 34 | return result; 35 | } 36 | if (remark.length > 20) { 37 | result.response = { 38 | message: '备注不能超过20个字符!', 39 | }; 40 | return result; 41 | } 42 | if (purviews.length === 0 || purviews.some(purview => !database.data.config.purviews[purview])) { 43 | result.response = { 44 | message: '权限设置不合法!', 45 | }; 46 | return result; 47 | } 48 | 49 | Object.assign(curItem, {roleName, remark, purviews}); 50 | 51 | result.statusCode = 200; 52 | result.response = ''; 53 | 54 | return result; 55 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/views/Header/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | position: absolute; 4 | z-index: 100; 5 | width: 100%; 6 | background: rgba(23, 32, 45, 0.6); 7 | color: #fff; 8 | a { 9 | color: #fff; 10 | &:hover { 11 | color: #aaa; 12 | } 13 | &.active { 14 | font-weight: bold; 15 | color: #4a9eff; 16 | } 17 | } 18 | .link { 19 | cursor: pointer; 20 | &:hover { 21 | color: #aaa; 22 | } 23 | } 24 | h1 { 25 | letter-spacing: 2px; 26 | margin: 0; 27 | color: #fff; 28 | font-size: 25px; 29 | font-weight: bold; 30 | margin-right: 30px !important; 31 | } 32 | .logo { 33 | margin-right: 10px !important; 34 | } 35 | .user { 36 | background: rgba(0, 118, 225, 0.7); 37 | padding: 0 20px; 38 | } 39 | 40 | .g-doc { 41 | display: flex; 42 | justify-content: space-between; 43 | line-height: 50px; 44 | > .main { 45 | display: flex; 46 | align-items: center; 47 | > * { 48 | margin-right: 20px; 49 | } 50 | } 51 | > .sider { 52 | display: flex; 53 | align-items: center; 54 | > * { 55 | margin-left: 20px; 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/api.ts: -------------------------------------------------------------------------------- 1 | import {ListItem, ListSearch, ListSummary, UpdateItem} from 'entity/member'; 2 | 3 | import {CommonResourceAPI} from 'common/resource'; 4 | import ajax from 'common/request'; 5 | 6 | export class API extends CommonResourceAPI { 7 | public searchList(request: ListSearch): Promise<{list: ListItem[]; listSummary: ListSummary}> { 8 | const {role, ...args} = request; 9 | args.roleId = role?.id; 10 | return ajax('get', '/api/member', this._filterEmpty(args)); 11 | } 12 | 13 | public deleteList(ids: string[]): Promise { 14 | return ajax('delete', '/api/member', {}, {ids}); 15 | } 16 | 17 | public changeListStatus(ids: string[], status: string): Promise { 18 | return ajax('put', '/api/member', {}, {ids, status}); 19 | } 20 | 21 | public getDetailItem(id: string): Promise { 22 | return ajax('get', '/api/member/:id', {id}); 23 | } 24 | 25 | public createItem(item: UpdateItem): Promise { 26 | const {username, role, ...info} = item; 27 | info.roleId = role?.id!; 28 | return ajax('post', '/api/member', {}, {username, info}); 29 | } 30 | 31 | public updateItem(item: UpdateItem): Promise { 32 | const {id, role, ...info} = item; 33 | info.roleId = role?.id!; 34 | return ajax('put', '/api/member/:id', {id}, {id, info}); 35 | } 36 | } 37 | 38 | export default new API(); 39 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/api.ts: -------------------------------------------------------------------------------- 1 | import {ListItem, ListSearch, ListSummary, UpdateItem} from 'entity/post'; 2 | 3 | import {CommonResourceAPI} from 'common/resource'; 4 | import ajax from 'common/request'; 5 | 6 | export class API extends CommonResourceAPI { 7 | public searchList(request: ListSearch): Promise<{list: ListItem[]; listSummary: ListSummary}> { 8 | const {editor, ...args} = request; 9 | args.editorId = editor?.id; 10 | return ajax('get', '/api/post', this._filterEmpty(args)); 11 | } 12 | 13 | public deleteList(ids: string[]): Promise { 14 | return ajax('delete', '/api/post', {}, {ids}); 15 | } 16 | 17 | public changeListStatus(ids: string[], status: string): Promise { 18 | return ajax('put', '/api/post', {}, {ids, status}); 19 | } 20 | 21 | public getDetailItem(id: string): Promise { 22 | return ajax('get', '/api/post/:id', {id}); 23 | } 24 | 25 | public createItem(item: UpdateItem): Promise { 26 | const {editors, ...info} = item; 27 | info.editorIds = editors.map((editor) => editor.id); 28 | return ajax('post', '/api/post', {}, info); 29 | } 30 | 31 | public updateItem(item: UpdateItem): Promise { 32 | const {editors, ...info} = item; 33 | const id = item.id; 34 | info.editorIds = editors.map((editor) => editor.id); 35 | return ajax('put', '/api/post/:id', {id}, info); 36 | } 37 | } 38 | 39 | export default new API(); 40 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Recommend/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | padding-top: 60px; 4 | padding-bottom: 60px; 5 | text-align: center; 6 | background: #1c212a; 7 | h2 { 8 | font-weight: 700; 9 | font-size: 30px; 10 | margin: 0 0 50px; 11 | color: #fff; 12 | } 13 | .summary { 14 | margin-top: 40px; 15 | > div { 16 | position: relative; 17 | background: rgba(255, 255, 255, 0.1); 18 | text-align: left; 19 | width: 585px; 20 | margin-right: 30px; 21 | padding: 20px 20px 20px 70px; 22 | color: #fff; 23 | border: #999 1px solid; 24 | float: left; 25 | &:last-child { 26 | margin-right: 0; 27 | } 28 | .anticon { 29 | position: absolute; 30 | font-size: 30px; 31 | top: 25px; 32 | left: 20px; 33 | } 34 | } 35 | h4 { 36 | color: #fff; 37 | font-size: 20px; 38 | line-height: 25px; 39 | margin: 0; 40 | } 41 | p { 42 | color: #ddd; 43 | margin-top: 15px; 44 | margin-bottom: 0; 45 | } 46 | } 47 | .g-activities { 48 | > article { 49 | background: #fff; 50 | height: 350px; 51 | border: none; 52 | } 53 | > .aside { 54 | height: 350px; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/article/articleAbout/views/Activities/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.m.less'; 3 | 4 | const Component: React.FC = () => { 5 | return ( 6 |
7 |

关于我们

8 |
9 |

10 | 一年前,我们将「自然」确立为 Ant Design 的核心价值观,来指导我们的设计工作。一年来,越来越多的产品通过使用 Ant Design 为成千上万用户提供优质的企业级服务;同时,也有越来越多的设计体系基于 Ant 11 | Design 蓬勃生长。这一次,我们将清晰阐述「自然」这一价值观,希望能启发或帮助大家完成自己的产品 / 体系构建;同时,你们的反馈和互动也会成为我们进步的源泉和动力。 12 |

13 | 14 |

15 | 对比建筑设计和工业设计,人机交互设计是一个相对年轻的领域,同时这三个领域又有诸多共性。纵观几十年的人机交互应用史,我们经历过初期的 16 | TCD(以技术为核心的设计)、BCD(以商业为核心的设计),直到现在拨乱反正的 17 | UCD(以用户为核心的设计)。那么未来会走向哪里?发展了几千年的建筑设计和几百年的工业设计,目前已经很少提及以用户为核心的设计。尤其在建筑设计领域,我们更多感受到的是大师对于人、环境、建筑三者和谐共处的探讨和实践。 18 |

19 | 20 |

我们可以以史为鉴,从建筑领域、工业产品领域获取灵感,来观察整个人机交互的发展现状,以及推测未来的发展方向。

21 | 22 |

所谓的功能:并非只存在于「人」或「工具」一方,而是借由双方的力量进行融合。 ——深泽直人

23 |

24 | 先看 25 | Hammer的设计。考虑到人在使用锤子时,需要很大力气敲击,但是由于塑料材质比较滑,所以设计师在手柄的下半部分添加了黑色橡胶,通过增强摩擦力。这看起来没问题,却忽略了一些使用细节。人在使用锤子时,往往先握住手柄中上部将钉子定位;再滑到手柄底部,大力敲打钉子。而这额外的橡胶却阻碍了滑动。 26 |

27 | 28 |

29 | 再看金槌的设计。由于是木制手柄,在一开始使用时没有多大区别。但是多次滑动和大力握持后,人的手汗和劲道会和木制手柄发生反应,从而改变手柄的形状。这些重复的互动行为,让手柄中部光滑,从而容易滑动;让手柄底部出现握痕,从而更容易施力。 30 |

31 |
32 |
33 | ); 34 | }; 35 | 36 | export default React.memo(Component); 37 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Main.tsx: -------------------------------------------------------------------------------- 1 | import Anchor from 'components/Anchor'; 2 | import ArticleBanner from 'components/ArticleBanner'; 3 | import React from 'react'; 4 | import {connect} from 'react-redux'; 5 | import useAnchorPage from 'hooks/useAnchorPage'; 6 | import useConsult from 'hooks/useConsult'; 7 | import Recommend from './Recommend'; 8 | import Special from './Special'; 9 | import banner from './imgs/banner.jpg'; 10 | import Activities from './Activities'; 11 | 12 | interface StoreProps { 13 | hash: string; 14 | } 15 | const Component: React.FC = ({dispatch, hash}) => { 16 | // useAnchorPage(hash, historyActions.getHistory()); 17 | const onConsult = useConsult(dispatch); 18 | return ( 19 | <> 20 | 26 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 41 | return { 42 | hash: state.route.location.hash, 43 | }; 44 | }; 45 | 46 | export default connect(mapStateToProps)(React.memo(Component)); 47 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/TabNavs/index.m.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | :global { 3 | :local(.root) { 4 | padding: 0 15px; 5 | line-height: 30px; 6 | height: 30px; 7 | display: flex; 8 | font-size: 12px; 9 | 10 | > div { 11 | background: #fff; 12 | padding: 0 20px; 13 | border: 1px solid @split-bd-color; 14 | border-radius: 5px 5px 0 0; 15 | margin-right: 1px; 16 | cursor: pointer; 17 | position: relative; 18 | 19 | > .trigger { 20 | position: absolute; 21 | left: 0; 22 | width: 100%; 23 | height: 100%; 24 | top: 0; 25 | font-size: 0; 26 | } 27 | 28 | > .title { 29 | overflow: hidden; 30 | display: block; 31 | width: 100%; 32 | height: 100%; 33 | } 34 | > .action { 35 | right: 2px; 36 | top: 2px; 37 | position: absolute; 38 | color: @disabled-color; 39 | font-size: 12px; 40 | visibility: hidden; 41 | 42 | &:hover { 43 | color: @text-color; 44 | cursor: pointer; 45 | } 46 | } 47 | 48 | &:hover { 49 | background: @layout-body-background; 50 | border-bottom-color: @layout-body-background; 51 | > .action { 52 | visibility: visible; 53 | } 54 | } 55 | 56 | &.cur { 57 | background: @layout-body-background; 58 | border-bottom-color: @layout-body-background; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/PurviewSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Select} from 'antd'; 3 | import {SelectProps} from 'antd/lib/select'; 4 | import {purviewNames} from 'entity/role'; 5 | 6 | const {Option, OptGroup} = Select; 7 | 8 | function generateOptions() { 9 | const options: {[key: string]: string[]} = {}; 10 | Object.keys(purviewNames).forEach((item) => { 11 | const [resource, action] = item.split('.'); 12 | if (!action) { 13 | options[resource] = []; 14 | } else { 15 | options[resource].push(item); 16 | } 17 | }); 18 | return options; 19 | } 20 | 21 | const options = generateOptions(); 22 | 23 | const filterOption = (input: string, option: React.ReactElement) => { 24 | const text = option.props.children; 25 | if (typeof text === 'string') { 26 | return text.toLowerCase().indexOf(input.toLowerCase()) >= 0; 27 | } 28 | return false; 29 | }; 30 | 31 | const Component: React.FC> = (props) => { 32 | return ( 33 | 44 | ); 45 | }; 46 | 47 | export default React.memo(Component); 48 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/views/index.m.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | :global { 3 | :local(.root) { 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: space-between; 7 | > * { 8 | width: 48%; 9 | } 10 | .g-actions { 11 | width: 100%; 12 | } 13 | .purviewsError { 14 | visibility: hidden; 15 | width: 100%; 16 | text-align: right; 17 | color: @error-color; 18 | padding-top: 5px; 19 | 20 | &.show { 21 | visibility: visible; 22 | } 23 | } 24 | .purviews { 25 | max-height: 400px; 26 | overflow: auto; 27 | line-height: 2; 28 | 29 | .ant-checkbox-wrapper { 30 | margin-right: 5px; 31 | } 32 | h4 { 33 | padding: 3px 10px; 34 | background: #666; 35 | color: #fff; 36 | margin: 0; 37 | } 38 | dl { 39 | border: 1px solid @border-color-base; 40 | border-top: none; 41 | margin: 0; 42 | } 43 | dt { 44 | font-weight: bold; 45 | border-top: 1px solid @light-bd-color; 46 | border-bottom: 1px solid @light-bd-color; 47 | padding: 3px 15px; 48 | background: @descriptions-bg; 49 | margin-top: 5px; 50 | margin-bottom: 5px; 51 | &:first-child { 52 | border-top: none; 53 | margin-top: 0; 54 | } 55 | } 56 | dd { 57 | display: inline-block; 58 | padding: 3px 15px; 59 | margin-bottom: 0; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/entity/index.ts: -------------------------------------------------------------------------------- 1 | export interface BaseListSummary { 2 | pageCurrent: number; 3 | pageSize: number; 4 | totalItems: number; 5 | totalPages: number; 6 | categorys?: {id: string; name: string; list: string[]}[]; 7 | } 8 | export interface BaseListItem { 9 | id: string; 10 | } 11 | export type BaseItemDetail = BaseListItem; 12 | export interface BaseListSearch { 13 | pageCurrent?: number; 14 | pageSize?: number; 15 | term?: string; 16 | category?: string; 17 | sorterOrder?: 'ascend' | 'descend'; 18 | sorterField?: string; 19 | } 20 | 21 | export type ListSearchFormData = Required>; 22 | 23 | export type ListView = 'list' | 'selector' | 'category' | ''; 24 | export type ItemView = 'detail' | 'edit' | 'create' | 'summary' | ''; 25 | export interface CommonResourceRouteParams { 26 | listView: ListView | L; 27 | listSearch: BaseListSearch; 28 | _listVer: number; 29 | itemView: ItemView | I; 30 | itemId: string; 31 | _itemVer: number; 32 | } 33 | 34 | export interface CommonResource { 35 | RouteParams: CommonResourceRouteParams; 36 | ListSearch: BaseListSearch; 37 | ListItem: BaseListItem; 38 | ListSummary: BaseListSummary; 39 | ListView: ListView | L; 40 | ItemDetail: BaseItemDetail; 41 | ItemView: ItemView | I; 42 | CreateItem: any; 43 | UpdateItem: any; 44 | } 45 | export interface ProjectConfig { 46 | tokenRenewalTime: number; 47 | noticeTimer: number; 48 | } 49 | export interface TabNav { 50 | id: string; 51 | title: string; 52 | url: string; 53 | } 54 | -------------------------------------------------------------------------------- /src/Global.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-redeclare */ 2 | // 将某些常用变量提升至global,对全局变量有洁癖者可忽略此文件 3 | import './Prepose'; 4 | 5 | import {actions, ModuleNames, pageNames} from './modules'; 6 | import {message, metaKeys} from './common/utils'; 7 | 8 | type Actions = typeof actions; 9 | type MetaKeys = typeof metaKeys; 10 | 11 | type EnumModuleNames = typeof ModuleNames; 12 | type Message = typeof message; 13 | 14 | declare global { 15 | type HistoryActions = import('./modules').HistoryActions; 16 | type RootState = import('./modules').RootState; 17 | type RouteViews = import('./modules').RouteViews; 18 | type LoadView = import('./modules').LoadView; 19 | type RouteData = RootState['route']['data']; 20 | type BaseRouteData = import('@medux/react-web-router').RouteData; 21 | type CommonErrorCode = import('./common/errors').CommonErrorCode; 22 | type DispatchProp = import('react-redux').DispatchProp; 23 | // const module: any; 24 | const pageNames: {[key: string]: string}; 25 | const message: Message; 26 | // 初始环境变量放在/public/index.html中, 以防止被 webpack 打包 27 | const initEnv: { 28 | version: string; 29 | staticPath: string; 30 | apiServerPath: {[key: string]: string}; 31 | production: boolean; 32 | }; 33 | const loadView: LoadView; 34 | const actions: Actions; 35 | const ModuleNames: EnumModuleNames; 36 | const metaKeys: MetaKeys; 37 | const historyActions: HistoryActions; 38 | } 39 | 40 | ((data: {[key: string]: any}) => { 41 | Object.keys(data).forEach((key) => { 42 | window[key] = data[key]; 43 | }); 44 | })({actions, ModuleNames, message, pageNames}); 45 | -------------------------------------------------------------------------------- /mock/post--api-post.js: -------------------------------------------------------------------------------- 1 | let {title = '', content = '', editorIds = []} = request.body; 2 | title = title.toString(); 3 | content = content.toString(); 4 | editorIds = Array.isArray(editorIds) ? editorIds : []; 5 | 6 | const result = { 7 | statusCode: 422, 8 | headers: { 9 | 'x-delay': 0, 10 | 'content-type': 'application/json; charset=utf-8', 11 | }, 12 | }; 13 | 14 | const users = database.data.users; 15 | 16 | if (title.length > 30 || title.length < 2) { 17 | result.response = { 18 | message: '标题必须为2-20个字符!', 19 | }; 20 | return result; 21 | } 22 | if (content.length > 50 || content.length < 2) { 23 | result.response = { 24 | message: '内容必须为2-50个字符!', 25 | }; 26 | return result; 27 | } 28 | if (editorIds.length === 0 || editorIds.some(id => !users[id] || users[id].roleId !== '3')) { 29 | result.response = { 30 | message: '责任编辑设置非法!', 31 | }; 32 | return result; 33 | } 34 | 35 | const verifyToken = database.action.users.verifyToken(request.cookies.token); 36 | 37 | if (verifyToken.statusCode === 200) { 38 | const uid = verifyToken.response.id; 39 | const curUser = users[uid]; 40 | if (curUser) { 41 | users[uid].post++; 42 | } 43 | const id = Date.now().toString(); 44 | const postsData = database.data.posts; 45 | postsData[id] = { 46 | id, 47 | createdTime: Date.now(), 48 | title, 49 | content, 50 | author: {id: uid, name: users[uid].nickname}, 51 | editors: editorIds.map(id => ({id, name: users[id].nickname})), 52 | status: 'pending', 53 | }; 54 | return { 55 | ...verifyToken, 56 | response: '', 57 | }; 58 | } 59 | return verifyToken; 60 | -------------------------------------------------------------------------------- /src/common/errors.ts: -------------------------------------------------------------------------------- 1 | export enum CommonErrorCode { 2 | unknown = 'unknown', 3 | notFound = 'notFound', 4 | unauthorized = 'unauthorized', 5 | forbidden = 'forbidden', 6 | redirect = 'redirect', 7 | refresh = 'refresh', 8 | authorizeExpired = 'authorizeExpired', 9 | handled = 'handled', 10 | noToast = 'noToast', 11 | } 12 | export class CustomError extends Error { 13 | private httpCode: {[key: string]: string} = { 14 | '401': CommonErrorCode.unauthorized, 15 | '402': CommonErrorCode.authorizeExpired, 16 | '403': CommonErrorCode.forbidden, 17 | '404': CommonErrorCode.notFound, 18 | '300': CommonErrorCode.refresh, 19 | '301': CommonErrorCode.redirect, 20 | '302': CommonErrorCode.redirect, 21 | }; 22 | 23 | public constructor(public code: string, message: string, public detail?: Detail) { 24 | super(message || 'CustomError'); 25 | if (parseFloat(code)) { 26 | this.code = this.httpCode[code]; 27 | } 28 | } 29 | } 30 | export class RedirectError extends CustomError { 31 | public constructor(url: string) { 32 | super(CommonErrorCode.redirect, '跳转中', url); 33 | } 34 | } 35 | export class UnauthorizedError extends CustomError { 36 | public constructor(redirect: boolean) { 37 | super(CommonErrorCode.unauthorized, '请登录', redirect); 38 | } 39 | } 40 | 41 | export class HandledError extends CustomError { 42 | public constructor(oerror: CustomError) { 43 | super(CommonErrorCode.handled, oerror.message || '', oerror); 44 | } 45 | } 46 | export interface ErrorEntity { 47 | code: string; 48 | message?: string; 49 | detail?: Detail; 50 | } 51 | -------------------------------------------------------------------------------- /src/common/request.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosError, AxiosResponse, Method} from 'axios'; 2 | 3 | import {CustomError} from 'common/errors'; 4 | 5 | const request = axios.create(); 6 | 7 | request.interceptors.request.use((req) => { 8 | return req; 9 | }); 10 | 11 | request.interceptors.response.use( 12 | (response: AxiosResponse) => { 13 | return response; 14 | }, 15 | (error: AxiosError<{message: string}>) => { 16 | const httpErrorCode = error.response ? error.response.status : 0; 17 | const statusText = error.response ? error.response.statusText : ''; 18 | const responseData = error.response ? error.response.data : ''; 19 | 20 | const errorMessage = responseData && responseData.message ? responseData.message : `${statusText}, failed to call ${error.config.url}`; 21 | throw new CustomError(httpErrorCode.toString(), errorMessage, responseData); 22 | } 23 | ); 24 | 25 | export default function ajax(method: Method, url: string, params: {[key: string]: any} = {}, data: {[key: string]: any} = {}): Promise { 26 | url = url.replace(/:\w+/g, (flag) => { 27 | const key = flag.substr(1); 28 | if (params[key]) { 29 | const val: string = params[key]; 30 | delete params[key]; 31 | return encodeURIComponent(val); 32 | } 33 | return ''; 34 | }); 35 | Object.keys(initEnv.apiServerPath).some((key) => { 36 | const reg = new RegExp(key); 37 | if (reg.test(url)) { 38 | url = url.replace(reg, initEnv.apiServerPath[key]); 39 | return true; 40 | } 41 | return false; 42 | }); 43 | 44 | return request.request({method, url, params, data}).then((response) => response.data); 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Activities/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ImgAD from './imgs/ad.png'; 3 | import Logo1 from './imgs/logo1.png'; 4 | import Logo2 from './imgs/logo2.png'; 5 | import Logo3 from './imgs/logo3.png'; 6 | import Logo4 from './imgs/logo4.png'; 7 | import styles from './index.m.less'; 8 | 9 | const Component: React.FC = () => { 10 | return ( 11 |
12 |

摘要简介

13 |
14 |
15 |

16 | 蚂蚁的企业级产品是一个庞大且复杂的体系。这类产品不仅量级巨大且功能复杂,而且变动和并发频繁,常常需要设计与开发能够快速的做出响应。同时这类产品中有存在很多类似的页面以及组件,可以通过抽象得到一些稳定且高复用性的内容。 17 |

18 |

19 | 随着商业化的趋势,越来越多的企业级产品对更好的用户体验有了进一步的要求。带着这样的一个终极目标,我们(蚂蚁金服体验技术部)经过大量的项目实践和总结,逐步打磨出一个服务于企业级产品的设计体系 20 | Ant Design。基于『确定』和『自然』的设计价值观,通过模块化的解决方案,降低冗余的生产成本,让设计者专注于更好的用户体验。 21 |

22 |
23 | 26 |
27 |
    28 |
  • 29 | logo 30 |
  • 31 |
  • 32 | logo 33 |
  • 34 |
  • 35 | logo 36 |
  • 37 |
  • 38 | logo 39 |
  • 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default React.memo(Component); 46 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/views/Search.tsx: -------------------------------------------------------------------------------- 1 | import {FromItemList} from 'common/utils'; 2 | import {Input} from 'antd'; 3 | import {ListSearch} from 'entity/role'; 4 | import {ListSearchFormData} from 'entity'; 5 | import PurviewSelector from 'components/PurviewSelector'; 6 | import React from 'react'; 7 | import SearchForm from 'components/SearchForm'; 8 | import {connect} from 'react-redux'; 9 | import useEventCallback from 'hooks/useEventCallback'; 10 | 11 | type FormData = ListSearchFormData; 12 | 13 | interface OwnProps { 14 | listSearch: ListSearch; 15 | defaultSearch?: ListSearch; 16 | fixedFields?: Partial; 17 | } 18 | const formItems: FromItemList = [ 19 | {name: 'roleName', label: '角色名称', formItem: }, 20 | { 21 | name: 'purviews', 22 | label: '用户权限', 23 | col: 2, 24 | formItem: , 25 | }, 26 | ]; 27 | 28 | const Component: React.FC = ({dispatch, listSearch, defaultSearch, fixedFields}) => { 29 | const onFinish = useEventCallback( 30 | (values: FormData) => { 31 | dispatch(actions.adminRole.doListSearch({...values})); 32 | }, 33 | [dispatch] 34 | ); 35 | const onReset = useEventCallback(() => { 36 | dispatch(actions.adminRole.resetListSearch(defaultSearch)); 37 | }, [defaultSearch, dispatch]); 38 | 39 | return ( 40 |
41 | values={listSearch} fixedFields={fixedFields} onReset={onReset} onFinish={onFinish} items={formItems} /> 42 |
43 | ); 44 | }; 45 | 46 | export default connect()(React.memo(Component)); 47 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Recommend/index.tsx: -------------------------------------------------------------------------------- 1 | import {AlertOutlined, BugOutlined} from '@ant-design/icons'; 2 | 3 | import React from 'react'; 4 | import ImgAD from './imgs/ad.png'; 5 | import styles from './index.m.less'; 6 | 7 | const Component: React.FC = () => { 8 | return ( 9 |
10 |
11 |

特色推荐

12 |
13 |
14 |

15 | 设计价值观为 Ant Design 的设计者以及基于 Ant Design 16 | 进行产品设计的设计者,提供评价设计好坏的内在标准,并提供有效的设计实践所遵循的规则。同时,它启示并激发了设计原则和设计模式。 17 |

18 |
    19 |
  • 在行为的执行中,充分利用行为分析、人工智能、传感器、元数据等一系列方式,辅助用户有效决策、减少用户额外操作,从而节省用户脑力和体力,让人机交互行为更自然。
  • 20 |
  • 21 | 在感知和认知中,视觉系统扮演着最重要的角色,通过提炼自然界中的客观规律并运用到界面设计中,从而创建更有层次产品体验;同时,适时加入听觉系统、触觉系统等,创建更多维、更真实的产品体验。详见视觉语言 22 |
  • 23 |
24 |
25 | 28 |
29 |
30 |
31 | 32 |

行业积累与技术沉淀

33 |

企业在相应行业和领域中拥有长期的业务积累,对市场与客户需求有深刻理解,并在行业内处于领先地位。

34 |
35 |
36 | 37 |

优质产品,业务创新

38 |

拥有被市场充分验证的优质产品与服务,创新产品形态和经营模式,提升客户价值,共同获得更高的市场收益

39 |
40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default React.memo(Component); 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2020-06-25 2 | 3 | ## v1.1.0 4 | 5 | ### feature 6 | 7 | - commonResource -> routeParams 中的 listKey、itemKey 改名为 listVer、itemVer,类型由 string 改为 number,它被当做当前搜索条件的 version。搜索条件相同的 2 次搜索,如果后面一次的 version 大于前一次的 version,则会重新搜索,否则将直接复用上次搜索结果。 8 | 9 | - commonResource -> listView 增加一种通用列表视图:category,它将当前的 list 搜索结果归类为不同的 group,适合用来展示含有子栏目的列表等。 10 | 11 | - commonResource 的 ModuleState 原来只支持一个 list 搜索结果和一个 currentItem 详情数据,支持多 list 和多 item,结构调整为: 12 | 13 | ```JS 14 | //原结构: 15 | { 16 | isModule: true, 17 | routeParams: {...} //路由参数,包含列表搜索条件、详情搜索条件(itemId) 18 | list: [] //列表搜索结果 19 | listSummary: {...} //列表搜索结果的摘要信息 20 | currentItem: {...} //详情搜索结果 21 | } 22 | //新结构: 23 | { 24 | isModule: true, 25 | routeParams: {...} //路由参数,包含列表搜索条件、详情搜索条件(itemId) 26 | list: { //listView 为 list的列表数据 27 | _listVer: 433243534634, //当前列表搜索结果的version 28 | listSearch: {...}, //当前列表搜索条件 29 | list: [] //列表搜索结果 30 | listSummary: {...} //列表搜索结果的摘要信息 31 | }, 32 | selector: { //listView 为 selector的列表数据 33 | _listVer: 433243534634, //当前列表搜索结果的version 34 | listSearch: {...}, //当前列表搜索条件 35 | list: [] //列表搜索结果 36 | listSummary: {...} //列表搜索结果的摘要信息 37 | }, 38 | category: { //listView 为 category的列表数据 39 | _listVer: 433243534634, //当前列表搜索结果的version 40 | listSearch: {...}, //当前列表搜索条件 41 | list: [] //列表搜索结果 42 | listSummary: {...} //列表搜索结果的摘要信息 43 | }, 44 | detail: { //itemView 为 detail的详情数据 45 | _itemVer: 433243534634, 46 | itemId: 2, 47 | itemDetail: {...} 48 | }, 49 | edit: { //itemView 为 edit的详情数据 50 | _itemVer: 433243534634, 51 | itemId: 2, 52 | itemDetail: {...} 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /src/modules/app/views/RegistrationAgreement/index.tsx: -------------------------------------------------------------------------------- 1 | import {Modal} from 'antd'; 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | 5 | interface StoreProps { 6 | showPop?: boolean; 7 | } 8 | 9 | const Component: React.FC = ({showPop, dispatch}) => { 10 | const onCancel = React.useCallback(() => { 11 | dispatch(actions.app.showRegistrationAgreement(false)); 12 | }, [dispatch]); 13 | 14 | return ( 15 | 16 |
17 |
特别提示:
18 | 您在使用百度公司提供的各项服务之前,请您务必审慎阅读、充分理解本协议各条款内容,特别是以粗体标注的部分,包括但不限于免除或者限制责任的条款。如您不同意本服务协议及/或随时对其的修改,您可以主动停止使用百度公司提供的服务;您一旦使用百度公司提供的服务,即视为您已了解并完全同意本服务协议各项内容,包括百度公司对服务协议随时所做的任何修改,并成为我们的用户。 19 |

您使用部分百度产品或服务时需要注册百度帐号,当您注册和使用百度帐号时应遵守下述要求:

20 | 1. 用户注册成功后,百度公司将给予每个用户一个用户帐号,用户可以自主设置帐号密码。该用户帐号和密码由用户负责保管;用户应当对以其用户帐号进行的所有活动和事件负法律责任。 21 |
22 | 2. 23 | 您按照注册页面提示填写信息、阅读并同意本协议且完成全部注册程序后,除百度平台的具体产品对帐户有单独的注册要求外,您可获得百度平台(baidu.com网站及客户端)帐号并成为百度用户,可通过百度帐户使用百度平台的各项产品和服务。 24 |
25 | 3. 26 | 为了方便您在百度产品中享有一致性的服务,如您已经在某一百度产品中登录百度帐号,在您首次使用其他百度产品时可能同步您的登录状态。此环节并不会额外收集、使用您的个人信息。如您想退出帐号登录,可在产品设置页面退出登录。 27 |
28 | 4. 29 | 百度帐号(即百度用户ID)的所有权归百度公司,用户按注册页面引导填写信息,阅读并同意本协议且完成全部注册程序后,即可获得百度帐号并成为用户。用户应提供及时、详尽及准确的个人资料,并不断更新注册资料,符合及时、详尽准确的要求。所有原始键入的资料将引用为注册资料。如果因注册信息不真实或更新不及时而引发的相关问题,百度公司不负任何责任。您可以通过百度帐号设置页面查询、更正您的信息,百度帐号设置页面地址: 30 |
31 |
32 | ); 33 | }; 34 | 35 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 36 | return { 37 | showPop: state.app!.showRegistrationAgreement, 38 | }; 39 | }; 40 | export default connect(mapStateToProps)(React.memo(Component)); 41 | -------------------------------------------------------------------------------- /mock/put--api-post-xxx@cHV0LS1hcGktcG9zdC1cdys=.js: -------------------------------------------------------------------------------- 1 | let {id = '', title = '', content = '', editorIds = []} = request.body; 2 | id = id.toString(); 3 | title = title.toString(); 4 | content = content.toString(); 5 | editorIds = Array.isArray(editorIds) ? editorIds : []; 6 | 7 | const result = { 8 | statusCode: 422, 9 | headers: { 10 | 'x-delay': 0, 11 | 'content-type': 'application/json; charset=utf-8', 12 | }, 13 | }; 14 | 15 | const postsData = database.data.posts; 16 | const curItem = postsData[id]; 17 | 18 | if (!curItem) { 19 | result.response = { 20 | message: '目标不存在!', 21 | }; 22 | return result; 23 | } 24 | if (curItem.fixed) { 25 | result.response = { 26 | message: '目标不允许修改!', 27 | }; 28 | return result; 29 | } 30 | const users = database.data.users; 31 | 32 | if (title.length > 30 || title.length < 2) { 33 | result.response = { 34 | message: '标题必须为2-20个字符!', 35 | }; 36 | return result; 37 | } 38 | if (content.length > 50 || content.length < 2) { 39 | result.response = { 40 | message: '内容必须为2-50个字符!', 41 | }; 42 | return result; 43 | } 44 | if (editorIds.length === 0 || editorIds.some(id => !users[id] || users[id].roleId !== '3')) { 45 | result.response = { 46 | message: '责任编辑设置非法!', 47 | }; 48 | return result; 49 | } 50 | 51 | const verifyToken = database.action.users.verifyToken(request.cookies.token); 52 | 53 | if (verifyToken.statusCode === 200) { 54 | const uid = verifyToken.response.id; 55 | if (uid !== curItem.author.id) { 56 | result.response = { 57 | message: '只能修改自己发表的信息!', 58 | }; 59 | return result; 60 | } 61 | Object.assign(curItem, {title, content, editors: editorIds.map(id => ({id, name: users[id].nickname}))}); 62 | return { 63 | ...verifyToken, 64 | response: '', 65 | }; 66 | } 67 | return verifyToken; 68 | -------------------------------------------------------------------------------- /src/modules/article/articleHome/views/Special/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | padding-top: 80px; 4 | text-align: center; 5 | h2 { 6 | font-weight: 700; 7 | font-size: 30px; 8 | margin: 0 0 50px; 9 | } 10 | .superiority { 11 | margin-top: 50px; 12 | margin-bottom: 100px; 13 | > figure { 14 | float: left; 15 | width: 253.5px; 16 | margin: 0 62px 0 0; 17 | &:last-child { 18 | margin-right: 0; 19 | } 20 | > img { 21 | display: inline-block; 22 | width: 48px; 23 | height: 48px; 24 | } 25 | > h4 { 26 | font-size: 20px; 27 | line-height: 25px; 28 | margin: 0 0 14px; 29 | &::before { 30 | content: ' '; 31 | display: block; 32 | margin: auto; 33 | width: 50px; 34 | height: 50px; 35 | background: url(./imgs/icon1.png) no-repeat center center; 36 | margin-bottom: 14px; 37 | } 38 | &.item1 { 39 | &::before { 40 | background-image: url(./imgs/icon1.png); 41 | } 42 | } 43 | &.item2 { 44 | &::before { 45 | background-image: url(./imgs/icon2.png); 46 | } 47 | } 48 | &.item3 { 49 | &::before { 50 | background-image: url(./imgs/icon3.png); 51 | } 52 | } 53 | &.item4 { 54 | &::before { 55 | background-image: url(./imgs/icon4.png); 56 | } 57 | } 58 | } 59 | > p { 60 | font-size: 14px; 61 | color: #666; 62 | letter-spacing: 0; 63 | line-height: 26px; 64 | margin: 0; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /public/icon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 45 | 46 | 47 |
48 |
LIVE
49 |
MESSAGE
50 |
PICTURE
51 |
PLAY_CIRCLE
52 |
RELOAD
53 |
LOCATION
54 |
HEART
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/views/List.tsx: -------------------------------------------------------------------------------- 1 | import {ItemDetail, ListSearch} from 'entity/post'; 2 | import React, {useCallback} from 'react'; 3 | 4 | import {ItemView} from 'entity'; 5 | import {Modal} from 'antd'; 6 | import {connect} from 'react-redux'; 7 | import Detail from './Detail'; 8 | import Editor from './Editor'; 9 | import Search from './Search'; 10 | import Table from './Table'; 11 | 12 | interface StoreProps { 13 | listSearch: ListSearch; 14 | itemView: ItemView; 15 | currentItem?: ItemDetail; 16 | } 17 | 18 | const Component: React.FC = ({dispatch, listSearch, currentItem, itemView}) => { 19 | const onHideCurrent = useCallback(() => { 20 | dispatch(actions.adminPost.closeCurrentItem()); 21 | }, [dispatch]); 22 | 23 | return ( 24 |
25 |

信息列表

26 | 27 | 28 | {currentItem && itemView === 'detail' && ( 29 | 30 | 31 | 32 | )} 33 | {currentItem && (itemView === 'edit' || itemView === 'create') && ( 34 | 35 | 36 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 43 | const thisModule = state.adminPost!; 44 | const itemView = thisModule.routeParams!.itemView; 45 | return { 46 | currentItem: thisModule[itemView]?.itemDetail, 47 | listSearch: thisModule.routeParams!.listSearch, 48 | itemView, 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps)(Component); 53 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/views/List.tsx: -------------------------------------------------------------------------------- 1 | import {ItemDetail, ListSearch} from 'entity/role'; 2 | import React, {useCallback} from 'react'; 3 | 4 | import {ItemView} from 'entity'; 5 | import {Modal} from 'antd'; 6 | import {connect} from 'react-redux'; 7 | import Detail from './Detail'; 8 | import Editor from './Editor'; 9 | import Search from './Search'; 10 | import Table from './Table'; 11 | 12 | interface StoreProps { 13 | listSearch: ListSearch; 14 | itemView: ItemView; 15 | currentItem?: ItemDetail; 16 | } 17 | 18 | const Component: React.FC = ({dispatch, listSearch, currentItem, itemView}) => { 19 | const onHideCurrent = useCallback(() => { 20 | dispatch(actions.adminRole.closeCurrentItem()); 21 | }, [dispatch]); 22 | 23 | return ( 24 |
25 |

角色列表

26 | 27 |
28 | {currentItem && itemView === 'detail' && ( 29 | 30 | 31 | 32 | )} 33 | {currentItem && (itemView === 'edit' || itemView === 'create') && ( 34 | 35 | 36 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 43 | const thisModule = state.adminRole!; 44 | const itemView = thisModule.routeParams!.itemView; 45 | return { 46 | currentItem: thisModule[itemView]?.itemDetail, 47 | listSearch: thisModule.routeParams!.listSearch, 48 | itemView, 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps)(React.memo(Component)); 53 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/views/List.tsx: -------------------------------------------------------------------------------- 1 | import {ItemDetail, ListSearch} from 'entity/member'; 2 | import React, {useCallback} from 'react'; 3 | 4 | import {ItemView} from 'entity'; 5 | import {Modal} from 'antd'; 6 | import {connect} from 'react-redux'; 7 | import Detail from './Detail'; 8 | import Editor from './Editor'; 9 | import Search from './Search'; 10 | import Table from './Table'; 11 | 12 | interface StoreProps { 13 | listSearch: ListSearch; 14 | itemView: ItemView; 15 | currentItem?: ItemDetail; 16 | } 17 | 18 | const Component: React.FC = ({dispatch, listSearch, currentItem, itemView}) => { 19 | const onHideCurrent = useCallback(() => { 20 | dispatch(actions.adminMember.closeCurrentItem()); 21 | }, [dispatch]); 22 | 23 | return ( 24 |
25 |

用户列表

26 | 27 |
28 | {currentItem && itemView === 'detail' && ( 29 | 30 | 31 | 32 | )} 33 | {currentItem && (itemView === 'edit' || itemView === 'create') && ( 34 | 35 | 36 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 43 | const thisModule = state.adminMember!; 44 | const itemView = thisModule.routeParams!.itemView; 45 | return { 46 | currentItem: thisModule[itemView]?.itemDetail, 47 | listSearch: thisModule.routeParams!.listSearch, 48 | itemView, 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps)(React.memo(Component)); 53 | -------------------------------------------------------------------------------- /mock/put--api-member-xxx@cHV0LS1hcGktbWVtYmVyLVx3Kw==.js: -------------------------------------------------------------------------------- 1 | let {id = '', info} = request.body; 2 | id = id.toString(); 3 | info = info || {}; 4 | 5 | const result = { 6 | statusCode: 422, 7 | headers: { 8 | 'x-delay': 0, 9 | 'content-type': 'application/json; charset=utf-8', 10 | }, 11 | }; 12 | 13 | const users = database.data.users; 14 | const roles = database.data.roles; 15 | const curItem = users[id]; 16 | if (!curItem) { 17 | result.response = { 18 | message: '目标不存在!', 19 | }; 20 | return result; 21 | } 22 | if (curItem.fixed) { 23 | result.response = { 24 | message: '目标不允许修改!', 25 | }; 26 | return result; 27 | } 28 | let {nickname = '', gender = 'unknow', roleId = '4', status = 'enable', email = ''} = info; 29 | nickname = nickname.toString(); 30 | gender = gender.toString(); 31 | roleId = roleId.toString(); 32 | status = status.toString(); 33 | email = email.toString(); 34 | 35 | if (nickname.length > 20 || nickname.length < 2) { 36 | result.response = { 37 | message: '呢称必须为2-20位!', 38 | }; 39 | return result; 40 | } 41 | if (!['male', 'female', 'unknow'].includes(gender)) { 42 | result.response = { 43 | message: '性别不合法!', 44 | }; 45 | return result; 46 | } 47 | 48 | if (!['enable', 'disable'].includes(status)) { 49 | result.response = { 50 | message: '状态不合法!', 51 | }; 52 | return result; 53 | } 54 | if (email.length > 50) { 55 | result.response = { 56 | message: 'Email不合法', 57 | }; 58 | return result; 59 | } 60 | if (!roles[roleId]) { 61 | result.response = { 62 | message: '所选角色不合法', 63 | }; 64 | return result; 65 | } 66 | roles[curItem.roleId].owner--; 67 | roles[roleId].owner++; 68 | 69 | curItem.nickname = nickname; 70 | curItem.gender = gender; 71 | curItem.roleId = roleId; 72 | curItem.roleName = roles[roleId].roleName; 73 | curItem.status = status; 74 | curItem.email = email; 75 | 76 | result.statusCode = 200; 77 | result.response = ''; 78 | 79 | return result; 80 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/views/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import {Link} from '@medux/react-web-router'; 2 | import QRcode from 'assets/imgs/qq.png'; 3 | import React from 'react'; 4 | import styles from './index.m.less'; 5 | 6 | const Component: React.FC = () => { 7 | return ( 8 |
9 |
10 |
11 |
Medux框架系列
12 |
13 | medux/core 14 |
15 |
16 | medux/web 17 |
18 |
19 | medux/react 20 |
21 |
22 |
23 |
资源推荐
24 |
25 | Ant Design 26 |
27 |
28 | Ant Design Pro 29 |
30 |
31 | Typescript 32 |
33 |
34 |
35 |
意见反馈
36 |
37 | Bug报告 38 |
39 |
40 | 在线留言反馈 41 |
42 |
43 | 新手入门手册 44 |
45 |
46 |
47 |
联系我们
48 |
49 | wooline@qq.com 50 |
51 |
52 | QQ群号929696953 53 |
54 |
55 |
56 |
QQ交流群
57 |
58 | logo 59 |
60 |
61 |
62 | © 2019 wooline@qq.com. All Rights Reserved. 粤ICP备9531688号-1 63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default React.memo(Component); 70 | -------------------------------------------------------------------------------- /src/modules/app/views/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import 'assets/css/global.m.less'; 2 | import 'assets/css/override.less'; 3 | import 'moment/locale/zh-cn'; 4 | 5 | import {ConfigProvider} from 'antd'; 6 | import NotFound from 'components/NotFound'; 7 | import React from 'react'; 8 | import {Switch} from '@medux/react-web-router'; 9 | import {connect} from 'react-redux'; 10 | import moment from 'moment'; 11 | import zhCN from 'antd/es/locale/zh_CN'; 12 | import GlobalLoading from '../GlobalLoading'; 13 | import LoginPage from '../LoginPage'; 14 | import LoginPop from '../LoginPop'; 15 | import RegisterPage from '../RegisterPage'; 16 | import RegisterPop from '../RegisterPop'; 17 | import RegistrationAgreement from '../RegistrationAgreement'; 18 | 19 | moment.locale('zh-cn'); 20 | 21 | const AdminLayout = loadView('adminLayout', 'main'); 22 | const ArticleLayout = loadView('articleLayout', 'main'); 23 | 24 | interface StoreProps { 25 | routeViews: RouteViews; 26 | } 27 | 28 | const Component: React.FC = ({routeViews}) => { 29 | // eslint-disable-next-line no-restricted-globals 30 | const title = `@Medux-${pageNames[location.pathname] || document.title || pageNames['/']}`; 31 | React.useEffect(() => { 32 | document.title = title; 33 | }, [title]); 34 | return ( 35 | 36 | }> 37 | {routeViews.app?.loginPage && } 38 | {routeViews.app?.registerPage && } 39 | {routeViews.adminLayout?.main && } 40 | {routeViews.articleLayout?.main && } 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 51 | return { 52 | routeViews: state.route.data.views, 53 | }; 54 | }; 55 | 56 | export default connect(mapStateToProps)(React.memo(Component)); 57 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import {Layout} from 'antd'; 2 | import NotFound from 'components/NotFound'; 3 | import React from 'react'; 4 | import {Switch} from '@medux/react-web-router'; 5 | import {connect} from 'react-redux'; 6 | import TabNavs from '../TabNavs'; 7 | import Navs from '../Navs'; 8 | import Header from '../Header'; 9 | import Flag from '../Flag'; 10 | import styles from './index.m.less'; 11 | 12 | const {Content} = Layout; 13 | const AdminHome = loadView('adminHome', 'main'); 14 | const AdminMember = loadView('adminMember', 'list'); 15 | const AdminPost = loadView('adminPost', 'list'); 16 | const AdminRole = loadView('adminRole', 'list'); 17 | 18 | interface StoreProps { 19 | routeViews: RouteViews; 20 | hasLogin: boolean; 21 | siderCollapsed: boolean; 22 | } 23 | 24 | const Component: React.FC = ({routeViews, siderCollapsed, hasLogin}) => { 25 | return hasLogin ? ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | }> 38 | {routeViews.adminHome?.main && } 39 | {routeViews.adminRole?.list && } 40 | {routeViews.adminMember?.list && } 41 | {routeViews.adminPost?.list && } 42 | 43 | 44 | 45 | 46 | ) : null; 47 | }; 48 | 49 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 50 | return { 51 | routeViews: state.route.data.views, 52 | hasLogin: state.app!.curUser.hasLogin, 53 | siderCollapsed: !!state.adminLayout!.siderCollapsed, 54 | }; 55 | }; 56 | 57 | export default connect(mapStateToProps)(React.memo(Component)); 58 | -------------------------------------------------------------------------------- /src/entity/post.ts: -------------------------------------------------------------------------------- 1 | import {enumOptions} from 'common/utils'; 2 | import {BaseListItem, BaseListSearch, BaseListSummary, CommonResource, CommonResourceRouteParams} from './index'; 3 | 4 | export enum Status { 5 | '待审核' = 'pending', 6 | '审核通过' = 'resolved', 7 | '审核拒绝' = 'rejected', 8 | } 9 | export const DStatus = enumOptions(Status); 10 | export interface ListItem extends BaseListItem { 11 | title: string; 12 | author: {id: string; name: string}; 13 | editors: Array<{id: string; name: string}>; 14 | status: Status; 15 | createdTime: number; 16 | fixed?: boolean; 17 | } 18 | export interface ListSearch extends BaseListSearch { 19 | title?: string; 20 | author?: string; 21 | editor?: {id: string; name: string}; 22 | editorId?: string; 23 | createdTime?: [number, number]; 24 | status?: Status; 25 | } 26 | export interface ItemDetail extends ListItem { 27 | content: string; 28 | } 29 | export interface ListSummary extends BaseListSummary {} 30 | 31 | export interface UpdateItem { 32 | id: string; 33 | title: string; 34 | content: string; 35 | editors: Array<{id: string; name: string}>; 36 | editorIds: string[]; 37 | } 38 | export interface RouteParams extends CommonResourceRouteParams { 39 | listSearch: ListSearch; 40 | } 41 | export interface Resource extends CommonResource { 42 | RouteParams: RouteParams; 43 | ListSearch: ListSearch; 44 | ListItem: ListItem; 45 | ListSummary: ListSummary; 46 | ItemDetail: ItemDetail; 47 | UpdateItem: UpdateItem; 48 | } 49 | 50 | // 定义本模块的路由参数类型 51 | 52 | export const defaultRouteParams: RouteParams = { 53 | listSearch: { 54 | pageSize: 10, 55 | pageCurrent: 1, 56 | term: undefined, 57 | category: undefined, 58 | sorterField: undefined, 59 | sorterOrder: undefined, 60 | title: undefined, 61 | author: undefined, 62 | editor: undefined, 63 | editorId: undefined, 64 | status: undefined, 65 | createdTime: undefined, 66 | }, 67 | listView: '', 68 | _listVer: 0, 69 | itemId: '', 70 | itemView: '', 71 | _itemVer: 0, 72 | }; 73 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/TabNavEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Form, Input} from 'antd'; 2 | import {DispatchProp, connect} from 'react-redux'; 3 | import React, {useCallback} from 'react'; 4 | 5 | import {TabNav} from 'entity'; 6 | import {getFormDecorators} from 'common/utils'; 7 | import useEventCallback from 'hooks/useEventCallback'; 8 | import styles from './index.m.less'; 9 | 10 | const defCurItem: TabNav = {id: '', title: '', url: ''}; 11 | interface StoreProps { 12 | curItem?: TabNav; 13 | } 14 | 15 | interface FormData { 16 | title: string; 17 | } 18 | const formDecorators = getFormDecorators({ 19 | title: {rules: [{required: true, message: '请输入书签名'}]}, 20 | }); 21 | 22 | const Component: React.FC = ({curItem = defCurItem, dispatch}) => { 23 | const onFinish = useEventCallback( 24 | (values: FormData) => { 25 | const {title} = values; 26 | dispatch(actions.adminLayout.updateTabNav({...curItem, title})); 27 | }, 28 | [dispatch, curItem] 29 | ); 30 | 31 | const onCancel = useCallback(() => { 32 | dispatch(actions.adminLayout.closeTabNavEditor()); 33 | }, [dispatch]); 34 | 35 | const initialValues = {title: curItem.title}; 36 | 37 | return ( 38 |
39 |

{curItem.id ? '重命名当前书签:' : '将当前页面加入书签:'}

40 |
41 | 42 | 43 | 44 |
45 | 48 | 49 |
50 | 51 |
52 | ); 53 | }; 54 | 55 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 56 | return { 57 | curItem: state.adminLayout!.tabNavEditor, 58 | }; 59 | }; 60 | 61 | export default connect(mapStateToProps)(React.memo(Component)); 62 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/views/Selector.tsx: -------------------------------------------------------------------------------- 1 | import {ListItem, ListSearch, ItemDetail} from 'entity/member'; 2 | import React, {useCallback} from 'react'; 3 | 4 | import {ItemView} from 'entity'; 5 | import {Modal} from 'antd'; 6 | import {connect} from 'react-redux'; 7 | import SelectorTable from './SelectorTable'; 8 | import Detail from './Detail'; 9 | import Search from './Search'; 10 | 11 | interface StoreProps { 12 | listSearch: ListSearch; 13 | itemView: ItemView; 14 | currentItem?: ItemDetail; 15 | } 16 | interface OwnProps { 17 | fixedSearchField?: Partial; 18 | defaultSearch?: Partial; 19 | limit?: number | [number, number]; 20 | value?: ListItem[]; 21 | onChange?: (items: ListItem[]) => void; 22 | } 23 | 24 | const Component: React.FC = ({dispatch, listSearch, currentItem, itemView, fixedSearchField, defaultSearch = fixedSearchField, limit, value, onChange}) => { 25 | const onHideCurrent = useCallback(() => { 26 | dispatch(actions.adminMember.closeCurrentItem()); 27 | }, [dispatch]); 28 | 29 | return ( 30 |
31 | 32 | 33 | {currentItem && itemView === 'summary' && ( 34 | 35 | 36 | 37 | )} 38 |
39 | ); 40 | }; 41 | 42 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 43 | const thisModule = state.adminMember!; 44 | const itemView = thisModule.routeParams!.itemView; 45 | return { 46 | currentItem: thisModule[itemView]?.itemDetail, 47 | listSearch: thisModule.routeParams!.listSearch, 48 | itemView, 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps)(React.memo(Component)); 53 | -------------------------------------------------------------------------------- /src/components/RangeDatePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, {useMemo} from 'react'; 2 | 3 | import {DatePicker} from 'antd'; 4 | import moment from 'moment'; 5 | import useEventCallback from 'hooks/useEventCallback'; 6 | 7 | export interface Props { 8 | className?: string; 9 | allowClear?: boolean; 10 | style?: React.CSSProperties; 11 | disabledDate?: (currentDate: moment.Moment | null) => boolean; 12 | value?: [number, number]; 13 | onChange?: (value?: [number, number]) => void; 14 | } 15 | 16 | const Component: React.FC = (props) => { 17 | const {value, onChange, disabledDate, allowClear = true, className, style} = props; 18 | const handleChange = useEventCallback( 19 | (dates: any) => { 20 | if (dates && dates[0] && dates[1]) { 21 | const start = dates[0].startOf('day').valueOf(); 22 | const end = dates[1].endOf('day').valueOf(); 23 | if (onChange) { 24 | onChange([start, end]); 25 | } 26 | } else if (onChange) { 27 | onChange(undefined); 28 | } 29 | }, 30 | [onChange] 31 | ); 32 | const ranges = useMemo<{[key: string]: [any, any]}>(() => { 33 | const today = moment().endOf('day'); 34 | return { 35 | 今天: [moment().startOf('day'), moment().endOf('day')], 36 | 本周: [moment().startOf('week'), moment().endOf('week')], 37 | 本月: [moment().startOf('month'), moment().endOf('month')], 38 | 今年: [moment().startOf('year'), moment().endOf('year')], 39 | 近周: [moment().subtract(1, 'week').startOf('day'), today], 40 | 近月: [moment().subtract(1, 'month').startOf('day'), today], 41 | 近年: [moment().subtract(1, 'year').startOf('day'), today], 42 | }; 43 | }, []); 44 | const values: [any, any] = useMemo(() => { 45 | const tvalue = value || [0, 0]; 46 | const start = tvalue[0] ? moment(tvalue[0]) : undefined; 47 | const end = tvalue[1] ? moment(tvalue[1]) : undefined; 48 | return [start, end]; 49 | }, [value]); 50 | 51 | return ; 52 | }; 53 | 54 | export default React.memo(Component); 55 | -------------------------------------------------------------------------------- /mock/get--api-role$@Z2V0LS1hcGktcm9sZVwk.js: -------------------------------------------------------------------------------- 1 | let {term = '', sorterField = '', sorterOrder = '', roleName = '', pageCurrent, pageSize, purviews} = request.query; 2 | 3 | term = term.toString(); 4 | sorterField = sorterField.toString(); 5 | sorterOrder = sorterOrder.toString(); 6 | roleName = roleName.toString(); 7 | purviews = Array.isArray(purviews) ? purviews : []; 8 | pageCurrent = parseInt(pageCurrent) || 1; 9 | pageSize = parseInt(pageSize) || 10; 10 | 11 | const start = (pageCurrent - 1) * pageSize; 12 | const end = start + pageSize; 13 | 14 | const result = { 15 | statusCode: 422, 16 | headers: { 17 | 'x-delay': 0, 18 | 'content-type': 'application/json; charset=utf-8', 19 | }, 20 | }; 21 | const rolesData = database.data.roles; 22 | let resourceList = Object.keys(rolesData).map(id => { 23 | return rolesData[id]; 24 | }); 25 | 26 | if (typeof request.query.term === 'string') { 27 | if (term) { 28 | resourceList = resourceList.filter(item => item.roleName.includes(term)); 29 | } 30 | } else { 31 | if (purviews.some(purview => !database.data.config.purviews[purview])) { 32 | result.response = { 33 | message: '权限搜索条件不合法!', 34 | }; 35 | return result; 36 | } 37 | 38 | if (roleName) { 39 | resourceList = resourceList.filter(item => item.roleName.includes(roleName)); 40 | } 41 | if (purviews.length > 0) { 42 | resourceList = resourceList.filter(item => { 43 | const str = item.purviews.join(','); 44 | return purviews.some(purview => str.includes(purview)); 45 | }); 46 | } 47 | if (sorterField === 'createdTime') { 48 | if (sorterOrder === 'ascend') { 49 | resourceList.sort((a, b) => { 50 | return a.createdTime - b.createdTime; 51 | }); 52 | } else if (sorterOrder === 'descend') { 53 | resourceList.sort((a, b) => { 54 | return b.createdTime - a.createdTime; 55 | }); 56 | } 57 | } 58 | } 59 | 60 | const totalItems = resourceList.length; 61 | 62 | result.statusCode = 200; 63 | result.response = { 64 | listSummary: { 65 | pageCurrent, 66 | pageSize, 67 | totalItems, 68 | totalPages: Math.ceil(resourceList.length / pageSize), 69 | }, 70 | list: resourceList.slice(start, end), 71 | }; 72 | 73 | return result; 74 | -------------------------------------------------------------------------------- /mock/post--api-member.js: -------------------------------------------------------------------------------- 1 | let {username = '', password = '123456', info} = request.body; 2 | username = username.toString(); 3 | password = password.toString(); 4 | info = info || {}; 5 | 6 | const result = { 7 | statusCode: 422, 8 | headers: { 9 | 'x-delay': 0, 10 | 'content-type': 'application/json; charset=utf-8', 11 | }, 12 | }; 13 | const users = database.data.users; 14 | const roles = database.data.roles; 15 | let {nickname = username, gender = 'unknow', roleId = '4', status = 'enable', email = ''} = info; 16 | nickname = nickname.toString(); 17 | gender = gender.toString(); 18 | roleId = roleId.toString(); 19 | status = status.toString(); 20 | email = email.toString(); 21 | 22 | if (users[username]) { 23 | result.response = { 24 | message: '用户名已存在!', 25 | }; 26 | return result; 27 | } 28 | if (!/^\w{5,20}$/.test(username)) { 29 | result.response = { 30 | message: '用户名必须为5-20位数字或字母!', 31 | }; 32 | return result; 33 | } 34 | if (password.length > 20 || password.length < 5) { 35 | result.response = { 36 | message: '密码长度必须为5-20位!', 37 | }; 38 | return result; 39 | } 40 | if (nickname.length > 20 || nickname.length < 2) { 41 | result.response = { 42 | message: '呢称必须为2-20位!', 43 | }; 44 | return result; 45 | } 46 | if (!['male', 'female', 'unknow'].includes(gender)) { 47 | result.response = { 48 | message: '性别不合法!', 49 | }; 50 | return result; 51 | } 52 | 53 | if (!['enable', 'disable'].includes(status)) { 54 | result.response = { 55 | message: '状态不合法!', 56 | }; 57 | return result; 58 | } 59 | if (email.length > 50) { 60 | result.response = { 61 | message: 'Email不合法', 62 | }; 63 | return result; 64 | } 65 | if (!roles[roleId]) { 66 | result.response = { 67 | message: '所选角色不合法', 68 | }; 69 | return result; 70 | } 71 | roles[roleId].owner++; 72 | users[username] = { 73 | id: username, 74 | username: username, 75 | password: password, 76 | hasLogin: true, 77 | post: 0, 78 | avatar: '/client/imgs/u1.jpg', 79 | loginTime: Date.now(), 80 | createdTime: Date.now(), 81 | nickname, 82 | gender, 83 | roleId, 84 | roleName: roles[roleId].roleName, 85 | status, 86 | email, 87 | }; 88 | 89 | result.statusCode = 200; 90 | result.response = ''; 91 | 92 | return result; 93 | -------------------------------------------------------------------------------- /mock/get--api-post$@Z2V0LS1hcGktcG9zdFwk.js: -------------------------------------------------------------------------------- 1 | let {sorterField = '', sorterOrder = '', title = '', author = '', status = '', editorId = '', pageCurrent, pageSize, createdTime} = request.query; 2 | 3 | sorterField = sorterField.toString(); 4 | sorterOrder = sorterOrder.toString(); 5 | title = title.toString(); 6 | author = author.toString(); 7 | status = status.toString(); 8 | editorId = editorId.toString(); 9 | createdTime = Array.isArray(createdTime) ? createdTime : []; 10 | pageCurrent = parseInt(pageCurrent) || 1; 11 | pageSize = parseInt(pageSize) || 10; 12 | 13 | const start = (pageCurrent - 1) * pageSize; 14 | const end = start + pageSize; 15 | 16 | const result = { 17 | statusCode: 422, 18 | headers: { 19 | 'x-delay': 0, 20 | 'content-type': 'application/json; charset=utf-8', 21 | }, 22 | }; 23 | const postsData = database.data.posts; 24 | 25 | let resourceList = Object.keys(postsData).map(id => { 26 | return postsData[id]; 27 | }); 28 | 29 | if (title) { 30 | resourceList = resourceList.filter(item => item.title.includes(title)); 31 | } 32 | if (status) { 33 | resourceList = resourceList.filter(item => item.status === status); 34 | } 35 | if (author) { 36 | resourceList = resourceList.filter(item => item.author.name === author || item.author.id === author); 37 | } 38 | if (editorId) { 39 | resourceList = resourceList.filter(item => item.editors[0].id === editorId || (item.editors[1] && item.editors[1].id === editorId)); 40 | } 41 | if (createdTime.length === 2) { 42 | resourceList = resourceList.filter(item => item.createdTime > createdTime[0] && item.createdTime < createdTime[1]); 43 | } 44 | if (sorterField === 'createdTime') { 45 | if (sorterOrder === 'ascend') { 46 | resourceList.sort((a, b) => { 47 | return a.createdTime - b.createdTime; 48 | }); 49 | } else if (sorterOrder === 'descend') { 50 | resourceList.sort((a, b) => { 51 | return b.createdTime - a.createdTime; 52 | }); 53 | } 54 | } 55 | 56 | const totalItems = resourceList.length; 57 | 58 | result.statusCode = 200; 59 | result.response = { 60 | listSummary: { 61 | pageCurrent, 62 | pageSize, 63 | totalItems, 64 | totalPages: Math.ceil(resourceList.length / pageSize), 65 | }, 66 | list: resourceList.slice(start, end), 67 | }; 68 | return result; 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | - [2020-06-25 v1.1.0](https://github.com/wooline/medux-react-admin/blob/master/CHANGELOG.md) 4 | 5 | # 项目介绍 6 | 7 | 本项目主要用来展示如何将 [**@medux**](https://github.com/wooline/medux) 应用于 web 后台管理系统,光看 UI 界面你可能看不到太多闪光点,也没有太多特别之处,网上这样的开源后台前端系统也很多,甚至你分分钟就能自己搭建一个,因为通用化的后台界面无非就是这样子。然而系统的架构理念、代码的组织风格、工程的模块化、路由的设计等等幕后工作却有如八仙过海,各显智慧。 8 | 9 | 所以你可能需要了解本项目的设计思路、并深入到工程结构与代码中才能感受到本项目的特别之处。 10 | 11 | - [**在线预览**](http://medux-react-admin.80zp.com) 12 | 13 | ### 无需登录可访问的页面 14 | 15 | - [/login](http://medux-react-admin.80zp.com/login) 16 | - [/register](http://medux-react-admin.80zp.com/register) 17 | - [/article/home](http://medux-react-admin.80zp.com/article/home) 18 | - [/article/service](http://medux-react-admin.80zp.com/article/service) 19 | - [/article/about](http://medux-react-admin.80zp.com/article/about) 20 | 21 | ### 需要登录才能访问的页面 22 | 23 | - [/admin/home](http://medux-react-admin.80zp.com/admin/home) 24 | - [/admin/member](http://medux-react-admin.80zp.com/admin/member/list) 25 | - [/admin/role](http://medux-react-admin.80zp.com/admin/role/list) 26 | - [/admin/post](http://medux-react-admin.80zp.com/admin/post/list) 27 | 28 | # 查看更多项目介绍 29 | 30 | - [掘金](https://juejin.im/post/5eb8ecdb6fb9a04332125bf8) | [知乎](https://zhuanlan.zhihu.com/p/139732293) | [语雀](https://www.yuque.com/medux/docs/medux-react-admin) 31 | 32 | # 安装及运行 33 | 34 | 本项目使用[@medux/react-web-router](https://github.com/wooline/medux/tree/master/packages/react-web-router) + [ANTD 4](https://ant.design/index-cn) 开发,全程使用 React Hooks,并配备了比较完善的脚手架。 35 | 36 | ``` 37 | // 注意一下,因为本项目风格检查要求以 LF 为换行符 38 | // 所以请先关闭 Git 配置中 autocrlf 39 | git config --global core.autocrlf false 40 | git clone https://github.com/wooline/medux-react-admin.git 41 | cd medux-react-admin 42 | yarn install 43 | ``` 44 | 45 | ### 以开发模式运行 46 | 47 | - 运行 `yarn start`,会自动启动一个开发服务器。 48 | - 开发模式时 React 热更新使用最新的 [React Fast Refresh](https://www.npmjs.com/package/react-refresh) 方案,需要安装最新的 React Developer Tools。 49 | 50 | ### 查看更多代码说明 51 | 52 | - [掘金](https://juejin.im/post/5eb8fa3ff265da7bb65fbddc) | [知乎](https://zhuanlan.zhihu.com/p/139734752) | [语雀](https://www.yuque.com/medux/docs/medux-react-admin-2) 53 | 54 | --- 55 | 56 | **欢迎批评指正,觉得还不错的别忘了给个 Star >\_<,如有错误或 Bug 请反馈** 57 | 58 | QQ 群交流:929696953 59 | 60 | ![QQ群交流](https://cdn.nlark.com/yuque/0/2020/png/1294343/1587232895054-aca0f46f-c5d0-46d6-973e-2e9dd76120d4.png) 61 | -------------------------------------------------------------------------------- /src/entity/member.ts: -------------------------------------------------------------------------------- 1 | import {enumOptions} from 'common/utils'; 2 | import {BaseListItem, BaseListSearch, BaseListSummary, CommonResource, CommonResourceRouteParams} from './index'; 3 | 4 | export enum Gender { 5 | '男' = 'male', 6 | '女' = 'female', 7 | '未知' = 'unknow', 8 | } 9 | 10 | export const DGender = enumOptions(Gender); 11 | 12 | export enum Status { 13 | '启用' = 'enable', 14 | '禁用' = 'disable', 15 | } 16 | export const DStatus = enumOptions(Status); 17 | 18 | export interface ListSearch extends BaseListSearch { 19 | username?: string; 20 | nickname?: string; 21 | email?: string; 22 | role?: {id: string; name: string}; 23 | roleId?: string; 24 | loginTime?: [number, number]; 25 | status?: Status; 26 | } 27 | export interface ListItem extends BaseListItem { 28 | username: string; 29 | nickname: string; 30 | gender: Gender; 31 | post: number; 32 | role: {id: string} | undefined; 33 | roleId: string; 34 | roleName: string; 35 | status: Status; 36 | loginTime: number; 37 | createdTime: number; 38 | email: string; 39 | fixed?: boolean; 40 | } 41 | export interface ItemDetail extends ListItem { 42 | score: number; 43 | account: number; 44 | } 45 | export interface ListSummary extends BaseListSummary {} 46 | 47 | export interface UpdateItem { 48 | id: string; 49 | username: string; 50 | nickname: string; 51 | gender: Gender; 52 | role: {id: string} | undefined; 53 | roleId: string; 54 | status: Status; 55 | email: string; 56 | } 57 | export interface RouteParams extends CommonResourceRouteParams { 58 | listSearch: ListSearch; 59 | } 60 | export interface Resource extends CommonResource { 61 | RouteParams: RouteParams; 62 | ListSearch: ListSearch; 63 | ListItem: ListItem; 64 | ListSummary: ListSummary; 65 | ItemDetail: ItemDetail; 66 | UpdateItem: UpdateItem; 67 | } 68 | 69 | // 定义本模块的路由参数类型 70 | export const defaultRouteParams: RouteParams = { 71 | listSearch: { 72 | pageSize: 10, 73 | pageCurrent: 1, 74 | term: undefined, 75 | category: undefined, 76 | sorterField: undefined, 77 | sorterOrder: undefined, 78 | username: undefined, 79 | nickname: undefined, 80 | status: undefined, 81 | email: undefined, 82 | role: undefined, 83 | roleId: undefined, 84 | loginTime: undefined, 85 | }, 86 | listView: '', 87 | _listVer: 0, 88 | itemId: '', 89 | itemView: '', 90 | _itemVer: 0, 91 | }; 92 | -------------------------------------------------------------------------------- /src/modules/app/components/FormLayout/index.m.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 100%; 4 | height: 100%; 5 | background: @light-bg-color; 6 | .warp { 7 | width: 1000px; 8 | position: absolute; 9 | left: 50%; 10 | top: 50%; 11 | transform: translate(-50%, -50%); 12 | background: url('./imgs/bg.png') no-repeat top right; 13 | &::before { 14 | content: ''; 15 | position: absolute; 16 | display: block; 17 | width: 300px; 18 | height: 300px; 19 | background: url('./imgs/bg.png') no-repeat; 20 | background-size: 100% 100%; 21 | bottom: 0; 22 | left: 0; 23 | z-index: -1; 24 | } 25 | > .pattern { 26 | color: #dfdfdf; 27 | position: absolute; 28 | font-size: 300px; 29 | top: 0; 30 | right: 0; 31 | z-index: -1; 32 | opacity: 0.5; 33 | &:last-child { 34 | font-size: 250px; 35 | bottom: 0; 36 | left: 0; 37 | top: auto; 38 | right: auto; 39 | } 40 | } 41 | > .panel { 42 | margin: 100px; 43 | width: 800px; 44 | box-shadow: 0 12px 20px #d8e0e6; 45 | display: flex; 46 | } 47 | } 48 | 49 | .welcome { 50 | position: relative; 51 | background: @primary-color; 52 | color: #fff; 53 | flex: 1 1 50%; 54 | padding: 30px; 55 | line-height: 2; 56 | a { 57 | color: #fff; 58 | &:hover { 59 | opacity: 0.7; 60 | } 61 | } 62 | h2 { 63 | font-size: 30px; 64 | color: #fff; 65 | padding-top: 40px; 66 | } 67 | .hd { 68 | position: absolute; 69 | left: 10px; 70 | top: 10px; 71 | > * { 72 | vertical-align: middle; 73 | } 74 | } 75 | .logo { 76 | font-size: 40px; 77 | } 78 | } 79 | .form { 80 | position: relative; 81 | flex: 1 1 50%; 82 | padding: 50px 30px 30px; 83 | background: url('./imgs/bg-icon2.png') no-repeat -30% 0 #fff; 84 | 85 | &::before { 86 | content: ''; 87 | position: absolute; 88 | display: block; 89 | width: 114px; 90 | height: 56px; 91 | background: url('./imgs/bg-icon1.png') no-repeat; 92 | background-size: 100% 100%; 93 | bottom: 0; 94 | right: 0; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/modules/article/articleLayout/views/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import {LogoutOutlined, UserAddOutlined} from '@ant-design/icons'; 2 | import React, {useCallback} from 'react'; 3 | 4 | import {CurUser} from 'entity/session'; 5 | import {Link} from '@medux/react-web-router'; 6 | import LoginLink from 'components/LoginLink'; 7 | import Logo from 'assets/imgs/logo.png'; 8 | import {connect} from 'react-redux'; 9 | import styles from './index.m.less'; 10 | 11 | interface StoreProps { 12 | pathname: string; 13 | curUser: CurUser; 14 | } 15 | 16 | const Component: React.FC = ({pathname, curUser, dispatch}) => { 17 | const onLogout = useCallback(() => { 18 | dispatch(actions.app.logout()); 19 | }, [dispatch]); 20 | return ( 21 |
22 |
23 |
24 | logo 25 |

帮助中心

26 | 27 | 用户指南 28 | 29 | 30 | 售后服务 31 | 32 | 33 | 关于我们 34 | 35 |
36 |
37 | {curUser.hasLogin ? ( 38 | <> 39 | 欢迎您 {curUser.username} 40 | 41 | 管理中心 42 | 43 | 44 | 45 |  退出 46 | 47 | 48 | ) : ( 49 | <> 50 | 登录 51 | 52 | 53 |  注册 54 | 55 | 56 | )} 57 |
58 |
59 |
60 | ); 61 | }; 62 | // TODO 63 | // {metaKeys.UserHomePathname} 64 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 65 | const app = state.app!; 66 | return { 67 | pathname: state.route.location.pathname, 68 | curUser: app.curUser!, 69 | }; 70 | }; 71 | 72 | export default connect(mapStateToProps)(React.memo(Component)); 73 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/views/Search.tsx: -------------------------------------------------------------------------------- 1 | import {DStatus, ListSearch} from 'entity/member'; 2 | import {Input, Select} from 'antd'; 3 | import React, {useCallback} from 'react'; 4 | 5 | import {FromItemList} from 'common/utils'; 6 | import {ListSearchFormData} from 'entity'; 7 | import RangeDatePicker from 'components/RangeDatePicker'; 8 | import SearchForm from 'components/SearchForm'; 9 | import {connect} from 'react-redux'; 10 | import useEventCallback from 'hooks/useEventCallback'; 11 | 12 | type FormData = ListSearchFormData; 13 | 14 | const RoleSelector = loadView('adminRole', 'selector', {forwardRef: true}); 15 | 16 | const Option = Select.Option; 17 | 18 | interface OwnProps { 19 | listSearch: ListSearch; 20 | defaultSearch?: ListSearch; 21 | fixedFields?: Partial; 22 | } 23 | 24 | const formItems: FromItemList = [ 25 | {name: 'username', label: '用户名', formItem: }, 26 | {name: 'nickname', label: '呢称', formItem: }, 27 | { 28 | name: 'status', 29 | label: '状态', 30 | formItem: ( 31 | 38 | ), 39 | }, 40 | { 41 | name: 'role', 42 | label: '角色', 43 | formItem: , 44 | }, 45 | { 46 | name: 'email', 47 | label: 'Email', 48 | formItem: , 49 | }, 50 | {name: 'loginTime', label: '登录时间', formItem: }, 51 | ]; 52 | 53 | const Component: React.FC = ({dispatch, listSearch, defaultSearch, fixedFields}) => { 54 | const onFinish = useCallback( 55 | (values: FormData) => { 56 | dispatch(actions.adminMember.doListSearch({...values})); 57 | }, 58 | [dispatch] 59 | ); 60 | const onReset = useEventCallback(() => { 61 | dispatch(actions.adminMember.resetListSearch(defaultSearch)); 62 | }, [defaultSearch, dispatch]); 63 | 64 | return ( 65 |
66 | values={listSearch} fixedFields={fixedFields} senior={4} expand={!!listSearch.email || !!listSearch.loginTime} onReset={onReset} onFinish={onFinish} items={formItems} /> 67 |
68 | ); 69 | }; 70 | 71 | export default connect()(React.memo(Component)); 72 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 85 | 90 | 91 | 92 | 93 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/views/Search.tsx: -------------------------------------------------------------------------------- 1 | import {DStatus, ListSearch} from 'entity/post'; 2 | import {Input, Select} from 'antd'; 3 | import React, {useCallback} from 'react'; 4 | 5 | import {FromItemList} from 'common/utils'; 6 | import {ListSearchFormData} from 'entity'; 7 | import {Status as MemberStaus} from 'entity/member'; 8 | import RangeDatePicker from 'components/RangeDatePicker'; 9 | import ResourceSelector from 'components/ResourceSelector'; 10 | import SearchForm from 'components/SearchForm'; 11 | import {connect} from 'react-redux'; 12 | import useEventCallback from 'hooks/useEventCallback'; 13 | 14 | type FormData = ListSearchFormData; 15 | 16 | const MemberSelector = loadView('adminMember', 'selector'); 17 | 18 | const Option = Select.Option; 19 | 20 | interface OwnProps { 21 | listSearch: ListSearch; 22 | defaultSearch?: ListSearch; 23 | fixedFields?: Partial; 24 | } 25 | 26 | const formItems: FromItemList = [ 27 | {name: 'title', label: '标题', formItem: }, 28 | {name: 'author', label: '作者', formItem: }, 29 | { 30 | name: 'status', 31 | label: '状态', 32 | formItem: ( 33 | 40 | ), 41 | }, 42 | { 43 | name: 'editor', 44 | label: '责任编辑', 45 | formItem: ( 46 | } 52 | /> 53 | ), 54 | }, 55 | {name: 'createdTime', label: '发表时间', formItem: }, 56 | ]; 57 | 58 | const Component: React.FC = ({dispatch, listSearch, defaultSearch, fixedFields}) => { 59 | const onFinish = useCallback( 60 | (values: FormData) => { 61 | dispatch(actions.adminPost.doListSearch({...values})); 62 | }, 63 | [dispatch] 64 | ); 65 | const onReset = useEventCallback(() => { 66 | dispatch(actions.adminPost.resetListSearch(defaultSearch)); 67 | }, [defaultSearch, dispatch]); 68 | 69 | return ( 70 |
71 | values={listSearch} fixedFields={fixedFields} onReset={onReset} onFinish={onFinish} items={formItems} /> 72 |
73 | ); 74 | }; 75 | 76 | export default connect()(React.memo(Component)); 77 | -------------------------------------------------------------------------------- /src/components/PurviewEditor.tsx: -------------------------------------------------------------------------------- 1 | import {Checkbox} from 'antd'; 2 | import {CheckboxChangeEvent} from 'antd/lib/checkbox'; 3 | import React from 'react'; 4 | 5 | import {purviewNames} from 'entity/role'; 6 | import useEventCallback from 'hooks/useEventCallback'; 7 | 8 | const options: {[key: string]: string[]} = {}; 9 | Object.keys(purviewNames).forEach((item) => { 10 | const [resource, action] = item.split('.'); 11 | if (!action) { 12 | options[resource] = []; 13 | } else { 14 | options[resource].push(item); 15 | } 16 | }); 17 | 18 | interface Props { 19 | value?: string[]; 20 | onChange?: (value?: string[]) => void; 21 | } 22 | const Component: React.FC = ({value = [], onChange}) => { 23 | const onItemChange = useEventCallback( 24 | (e: CheckboxChangeEvent) => { 25 | if (onChange) { 26 | const {checked, name = ''} = e.target; 27 | if (!checked) { 28 | const arr = value.filter((item) => item !== name); 29 | onChange(arr.length ? arr.sort() : undefined); 30 | } else { 31 | onChange([...value, name].sort()); 32 | } 33 | } 34 | }, 35 | [onChange, value] 36 | ); 37 | const onResourceChange = useEventCallback( 38 | (e: CheckboxChangeEvent) => { 39 | if (onChange) { 40 | const {checked, name = ''} = e.target; 41 | if (!checked) { 42 | const arr = value.filter((item) => item.split('.')[0] !== name); 43 | onChange(arr.length ? arr.sort() : undefined); 44 | } else { 45 | onChange(Array.from(new Set([...value, ...options[name]])).sort()); 46 | } 47 | } 48 | }, 49 | [onChange, value] 50 | ); 51 | 52 | const purviews: {[key: string]: boolean} = value.reduce((pre, cur) => { 53 | pre[cur] = true; 54 | return pre; 55 | }, {}); 56 | const items: {[key: string]: string[]} = value.reduce((pre, cur) => { 57 | const [resource] = cur.split('.'); 58 | if (!pre[resource]) { 59 | pre[resource] = []; 60 | } 61 | pre[resource].push(cur); 62 | return pre; 63 | }, {}); 64 | return ( 65 |
66 | {Object.keys(purviewNames).map((item) => { 67 | const [, action] = item.split('.'); 68 | return action ? ( 69 |
70 | 71 | {purviewNames[item]} 72 |
73 | ) : ( 74 |
75 | 76 | {purviewNames[item]} 77 |
78 | ); 79 | })} 80 |
81 | ); 82 | }; 83 | 84 | export default React.memo(Component); 85 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/views/Detail.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Descriptions} from 'antd'; 2 | import {DGender, DStatus, ItemDetail} from 'entity/member'; 3 | import {DeleteOutlined, FormOutlined} from '@ant-design/icons'; 4 | 5 | import DateTime from 'components/DateTime'; 6 | import React from 'react'; 7 | import {connect} from 'react-redux'; 8 | import useDetail from 'hooks/useDetail'; 9 | import styles from './index.m.less'; 10 | 11 | const DescriptionsItem = Descriptions.Item; 12 | const formOutlined = ; 13 | const deleteOutlined = ; 14 | 15 | interface OwnProps { 16 | primaryMode: boolean; 17 | currentItem: ItemDetail; 18 | } 19 | 20 | export const ModalSubmitLayout = { 21 | wrapperCol: {span: 15, offset: 6}, 22 | }; 23 | const Component: React.FC = ({dispatch, primaryMode, currentItem}) => { 24 | const {onHide, onEdit, onDelete} = useDetail(dispatch, actions.adminMember, currentItem); 25 | const disabled = currentItem.fixed ? 'disable' : ''; 26 | return ( 27 |
28 | 29 | {currentItem.username} 30 | {currentItem.nickname} 31 | {currentItem.roleName} 32 | {DGender.keyToName[currentItem.gender]} 33 | {currentItem.post} 34 | 35 | 36 | 37 | {DStatus.keyToName[currentItem.status]} 38 | {currentItem.email} 39 | 40 | 41 | 42 | {currentItem.score} 43 | {currentItem.account} 44 | 4 45 | 46 |
47 | 50 | {primaryMode && ( 51 | <> 52 | 55 | 58 | 59 | )} 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default connect()(React.memo(Component)); 66 | -------------------------------------------------------------------------------- /src/modules/admin/adminMember/views/SelectorTable.tsx: -------------------------------------------------------------------------------- 1 | import {DStatus, ListItem, ListSearch, ListSummary} from 'entity/member'; 2 | import MTable, {ColumnProps} from 'components/MTable'; 3 | import React, {useMemo} from 'react'; 4 | 5 | import {connect} from 'react-redux'; 6 | import useSelector from 'hooks/useSelector'; 7 | 8 | interface StoreProps { 9 | listSearch: ListSearch; 10 | list?: ListItem[]; 11 | listSummary?: ListSummary; 12 | } 13 | 14 | interface OwnProps { 15 | defaultSearch?: ListSearch; 16 | selectLimit?: number | [number, number]; 17 | selectedRows?: ListItem[]; 18 | onSelectdChange?: (items: ListItem[]) => void; 19 | } 20 | 21 | const Component: React.FC = ({dispatch, onSelectdChange, selectLimit, selectedRows, list, listSearch, listSummary, defaultSearch}) => { 22 | const {onShowDetail, rowSelection, onChange} = useSelector(dispatch, actions.adminMember, listSearch, defaultSearch, selectedRows, onSelectdChange, selectLimit); 23 | const columns = useMemo[]>( 24 | () => [ 25 | { 26 | title: '用户名', 27 | dataIndex: 'username', 28 | width: '11%', 29 | }, 30 | { 31 | title: '呢称', 32 | dataIndex: 'nickname', 33 | width: '11%', 34 | }, 35 | { 36 | title: '角色', 37 | dataIndex: 'roleName', 38 | width: '12%', 39 | }, 40 | { 41 | title: 'Email', 42 | dataIndex: 'email', 43 | ellipsis: true, 44 | }, 45 | { 46 | title: '最后登录', 47 | dataIndex: 'loginTime', 48 | width: '18%', 49 | sorter: true, 50 | timestamp: true, 51 | }, 52 | { 53 | title: '状态', 54 | dataIndex: 'status', 55 | width: '6%', 56 | render: (status: string) => {DStatus.keyToName[status]}, 57 | }, 58 | { 59 | title: '操作', 60 | dataIndex: 'fixed', 61 | width: '9%', 62 | align: 'center', 63 | className: 'actions', 64 | render: (id: string, record) => onShowDetail(record.id)}>详细, 65 | }, 66 | ], 67 | [onShowDetail] 68 | ); 69 | 70 | return ( 71 |
72 | scroll={{y: 410}} onChange={onChange as any} listSearch={listSearch} rowSelection={rowSelection} columns={columns} dataSource={list} listSummary={listSummary} /> 73 |
74 | ); 75 | }; 76 | 77 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 78 | const thisModule = state.adminMember!; 79 | const {list, listSummary} = thisModule.selector || {}; 80 | return {list, listSummary, listSearch: thisModule.routeParams?.listSearch!}; 81 | }; 82 | 83 | export default connect(mapStateToProps)(React.memo(Component)); 84 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/views/Detail.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Descriptions} from 'antd'; 2 | import {DStatus, ItemDetail} from 'entity/post'; 3 | import {DeleteOutlined, FormOutlined} from '@ant-design/icons'; 4 | import React, {useCallback} from 'react'; 5 | 6 | import DateTime from 'components/DateTime'; 7 | import {connect} from 'react-redux'; 8 | import useDetail from 'hooks/useDetail'; 9 | import styles from './index.m.less'; 10 | 11 | const DescriptionsItem = Descriptions.Item; 12 | const formOutlined = ; 13 | const deleteOutlined = ; 14 | 15 | interface OwnProps { 16 | primaryMode: boolean; 17 | currentItem: ItemDetail; 18 | } 19 | 20 | export const ModalSubmitLayout = { 21 | wrapperCol: {span: 15, offset: 6}, 22 | }; 23 | const Component: React.FC = ({dispatch, primaryMode, currentItem}) => { 24 | const {onHide, onEdit, onDelete} = useDetail(dispatch, actions.adminPost, currentItem); 25 | const disabled = currentItem.fixed ? 'disable' : ''; 26 | const onShowMembers = useCallback( 27 | (username: string) => { 28 | dispatch(actions.adminMember.noneListSearch({username})); 29 | }, 30 | [dispatch] 31 | ); 32 | return ( 33 |
34 | 35 | 36 | {currentItem.title} 37 | 38 | 39 | onShowMembers(currentItem.author.id)}>{currentItem.author.name} 40 | 41 | 42 | {currentItem.editors.map((editor) => ( 43 | onShowMembers(editor.id)}> 44 | {editor.name} 45 | 46 | ))} 47 | 48 | {DStatus.keyToName[currentItem.status]} 49 | 50 | 51 | 52 | 53 | {currentItem.content} 54 | 55 | 56 |
57 | 60 | {primaryMode && ( 61 | <> 62 | 65 | 68 | 69 | )} 70 |
71 |
72 | ); 73 | }; 74 | 75 | export default connect()(React.memo(Component)); 76 | -------------------------------------------------------------------------------- /mock/get--api-member$@Z2V0LS1hcGktbWVtYmVyXCQ=.js: -------------------------------------------------------------------------------- 1 | let {sorterField = '', sorterOrder = '', username = '', nickname = '', status = '', email = '', roleId = '', pageCurrent, pageSize, loginTime} = request.query; 2 | 3 | sorterField = sorterField.toString(); 4 | sorterOrder = sorterOrder.toString(); 5 | username = username.toString(); 6 | nickname = nickname.toString(); 7 | status = status.toString(); 8 | email = email.toString(); 9 | roleId = roleId.toString(); 10 | loginTime = Array.isArray(loginTime) ? loginTime : []; 11 | pageCurrent = parseInt(pageCurrent) || 1; 12 | pageSize = parseInt(pageSize) || 10; 13 | 14 | const start = (pageCurrent - 1) * pageSize; 15 | const end = start + pageSize; 16 | 17 | const result = { 18 | statusCode: 422, 19 | headers: { 20 | 'x-delay': 0, 21 | 'content-type': 'application/json; charset=utf-8', 22 | }, 23 | }; 24 | const usersData = database.data.users; 25 | let resourceList = Object.keys(usersData).map(id => { 26 | return usersData[id]; 27 | }); 28 | 29 | if (username) { 30 | resourceList = resourceList.filter(item => item.username === username); 31 | } 32 | if (nickname) { 33 | resourceList = resourceList.filter(item => item.nickname.includes(nickname)); 34 | } 35 | if (status) { 36 | resourceList = resourceList.filter(item => item.status === status); 37 | } 38 | if (email) { 39 | resourceList = resourceList.filter(item => item.email === email); 40 | } 41 | if (roleId) { 42 | resourceList = resourceList.filter(item => item.roleId === roleId); 43 | } 44 | if (loginTime.length === 2) { 45 | resourceList = resourceList.filter(item => item.loginTime > loginTime[0] && item.loginTime < loginTime[1]); 46 | } 47 | if (sorterField === 'post') { 48 | if (sorterOrder === 'ascend') { 49 | resourceList.sort((a, b) => { 50 | return a.post - b.post; 51 | }); 52 | } else if (sorterOrder === 'descend') { 53 | resourceList.sort((a, b) => { 54 | return b.post - a.post; 55 | }); 56 | } 57 | } 58 | if (sorterField === 'createdTime') { 59 | if (sorterOrder === 'ascend') { 60 | resourceList.sort((a, b) => { 61 | return a.createdTime - b.createdTime; 62 | }); 63 | } else if (sorterOrder === 'descend') { 64 | resourceList.sort((a, b) => { 65 | return b.createdTime - a.createdTime; 66 | }); 67 | } 68 | } 69 | if (sorterField === 'loginTime') { 70 | if (sorterOrder === 'ascend') { 71 | resourceList.sort((a, b) => { 72 | return a.loginTime - b.loginTime; 73 | }); 74 | } else if (sorterOrder === 'descend') { 75 | resourceList.sort((a, b) => { 76 | return b.loginTime - a.loginTime; 77 | }); 78 | } 79 | } 80 | const totalItems = resourceList.length; 81 | 82 | result.statusCode = 200; 83 | result.response = { 84 | listSummary: { 85 | pageCurrent, 86 | pageSize, 87 | totalItems, 88 | totalPages: Math.ceil(resourceList.length / pageSize), 89 | }, 90 | list: resourceList.slice(start, end), 91 | }; 92 | return result; 93 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import {Avatar, Badge, Dropdown, Menu} from 'antd'; 2 | import {BellOutlined, CloseCircleOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, QuestionCircleOutlined, SettingOutlined, UserOutlined} from '@ant-design/icons'; 3 | import React, {useCallback, useMemo} from 'react'; 4 | 5 | import {CurUser} from 'entity/session'; 6 | import {Link} from '@medux/react-web-router'; 7 | import {connect} from 'react-redux'; 8 | import styles from './index.m.less'; 9 | 10 | interface StoreProps { 11 | notices: number; 12 | curUser: CurUser; 13 | siderCollapsed: boolean; 14 | } 15 | 16 | const Component: React.FC = ({dispatch, siderCollapsed, curUser, notices}) => { 17 | const toggleSider = useCallback(() => { 18 | dispatch(actions.adminLayout.putSiderCollapsed(!siderCollapsed)); 19 | }, [siderCollapsed, dispatch]); 20 | const onMenuItemClick = useCallback( 21 | ({key}: {key: string}) => { 22 | if (key === 'logout') { 23 | dispatch(actions.app.logout()); 24 | } else if (key === 'triggerError') { 25 | setTimeout(() => { 26 | throw new Error('自定义出错!'); 27 | }, 0); 28 | } 29 | }, 30 | [dispatch] 31 | ); 32 | const menu = useMemo( 33 | () => ( 34 | 35 | 36 | 个人中心 37 | 38 | 39 | 设置 40 | 41 | 42 | 触发报错 43 | 44 | 45 | 46 | 退出登录 47 | 48 | 49 | ), 50 | [onMenuItemClick] 51 | ); 52 | return ( 53 |
54 |
55 | {siderCollapsed ? : } 56 | 57 | 帮助指南 58 | 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | {curUser.username} 68 | 69 | 70 |
71 |
72 | ); 73 | }; 74 | 75 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 76 | return { 77 | notices: state.app!.notices.count, 78 | curUser: state.app!.curUser!, 79 | siderCollapsed: !!state.adminLayout!.siderCollapsed, 80 | }; 81 | }; 82 | 83 | export default connect(mapStateToProps)(React.memo(Component)); 84 | -------------------------------------------------------------------------------- /src/modules/admin/adminLayout/views/TabNavs/index.tsx: -------------------------------------------------------------------------------- 1 | import {CloseCircleOutlined, PlusCircleOutlined} from '@ant-design/icons'; 2 | import React, {useCallback} from 'react'; 3 | 4 | import {Popover} from 'antd'; 5 | import {TabNav} from 'entity'; 6 | import {connect} from 'react-redux'; 7 | import TabNavEditor from '../TabNavEditor'; 8 | import styles from './index.m.less'; 9 | 10 | interface StoreProps { 11 | tabNavCurId?: string; 12 | tabNavEditor?: TabNav; 13 | tabNavs: TabNav[]; 14 | } 15 | 16 | const Component: React.FC = ({dispatch, tabNavs, tabNavCurId, tabNavEditor}) => { 17 | const onDelItem = useCallback( 18 | (item: TabNav) => { 19 | dispatch(actions.adminLayout.delTabNav(item.id)); 20 | }, 21 | [dispatch] 22 | ); 23 | 24 | const onClickItem = useCallback( 25 | (item: TabNav) => { 26 | dispatch(actions.adminLayout.clickTabNav(item)); 27 | }, 28 | [dispatch] 29 | ); 30 | const onCloseEditor = useCallback(() => { 31 | dispatch(actions.adminLayout.closeTabNavEditor()); 32 | }, [dispatch]); 33 | const onSwitchCreator = useCallback( 34 | (open: boolean) => { 35 | if (open) { 36 | dispatch(actions.adminLayout.openTabNavCreator()); 37 | } else { 38 | dispatch(actions.adminLayout.closeTabNavEditor()); 39 | } 40 | }, 41 | [dispatch] 42 | ); 43 | 44 | return ( 45 |
46 | {tabNavs.map((item) => ( 47 |
48 | :
} 50 | trigger="click" 51 | visible={!!tabNavEditor && tabNavEditor.id === item.id} 52 | onVisibleChange={(visible) => { 53 | if (visible) { 54 | onClickItem(item); 55 | } else { 56 | onCloseEditor(); 57 | } 58 | }} 59 | > 60 | trigger 61 | 62 | {item.title} 63 | onDelItem(item)} /> 64 |
65 | ))} 66 | :
} 70 | trigger="click" 71 | > 72 |
73 | 收藏 74 |
75 | 76 |
77 | ); 78 | }; 79 | 80 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 81 | const adminLayout = state.adminLayout!; 82 | return { 83 | tabNavs: adminLayout.tabNavs, 84 | tabNavCurId: adminLayout.tabNavCurId, 85 | tabNavEditor: adminLayout.tabNavEditor, 86 | }; 87 | }; 88 | 89 | export default connect(mapStateToProps)(React.memo(Component)); 90 | -------------------------------------------------------------------------------- /src/entity/role.ts: -------------------------------------------------------------------------------- 1 | import {BaseListItem, BaseListSearch, BaseListSummary, CommonResource, CommonResourceRouteParams} from './index'; 2 | 3 | export const purviewNames: {[key: string]: string} = { 4 | role: '角色', 5 | 'role.all': '角色管理', 6 | user: '用户', 7 | 'user.create': '新增', 8 | 'user.delete': '删除', 9 | 'user.update': '修改', 10 | 'user.list': '列表', 11 | 'user.detail': '详情', 12 | 'user.review': '审核', 13 | post: '信息', 14 | 'post.create': '新增', 15 | 'post.delete': '删除', 16 | 'post.update': '修改', 17 | 'post.list': '列表', 18 | 'post.detail': '详情', 19 | 'post.review': '审核', 20 | }; 21 | 22 | export interface Purview { 23 | [resource: string]: string[]; 24 | } 25 | export const purviewData: Purview = { 26 | user: ['list', 'create', 'update', 'delete', 'detail', 'review'], 27 | post: ['list', 'create', 'update', 'delete', 'detail', 'review'], 28 | }; 29 | export interface MenuItem { 30 | name: string; 31 | icon?: string; 32 | keys: string | string[]; 33 | link?: string; 34 | children?: MenuItem[]; 35 | target?: string; 36 | disable?: boolean; 37 | } 38 | 39 | export const MenuData: MenuItem[] = [ 40 | { 41 | name: '概要总览', 42 | icon: 'dashboard', 43 | keys: '/admin/home', 44 | }, 45 | { 46 | name: '用户管理', 47 | icon: 'user', 48 | keys: 'member', 49 | children: [ 50 | {name: '用户列表', keys: ['/admin/member/list', '/admin/member/list/detail/:id'], link: '/admin/member/list#q=%7B"adminMember"%3A%7B"_listVer"%3A${listVer}%7D%7D'}, 51 | {name: '角色管理', keys: '/admin/role/list#q=%7B"adminRole"%3A%7B"_listVer"%3A${listVer}%7D%7D'}, 52 | ], 53 | }, 54 | { 55 | name: '信息管理', 56 | icon: 'post', 57 | keys: '/admin/finance', 58 | children: [{name: '信息列表', keys: ['/admin/post/list', '/admin/post/list/detail/:id'], link: '/admin/post/list#q=%7B"adminPost"%3A%7B"_listVer"%3A${listVer}%7D%7D'}], 59 | }, 60 | ]; 61 | export interface ListSearch extends BaseListSearch { 62 | roleName?: string; 63 | purviews?: string[]; 64 | } 65 | 66 | export interface ListItem extends BaseListItem { 67 | roleName: string; 68 | purviews: string[]; 69 | owner: number; 70 | createdTime: number; 71 | remark: string; 72 | fixed?: boolean; 73 | } 74 | 75 | export interface ItemDetail extends ListItem {} 76 | export interface UpdateItem { 77 | id: string; 78 | roleName: string; 79 | purviews: string[]; 80 | remark: string; 81 | } 82 | 83 | export interface ListSummary extends BaseListSummary {} 84 | export interface RouteParams extends CommonResourceRouteParams { 85 | listSearch: ListSearch; 86 | } 87 | export interface Resource extends CommonResource { 88 | RouteParams: RouteParams; 89 | ListSearch: ListSearch; 90 | ListItem: ListItem; 91 | ListSummary: ListSummary; 92 | ItemDetail: ItemDetail; 93 | UpdateItem: UpdateItem; 94 | } 95 | 96 | export const defaultRouteParams: RouteParams = { 97 | listSearch: { 98 | pageSize: 10, 99 | pageCurrent: 1, 100 | term: undefined, 101 | category: undefined, 102 | sorterField: undefined, 103 | sorterOrder: undefined, 104 | roleName: undefined, 105 | purviews: undefined, 106 | }, 107 | listView: '', 108 | _listVer: 0, 109 | itemId: '', 110 | itemView: '', 111 | _itemVer: 0, 112 | }; 113 | -------------------------------------------------------------------------------- /src/components/SearchForm.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Form} from 'antd'; 2 | 3 | import {DownOutlined, UpOutlined} from '@ant-design/icons'; 4 | import React, {useCallback, useMemo, useState} from 'react'; 5 | import {FromItem} from 'common/utils'; 6 | import useEventCallback from 'hooks/useEventCallback'; 7 | 8 | interface Props { 9 | items: FromItem>[]; 10 | onFinish: (values: FormData) => void; 11 | values?: Partial; 12 | fixedFields?: Partial; 13 | senior?: number; 14 | cols?: number; 15 | 16 | expand?: boolean; 17 | onReset?: () => void; 18 | } 19 | 20 | function Component(props: Props) { 21 | const {items, onFinish, onReset, fixedFields, values, cols = 4} = props; 22 | const [expand, setExpand] = useState(() => { 23 | return !!props.expand; 24 | }); 25 | const list = useMemo(() => { 26 | return fixedFields ? items.filter((item) => fixedFields[item.name!] === undefined) : items; 27 | }, [fixedFields, items]); 28 | const {senior = list.length} = props; 29 | const shrink = expand ? list.length : senior; 30 | 31 | const {colWidth, arr} = useMemo(() => { 32 | const cWidth = parseFloat((100 / cols).toFixed(2)); 33 | const cArr: number[] = []; 34 | let cur = 0; 35 | list.forEach((item) => { 36 | // eslint-disable-next-line no-control-regex 37 | const label = Math.ceil(item.label!.replace(/[^\x00-\xff]/g, 'aa').length / 2); 38 | const col = item.col || 1; 39 | if (cur + col > cols) { 40 | cur = 0; 41 | } 42 | item.cite = cur; 43 | if (label > (cArr[cur] || 0)) { 44 | cArr[cur] = label; 45 | } 46 | cur += col; 47 | }); 48 | return {colWidth: cWidth, arr: cArr}; 49 | }, [cols, list]); 50 | const fields = useMemo(() => { 51 | return values ? Object.keys(values).map((name) => ({name, value: values[name]})) : []; 52 | }, [values]); 53 | const onFinishHandler = useEventCallback( 54 | (vals: T) => { 55 | Object.assign(vals, fixedFields); 56 | onFinish(vals); 57 | }, 58 | [fixedFields, onFinish] 59 | ); 60 | const toggle = useCallback(() => { 61 | setExpand((_expand) => !_expand); 62 | }, []); 63 | return ( 64 |
65 |
66 | {list.map((item, index) => ( 67 | = shrink ? 'none' : 'flex', width: `${colWidth * (item.col || 1)}%`}} 71 | key={item.name} 72 | label={ 73 | 74 | {item.label} 75 | 76 | } 77 | > 78 | {item.formItem!} 79 | 80 | ))} 81 |
82 | 85 | 86 | {list.length > senior && ( 87 | 88 | {expand ? '收起' : '展开'} {expand ? : } 89 | 90 | )} 91 |
92 | 93 |
94 | ); 95 | } 96 | 97 | export default React.memo(Component) as typeof Component; 98 | -------------------------------------------------------------------------------- /src/modules/admin/adminPost/views/Editor.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Form, Input} from 'antd'; 2 | import {ItemDetail, UpdateItem} from 'entity/post'; 3 | import React, {useCallback} from 'react'; 4 | 5 | import {CustomError} from 'common/errors'; 6 | import {Status as MemberStaus} from 'entity/member'; 7 | import ResourceSelector from 'components/ResourceSelector'; 8 | import {connect} from 'react-redux'; 9 | import {getFormDecorators} from 'common/utils'; 10 | import useEventCallback from 'hooks/useEventCallback'; 11 | 12 | type FormData = Omit; 13 | const MemberSelector = loadView('adminMember', 'selector'); 14 | 15 | const FormItem = Form.Item; 16 | 17 | export const formItemLayout = { 18 | labelCol: { 19 | span: 4, 20 | }, 21 | wrapperCol: { 22 | span: 19, 23 | }, 24 | }; 25 | 26 | const fromDecorators = getFormDecorators({ 27 | title: {rules: [{required: true, message: '请输入标题'}]}, 28 | content: {rules: [{required: true, message: '请输入内容'}]}, 29 | editors: {rules: [{required: true, message: '请选择责任编辑'}]}, 30 | editorIds: {}, 31 | }); 32 | 33 | interface OwnProps { 34 | currentItem: ItemDetail; 35 | } 36 | 37 | const Component: React.FC = ({dispatch, currentItem}) => { 38 | const [form] = Form.useForm(); 39 | 40 | const onHide = useCallback(() => { 41 | dispatch(actions.adminPost.closeCurrentItem()); 42 | }, [dispatch]); 43 | 44 | const onReset = useCallback(() => { 45 | form.resetFields(); 46 | }, [form]); 47 | 48 | const handleSubmit = useCallback( 49 | (error: CustomError) => { 50 | if (error instanceof CustomError) { 51 | form.setFields([{name: 'title', errors: [error.message]}]); 52 | message.error(error.message!); 53 | } 54 | }, 55 | [form] 56 | ); 57 | 58 | const id = currentItem.id; 59 | const onFinish = useEventCallback( 60 | (values: FormData) => { 61 | if (id) { 62 | dispatch(actions.adminPost.updateItem({...values, id}, handleSubmit)); 63 | } else { 64 | dispatch(actions.adminPost.createItem({...values, id}, handleSubmit)); 65 | } 66 | }, 67 | [dispatch, handleSubmit, id] 68 | ); 69 | 70 | return ( 71 |
72 | 73 | } 80 | /> 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | 92 | 95 | 96 |
97 | 98 | ); 99 | }; 100 | 101 | export default connect()(React.memo(Component)); 102 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/views/Editor.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Form, Input} from 'antd'; 2 | import {ItemDetail, UpdateItem} from 'entity/role'; 3 | import React, {useCallback} from 'react'; 4 | 5 | import {CaretDownOutlined} from '@ant-design/icons'; 6 | import {CustomError} from 'common/errors'; 7 | import PurviewEditor from 'components/PurviewEditor'; 8 | import {connect} from 'react-redux'; 9 | import {getFormDecorators} from 'common/utils'; 10 | import useEventCallback from 'hooks/useEventCallback'; 11 | import styles from './index.m.less'; 12 | 13 | type FormData = Omit; 14 | 15 | const FormItem = Form.Item; 16 | 17 | const formItemLayout = { 18 | labelCol: { 19 | span: 6, 20 | }, 21 | wrapperCol: { 22 | span: 18, 23 | }, 24 | }; 25 | const purviewsLayout = { 26 | labelCol: { 27 | span: 0, 28 | }, 29 | wrapperCol: { 30 | span: 25, 31 | }, 32 | }; 33 | 34 | interface OwnProps { 35 | currentItem: ItemDetail; 36 | } 37 | 38 | const fromDecorators = getFormDecorators({ 39 | roleName: {rules: [{required: true, message: '请输入角色名称'}]}, 40 | purviews: {rules: [{required: true, message: '请至少配置一项权限', type: 'array'}]}, 41 | remark: {}, 42 | }); 43 | 44 | const Component: React.FC = ({dispatch, currentItem}) => { 45 | const [form] = Form.useForm(); 46 | const initialValues: FormData = currentItem; 47 | const handleSubmit = useCallback( 48 | (error: CustomError) => { 49 | if (error instanceof CustomError) { 50 | form.setFields([{name: 'purviews', errors: [error.message]}]); 51 | message.error(error.message!); 52 | } 53 | }, 54 | [form] 55 | ); 56 | const onHide = useCallback(() => { 57 | dispatch(actions.adminRole.closeCurrentItem()); 58 | }, [dispatch]); 59 | const onReset = useCallback(() => { 60 | form.resetFields(); 61 | }, [form]); 62 | 63 | const id = currentItem.id; 64 | const onFinish = useEventCallback( 65 | (values: FormData) => { 66 | if (id) { 67 | dispatch(actions.adminRole.updateItem({...values, id}, handleSubmit)); 68 | } else { 69 | dispatch(actions.adminRole.createItem({...values, id}, handleSubmit)); 70 | } 71 | }, 72 | [dispatch, handleSubmit, id] 73 | ); 74 | 75 | return ( 76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 | 86 | 87 |

88 | 89 | 权限设置 90 |

91 | 92 | 93 | 94 |
95 |
96 | 99 | 102 | 103 |
104 | 105 | ); 106 | }; 107 | 108 | export default connect()(React.memo(Component)); 109 | -------------------------------------------------------------------------------- /src/modules/admin/adminRole/views/Detail.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Descriptions} from 'antd'; 2 | import {CaretDownOutlined, DeleteOutlined, FormOutlined} from '@ant-design/icons'; 3 | import React, {useMemo} from 'react'; 4 | 5 | import DateTime from 'components/DateTime'; 6 | import {ItemDetail, purviewNames} from 'entity/role'; 7 | import {connect} from 'react-redux'; 8 | 9 | import useDetail from 'hooks/useDetail'; 10 | import styles from './index.m.less'; 11 | 12 | const DescriptionsItem = Descriptions.Item; 13 | const formOutlined = ; 14 | const deleteOutlined = ; 15 | 16 | interface OwnProps { 17 | primaryMode: boolean; 18 | currentItem: ItemDetail; 19 | } 20 | export const formItemLayout = { 21 | labelCol: { 22 | span: 6, 23 | }, 24 | wrapperCol: { 25 | span: 15, 26 | }, 27 | }; 28 | export const ModalSubmitLayout = { 29 | wrapperCol: {span: 15, offset: 6}, 30 | }; 31 | 32 | const Component: React.FC = ({dispatch, primaryMode, currentItem}) => { 33 | const {onHide, onEdit, onDelete} = useDetail(dispatch, actions.adminRole, currentItem); 34 | 35 | const {purvieList, disabled} = useMemo(() => { 36 | const purviews: {[key: string]: boolean} = currentItem.purviews.reduce((pre, cur) => { 37 | pre[cur] = true; 38 | return pre; 39 | }, {}); 40 | const options: {[key: string]: string[]} = {}; 41 | Object.keys(purviewNames).forEach((item) => { 42 | const [resource, action] = item.split('.'); 43 | if (!action) { 44 | options[resource] = []; 45 | } else if (purviews[item]) { 46 | options[resource].push(item); 47 | } 48 | }); 49 | const items: string[] = []; 50 | Object.keys(options).forEach((key) => { 51 | const actions = options[key]; 52 | if (actions.length) { 53 | items.push(key, ...actions); 54 | } 55 | }); 56 | 57 | return { 58 | purvieList: items.map((item) => { 59 | const [resource, action] = item.split('.'); 60 | return action ?
{purviewNames[item]}
:
{purviewNames[resource]}
; 61 | }), 62 | disabled: currentItem.fixed ? 'disable' : '', 63 | }; 64 | }, [currentItem]); 65 | 66 | return ( 67 |
68 | 69 | {currentItem.roleName} 70 | {currentItem.owner} 71 | 72 | 73 | 74 | {currentItem.remark} 75 | 76 |
77 |

78 | 79 | 权限设置 80 |

81 |
{purvieList}
82 |
83 |
84 | 87 | {primaryMode && ( 88 | <> 89 | 92 | 95 | 96 | )} 97 |
98 |
99 | ); 100 | }; 101 | 102 | export default connect()(React.memo(Component)); 103 | -------------------------------------------------------------------------------- /src/modules/app/views/LoginForm/index.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Checkbox, Form, Input} from 'antd'; 2 | import {CurUser, LoginRequest} from 'entity/session'; 3 | import {LockOutlined, UserOutlined} from '@ant-design/icons'; 4 | import React, {useCallback} from 'react'; 5 | 6 | import {connect} from 'react-redux'; 7 | import {getFormDecorators} from 'common/utils'; 8 | import useLoginLink from 'hooks/useLoginLink'; 9 | import styles from './index.m.less'; 10 | 11 | type FormData = Required; 12 | 13 | const userOutlined = ; 14 | const lockOutlined = ; 15 | 16 | const initialValues: Partial = { 17 | username: 'admin', 18 | password: '123456', 19 | keep: false, 20 | }; 21 | 22 | interface StoreProps { 23 | curUser: CurUser; 24 | isPop: boolean; 25 | } 26 | 27 | const fromDecorators = getFormDecorators({ 28 | username: {rules: [{required: true, message: '请输入用户名!', whitespace: true}]}, 29 | password: {rules: [{required: true, message: '请输入密码!', whitespace: true}]}, 30 | keep: {valuePropName: 'checked'}, 31 | }); 32 | 33 | const Component: React.FC = ({curUser, isPop, dispatch}) => { 34 | const [form] = Form.useForm(); 35 | const {handleUserHome, handleLogout, handleRegister} = useLoginLink(isPop, dispatch); 36 | const onFinish = useCallback( 37 | (values: FormData) => { 38 | const result: Promise = dispatch(actions.app.login(values)) as any; 39 | result.catch((err) => { 40 | form.setFields([{name: 'password', errors: [err.message]}]); 41 | }); 42 | }, 43 | [dispatch, form] 44 | ); 45 | 46 | React.useEffect(() => { 47 | if (!isPop) { 48 | form.resetFields(); 49 | } 50 | }, [isPop, form]); 51 | return ( 52 |
53 |

用户登录

54 | 55 | {curUser.hasLogin && !curUser.expired ? ( 56 |
57 |

58 | 亲爱的{' '} 59 | 60 | {curUser.username} 61 | 62 | ,您已登录,是否要退出当前登录? 63 |

64 | 67 | 68 | ) : ( 69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 自动登录 79 | 80 | 81 | 注册新用户 82 | 83 | 86 | 87 | 88 | )} 89 |
90 | ); 91 | }; 92 | 93 | const mapStateToProps: (state: RootState) => StoreProps = (state) => { 94 | return { 95 | curUser: state.app!.curUser!, 96 | isPop: state.app!.showLoginOrRegisterPop === 'login', 97 | }; 98 | }; 99 | 100 | export default connect(mapStateToProps)(React.memo(Component)); 101 | -------------------------------------------------------------------------------- /src/hooks/useSelector.ts: -------------------------------------------------------------------------------- 1 | import {BaseListItem, BaseListSearch} from 'entity'; 2 | import {useCallback, useEffect, useMemo} from 'react'; 3 | 4 | import {CommonResourceActions} from 'common/resource'; 5 | import useEventCallback from './useEventCallback'; 6 | 7 | export default function Hooks( 8 | dispatch: (action: any) => void, 9 | resourceActions: CommonResourceActions, 10 | listSearch: BaseListSearch, 11 | defaultSearch?: BaseListSearch, 12 | selectedRows?: ListItem[], 13 | onSelectdChange?: (items: ListItem[]) => void, 14 | selectLimit?: number | [number, number] 15 | ) { 16 | const sorterStr = [listSearch?.sorterField, listSearch?.sorterOrder].join(''); 17 | const onShowDetail = useCallback( 18 | (item: ListItem | string) => { 19 | dispatch(resourceActions.openCurrentItem(item, 'summary')); 20 | }, 21 | [dispatch, resourceActions] 22 | ); 23 | const onClearSelect = useCallback(() => { 24 | if (onSelectdChange) { 25 | onSelectdChange([]); 26 | } 27 | }, [onSelectdChange]); 28 | 29 | const onRowSelect = useEventCallback( 30 | (record: ListItem) => { 31 | const selRows = selectedRows || []; 32 | const rows = selRows.filter((item) => item.id !== record.id); 33 | if (rows.length === selRows.length) { 34 | rows.push(record); 35 | } 36 | if (onSelectdChange) { 37 | onSelectdChange(rows); 38 | } 39 | }, 40 | [onSelectdChange, selectedRows] 41 | ); 42 | const onAllSelect = useEventCallback( 43 | (checked: boolean, curRows: ListItem[], changeRows: ListItem[]) => { 44 | const selRows = selectedRows || []; 45 | 46 | let rows: ListItem[] = []; 47 | if (checked) { 48 | rows = [...selRows, ...changeRows]; 49 | } else { 50 | const changeRowsKeys: {[key: string]: boolean} = changeRows.reduce((pre, cur) => { 51 | pre[cur.id] = true; 52 | return pre; 53 | }, {}); 54 | rows = selRows.filter((item) => !changeRowsKeys[item.id]); 55 | } 56 | if (onSelectdChange) { 57 | onSelectdChange(rows); 58 | } 59 | }, 60 | [onSelectdChange, selectedRows] 61 | ); 62 | 63 | const onChange = useCallback( 64 | (pagination: {current: number; pageSize: number}, filter: any, sorter: {field: string; order: 'ascend' | 'descend' | undefined}) => { 65 | const {current, pageSize} = pagination; 66 | const sorterField = (sorter.order && sorter.field) || undefined; 67 | const sorterOrder = sorter.order || undefined; 68 | const currentSorter = [sorterField, sorterOrder].join(''); 69 | const pageCurrent = currentSorter !== sorterStr ? 1 : current; 70 | 71 | dispatch( 72 | resourceActions.searchList({ 73 | params: { 74 | pageCurrent, 75 | pageSize, 76 | sorterField, 77 | sorterOrder, 78 | }, 79 | extend: 'current', 80 | }) 81 | ); 82 | }, 83 | [dispatch, resourceActions, sorterStr] 84 | ); 85 | 86 | const rowSelection = useMemo( 87 | () => ({ 88 | selectedRows, 89 | selectLimit, 90 | onClear: onClearSelect, 91 | onSelect: onRowSelect, 92 | onSelectAll: onAllSelect, 93 | }), 94 | [onAllSelect, onClearSelect, onRowSelect, selectedRows, selectLimit] 95 | ); 96 | useEffect(() => { 97 | dispatch(resourceActions.searchList({params: defaultSearch || {}, extend: 'default'}, 'selector')); 98 | // eslint-disable-next-line react-hooks/exhaustive-deps 99 | }, []); 100 | return {onChange, onAllSelect, onRowSelect, onClearSelect, onShowDetail, rowSelection}; 101 | } 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medux-react-admin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "type": "tsc", 7 | "typeSrc": "tsc -P ./src", 8 | "eslint": "eslint --cache .", 9 | "eslintSrc": "eslint --cache ./src", 10 | "stylelint": "stylelint \"./src/**/*.less\"", 11 | "checkCode": "yarn type & yarn typeSrc & yarn eslint & yarn eslintSrc & yarn stylelint", 12 | "start": "yarn checkCode && cross-env NODE_ENV=development SITE=local node build/start.js", 13 | "commit": "git-cz", 14 | 15 | "build-local": "npm run types-check && cross-env NODE_ENV=production SITE=local node build/build.js", 16 | "build-prod": "npm run types-check && cross-env NODE_ENV=production SITE=prod node build/build.js", 17 | "analyzer": "cross-env NODE_ENV=production SITE=analyzer node build/build.js", 18 | "push": "scp -r dist/prod/* root@127.0.0.1:/var/www/medux-react-admin", 19 | "eslintConfig": "eslint --print-config /Users/zy/work/test/medux-react-admin/src/common/resource.ts > eslintConfig.txt" 20 | }, 21 | "config": { 22 | "commitizen": { 23 | "path": "node_modules/cz-conventional-changelog" 24 | } 25 | }, 26 | "husky": { 27 | "hooks": { 28 | "pre-commit": "lint-staged" 29 | } 30 | }, 31 | "lint-staged": { 32 | "*.(ts|tsx|js|jsx)": "eslint", 33 | "*.less": "stylelint" 34 | }, 35 | "author": { 36 | "name": "wooline", 37 | "email": "wooline@qq.com" 38 | }, 39 | "license": "MIT", 40 | "private": true, 41 | "engines": { 42 | "node": ">=9.0.0" 43 | }, 44 | "browserslist": [ 45 | "chrome >= 70" 46 | ], 47 | "baseConf": { 48 | "version": "1.0.0", 49 | "siteName": "@medux", 50 | "clientPublicPath": "/", 51 | "server": "http://localhost:7445", 52 | "mock": true, 53 | "proxy": { 54 | "/api/**": { 55 | "target": "http://192.168.0.232:8000", 56 | "pathRewrite": { 57 | "^/api": "" 58 | }, 59 | "xfwd": true, 60 | "secure": false, 61 | "changeOrigin": true, 62 | "timeout": 3000, 63 | "proxyTimeout": 3000 64 | } 65 | } 66 | }, 67 | "vendors": { 68 | "base": [ 69 | "@babel", 70 | "react", 71 | "react-dom", 72 | "@medux", 73 | "lodash", 74 | "process", 75 | "warning", 76 | "axios", 77 | "moment", 78 | "dayjs", 79 | "performance-now", 80 | "shallowequal" 81 | ], 82 | "ui": [ 83 | "antd", 84 | "@ant-design", 85 | "async-validator", 86 | "resize-observer-polyfill", 87 | "add-dom-event-listener", 88 | "dom-align", 89 | "react-lifecycles-compat", 90 | "scroll-into-view-if-needed", 91 | "compute-scroll-into-view", 92 | "raf", 93 | "mini-store", 94 | "rc-[\\w-]+" 95 | ] 96 | }, 97 | "dependencies": { 98 | "@medux/react-web-router": "~1.1.1-alpha.6", 99 | "antd": "~4.2.0", 100 | "axios": "~0.19.2", 101 | "lodash": "~4.17.15", 102 | "path-to-regexp": "~6.1.0", 103 | "react": "~16.13.0", 104 | "react-dom": "~16.13.0", 105 | "react-redux": "~7.2.0", 106 | "fast-deep-equal": "~3.1.3" 107 | }, 108 | "devDependencies": { 109 | "@medux/dev-pkg": "~1.0.4-alpha.2", 110 | "@medux/dev-utils": "~1.0.8", 111 | "@medux/eslint-plugin-recommended": "~1.0.4", 112 | "@medux/stylelint-config-recommended": "~1.0.0", 113 | "@pmmmwh/react-refresh-webpack-plugin": "~0.3.0", 114 | "@types/lodash": "~4.14.149", 115 | "@types/react": "~16.9.23", 116 | "@types/react-dom": "~16.9.5", 117 | "@types/react-redux": "~7.1.9", 118 | "@types/history": "~4.7.7", 119 | "antd-dayjs-webpack-plugin": "~1.0.0", 120 | "babel-plugin-import": "~1.13.0", 121 | "commitizen": "~4.1.2", 122 | "cz-conventional-changelog": "~3.2.0", 123 | "less": "~3.10.1", 124 | "less-loader": "~5.0.0", 125 | "postcss-less": "~3.1.4", 126 | "raw-loader": "~4.0.0", 127 | "react-markdown": "^4.3.1", 128 | "react-refresh": "~0.8.1", 129 | "sass-resources-loader": "~2.0.1", 130 | "typescript": "~3.8.3" 131 | } 132 | } 133 | --------------------------------------------------------------------------------