├── .editorconfig ├── .eslintcache ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── babel.config.js ├── commitlint.config.js ├── env ├── dev.env ├── prod.env └── test.env ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.tsx ├── assets │ ├── css │ │ ├── common.scss │ │ ├── core.scss │ │ ├── index.scss │ │ └── init.scss │ ├── fonts │ │ ├── iconfont.eot │ │ ├── iconfont.svg │ │ ├── iconfont.ttf │ │ └── iconfont.woff │ ├── images │ │ ├── 01.png │ │ ├── 02.png │ │ ├── 404.png │ │ └── tip-pop-bg.png │ └── js │ │ └── a.js ├── common │ ├── AppContext.tsx │ ├── AppEvent.ts │ ├── Resolution.ts │ └── TContext.tsx ├── components │ ├── BaseComponent │ │ └── index.tsx │ ├── Button │ │ ├── ButtonCheck.ts │ │ └── index.tsx │ ├── DropDownMenu │ │ ├── index.scss │ │ ├── index.tsx │ │ └── popUtils.ts │ ├── ErrorBoundary │ │ ├── GlobalError │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── ViewError │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── index.tsx │ ├── NoMatch │ │ └── index.tsx │ ├── Page │ │ ├── index.scss │ │ └── index.tsx │ └── ResolutionCom │ │ └── ResolutionComp.tsx ├── index.scss ├── index.tsx ├── pages │ ├── Home │ │ ├── HomeChild │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── index.scss │ │ └── index.tsx │ └── Page1 │ │ ├── index.scss │ │ └── index.tsx ├── router │ ├── RouterUI.tsx │ └── index.ts ├── services │ ├── BaseService.ts │ ├── ServerResponseManager.ts │ ├── api │ │ ├── index.ts │ │ └── user │ │ │ └── index.ts │ ├── axios.ts │ └── serviceConfig.ts ├── store │ ├── actionCreaters │ │ ├── actionCreator.ts │ │ ├── index.ts │ │ └── user.ts │ ├── actions │ │ └── user.ts │ ├── connect.ts │ ├── index.ts │ └── reducers │ │ ├── history.ts │ │ ├── index.ts │ │ └── user.ts ├── types │ ├── IContext.ts │ ├── IRedux.ts │ ├── IRouterPage.ts │ ├── enum.ts │ └── service │ │ ├── responseData.ts │ │ └── user.ts ├── typings │ ├── custom.d.ts │ └── global.d.ts └── util │ ├── DomUtil.ts │ ├── generateId.ts │ ├── handleUrlParams.ts │ ├── index.ts │ ├── stringUtil.ts │ └── variableTypeDetection.ts ├── tsconfig.eslint.json ├── tsconfig.json ├── webpack ├── webpack.base.js ├── webpack.dev.js ├── webpack.prod.js └── webpackUtils │ ├── plugins.js │ ├── resolve.js │ ├── util.js │ └── variable.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.{js,json,ts,tsx}] 14 | indent_style = space 15 | indent_size = 4 16 | quote_type = single 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"E:\\react脚手架\\react-project-tmp\\src\\App.tsx":"1","E:\\react脚手架\\react-project-tmp\\src\\common\\AppContext.tsx":"2","E:\\react脚手架\\react-project-tmp\\src\\common\\AppEvent.ts":"3","E:\\react脚手架\\react-project-tmp\\src\\common\\Resolution.ts":"4","E:\\react脚手架\\react-project-tmp\\src\\common\\TContext.tsx":"5","E:\\react脚手架\\react-project-tmp\\src\\components\\BaseComponent\\index.tsx":"6","E:\\react脚手架\\react-project-tmp\\src\\components\\ErrorBoundary\\GlobalError\\index.tsx":"7","E:\\react脚手架\\react-project-tmp\\src\\components\\ErrorBoundary\\index.tsx":"8","E:\\react脚手架\\react-project-tmp\\src\\components\\ErrorBoundary\\ViewError\\index.tsx":"9","E:\\react脚手架\\react-project-tmp\\src\\components\\NoMatch\\index.tsx":"10","E:\\react脚手架\\react-project-tmp\\src\\components\\Page\\index.tsx":"11","E:\\react脚手架\\react-project-tmp\\src\\components\\ResolutionCom\\ResolutionComp.tsx":"12","E:\\react脚手架\\react-project-tmp\\src\\index.tsx":"13","E:\\react脚手架\\react-project-tmp\\src\\pages\\Home\\HomeChild\\index.tsx":"14","E:\\react脚手架\\react-project-tmp\\src\\pages\\Home\\index.tsx":"15","E:\\react脚手架\\react-project-tmp\\src\\pages\\Page1\\index.tsx":"16","E:\\react脚手架\\react-project-tmp\\src\\router\\index.ts":"17","E:\\react脚手架\\react-project-tmp\\src\\router\\RouterUI.tsx":"18","E:\\react脚手架\\react-project-tmp\\src\\services\\api\\index.ts":"19","E:\\react脚手架\\react-project-tmp\\src\\services\\api\\user\\index.ts":"20","E:\\react脚手架\\react-project-tmp\\src\\services\\axios.ts":"21","E:\\react脚手架\\react-project-tmp\\src\\services\\BaseService.ts":"22","E:\\react脚手架\\react-project-tmp\\src\\services\\ServerResponseManager.ts":"23","E:\\react脚手架\\react-project-tmp\\src\\services\\serviceConfig.ts":"24","E:\\react脚手架\\react-project-tmp\\src\\store\\actionCreaters\\actionCreator.ts":"25","E:\\react脚手架\\react-project-tmp\\src\\store\\actionCreaters\\index.ts":"26","E:\\react脚手架\\react-project-tmp\\src\\store\\actionCreaters\\user.ts":"27","E:\\react脚手架\\react-project-tmp\\src\\store\\actions\\user.ts":"28","E:\\react脚手架\\react-project-tmp\\src\\store\\connect.ts":"29","E:\\react脚手架\\react-project-tmp\\src\\store\\index.ts":"30","E:\\react脚手架\\react-project-tmp\\src\\store\\reducers\\history.ts":"31","E:\\react脚手架\\react-project-tmp\\src\\store\\reducers\\index.ts":"32","E:\\react脚手架\\react-project-tmp\\src\\store\\reducers\\user.ts":"33","E:\\react脚手架\\react-project-tmp\\src\\types\\enum.ts":"34","E:\\react脚手架\\react-project-tmp\\src\\types\\IContext.ts":"35","E:\\react脚手架\\react-project-tmp\\src\\types\\IRedux.ts":"36","E:\\react脚手架\\react-project-tmp\\src\\types\\IRouterPage.ts":"37","E:\\react脚手架\\react-project-tmp\\src\\types\\service\\responseData.ts":"38","E:\\react脚手架\\react-project-tmp\\src\\types\\service\\user.ts":"39","E:\\react脚手架\\react-project-tmp\\src\\typings\\custom.d.ts":"40","E:\\react脚手架\\react-project-tmp\\src\\typings\\global.d.ts":"41","E:\\react脚手架\\react-project-tmp\\src\\util\\generateId.ts":"42","E:\\react脚手架\\react-project-tmp\\src\\util\\handleUrlParams.ts":"43","E:\\react脚手架\\react-project-tmp\\src\\util\\index.ts":"44","E:\\react脚手架\\react-project-tmp\\src\\util\\stringUtil.ts":"45","E:\\react脚手架\\react-project-tmp\\src\\util\\variableTypeDetection.ts":"46"},{"size":745,"mtime":1631865620552,"results":"47","hashOfConfig":"48"},{"size":966,"mtime":1631780635956,"results":"49","hashOfConfig":"48"},{"size":851,"mtime":1631780648059,"results":"50","hashOfConfig":"48"},{"size":496,"mtime":1631780654021,"results":"51","hashOfConfig":"48"},{"size":174,"mtime":1631780658403,"results":"52","hashOfConfig":"48"},{"size":1377,"mtime":1631867195005,"results":"53","hashOfConfig":"48"},{"size":1208,"mtime":1631780669327,"results":"54","hashOfConfig":"48"},{"size":1901,"mtime":1631867195006,"results":"55","hashOfConfig":"48"},{"size":716,"mtime":1631780675795,"results":"56","hashOfConfig":"48"},{"size":101,"mtime":1631780762626,"results":"57","hashOfConfig":"48"},{"size":537,"mtime":1631780781012,"results":"58","hashOfConfig":"48"},{"size":2549,"mtime":1631781703438,"results":"59","hashOfConfig":"48"},{"size":1135,"mtime":1631781232725,"results":"60","hashOfConfig":"48"},{"size":1153,"mtime":1631867197445,"results":"61","hashOfConfig":"48"},{"size":1818,"mtime":1631780926541,"results":"62","hashOfConfig":"48"},{"size":1053,"mtime":1631867197450,"results":"63","hashOfConfig":"48"},{"size":496,"mtime":1631780964308,"results":"64","hashOfConfig":"48"},{"size":1482,"mtime":1631781076121,"results":"65","hashOfConfig":"48"},{"size":132,"mtime":1631781090049,"results":"66","hashOfConfig":"48"},{"size":346,"mtime":1631781086569,"results":"67","hashOfConfig":"48"},{"size":2032,"mtime":1631781093866,"results":"68","hashOfConfig":"48"},{"size":1393,"mtime":1631781097209,"results":"69","hashOfConfig":"48"},{"size":1312,"mtime":1631781755143,"results":"70","hashOfConfig":"48"},{"size":285,"mtime":1631781134065,"results":"71","hashOfConfig":"48"},{"size":773,"mtime":1631781139372,"results":"72","hashOfConfig":"48"},{"size":104,"mtime":1631781144930,"results":"73","hashOfConfig":"48"},{"size":468,"mtime":1631781147752,"results":"74","hashOfConfig":"48"},{"size":173,"mtime":1631781150929,"results":"75","hashOfConfig":"48"},{"size":1642,"mtime":1631781167579,"results":"76","hashOfConfig":"48"},{"size":1325,"mtime":1631781909159,"results":"77","hashOfConfig":"48"},{"size":2263,"mtime":1631781963681,"results":"78","hashOfConfig":"48"},{"size":220,"mtime":1631781159576,"results":"79","hashOfConfig":"48"},{"size":1249,"mtime":1631781163012,"results":"80","hashOfConfig":"48"},{"size":79,"mtime":1631781186297,"results":"81","hashOfConfig":"48"},{"size":151,"mtime":1631781189275,"results":"82","hashOfConfig":"48"},{"size":290,"mtime":1631781191898,"results":"83","hashOfConfig":"48"},{"size":347,"mtime":1631781195117,"results":"84","hashOfConfig":"48"},{"size":358,"mtime":1631781181042,"results":"85","hashOfConfig":"48"},{"size":258,"mtime":1631781184123,"results":"86","hashOfConfig":"48"},{"size":0,"mtime":1628059377292,"results":"87","hashOfConfig":"48"},{"size":254,"mtime":1631781201828,"results":"88","hashOfConfig":"48"},{"size":1608,"mtime":1631867197455,"results":"89","hashOfConfig":"48"},{"size":1533,"mtime":1631867197460,"results":"90","hashOfConfig":"48"},{"size":959,"mtime":1631781218482,"results":"91","hashOfConfig":"48"},{"size":2237,"mtime":1631781221672,"results":"92","hashOfConfig":"48"},{"size":526,"mtime":1631781223949,"results":"93","hashOfConfig":"48"},{"filePath":"94","messages":"95","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"o1ppuz",{"filePath":"96","messages":"97","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"98","messages":"99","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"100","messages":"101","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"102","messages":"103","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"104","messages":"105","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"106","messages":"107","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"108","messages":"109","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"110","messages":"111","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"112","messages":"113","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"114","messages":"115","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"116","messages":"117","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"118","messages":"119","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"120","messages":"121","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"122","messages":"123","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"124","messages":"125","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"126","messages":"127","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"128","messages":"129","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"130","messages":"131","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"132","messages":"133","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"134","messages":"135","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"136","messages":"137","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"138","messages":"139","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"140","messages":"141","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"142","messages":"143","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"144","messages":"145","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"146","messages":"147","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"148","messages":"149","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"150","messages":"151","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"152","messages":"153","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"154","messages":"155","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"156","messages":"157","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"158","messages":"159","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"160","messages":"161","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"162","messages":"163","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"164","messages":"165","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"166","messages":"167","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"168","messages":"169","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"170","messages":"171","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"172","messages":"173","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"174","messages":"175","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"176","messages":"177","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"178","messages":"179","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"180","messages":"181","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"182","messages":"183","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"184","messages":"185","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"E:\\react脚手架\\react-project-tmp\\src\\App.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\common\\AppContext.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\common\\AppEvent.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\common\\Resolution.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\common\\TContext.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\components\\BaseComponent\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\components\\ErrorBoundary\\GlobalError\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\components\\ErrorBoundary\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\components\\ErrorBoundary\\ViewError\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\components\\NoMatch\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\components\\Page\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\components\\ResolutionCom\\ResolutionComp.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\pages\\Home\\HomeChild\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\pages\\Home\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\pages\\Page1\\index.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\router\\index.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\router\\RouterUI.tsx",[],"E:\\react脚手架\\react-project-tmp\\src\\services\\api\\index.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\services\\api\\user\\index.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\services\\axios.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\services\\BaseService.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\services\\ServerResponseManager.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\services\\serviceConfig.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\actionCreaters\\actionCreator.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\actionCreaters\\index.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\actionCreaters\\user.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\actions\\user.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\connect.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\index.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\reducers\\history.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\reducers\\index.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\store\\reducers\\user.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\types\\enum.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\types\\IContext.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\types\\IRedux.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\types\\IRouterPage.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\types\\service\\responseData.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\types\\service\\user.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\typings\\custom.d.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\typings\\global.d.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\util\\generateId.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\util\\handleUrlParams.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\util\\index.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\util\\stringUtil.ts",[],"E:\\react脚手架\\react-project-tmp\\src\\util\\variableTypeDetection.ts",[]] -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.html 2 | node_modules 3 | coverage 4 | dist 5 | env 6 | postcss.config.js 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const prettierOptions = JSON.parse(fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8')); 5 | 6 | module.exports = { 7 | root: true, 8 | parser: '@typescript-eslint/parser', 9 | // 使用 airbnb 拓展插件规范相关库 10 | // prettier 已内置了许多相关插件 11 | extends: ['airbnb-typescript', 'prettier'], 12 | // 拓展和支持相关能力的插件库 13 | plugins: ['prettier', 'react', 'react-hooks', 'jsx-a11y', '@typescript-eslint'], 14 | env: { 15 | //指定代码的运行环境 16 | jest: true, 17 | browser: true, 18 | node: true, 19 | es6: true, 20 | }, 21 | parserOptions: { 22 | //指定要使用的es版本 23 | ecmaVersion: 6, 24 | //向后兼容 25 | createDefaultProgram: true, 26 | sourceType: 'module', 27 | ecmaFeatures: { 28 | jsx: true, 29 | globalReturn: true, 30 | }, 31 | project: './tsconfig.eslint.json', 32 | // project: {}, 33 | }, 34 | settings: { 35 | 'import/parsers': { 36 | '@typescript-eslint/parser': ['.ts', '.tsx'], 37 | }, 38 | 'import/resolver': { 39 | webpack: { 40 | config: './webpack/webpack.base.js', 41 | }, 42 | typescript: { 43 | alwaysTryTypes: true, 44 | directory: './tsconfig.json', 45 | }, 46 | }, 47 | react: { 48 | version: 'detect', 49 | }, 50 | }, 51 | 52 | rules: { 53 | 'jsx-no-lambda': 0, 54 | semi: [2, 'always'], 55 | '@typescript-eslint/interface-name-prefix': 0, 56 | '@typescript-eslint/no-empty-interface': 0, 57 | 'object-shorthand': [0, 'never'], 58 | //单引号 59 | quotes: 'off', 60 | '@typescript-eslint/quotes': 'off', 61 | '@typescript-eslint/no-var-requires': 0, 62 | 'member-ordering': 0, 63 | 'object-literal-sort-keys': 0, 64 | 'no-shadowed-variable': 0, 65 | 'no-consecutive-blank-lines': 0, 66 | 'no-string-literal': 0, 67 | 'jsx-no-multiline-js': 0, 68 | 'jsx-boolean-value': 0, 69 | 'arrow-parens': 0, 70 | 'no-implicit-dependencies': 0, 71 | 'no-submodule-imports': 0, 72 | 'no-case-declarations': 1, 73 | '@typescript-eslint/no-empty-function': 0, 74 | '@typescript-eslint/indent': 'off', 75 | 'jsx-alignment': 0, 76 | 'jsx-wrap-multiline': 0, 77 | '@typescript-eslint/camelcase': 0, 78 | 'prettier/prettier': ['error', prettierOptions], 79 | 'arrow-body-style': [2, 'as-needed'], 80 | 'class-methods-use-this': 0, 81 | 'import/extensions': 'off', 82 | 'no-param-reassign': 1, 83 | 'jsx-a11y/aria-props': 2, 84 | 'jsx-a11y/heading-has-content': 0, 85 | 'jsx-a11y/label-has-associated-control': [ 86 | 2, 87 | { 88 | // NOTE: If this error triggers, either disable it or add 89 | // your custom components, labels and attributes via these options 90 | // See https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-associated-control.md 91 | controlComponents: ['Input'], 92 | }, 93 | ], 94 | 'jsx-a11y/label-has-for': 0, 95 | 'jsx-a11y/mouse-events-have-key-events': 2, 96 | 'jsx-a11y/role-has-required-aria-props': 2, 97 | 'jsx-a11y/role-supports-aria-props': 2, 98 | 'max-len': 0, 99 | 'newline-per-chained-call': 0, 100 | 'no-confusing-arrow': 0, 101 | 'no-console': 'off', 102 | 'no-use-before-define': 0, 103 | 'prefer-template': 2, 104 | 'react-hooks/rules-of-hooks': 'error', 105 | 'react/forbid-prop-types': 0, 106 | 'react/jsx-first-prop-new-line': [2, 'multiline'], 107 | //jsx 不允许显示js后缀 108 | 'react/jsx-filename-extension': 'off', 109 | // 如果属性值为 true, 可以直接省略 110 | 'react/jsx-boolean-value': 1, 111 | // 对于没有子元素的标签来说总是自己关闭标签 112 | 'react/self-closing-comp': 1, 113 | // 当在 render() 里使用事件处理方法时,提前在构造函数里把 this 绑定上去 114 | 'react/jsx-no-bind': 0, 115 | // React中函数的先后顺序 116 | 'react/sort-comp': 'off', 117 | // React组件名使用帕斯卡命名, 实例使用骆驼式命名 118 | 'react/jsx-pascal-case': 1, 119 | // didmount不使用setState 120 | 'react/no-did-mount-set-state': 0, 121 | // didupdate不使用setState 122 | 'react/no-did-update-set-state': 1, 123 | // 禁止使用嵌套的三目运算 124 | 'no-nested-ternary': 'off', 125 | // 解构赋值 126 | 'react/destructuring-assignment': [0, 'always'], 127 | // 属性验证 128 | 'react/prop-types': 'off', 129 | 130 | // 多余的依赖 131 | 'import/no-extraneous-dependencies': 'off', 132 | // jsx关闭位置 133 | 'react/jsx-closing-tag-location': 1, 134 | // 多行 135 | 'react/jsx-wrap-multilines': 'off', 136 | // 一行一个表达式 137 | 'react/jsx-one-expression-per-line': 'off', 138 | // will update不能使用setState 139 | 'react/no-will-update-set-state': 1, 140 | // setState 使用state 141 | 'react/no-access-state-in-setstate': 1, 142 | // button 需要类型 143 | 'react/button-has-type': 1, 144 | // jsx 参考: https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules 145 | // JSX属性值强制使用双引号 146 | 'jsx-quotes': 1, 147 | // 使用click必须有其他keybord事件 148 | 'jsx-a11y/click-events-have-key-events': 'off', 149 | // 150 | 'jsx-a11y/no-noninteractive-element-interactions': 'off', 151 | // alt 152 | 'jsx-a11y/alt-text': 2, 153 | // 交互的需要role 154 | 'jsx-a11y/no-static-element-interactions': 'off', 155 | // 锚无效 156 | 'jsx-a11y/anchor-is-valid': 'warn', 157 | 'jsx-a11y/anchor-has-content': 'warn', 158 | 'react/jsx-no-target-blank': 0, 159 | 'react/jsx-props-no-spreading': 0, 160 | 'react/jsx-uses-vars': 2, 161 | 'react/require-default-props': 0, 162 | 'react/require-extension': 0, 163 | 'require-yield': 0, 164 | 'space-before-function-paren': 0, 165 | indent: 'off', 166 | }, 167 | globals: { 168 | document: false, 169 | window: false, 170 | eruda: false, 171 | Stats: false, 172 | }, 173 | }; 174 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | # testing 4 | /coverage 5 | # production 6 | /dist 7 | eslintError.html 8 | .eslintcache 9 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # npx --no-install pretty-quick --staged 5 | npx --no-install lint-staged 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | package-lock.json 4 | yarn.lock 5 | # package.json 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid", 9 | "endOfLine": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 保存时格式化 3 | "editor.formatOnSave": true, 4 | // 相当于是 vscode 保存时调用的钩子事件 5 | "editor.codeActionsOnSave": { 6 | // 保存时使用 ESLint 修复可修复错误 7 | "source.fixAll.eslint": true 8 | }, 9 | // 针对对应文件设置默认格式化插件 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[javascriptreact]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[typescript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[typescriptreact]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to react-project-template 👋

2 |

3 | Version 4 | 5 | 6 | 7 | License: MIT 8 | 9 |

10 | 11 | > react 项目模板 12 | 13 | ### !!! 最新vite+router v6 + react 请点击 🏠 [Homepage](https://github.com/qyjandroid/react-hooks-vite) 14 | 15 | ### 🏠 [Homepage](https://github.com/qyjandroid/react-project-template) 16 | 17 | ## Prerequisites 18 | 19 | - npm >=6.14.6 20 | - node >=12.18.4 21 | 22 | ## Install 23 | 24 | ```sh 25 | yarn install 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```sh 31 | yarn run start 32 | ``` 33 | 34 | ## 项目目录结构 35 | ```markdown 36 | react-project-template 37 | ├── .babelrc # babel配置 38 | ├── Webpack # webpack公用配置目录 39 | │ │ ├──plugins # 公用插件集合 40 | │ │ ├──resolve # webpack resolve配置 41 | │ │ ├──utils # webpack 工具类 42 | │ │ ├──variable # webpack 变量配置 43 | │ ├── webpack.base.js # Webpack 基础配置文件 44 | │ ├── webpack.dev.js # Webpack 开发环境配置文件 45 | │ └── webpack.prod.js # Webpack 生产环境配置文件 46 | ├── yarn.lock # 锁定 npm 包依赖版本文件 47 | ├── package.json 48 | ├── postcss.config.js # 自动兼容 CSS3 样式配置文件 49 | ├── .editorconfig # IDE格式化规范 50 | ├── .eslintignore # eslint忽略文件配置 51 | ├── .eslintrc.js # eslint配置文件 52 | ├── .prettierignore # prettierc忽略文件配置 53 | ├── .prettierrc # prettierc配置文件 54 | ├── .husky # 配置git提交钩子 55 | ├── .commitlint.config.js # 配置git提交规范 56 | ├── tsconfig.eslint.js # eslint检查typescript配置项配置文件 57 | ├── eslintError.html # eslint报告文件 58 | ├── public # 存放html模板 59 | ├── README.md 60 | ├── src 61 | │ ├── assets # 存放会被 Webpack 处理的静态资源文件:一般是自己写的 js、css 或者图片等静态资源 62 | │ │ ├── fonts # iconfont 目录 63 | │ │ ├── images # 图片资源目录 64 | │ │ ├── css # 全局样式目录 65 | │ │ │ ├── common.scss # 全局通用样式目录 66 | │ │ │ ├── core.scss # 全局sass 变量目录,直接使用,不需要引用,全局已统一引入。 67 | │ │ │ └── init.scss # 全局初始化css 68 | │ │ └── js # 全局js 69 | │ ├── common # 存放项目通用文件 70 | │ │ ├── Resolution.ts # 布局适配配置中心 71 | │ │ └── AppContext.ts # 全局App上下文 72 | │ ├── components # 项目中通用的业务组件目录 73 | │ ├── config # 项目配置文件 74 | │ ├── pages # 项目页面目录 75 | │ ├── typings # 项目中d.ts 声明文件目录 76 | │ ├── types # 项目中声明文件 77 | │ │ ├── service # 项目中服务相关声明文件 78 | │ │ ├── enum.ts # 项目中枚举类型 79 | │ │ ├── IContext.ts # 全局App上下文声明 80 | │ │ ├── IRedux.ts # redux相关声明 81 | │ │ └── IRouterPage.ts # 路由相关声明 82 | │ ├── uiLibrary # 组件库 83 | │ ├── routes # 路由目录 84 | │ │ ├── index.tsx # 路由配置入口文件 85 | │ │ └── RouterUI.tsx # 路由转换 86 | │ ├── services # 和后端相关的文件目录 87 | │ │ ├── api # 调用后端接口定义目录 88 | │ │ │ ├── index.ts 89 | │ │ ├── axios.ts # 基于 axios 二次封装 90 | │ │ ├── BaseService.ts # 基础请求服务类型 91 | │ │ ├── ServerResponseManager.ts # 服务返回统一管理 92 | │ │ ├── serviceConfig.ts # 服务地址配置文件 93 | │ ├── store # redux 仓库 94 | │ │ ├── actionCreaters # action创建与分发绑定 95 | │ │ ├── action # 项目中action 96 | │ │ ├── reducers # 项目中reducers 97 | │ │ │ ├──history # 项目中路由相关history 98 | │ │ ├── index.ts # 全局 store 获取 99 | │ │ ├── connect.ts # react 页面与store 连接 100 | │ ├── utils # 全局通用工具函数目录 101 | │ ├── App.tsx # App全局 102 | │ ├── index.tsx # 项目入口文件 103 | │ ├── index.scss # 项目入口引入的scss 104 | └── tsconfig.json # TS 配置文件 105 | ``` 106 | 107 | ## Tips 108 | 109 | - 项目中引入了 `core.scss `,直接使用,不需要@import 110 | 111 | - 构建项目时会自动兼容 CSS3 样式,所以不需要自己去写浏览器兼容样式 112 | 113 | - 项目支持配置式路由 114 | 115 | - 项目中集成了 `connected-react-router` ,路由存储在store中,界面直接从store获取 116 | 117 | - 项目中默认配置了一些常用工具函数 118 | 119 | - 项目中针对 `axios` 做了二次封装 120 | 121 | - 项目直接使用px即可 122 | 123 | - 项目大量使用装饰器,来简化代码 124 | 125 | 126 | ## 脚手架结合使用 127 | 128 | - 需要使用该项目模板,可以通过 [quanyj-react-cli](https://github.com/qyjandroid/react-cli) 脚手架获取 129 | - Install 130 | ```js 131 | npm install quanyj-react-cli -g 132 | ``` 133 | - create project 134 | ```js 135 | react-cli create 项目名称 -f 136 | ``` 137 | 138 | 139 | ## TodoList 140 | - [x] eslint 141 | - [x] commit 提交检查 142 | - [ ] webpack 编写Ts语法 143 | - [ ] Vite 支持 144 | - [ ] yml 文件 (CI/CD) 145 | 146 | 147 | ## Author 148 | 149 | 👤 **quanyj ** 150 | 151 | * 掘金: https://juejin.cn/user/923245496789255 152 | * Github: [@qyjandroid](https://github.com/qyjandroid) 153 | 154 | ## 🤝 Contributing 155 | 156 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/qyjandroid/react-project-template/issues). 157 | 158 | ## Show your support 159 | 160 | Give a ⭐️ if this project helped you! 161 | 162 | *** 163 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ 164 | 165 | 166 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | var isDev = false; 2 | if (process.env.NODE_ENV === 'dev') { 3 | isDev = true; 4 | } 5 | 6 | module.exports = function (api) { 7 | api.cache(true); 8 | const presets = [ 9 | [ 10 | '@babel/preset-react', 11 | { 12 | development: isDev, 13 | }, 14 | ], 15 | [ 16 | '@babel/preset-env', 17 | { 18 | targets: { 19 | browsers: ['>0.25%', 'not ie 11', 'not op_mini all'], 20 | }, 21 | }, 22 | ], 23 | [ 24 | '@babel/preset-typescript', 25 | { 26 | isTSX: true, 27 | allExtensions: true, 28 | }, 29 | ], 30 | ]; 31 | const plugins = [ 32 | [ 33 | '@babel/plugin-proposal-decorators', 34 | { 35 | legacy: true, 36 | }, 37 | ], 38 | [ 39 | '@babel/plugin-transform-runtime', 40 | { 41 | corejs: 3, 42 | regenerator: true, 43 | }, 44 | ], 45 | ]; 46 | 47 | // if (isDev) { 48 | // plugins.push([ 49 | // 'react-refresh/babel', 50 | // { 51 | // skipEnvCheck: true, 52 | // }, 53 | // ]).filter(Boolean); 54 | // } 55 | 56 | return { 57 | // 这个不设置的话,webpack 魔法注释会被删除,魔法注释用于分包 58 | comments: true, 59 | presets, 60 | plugins, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // git commit 规范 2 | // <类型>[可选的作用域]: <描述> 3 | //git commit -m 'feat: 增加 xxx 功能' 4 | //git commit -m 'bug: 修复 xxx 功能' 5 | // # 主要type 6 | // feat: 增加新功能 7 | // fix: 修复bug 8 | //build: 主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交 9 | //ci: 主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交 10 | //docs: 文档更新 11 | //perf: 性能,体验优化 12 | //refactor: 代码重构时使用 13 | // style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号 14 | // refactor: 代码重构时使用 15 | // revert: 执行git revert打印的message 16 | //chore: 不属于以上类型的其他类型 17 | // test: 添加测试或者修改现有测试 18 | 19 | module.exports = { 20 | extends: ['@commitlint/config-conventional'], 21 | }; 22 | -------------------------------------------------------------------------------- /env/dev.env: -------------------------------------------------------------------------------- 1 | ENV=dev 2 | CDN_ROOT= 3 | C_API_SERVER= -------------------------------------------------------------------------------- /env/prod.env: -------------------------------------------------------------------------------- 1 | ENV=prod 2 | CDN_ROOT=https://xxx.com 3 | C_API_SERVER= 4 | -------------------------------------------------------------------------------- /env/test.env: -------------------------------------------------------------------------------- 1 | ENV=test 2 | CDN_ROOT= 3 | C_API_SERVER= -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-project-template", 3 | "version": "1.0.0", 4 | "description": "react 项目模板", 5 | "main": "src/index.tsx", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com.cnpmjs.org/qyjandroid/react-project-template.git" 9 | }, 10 | "author": "quanyj ", 11 | "license": "MIT", 12 | "scripts": { 13 | "start": "cross-env NODE_ENV=dev webpack serve --config webpack/webpack.dev.js", 14 | "build:test": "cross-env NODE_ENV=test webpack --config webpack/webpack.dev.js", 15 | "build:prod": "cross-env NODE_ENV=prod webpack --config webpack/webpack.prod.js", 16 | "type-check": "tsc --watch", 17 | "lint": "eslint src/**/*.{ts,tsx} -f html --cache -o ./eslintError.html", 18 | "lint:fix": "eslint src/**/*.{ts,tsx} --fix", 19 | "prepare": "husky install" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/qyjandroid/react-project-template/issues" 23 | }, 24 | "dependencies": { 25 | "@babel/runtime-corejs3": "^7.15.4", 26 | "axios": "^0.21.1", 27 | "connected-react-router": "^6.9.1", 28 | "events": "^3.3.0", 29 | "immer": "^9.0.5", 30 | "lodash": "^4.17.21", 31 | "react": "^17.0.2", 32 | "react-dom": "^17.0.2", 33 | "react-modal": "^3.14.3", 34 | "react-redux": "^7.2.4", 35 | "react-router-dom": "^5.2.0", 36 | "redux": "^4.1.1", 37 | "redux-devtools-extension": "^2.13.9", 38 | "typescript": "^4.3.5" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.14.8", 42 | "@babel/core": "^7.14.8", 43 | "@babel/plugin-proposal-decorators": "^7.14.5", 44 | "@babel/plugin-transform-react-jsx-source": "^7.14.5", 45 | "@babel/plugin-transform-runtime": "^7.15.0", 46 | "@babel/preset-env": "^7.14.8", 47 | "@babel/preset-react": "^7.14.5", 48 | "@babel/preset-typescript": "^7.14.5", 49 | "@commitlint/cli": "^13.1.0", 50 | "@commitlint/config-conventional": "^13.1.0", 51 | "@types/lodash": "^4.14.172", 52 | "@types/react": "^17.0.15", 53 | "@types/react-dom": "^17.0.9", 54 | "@types/react-router-dom": "^5.1.8", 55 | "@typescript-eslint/eslint-plugin": "^4.31.1", 56 | "@typescript-eslint/parser": "^4.31.1", 57 | "babel-loader": "^8.2.2", 58 | "clean-webpack-plugin": "^4.0.0-alpha.0", 59 | "copy-webpack-plugin": "^9.0.1", 60 | "cross-env": "^7.0.3", 61 | "css-loader": "^6.2.0", 62 | "dotenv": "^10.0.0", 63 | "dotenv-webpack": "^7.0.3", 64 | "eslint": "^7.32.0", 65 | "eslint-config-airbnb-base": "^14.2.1", 66 | "eslint-config-airbnb-typescript": "^14.0.0", 67 | "eslint-config-prettier": "^8.3.0", 68 | "eslint-plugin-import": "^2.24.2", 69 | "eslint-plugin-jsx-a11y": "^6.4.1", 70 | "eslint-plugin-prettier": "^4.0.0", 71 | "eslint-plugin-react": "^7.25.1", 72 | "eslint-plugin-react-hooks": "^4.2.0", 73 | "html-webpack-plugin": "^5.3.2", 74 | "husky": "^7.0.2", 75 | "lint-staged": "^11.1.2", 76 | "mini-css-extract-plugin": "^2.1.0", 77 | "postcss": "8.3.6", 78 | "postcss-flexbugs-fixes": "5.0.2", 79 | "postcss-import": "14.0.2", 80 | "postcss-loader": "6.1.1", 81 | "postcss-preset-env": "7.8.0", 82 | "postcss-pxtorem": "6.0.0", 83 | "prettier": "^2.4.0", 84 | "sass": "^1.55.0", 85 | "sass-loader": "^12.6.0", 86 | "style-loader": "^3.2.1", 87 | "style-resources-loader": "^1.4.1", 88 | "thread-loader": "^3.0.4", 89 | "webpack": "^5.74.0", 90 | "webpack-cli": "^4.10.0", 91 | "webpack-dev-server": "^3.11.3", 92 | "webpack-merge": "^5.8.0" 93 | }, 94 | "browserslist": { 95 | "development": [ 96 | "last 1 chrome version", 97 | "last 1 firefox version", 98 | "last 1 safari version" 99 | ], 100 | "production": [ 101 | "ie > 10", 102 | ">1%", 103 | "not dead", 104 | "not op_mini all" 105 | ] 106 | }, 107 | "lint-staged": { 108 | "*.{ts,tsx}": [ 109 | "yarn run lint", 110 | "git add --force" 111 | ], 112 | "*.{md,json}": [ 113 | "git add --force" 114 | ] 115 | }, 116 | "engines": { 117 | "npm": ">=6.14.6", 118 | "node": ">=12.18.4" 119 | }, 120 | "peerDependencies": { 121 | "eslint-config-airbnb-base": "^14.2.1" 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-flexbugs-fixes'), 4 | require('postcss-import'), 5 | require('postcss-preset-env'), 6 | require('postcss-pxtorem')({ 7 | rootValue: 100, 8 | unitPrecision: 5, 9 | minPixelValue: 2, // 设置要替换的最小像素值 10 | propWhiteList: ['*'], // Enables converting of all properties – default is just font sizes. 11 | selectorBlackList: ['.ig-'], // 忽略的选择器 .ig- 表示 .ig- 开头的都不会转换 12 | }), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import routerConfig from './router/index'; 3 | import RouterUI from '@/router/RouterUI'; 4 | import AppContext from './common/AppContext'; 5 | import { IAppContext } from '@/types/IContext'; 6 | 7 | document.title = 'React App'; 8 | 9 | class App extends React.Component { 10 | private a = 2; 11 | 12 | getLiveContextValue = (): IAppContext => ({ 13 | token: '123', 14 | uid: 1233, 15 | test: this.test, 16 | }); 17 | 18 | test = () => { 19 | console.log('aa=2=', this.a); 20 | }; 21 | 22 | render() { 23 | console.log('abc331'); 24 | return ( 25 | <> 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /src/assets/css/common.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/css/common.scss -------------------------------------------------------------------------------- /src/assets/css/core.scss: -------------------------------------------------------------------------------- 1 | //注意,此文件只能存放变量,不允许写css样式,此文件为全局引入,写入css样式会出现多次打包问题。 2 | 3 | $ani: all 0.3s cubic-bezier(0.445, 0.05, 0.55, 0.95); 4 | 5 | @mixin absolute($top: 0, $right: 0, $bottom: 0, $left: 0) { 6 | position: absolute; 7 | top: $top; 8 | right: $right; 9 | left: $left; 10 | bottom: $bottom; 11 | } 12 | 13 | @mixin wh($w: 100%, $h: 100%) { 14 | width: $w; 15 | height: $h; 16 | } 17 | 18 | @mixin relative($w, $h) { 19 | position: relative; 20 | @include wh($w, $h); 21 | } 22 | @mixin ellipsis() { 23 | overflow: hidden; 24 | text-overflow: ellipsis; //文本溢出显示省略号 25 | white-space: nowrap; //文本不会换行(单行文本溢出) 26 | } 27 | 28 | @mixin ani($name: $ani) { 29 | transition: $name; 30 | } 31 | 32 | @mixin activeBg($url, $isDisabled: false, $size: 100% 100%, $repeat: no-repeat, $suffix: ".png", $pre: "~@images/") { 33 | background: url($pre + $url + $suffix) $repeat; 34 | background-size: $size; 35 | opacity: 1; 36 | &:hover { 37 | background: url($pre + $url + "-hover" + $suffix) $repeat; 38 | background-size: $size; 39 | opacity: 1; 40 | } 41 | &:active { 42 | background: url($pre + $url + "-active" + $suffix) $repeat; 43 | background-size: $size; 44 | opacity: 0.8; 45 | } 46 | @if $isDisabled { 47 | &.disabled { 48 | background: url($pre + $url + "-disabled" + $suffix) $repeat; 49 | background-size: $size; 50 | &:hover { 51 | background: url($pre + $url + "-disabled" + $suffix) $repeat; 52 | background-size: $size; 53 | } 54 | &:active { 55 | background: url($pre + $url + "-disabled" + $suffix) $repeat; 56 | background-size: $size; 57 | } 58 | } 59 | } 60 | } 61 | 62 | @mixin bgFill($url) { 63 | background: url("~@images/" + $url) no-repeat center; 64 | background-size: 100% 100%; 65 | } 66 | 67 | @mixin verticalAlign($width: 0, $height: 0, $border: 0, $fontSize: 0.1rem) { 68 | width: 2 * $width; 69 | height: 2 * $height; 70 | border-width: 2 * $border; 71 | font-size: 2 * $fontSize; 72 | line-height: 2 * $height; 73 | transform: scale(0.5, 0.5); 74 | transform-origin: center; 75 | } 76 | 77 | @mixin flex() { 78 | display: flex; 79 | flex-direction: row; 80 | } 81 | @mixin flex-col() { 82 | display: flex; 83 | flex-direction: column; 84 | } 85 | -------------------------------------------------------------------------------- /src/assets/css/index.scss: -------------------------------------------------------------------------------- 1 | @import "./common.scss"; 2 | @import "./init.scss"; 3 | 4 | .text1 { 5 | font-size: 30px; 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/css/init.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | position: relative; 4 | margin: 0; 5 | font-family: "微软雅黑", "思源黑体", "黑体"; 6 | -webkit-user-drag: none; 7 | user-select: none; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | h1 { 16 | font-size: 2em; 17 | margin: 0.67em 0; 18 | } 19 | 20 | ul, 21 | li { 22 | list-style: none; 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | a { 28 | color: #fff; 29 | background-color: transparent; 30 | } 31 | 32 | #root { 33 | position: relative; 34 | width: 100%; 35 | height: 100%; 36 | font-size: 25px; 37 | } 38 | 39 | ::-webkit-scrollbar { 40 | width: 6px; 41 | height: 10px; 42 | } 43 | ::-webkit-scrollbar-thumb { 44 | border-radius: 5px; 45 | box-shadow: inset 0 0 0.05rem rgba(69, 68, 73, 0.4); 46 | background: #82adda; 47 | } 48 | ::-webkit-scrollbar-track { 49 | box-shadow: inset 0 0 0.05rem rgba(69, 68, 73, 0.4); 50 | border-radius: 0; 51 | background: #0e2e6a; 52 | } 53 | -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/fonts/iconfont.eot -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by iconfont 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /src/assets/images/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/images/01.png -------------------------------------------------------------------------------- /src/assets/images/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/images/02.png -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/images/404.png -------------------------------------------------------------------------------- /src/assets/images/tip-pop-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/assets/images/tip-pop-bg.png -------------------------------------------------------------------------------- /src/assets/js/a.js: -------------------------------------------------------------------------------- 1 | const a=1; -------------------------------------------------------------------------------- /src/common/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentClass } from 'react'; 2 | import BaseComponent from '@/components/BaseComponent'; 3 | import TContext from './TContext'; 4 | import { IAppContext } from '@/types/IContext'; 5 | 6 | const AppContext = TContext({} as any); 7 | 8 | export default AppContext; 9 | 10 | export const { Consumer } = AppContext; 11 | 12 | export const { Provider } = AppContext; 13 | 14 | export function withAppContext(Com: React.ComponentType) { 15 | return class

extends BaseComponent { 16 | render() { 17 | const { props } = this; 18 | return ( 19 | {context => } 20 | ); 21 | } 22 | }; 23 | } 24 | 25 | export type ComponentDecorator

= >(WrappedComponent: T) => T; 26 | 27 | // export const withRouter: ComponentDecorator = nativeWithRouter as any 28 | 29 | export const withAppContextDecorators: ComponentDecorator = withAppContext as any; 30 | -------------------------------------------------------------------------------- /src/common/AppEvent.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | const PREFIX = 'app_'; 4 | 5 | export function getAppEventType(type: string) { 6 | return `${PREFIX}${type}`; 7 | } 8 | 9 | /** 10 | * 11 | * app全局事件触发 12 | * @class AppEventEmitterUtils 13 | */ 14 | class AppEventEmitterUtils { 15 | private emitter: EventEmitter; 16 | 17 | constructor() { 18 | this.emitter = new EventEmitter(); 19 | } 20 | 21 | off = (type, cb) => this.emitter.off(getAppEventType(type), cb); 22 | 23 | emit = (type, data?) => this.emitter.emit(getAppEventType(type), data); 24 | 25 | on = (type, cb) => this.emitter.on(getAppEventType(type), cb); 26 | 27 | destroy() { 28 | if (this.emitter) { 29 | // 移除全部监听 30 | this.emitter.removeAllListeners(); 31 | } 32 | } 33 | } 34 | const appEventEmitter = new AppEventEmitterUtils(); 35 | export default appEventEmitter; 36 | 37 | export enum AppEventType { 38 | InvalidToken = 'invalid-token', 39 | } 40 | -------------------------------------------------------------------------------- /src/common/Resolution.ts: -------------------------------------------------------------------------------- 1 | import { DeviceType } from '@/types/enum'; 2 | 3 | export default class Resolution { 4 | static DesignScreen = { 5 | [DeviceType.PC]: { 6 | WIDTH: 845, 7 | RATIO: 100, 8 | }, 9 | [DeviceType.MOBILE]: { 10 | WIDTH: 750, 11 | RATIO: 100, 12 | }, 13 | [DeviceType.TV]: { 14 | WIDTH: 1920, 15 | RATIO: 100, 16 | }, 17 | }; 18 | 19 | /** 20 | * 获取设计分辨率 21 | */ 22 | static getDesign(deviceType: DeviceType = DeviceType.MOBILE) { 23 | return Resolution.DesignScreen[deviceType]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/TContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function (initValue: T) { 4 | const context: React.Context = React.createContext(initValue); 5 | return context; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/BaseComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import produce from 'immer'; 3 | 4 | /** 5 | * 6 | * 7 | * @class BaseComponent 8 | * @extends {React.Component} 9 | * @template P 10 | * @template S 11 | */ 12 | 13 | /** 14 | * UI基础组件 15 | */ 16 | export default class BaseComponent

extends React.PureComponent { 17 | constructor(props) { 18 | super(props); 19 | console.log('BaseComponent'); 20 | } 21 | 22 | /** 23 | * 重写setState方法 24 | * @param obj 25 | * @param callBack 26 | */ 27 | setState( 28 | obj: 29 | | ((prevState: Readonly, props: Readonly

) => Pick | S | null) 30 | | (Pick | S | null), 31 | callBack?: () => void, 32 | ): void { 33 | let fun; 34 | if (obj.constructor === Object) { 35 | fun = draft => { 36 | const keys = Object.keys(obj); 37 | // eslint-disable-next-line 38 | keys.map(item => { 39 | // eslint-disable-next-line 40 | draft[item] = obj[item]; 41 | }); 42 | }; 43 | } else if (obj.constructor === Function) { 44 | fun = obj; 45 | } 46 | 47 | super.setState(produce(fun), () => { 48 | if (callBack) { 49 | callBack(); 50 | } 51 | }); 52 | } 53 | 54 | componentDidCatch(error, info) { 55 | console.log('错误信息==', this.constructor.name, '=组件出错:', error, info); 56 | // 你同样可以将错误日志上报给服务器 57 | return false; 58 | } 59 | 60 | render() { 61 | return

; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Button/ButtonCheck.ts: -------------------------------------------------------------------------------- 1 | /** 2 | *按钮点击间隔检测 3 | * 4 | * @export 5 | * @class Check 6 | */ 7 | class ButtonCheck { 8 | private timeDelay: number; 9 | 10 | private time: number; 11 | 12 | constructor(timeDelay = 300) { 13 | this.timeDelay = timeDelay; 14 | this.time = null; 15 | } 16 | 17 | setTimeDelay(timeDelay = 300) { 18 | this.timeDelay = timeDelay; 19 | } 20 | 21 | check() { 22 | const curTime = new Date().valueOf(); 23 | if (this.time) { 24 | // 如果按钮检测 25 | if (curTime - this.time <= this.timeDelay) { 26 | return false; 27 | } 28 | } 29 | this.time = curTime; 30 | return true; 31 | } 32 | } 33 | 34 | export default ButtonCheck; 35 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from '../BaseComponent/index'; 3 | import ButtonCheck from './ButtonCheck'; 4 | import { setTimeout, classNames, isMobile } from '@/util/index'; 5 | 6 | interface IProps { 7 | disabled?: boolean; 8 | className: string; 9 | statId?: number; 10 | clickId?: string; 11 | style?: any; 12 | onClick?: Function; 13 | statMap?: any; 14 | children?: any; 15 | onMouseLeave?: any; 16 | onMouseEnter?: any; 17 | delayTime?: number; 18 | stopPropagation?: boolean; 19 | } 20 | export default class Button extends BaseComponent { 21 | private curTimeout: any; 22 | 23 | private timeout: any; 24 | 25 | private buttonCheck; 26 | 27 | private isMob; 28 | 29 | static defaultProps = { 30 | disabled: false, 31 | }; 32 | 33 | state = { 34 | clicked: false, 35 | }; 36 | 37 | constructor(props) { 38 | super(props); 39 | this.buttonCheck = new ButtonCheck(props.delayTime); 40 | this.isMob = isMobile(); 41 | } 42 | 43 | onItemClick = (e: any) => { 44 | const { stopPropagation, disabled, delayTime, onClick } = this.props; 45 | if (stopPropagation) { 46 | e.nativeEvent.stopImmediatePropagation(); 47 | e.stopPropagation(); 48 | e.preventDefault(); 49 | } 50 | if (!this.buttonCheck.check()) return; 51 | try { 52 | if (disabled === true) return; 53 | this.curTimeout = setTimeout(() => { 54 | this.setState({ clicked: true }); 55 | }, 10); 56 | if (this.timeout) { 57 | this.timeout(); 58 | } 59 | this.timeout = setTimeout(() => { 60 | this.setState({ clicked: false }); 61 | }, delayTime || 200); 62 | if (onClick) { 63 | onClick(e); 64 | } 65 | } catch (error) { 66 | console.log(error); 67 | } 68 | }; 69 | 70 | componentWillUnmount = () => { 71 | if (this.curTimeout) { 72 | this.curTimeout(); 73 | } 74 | if (this.timeout) { 75 | this.timeout(); 76 | } 77 | }; 78 | 79 | render() { 80 | const { className, style, statId, clickId, disabled, ...other } = this.props; 81 | const { clicked } = this.state; 82 | const styles = disabled || this.isMob ? style : { cursor: 'pointer', ...style }; 83 | return ( 84 |
90 | {this.props.children} 91 |
92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/DropDownMenu/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/components/DropDownMenu/index.scss -------------------------------------------------------------------------------- /src/components/DropDownMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseComponent from '../BaseComponent'; 3 | import { isContains, addEventListenerWrap } from './popUtils'; 4 | import './index.scss'; 5 | import Button from '../Button'; 6 | 7 | interface Props { 8 | /** 9 | * 按钮 10 | * @memberOf Props 11 | */ 12 | renderBtnView: () => JSX.Element; 13 | 14 | /** 15 | * 16 | * 17 | * 弹窗内容 18 | * @memberOf Props 19 | */ 20 | renderPopContentView: () => JSX.Element; 21 | 22 | /** 23 | * 24 | * 弹窗根节点范围 25 | * @memberOf Props 26 | */ 27 | getRootDomNode?: () => HTMLElement; 28 | 29 | /** 30 | * 样式 31 | * @type {string} 32 | * @memberOf Props 33 | */ 34 | className?: string; 35 | } 36 | 37 | interface State { 38 | /** 39 | * 40 | * 是否显示pop 41 | * @type {boolean} 42 | * @memberOf State 43 | */ 44 | showPop: boolean; 45 | } 46 | 47 | /** 48 | * 49 | * 更多-下拉菜单弹窗 50 | * @export 51 | * @class TipPop 52 | * @extends {BaseComponent} 53 | */ 54 | export default class DropDownMenu extends BaseComponent { 55 | private domListener = null; 56 | 57 | private popupRef = null; 58 | 59 | constructor(props) { 60 | super(props); 61 | this.state = { 62 | showPop: false, 63 | }; 64 | this.popupRef = React.createRef(); 65 | } 66 | 67 | componentDidUpdate(_privProps, prevState) { 68 | if (!prevState.showPop && this.state.showPop) { 69 | //弹窗状态发生改变,从隐藏到显示,添加监听器 70 | this.setListener(); 71 | } else if (prevState.showPop && !this.state.showPop) { 72 | ////弹窗状态发生改变,从隐藏到显示,取消监听器 73 | this.cancelListener(); 74 | } 75 | } 76 | 77 | /** 78 | * 79 | * 80 | * 设置监听 81 | * @memberOf DropDownMenu 82 | */ 83 | setListener = () => { 84 | //获取根节点 85 | const rootDom = this.getRootDomNode(); 86 | //默认取消一次监听 87 | this.cancelListener(); 88 | this.domListener = addEventListenerWrap( 89 | rootDom, 90 | 'click', 91 | event => { 92 | const { target } = event; 93 | const root = this.getRootDomNode(); 94 | const popupNode = this.getPopupDomNode(); 95 | //判断是根节点框中的点击事件,并且不是弹窗区域,为任意点击消失区域。 96 | if (isContains(root, target) && !isContains(popupNode, target)) { 97 | console.log('直接关闭===', target, isContains(popupNode, target)); 98 | //直接关闭 99 | this.hidePop(); 100 | } 101 | }, 102 | true, 103 | ); 104 | }; 105 | 106 | /** 107 | * 108 | * 109 | * 取消监听 110 | * @memberOf DropDownMenu 111 | */ 112 | cancelListener = () => { 113 | if (this.domListener) { 114 | this.domListener?.remove(); 115 | this.domListener = null; 116 | } 117 | }; 118 | 119 | /** 120 | * 121 | * 获取pop弹窗节点 122 | * @returns 123 | * 124 | * @memberOf DropDownMenu 125 | */ 126 | getPopupDomNode() { 127 | return this.popupRef.current || null; 128 | } 129 | 130 | /** 131 | * 132 | * 133 | * 获取默认根节点 134 | * @memberOf DropDownMenu 135 | */ 136 | getRootDomNode = (): HTMLElement => { 137 | const { getRootDomNode } = this.props; 138 | if (getRootDomNode) { 139 | return getRootDomNode(); 140 | } 141 | return window.document.body; 142 | }; 143 | 144 | /** 145 | * 146 | * 147 | * 显示弹窗 148 | * @memberOf DropDownMenu 149 | */ 150 | showPop = () => { 151 | const { showPop } = this.state; 152 | console.log('点击===', showPop); 153 | //这里弹窗打开,再次点击按钮,可以关闭弹窗 154 | if (showPop) { 155 | this.setState({ 156 | showPop: false, 157 | }); 158 | return; 159 | } 160 | 161 | this.setState({ 162 | showPop: true, 163 | }); 164 | }; 165 | 166 | /** 167 | * 168 | * 169 | * 隐藏弹窗 170 | * @memberOf DropDownMenu 171 | */ 172 | hidePop = () => { 173 | this.setState({ 174 | showPop: false, 175 | }); 176 | }; 177 | 178 | render() { 179 | const { className } = this.props; 180 | const { showPop } = this.state; 181 | return ( 182 |
183 | 186 | {showPop ? ( 187 |
{this.props.renderPopContentView()}
188 | ) : null} 189 |
190 | ); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/components/DropDownMenu/popUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 判断是否包含 4 | * @export 5 | * @param {(Node | null | undefined)} root 6 | * @param {Node} [n] 7 | * @returns 8 | */ 9 | export function isContains(root: Node | null | undefined, n?: Node) { 10 | if (!root) { 11 | return false; 12 | } 13 | 14 | return root.contains(n); 15 | } 16 | 17 | /** 18 | * 19 | * 添加监听 20 | * @export 21 | * @param {any} target 22 | * @param {any} eventType 23 | * @param {any} cb 24 | * @param {any} [option] 25 | * @returns 26 | */ 27 | export function addEventListenerWrap(target, eventType, cb, option?) { 28 | if (target.addEventListener) { 29 | target.addEventListener(eventType, cb, option); 30 | } 31 | return { 32 | remove: () => { 33 | if (target.removeEventListener) { 34 | target.removeEventListener(eventType, cb); 35 | } 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/GlobalError/index.scss: -------------------------------------------------------------------------------- 1 | .error-custom-modal-style { 2 | background-color: #1766bf; 3 | z-index: 9999; 4 | background-size: 100% 100%; 5 | } 6 | .error-custom-style { 7 | height: 100%; 8 | width: 100%; 9 | background-size: 100% 100%; 10 | } 11 | 12 | .error-boundary-modal-overlay { 13 | position: absolute; 14 | top: 0; 15 | width: 100%; 16 | bottom: 0; 17 | } 18 | 19 | .error-boundary-modal-overlay::before { 20 | width: 100%; 21 | height: 100%; 22 | content: ""; 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | z-index: -1; 27 | } 28 | 29 | /* 禁止图片拖拽*/ 30 | .error-boundary-modal-overlay { 31 | position: absolute; 32 | top: 0; 33 | width: 100%; 34 | bottom: 0; 35 | &::before { 36 | // background: rgba(0, 0, 0, 0.4); 37 | // background-size: 100% 100%; 38 | width: 100%; 39 | height: 100%; 40 | content: ""; 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | z-index: -1; 45 | } 46 | } 47 | /* Error Boundary */ 48 | .error-boundary { 49 | font-size: 30px; 50 | position: absolute; 51 | top: 50%; 52 | left: 50%; 53 | transform: translate(-50%, -50%); 54 | width: 100%; 55 | text-align: center; 56 | color: #fff; 57 | &:focus { 58 | outline: none; 59 | } 60 | .reload { 61 | margin-left: 10px; 62 | color: #9810fd; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/GlobalError/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { classNames } from '@/util/index'; 3 | import Modal from 'react-modal'; 4 | import './index.scss'; 5 | 6 | export interface IGlobalErrorProps { 7 | message?: string; 8 | isDialog?: boolean; 9 | } 10 | 11 | const onReload = (e: React.MouseEvent) => { 12 | e.preventDefault(); 13 | window.location.href = window.location.href.split('#')[0]; 14 | }; 15 | 16 | const GlobalError: React.FC = ({ message, isDialog }) => { 17 | if (isDialog === false) { 18 | return ( 19 |
20 |
21 | {message} 22 | 23 | 刷新 24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | return ( 31 | 37 |
38 | {message} 39 | 40 | 刷新 41 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | GlobalError.defaultProps = { 48 | message: '页面崩溃啦!', 49 | }; 50 | 51 | export default GlobalError; 52 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/ViewError/index.scss: -------------------------------------------------------------------------------- 1 | .error-custom-style-page { 2 | position: relative; 3 | height: 100%; 4 | width: 100%; 5 | font-size: 20px; 6 | .error-custom-style-mantle-page { 7 | position: relative; 8 | width: 100%; 9 | height: 100%; 10 | .error-boundary-page { 11 | width: 100%; 12 | position: absolute; 13 | top: 50%; 14 | left: 50%; 15 | transform: translate(-50%, -50%); 16 | width: 100%; 17 | text-align: center; 18 | color: #fff; 19 | &:focus { 20 | outline: none; 21 | } 22 | .reload-page { 23 | margin-left: 10px; 24 | color: #9810fd; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/ViewError/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.scss'; 3 | 4 | export interface IViewErrorProps { 5 | message?: string; 6 | } 7 | 8 | const onReload = (e: React.MouseEvent) => { 9 | e.preventDefault(); 10 | window.location.href = window.location.href.split('#')[0]; 11 | }; 12 | 13 | const ViewError: React.FC = ({ message }) => ( 14 |
15 |
16 |
17 | {message} 18 | 19 | 刷新 20 | 21 |
22 |
23 |
24 | ); 25 | 26 | ViewError.defaultProps = { 27 | message: '加载失败,请重试!', 28 | }; 29 | 30 | export default ViewError; 31 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GlobalError from './GlobalError'; 3 | import ViewError from './ViewError'; 4 | 5 | type ErrComType = 'view' | 'global'; 6 | 7 | interface IErrorBoundaryProps { 8 | type: ErrComType; 9 | message?: string; 10 | isDialog?: boolean; 11 | } 12 | 13 | interface State { 14 | hasError: boolean; 15 | error?: any; 16 | } 17 | 18 | export default class ErrorBoundary extends React.Component { 19 | static defaultProps: IErrorBoundaryProps = { 20 | type: 'global', 21 | message: '页面崩溃啦!', 22 | isDialog: false, 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | hasError: false, 29 | }; 30 | } 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 33 | static getDerivedStateFromError(_error: Error) { 34 | return { hasError: true }; 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 38 | // componentDidCatch(error: Error, errorInfo: ErrorInfo) { 39 | componentDidCatch(error: any, errorInfo: any) { 40 | console.log('ErrorBoundary', error, errorInfo); 41 | this.setState({ 42 | error, 43 | }); 44 | //TODO 上报错误日志 45 | } 46 | 47 | getErrorComponent(type: ErrComType) { 48 | switch (type) { 49 | case 'view': 50 | return ViewError; 51 | default: 52 | return GlobalError; 53 | } 54 | } 55 | 56 | renderErrorComponent() { 57 | // const { message } = this.props; 58 | // const { error } = this.state; 59 | // const Com = this.getErrorComponent(type); 60 | 61 | return
123
; 62 | //return this.getErrorComponent() 63 | // return ; 64 | } 65 | 66 | render() { 67 | if (this.state.hasError) { 68 | return this.renderErrorComponent(); 69 | } 70 | return this.props.children; 71 | } 72 | } 73 | 74 | export function withErrorBoundary(Com: any, options: IErrorBoundaryProps & T) { 75 | return (props: any) => ( 76 | 77 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/NoMatch/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NoMatch = () =>
No Match
; 4 | 5 | export default NoMatch; 6 | -------------------------------------------------------------------------------- /src/components/Page/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/components/Page/index.scss -------------------------------------------------------------------------------- /src/components/Page/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './index.scss'; 3 | import BaseComponent from '@/components/BaseComponent'; 4 | import { RouteComponentProps } from 'react-router-dom'; 5 | 6 | /** 7 | * 8 | * 页面基础组件 9 | * @class Page 10 | * @extends {BaseComponent} 11 | */ 12 | class Page

extends BaseComponent { 13 | constructor(props) { 14 | super(props); 15 | //TODO 可以加入页面访问统计等需求 16 | console.log('page'); 17 | } 18 | 19 | render() { 20 | return

; 21 | } 22 | } 23 | 24 | export default Page; 25 | -------------------------------------------------------------------------------- /src/components/ResolutionCom/ResolutionComp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Resolutions from '@/common/Resolution'; 3 | 4 | interface IResolutionComProps { 5 | WIDTH?: number; 6 | } 7 | 8 | export default class ResolutionCom extends React.Component { 9 | componentDidMount(): void { 10 | this.resize(); 11 | window.onresize = this.resize; 12 | } 13 | 14 | resize = (): void => { 15 | //获取设计稿的尺寸 16 | const design = Resolutions.getDesign(); 17 | const html = document.documentElement; 18 | const getHtmlFs = () => parseFloat(window.getComputedStyle(html, null)['font-size']); 19 | const getScreenWidth = () => { 20 | let htmlWidth = 0; 21 | try { 22 | const htmlElement = document.documentElement; 23 | htmlWidth = Math.max( 24 | htmlElement.offsetWidth || 0, 25 | htmlElement.clientWidth || 0, 26 | htmlElement.getBoundingClientRect().width || 0, 27 | ); 28 | // 读取失败,其他方式读取 29 | if (!htmlWidth || htmlWidth <= 0) { 30 | if (window.orientation == 180 || window.orientation == 0) { 31 | //竖屏 32 | htmlWidth = 33 | window.innerWidth || 34 | (window.screen && window.screen.width) || 35 | (window.screen && window.screen.availWidth) || 36 | 0; 37 | } else if (window.orientation == 90 || window.orientation == -90) { 38 | //横屏 39 | htmlWidth = 40 | window.innerHeight || 41 | (window.screen && window.screen.height) || 42 | (window.screen && window.screen.availHeight) || 43 | 0; 44 | } 45 | } 46 | } catch (e) { 47 | console.log('获取屏幕宽度出错'); 48 | } 49 | return htmlWidth | 0; 50 | }; 51 | const WIDTH = this.props.WIDTH || design.WIDTH, 52 | //第一次进来没有设置过html标签font-size的时候 53 | screenWidth = getScreenWidth(), 54 | htmlFs = getHtmlFs(), 55 | mediaFs = (design.RATIO / WIDTH) * screenWidth; //获取页面宽度 设备宽度/fontSize=设计稿(750)/100=7.5; 56 | 57 | html.style.fontSize = `${mediaFs}px`; //根据页面大小算出font-size 58 | 59 | //以下是特殊处理 试过一台htc下的某个浏览器设置字体大小后再获取font-size会比所设的值会相对变小 所以设置大一点让它font-size的结果是想设的结果 60 | 61 | if (htmlFs !== mediaFs && Math.abs(htmlFs - mediaFs) > 2) { 62 | html.style.fontSize = '100px'; 63 | html.style.fontSize = `${(100 / getHtmlFs()) * mediaFs}px`; 64 | } 65 | }; 66 | 67 | render(): React.ReactNode { 68 | return this.props.children; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; 3 | src: url("./assets/fonts/iconfont.ttf"); 4 | } 5 | 6 | html, 7 | body { 8 | height: 100%; 9 | font-family: "iconfont" !important; 10 | margin: 0; 11 | color: #fff; 12 | } 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from '@/App'; 4 | import { Provider } from 'react-redux'; 5 | import history from '@/store/reducers/history'; 6 | import configureStore from '@/store'; 7 | import { ConnectedRouter } from 'connected-react-router'; 8 | import './index.scss'; 9 | import '@/assets/css/index.scss'; 10 | import ResolutionCom from '@/components/ResolutionCom/ResolutionComp'; 11 | import ErrorBoundary from './components/ErrorBoundary'; 12 | import Modal from 'react-modal'; 13 | 14 | try { 15 | const store = configureStore({}); 16 | const rootElement = document.getElementById('root'); 17 | 18 | Modal.setAppElement(rootElement); 19 | 20 | const renderApp = () => { 21 | ReactDOM.render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | , 31 | document.getElementById('root'), 32 | ); 33 | }; 34 | renderApp(); 35 | } catch (e) { 36 | console.log('e'); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/Home/HomeChild/index.scss: -------------------------------------------------------------------------------- 1 | .home-child { 2 | position: relative; 3 | width: 200px; 4 | height: 200px; 5 | background: red; 6 | color: #fff; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/Home/HomeChild/index.tsx: -------------------------------------------------------------------------------- 1 | import connect from '@/store/connect'; 2 | import React from 'react'; 3 | import BaseComponent from '@/components/BaseComponent'; 4 | import { push } from 'connected-react-router'; 5 | 6 | import './index.scss'; 7 | 8 | interface IHomeChildProps { 9 | pathname?: string; 10 | search?: string; 11 | hash?: string; 12 | push?: (path: string, s?: any) => void; 13 | } 14 | 15 | const mapStateToProps = { 16 | pathname: 'router.location.pathname', 17 | search: 'router.location.search', 18 | hash: 'router.location.hash', 19 | }; 20 | 21 | const mapDispatchToProps = { 22 | push, 23 | }; 24 | 25 | @connect(mapStateToProps, mapDispatchToProps) 26 | class HomeChild extends BaseComponent { 27 | handleGoPage = () => { 28 | this.props.push('/page1', { a: 1 }); 29 | }; 30 | 31 | render() { 32 | const { pathname, search, hash } = this.props; 33 | return ( 34 |
35 |
pathname::{pathname}
36 |
search::{search}
37 |
hash::{hash}
38 | 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default HomeChild; 47 | -------------------------------------------------------------------------------- /src/pages/Home/index.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | position: relative; 3 | width: 100%; 4 | .bg1 { 5 | position: relative; 6 | width: 200px; 7 | height: 300px; 8 | @include bgFill("01.png"); 9 | } 10 | .img1 { 11 | position: absolute; 12 | right: 0; 13 | top: 0; 14 | width: 200px; 15 | height: 300px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from '@/components/Page'; 3 | import { UPDATE_USER_ID } from '@/store/actions/user'; 4 | import connect from '@/store/connect'; 5 | import CPng from '@/assets//images/02.png'; 6 | import HomeChild from './HomeChild'; 7 | import { withAppContextDecorators } from '@/common/AppContext'; 8 | import './index.scss'; 9 | import { IAppContext } from '@/types/IContext'; 10 | 11 | interface IHomeProps { 12 | userId: number; 13 | updateId: (id: number) => void; 14 | } 15 | 16 | interface IHomeState { 17 | count: number; 18 | } 19 | 20 | const mapStateToProps = { 21 | userId: 'user.userId', 22 | }; 23 | 24 | const mapDispatchToProps = { 25 | updateId: UPDATE_USER_ID, 26 | }; 27 | 28 | type HomeProps = IHomeProps & IAppContext; 29 | //装饰器由下到上依次调用结果函数 30 | //参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。 31 | @withAppContextDecorators 32 | @connect(mapStateToProps, mapDispatchToProps) 33 | class Home extends Page { 34 | //重要,代表这个IAppContext是默认自带的,不需要外界传入,直接注入的。 35 | static defaultProps: IAppContext; 36 | 37 | constructor(props) { 38 | super(props); 39 | this.state = { 40 | count: 0, 41 | }; 42 | } 43 | 44 | componentDidMount = () => { 45 | setTimeout(() => { 46 | this.props.updateId(5); 47 | }, 3000); 48 | console.log(this.props.test()); 49 | this.setState({ 50 | count: 1, 51 | }); 52 | }; 53 | 54 | render() { 55 | const { userId } = this.props; 56 | const { count } = this.state; 57 | console.log('this.props==', this.props); 58 | return ( 59 |
60 |
Hel4lo, World1!{userId}
61 |
{count}
62 | 63 | 64 |
65 | ); 66 | } 67 | } 68 | 69 | export default Home; 70 | -------------------------------------------------------------------------------- /src/pages/Page1/index.scss: -------------------------------------------------------------------------------- 1 | .page1 { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | background: red; 6 | color: #fff; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/Page1/index.tsx: -------------------------------------------------------------------------------- 1 | import Page from '@/components/Page'; 2 | import React from 'react'; 3 | import connect from '@/store/connect'; 4 | import { goBack, CallHistoryMethodAction } from 'connected-react-router'; 5 | import './index.scss'; 6 | 7 | interface IPageProps { 8 | pathname: string; 9 | search: string; 10 | hash: string; 11 | goBack: () => CallHistoryMethodAction; 12 | } 13 | 14 | const mapStateToProps = { 15 | pathname: 'router.location.pathname', 16 | search: 'router.location.search', 17 | hash: 'router.location.hash', 18 | }; 19 | 20 | const mapDispatchToProps = { 21 | goBack: goBack, 22 | }; 23 | 24 | @connect(mapStateToProps, mapDispatchToProps) 25 | class Page1 extends Page { 26 | componentDidMount = () => { 27 | console.log('获取到的location===', this.props); 28 | }; 29 | 30 | handleGoBack = () => { 31 | this.props.goBack(); 32 | }; 33 | 34 | render() { 35 | return ( 36 |
37 |
Hello, Page122222!
38 | 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default Page1; 47 | -------------------------------------------------------------------------------- /src/router/RouterUI.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import BaseComponent from '@/components/BaseComponent'; 3 | import { Route, Switch, withRouter, RouteComponentProps } from 'react-router-dom'; 4 | import { IRouterPage } from '@/types/IRouterPage'; 5 | import NoMatch from '@/components/NoMatch'; 6 | 7 | interface IRouterUIProps { 8 | routers: IRouterPage[]; 9 | } 10 | 11 | type IProps = IRouterUIProps & RouteComponentProps; 12 | 13 | /** 14 | * 15 | * 渲染路由 16 | * @class RouterUI 17 | * @extends {BaseComponent} 18 | */ 19 | class RouterUI extends BaseComponent { 20 | /** 21 | * 生成router 22 | * @param {*} routers 23 | * @param {*} container 24 | * @param {*} recur 是否递归 25 | */ 26 | renderRouter = (routers: IRouterPage[] = []) => 27 | routers.map(router => { 28 | let { path, exact } = router; 29 | return ( 30 | this.renderPage(router)} 35 | /> 36 | ); 37 | }); 38 | 39 | renderPage = (router: IRouterPage) => { 40 | const { component, path, loadingFallback } = router; 41 | const Page = component; 42 | return ( 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | render() { 50 | const { routers } = this.props; 51 | return ( 52 | 53 | {this.renderRouter(routers)} 54 | 55 | 56 | ); 57 | } 58 | } 59 | 60 | export default withRouter(RouterUI); 61 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // named imports for React.lazy: https://github.com/facebook/react/issues/14603#issuecomment-726551598 4 | export function lazyImport(name) { 5 | return React.lazy( 6 | () => import(/* webpackChunkName: "[request]",webpackPrefetch: true */ `@/pages/${name}`), 7 | ); 8 | } 9 | 10 | // 使用[request] 支持实际解析的文件名 11 | // React.lazy( 12 | // () => import(/* webpackChunkName: "page1",webpackPrefetch: true */ '@/pages/Page1'), 13 | // ) 14 | 15 | export default { 16 | routes: [ 17 | { 18 | exact: true, 19 | path: '/', 20 | isDynamic: true, 21 | component: lazyImport('Home'), 22 | }, 23 | { 24 | exact: true, 25 | path: '/page1', 26 | isDynamic: true, 27 | component: lazyImport('Page1'), 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /src/services/BaseService.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios'; 2 | import getConfig from './serviceConfig'; 3 | import { get, post } from './axios'; 4 | 5 | const apiConfig = getConfig(); 6 | 7 | export default class BaseService { 8 | private baseUrl: string; 9 | 10 | constructor(baseUrl: string) { 11 | this.baseUrl = baseUrl; 12 | } 13 | 14 | private handelGetUrl = (url: string, params: any): string => { 15 | let curUrl = url; 16 | if (params) { 17 | const paramsArray: string[] = []; 18 | Object.keys(params).forEach(key => { 19 | if (params[key] !== undefined) { 20 | paramsArray.push(`${key}=${params[key]}`); 21 | } 22 | }); 23 | if (url.search(/\?/) === -1) { 24 | curUrl += `?${paramsArray.join('&')}`; 25 | } else { 26 | curUrl += `&${paramsArray.join('&')}`; 27 | } 28 | } 29 | return curUrl; 30 | }; 31 | 32 | getFullPath = (path: string) => `${this.baseUrl}/${path}`; 33 | 34 | getByQuery = (path: string, data: any = {}, config?: AxiosRequestConfig): Promise => { 35 | const curPath = this.handelGetUrl(path, data); 36 | return this.get(curPath, config); 37 | }; 38 | 39 | get = (path: string, config?: AxiosRequestConfig): Promise => 40 | get(this.getFullPath(path), config); 41 | 42 | post = (path: string, data: any = {}, config?: AxiosRequestConfig): Promise => 43 | post(this.getFullPath(path), data, config); 44 | } 45 | 46 | export const apiService = new BaseService(apiConfig.API_SERVER); 47 | -------------------------------------------------------------------------------- /src/services/ServerResponseManager.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError, AxiosResponse } from 'axios'; 2 | 3 | /** 4 | * 针对请求成功:返回的 code 码做不同的响应处理 5 | */ 6 | class ServerResponseSuccessManager { 7 | /** 8 | * 状态码解析器 9 | * @param response 10 | */ 11 | codeParser(response: AxiosResponse) { 12 | const code = response?.data?.errCode; 13 | const resData = response?.data?.data; 14 | const parser = { 15 | '10010': () => { 16 | this.handleCodeIs10010(resData); 17 | }, 18 | default: () => console.log('code 无法识别'), 19 | }; 20 | return parser[code] ? parser[code]() : parser.default; 21 | } 22 | 23 | /** 24 | * 状态码为 10010 的响应处理 25 | * @param resData 26 | */ 27 | handleCodeIs10010(resData) { 28 | if (resData === 'TOKEN_INVALID') { 29 | //appEventEmitter.emit("live-token-invalid"); 30 | // setTimeout(() => { 31 | // window.location.href = '/login'; 32 | // }, 1000); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * 针对请求失败的响应处理 39 | */ 40 | class ServerResponseFailedManager { 41 | /** 42 | * 请求失败时,需要提示的信息 43 | */ 44 | getErrorMessage(error: AxiosError) { 45 | console.error('error.response==', error.response); 46 | } 47 | } 48 | 49 | export const serverResponseSuccessManager = new ServerResponseSuccessManager(); 50 | export const serverResponseFailedManager = new ServerResponseFailedManager(); 51 | -------------------------------------------------------------------------------- /src/services/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as user from './user'; 2 | 3 | // api 中的 key 以单个模块命名,防止接口重名 4 | export const api = { 5 | user, 6 | }; 7 | -------------------------------------------------------------------------------- /src/services/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import { apiService } from '@/services/BaseService'; 2 | import { IResponseData } from '@/types/service/responseData'; 3 | import { IUserNameAndHeadInfo } from '@/types/service/user'; 4 | 5 | // 获取用户信息 6 | export const getUserInfo = (id: number) => 7 | apiService.post>('action-common/app/getUserInfo', { id }); 8 | -------------------------------------------------------------------------------- /src/services/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse, AxiosError, AxiosInstance } from 'axios'; 2 | import { 3 | serverResponseFailedManager, 4 | serverResponseSuccessManager, 5 | } from '@/services/ServerResponseManager'; 6 | 7 | const axiosInstance: AxiosInstance = axios.create({ 8 | timeout: 15000, 9 | headers: { 'Content-Type': 'application/json' }, 10 | }); 11 | 12 | //axiosInstance.defaults.headers.common.platform = "PC"; 13 | 14 | /** 15 | * request 拦截器 16 | */ 17 | axiosInstance.interceptors.request.use( 18 | (config: AxiosRequestConfig): AxiosRequestConfig => { 19 | // config.url = host + config.url; 20 | if (!navigator.onLine) { 21 | //toast("网络连接已断开,请稍后重试"); 22 | } 23 | return config; 24 | }, 25 | (error: AxiosError) => Promise.reject(error), 26 | ); 27 | 28 | /** 29 | * response 拦截器 30 | */ 31 | axiosInstance.interceptors.response.use( 32 | (response: AxiosResponse) => { 33 | if (response.data.code === 200) { 34 | return response; 35 | } 36 | // 针对请求成功:返回的 code 码做不同的响应 37 | serverResponseSuccessManager.codeParser(response); 38 | }, 39 | (error: AxiosError) => { 40 | // 针对请求失败:应该提示的错误信息 41 | serverResponseFailedManager.getErrorMessage(error); 42 | return Promise.reject(error.response); 43 | }, 44 | ); 45 | 46 | export function setAuthorization(token) { 47 | console.log('request:setAuthorization', token); 48 | axiosInstance.defaults.headers.common.Authorization = token; 49 | } 50 | 51 | // TODO:: add 拦截器(拦截之后返回T??),预设等等, 52 | export function get(url: string, config?: AxiosRequestConfig): Promise { 53 | return axiosInstance.get(url, config).then((res: AxiosResponse) => res && res.data); 54 | } 55 | 56 | export function post(url: string, data: any, config?: AxiosRequestConfig): Promise { 57 | return axiosInstance.post(url, data, config).then((res: AxiosResponse) => res.data); 58 | } 59 | 60 | export function getBlob(url: string): Promise { 61 | return axios 62 | .get(url, { 63 | responseType: 'blob', 64 | }) 65 | .then(res => res.data); 66 | } 67 | 68 | export default axiosInstance; 69 | -------------------------------------------------------------------------------- /src/services/serviceConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: quanyj 3 | * @Date: 2021-08-09 11:21:01 4 | * @Last Modified by: quanyj 5 | * @Last Modified time: 2021-09-16 16:32:13 6 | * 服务配置获取 7 | */ 8 | 9 | const CONFIG = { 10 | API_SERVER: process.env.C_API_SERVER, 11 | }; 12 | 13 | export default function getConfig() { 14 | return CONFIG; 15 | } 16 | -------------------------------------------------------------------------------- /src/store/actionCreaters/actionCreator.ts: -------------------------------------------------------------------------------- 1 | import { IActionParam } from '@/types/IRedux'; 2 | import { Dispatch } from 'redux'; 3 | import { getStore } from '..'; 4 | 5 | let innerDispatch: Dispatch; 6 | 7 | function getDispatch() { 8 | if (!innerDispatch) { 9 | innerDispatch = getStore().dispatch; 10 | } 11 | return innerDispatch; 12 | } 13 | 14 | export default function actionCreator(actionType?: string) { 15 | return function fn(_target: any, name: string, descriptor: PropertyDescriptor): void { 16 | // console.log("messageMethod", target); 17 | console.log('actionCreator', actionType, name, descriptor); 18 | 19 | const type = actionType || name; 20 | // eslint-disable-next-line no-param-reassign 21 | descriptor.value = (payload: any) => { 22 | getDispatch()({ 23 | type, 24 | payload, 25 | }); 26 | }; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/store/actionCreaters/index.ts: -------------------------------------------------------------------------------- 1 | import BaseUserInfoCreator from './user'; 2 | 3 | export const userInfoActions = new BaseUserInfoCreator(); 4 | -------------------------------------------------------------------------------- /src/store/actionCreaters/user.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /* eslint-disable class-methods-use-this */ 4 | import { UPDATE_USER_ID, UPDATE_SSO_TOKEN } from '@/store/actions/user'; 5 | import actionCreator from './actionCreator'; 6 | 7 | export default class BaseUserInfo { 8 | @actionCreator(UPDATE_USER_ID) 9 | updateUserJJUid(_data: number) {} 10 | 11 | @actionCreator(UPDATE_SSO_TOKEN) 12 | updateUserToken(_data: string) {} 13 | } 14 | -------------------------------------------------------------------------------- /src/store/actions/user.ts: -------------------------------------------------------------------------------- 1 | export const UPDATE_SSO_TOKEN = 'UPDATE_SSO_TOKEN'; 2 | export const UPDATE_USER_ID = 'UPDATE_USER_ID'; 3 | 4 | export const UPDATE_USER_NAME_HEAD_ID = 'UPDATE_USER_NAME_HEAD_ID'; 5 | -------------------------------------------------------------------------------- /src/store/connect.ts: -------------------------------------------------------------------------------- 1 | import * as rr from 'react-redux'; 2 | import * as _ from 'lodash'; 3 | 4 | const mapStateToProps = (filter: any) => (state: any) => { 5 | const keys = Object.keys(filter); 6 | if (!filter || keys.length === 0) { 7 | return state; 8 | } 9 | const retState = keys.reduce((obj, name: string) => { 10 | const path = filter[name]; 11 | if (typeof path === 'object') { 12 | // eslint-disable-next-line 13 | obj[name] = mapStateToProps(path)(state[name]); 14 | } else { 15 | // eslint-disable-next-line 16 | obj[name] = typeof path === 'boolean' ? _.get(state, name) : _.get(state, path); 17 | } 18 | return obj; 19 | }, {}); 20 | return retState; 21 | }; 22 | 23 | const mapDispatchToProps = (filter: Record) => dispatch => { 24 | const keys = Object.keys(filter); 25 | if (!filter || keys.length === 0) { 26 | return {}; 27 | } 28 | const ret = Object.keys(filter).reduce((obj, name) => { 29 | const path = filter[name]; 30 | if (typeof path !== 'string') { 31 | obj[name] = (...args) => { 32 | dispatch(path(...args)); 33 | }; 34 | } else { 35 | const paths = path.split('.'); 36 | const type = paths.length === 1 ? paths[0] : paths[paths.length - 1]; 37 | // eslint-disable-next-line 38 | obj[name] = payload => dispatch({ type, payload }); 39 | } 40 | 41 | return obj; 42 | }, {}); 43 | 44 | return ret; 45 | }; 46 | 47 | const connect = 48 | (stateFilter: any = {}, dispatchFilter: any = {}): Function => 49 | target => { 50 | const ret = rr.connect(mapStateToProps(stateFilter), mapDispatchToProps(dispatchFilter), null, { 51 | forwardRef: true, 52 | })(target); 53 | ret.compName = target.name; 54 | return ret; 55 | }; 56 | 57 | export default connect; 58 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Store, compose, applyMiddleware } from 'redux'; 2 | import rootReducer from './reducers/index'; 3 | import { routerMiddleware } from 'connected-react-router'; 4 | import history from './reducers/history'; 5 | import { IActionParam } from '@/types/IRedux'; 6 | 7 | let store = null; 8 | export function getStore() { 9 | return store as Store< 10 | any, 11 | { 12 | type: string; 13 | payload?: any; 14 | } 15 | >; 16 | } 17 | 18 | export default function configureStore(preloadedState?: any): Store { 19 | const composeEnhancer: typeof compose = 20 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 21 | store = createStore( 22 | rootReducer, // root reducer with router state 23 | preloadedState, 24 | composeEnhancer( 25 | applyMiddleware( 26 | routerMiddleware(history), // 用于 dispatching history actions, 27 | ), 28 | ), 29 | ); 30 | 31 | console.log('process.env.ENV ===', process.env.ENV); 32 | if (process.env.ENV == 'dev') { 33 | // eslint-disable-next-line no-underscore-dangle 34 | (window as any).__store = store; 35 | } 36 | 37 | return store; 38 | } 39 | -------------------------------------------------------------------------------- /src/store/reducers/history.ts: -------------------------------------------------------------------------------- 1 | import { createHashHistory } from 'history'; 2 | import produce from 'immer'; 3 | import { LOCATION_CHANGE } from 'connected-react-router'; 4 | import { IActionParam } from '@/types/IRedux'; 5 | 6 | let history = createHashHistory(); 7 | export default history; 8 | 9 | //import { push } from 'connected-react-router'; 10 | //提供了push,go,goBack,replace,block,goForward方法。 11 | //push("/home") || push({pathname:"/home",search:"name=1",hash:"1"}) 12 | //history 可以分为两部分,切换和修改,切换历史状态:back,forward,go对应浏览器的后退,跳转,前进。history.go(2);//前进两次 13 | 14 | //push 把页面状态保存在state对象中,当页面回来的时候,可以通过event.state获取到state对象。 15 | 16 | //查看connected-react-router 的connectRouter 方法,使immer与history 结合使用 17 | export interface HistoryState { 18 | location: any; 19 | action: any; 20 | } 21 | 22 | const injectQuery = location => { 23 | if (location && location.query) { 24 | // Don't inject query if it already exists in history 25 | return location; 26 | } 27 | 28 | const searchQuery = location && location.search; 29 | 30 | if (typeof searchQuery !== 'string' || searchQuery.length === 0) { 31 | return { 32 | ...location, 33 | query: {}, 34 | }; 35 | } 36 | 37 | // Ignore the `?` part of the search string e.g. ?username=codejockie 38 | const search = searchQuery.substring(1); 39 | // Split the query string on `&` e.g. ?username=codejockie&name=Kennedy 40 | const queries = search.split('&'); 41 | // Contruct query 42 | const query = queries.reduce((acc, currentQuery) => { 43 | // Split on `=`, to get key and value 44 | const [queryKey, queryValue] = currentQuery.split('='); 45 | return { 46 | ...acc, 47 | [queryKey]: queryValue, 48 | }; 49 | }, {}); 50 | 51 | return { 52 | ...location, 53 | query, 54 | }; 55 | }; 56 | 57 | const initHistoryState: HistoryState = { 58 | location: injectQuery(history.location), 59 | action: history.action, 60 | }; 61 | 62 | /* eslint-disable no-param-reassign */ 63 | export const reducer = produce((draft: HistoryState, actionParam: IActionParam) => { 64 | if (actionParam.type === LOCATION_CHANGE) { 65 | const { location, action } = actionParam.payload; 66 | draft.action = action; 67 | draft.location = injectQuery(location); 68 | return draft; 69 | } 70 | return draft; 71 | }, initHistoryState); 72 | -------------------------------------------------------------------------------- /src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import user from './user'; 3 | import { reducer } from './history'; 4 | 5 | export default combineReducers({ 6 | router: reducer, //必须是router connectRouter(history) 7 | user, 8 | }); 9 | -------------------------------------------------------------------------------- /src/store/reducers/user.ts: -------------------------------------------------------------------------------- 1 | import { IActionParam, IRequestStatus } from '@/types/IRedux'; 2 | import produce from 'immer'; 3 | 4 | import { UPDATE_SSO_TOKEN, UPDATE_USER_ID, UPDATE_USER_NAME_HEAD_ID } from '../actions/user'; 5 | 6 | export interface UserState { 7 | status: IRequestStatus; 8 | token: string; 9 | SSOToken: string; 10 | userId: number; 11 | nickName: string; 12 | headId: number; 13 | } 14 | 15 | const initialState: UserState = { 16 | token: null, 17 | SSOToken: null, 18 | status: IRequestStatus.none, 19 | nickName: '', 20 | userId: 0, 21 | headId: 0, 22 | }; 23 | 24 | /* eslint-disable no-param-reassign */ 25 | const reducer = produce((draft: UserState, action: IActionParam) => { 26 | switch (action.type) { 27 | case UPDATE_USER_ID: 28 | console.log('当前的用户信息==UPDATE_USER_ID=', action.payload); 29 | draft.userId = action.payload; 30 | return draft; 31 | case UPDATE_SSO_TOKEN: 32 | console.log('当前的用户信息==UPDATE_SSO_TOKEN=', action.payload); 33 | // draft.SSOToken = action.payload; 34 | draft.token = action.payload; 35 | return draft; 36 | case UPDATE_USER_NAME_HEAD_ID: 37 | draft.nickName = action.payload.name; 38 | draft.headId = action.payload.headId; 39 | return draft; 40 | default: 41 | return draft; 42 | } 43 | }, initialState); 44 | 45 | export default reducer; 46 | -------------------------------------------------------------------------------- /src/types/IContext.ts: -------------------------------------------------------------------------------- 1 | export interface IAppContext { 2 | /** 3 | * 用户ID 4 | */ 5 | uid?: number; 6 | 7 | /** 8 | * token 9 | */ 10 | token?: string; 11 | 12 | test: () => void; 13 | } 14 | -------------------------------------------------------------------------------- /src/types/IRedux.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux'; 2 | 3 | export interface IActionParam extends AnyAction { 4 | type: string; 5 | payload: any; 6 | } 7 | 8 | export enum IRequestStatus { 9 | none = 'none', 10 | done = 'done', 11 | error = 'error', 12 | requesting = 'requesting', 13 | } 14 | 15 | export default { 16 | IRequestStatus, 17 | }; 18 | -------------------------------------------------------------------------------- /src/types/IRouterPage.ts: -------------------------------------------------------------------------------- 1 | export interface IRouterPage { 2 | /** 3 | * 页面组件 4 | * 5 | * @type {any} 6 | * @memberof IRouterPage 7 | */ 8 | component?: any; 9 | /** 10 | * 当前路由路径 11 | */ 12 | path: string; 13 | /** 14 | * 是否严格匹配路由 15 | */ 16 | exact?: boolean; 17 | /** 18 | * 动态加载路由时的提示文案 19 | */ 20 | loadingFallback?: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/types/enum.ts: -------------------------------------------------------------------------------- 1 | export enum DeviceType { 2 | PC = 'pc', 3 | MOBILE = 'mobile', 4 | TV = 'tv', 5 | } 6 | -------------------------------------------------------------------------------- /src/types/service/responseData.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: quanyj 3 | * @Date: 2021-08-09 11:20:20 4 | * @Last Modified by: quanyj 5 | * @Last Modified time: 2021-09-16 16:33:00 6 | * 服务返回数据模型 7 | */ 8 | 9 | export interface IResponseData { 10 | errCode: number; 11 | errMsg?: string; 12 | data: T; 13 | } 14 | 15 | export type RSBoolean = IResponseData; 16 | 17 | export type RSString = IResponseData; 18 | -------------------------------------------------------------------------------- /src/types/service/user.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: quanyj 3 | * @Date: 2021-08-09 11:19:50 4 | * @Last Modified by: quanyj 5 | * @Last Modified time: 2021-09-16 16:33:03 6 | * 服务返回用户模块数据模型 7 | */ 8 | 9 | export interface IUserNameAndHeadInfo { 10 | name: string; 11 | headId: number; 12 | } 13 | -------------------------------------------------------------------------------- /src/typings/custom.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qyjandroid/react-project-template/6309d4b15dbcbd7a2b108d14ab0954c8ce08a9c1/src/typings/custom.d.ts -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module '*.scss'; 4 | declare module '*.svg'; 5 | declare module '*.png'; 6 | declare module '*.jpg'; 7 | declare module '*.jpeg'; 8 | declare module '*.gif'; 9 | declare module '*.bmp'; 10 | declare module '*.tiff'; 11 | -------------------------------------------------------------------------------- /src/util/DomUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 判断是否包含 4 | * @export 5 | * @param {(Node | null | undefined)} root 6 | * @param {Node} [n] 7 | * @returns 8 | */ 9 | export function isContains(root: Node | null | undefined, n?: Node) { 10 | if (!root) { 11 | return false; 12 | } 13 | 14 | return root.contains(n); 15 | } 16 | 17 | /** 18 | * 19 | * 添加监听 20 | * @export 21 | * @param {any} target 22 | * @param {any} eventType 23 | * @param {any} cb 24 | * @param {any} [option] 25 | * @returns 26 | */ 27 | export function addEventListenerWrap(target, eventType, cb, option?) { 28 | if (target.addEventListener) { 29 | //捕获阶段,从上到下捕获,option传true 30 | target.addEventListener(eventType, cb, option); 31 | } 32 | return { 33 | remove: () => { 34 | if (target.removeEventListener) { 35 | target.removeEventListener(eventType, cb); 36 | } 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/util/generateId.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成随机的 UUID 3 | * @param len 4 | * @param radix 5 | */ 6 | export function generateRandomUUID(len: number, radix: number) { 7 | let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 8 | let uuid = [], 9 | i; 10 | const curRadix = radix || chars.length; 11 | if (len) { 12 | for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * curRadix)]; 13 | } else { 14 | let r; 15 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; 16 | uuid[14] = '4'; 17 | for (i = 0; i < 36; i++) { 18 | if (!uuid[i]) { 19 | r = 0 | (Math.random() * 16); 20 | uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r]; 21 | } 22 | } 23 | } 24 | return uuid.join(''); 25 | } 26 | 27 | /** 28 | * 生成随机的ID 29 | * @param randomLength 30 | */ 31 | export function generateRandomID(randomLength: number) { 32 | // 引入时间戳 随机数前置 36进制 加入随机数长度控制 33 | // Number.prototype.toString([radix])中的 radix 指定要用于数字到字符串的转换的基数(从2到36) 34 | // 如果未指定 radix 参数,则默认值为 10,所有不在范围内的基数会报错 35 | // Math.random().toString(36)代表36进制 36 | // console.log(new Number(1337).valueOf().toString()); 37 | // 带时间戳 38 | return Number(Math.random().toString().substr(3, randomLength) + Date.now()).toString(36); 39 | } 40 | 41 | export function generateRandomString(len: number) { 42 | let i = 0, 43 | str = '', 44 | base = 19968, 45 | range = 10; 46 | // 19968 至 40869 47 | while (i < len) { 48 | i++; 49 | let lower = parseInt(`${Math.random() * range}`); 50 | str += String.fromCharCode(base + lower); 51 | } 52 | return str; 53 | } 54 | -------------------------------------------------------------------------------- /src/util/handleUrlParams.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取 URL 参数 3 | * @param name 4 | */ 5 | export function getUrlParam(name: string) { 6 | // 构造一个含有目标参数的正则表达式对象 7 | const reg = new RegExp(`(^|&)${name.toLowerCase()}=([^&]*)(&|$)`); 8 | // 匹配目标参数 9 | const r = window.location.search.substr(1).toLowerCase().match(reg); 10 | if (r != null) { 11 | //返回参数值 12 | return unescape(r[2]); 13 | } 14 | return null; 15 | } 16 | /** 17 | * url添加参数 18 | * @param {string} url - 需要添加参数的url 19 | * @param {object} params - 添加的参数,参数是'key:value'形式 20 | * @param {boolean} [encode=false] - 返回的url是否需要编码 21 | * @returns {string} 22 | */ 23 | export function addParams({ 24 | url = '', 25 | params = {}, 26 | encode = false, 27 | }: { 28 | url?: string; 29 | params: object; 30 | encode?: boolean; 31 | }) { 32 | if (!Object.keys(params).length) { 33 | return url; 34 | } 35 | const curUrl = decodeURIComponent(url); 36 | const [hostStr, searchStr] = curUrl.split('?'); 37 | let newParams = params; 38 | if (curUrl.includes('?')) { 39 | const oldParams = {}; 40 | searchStr.split('&').forEach(val => { 41 | const newVal = val.split('='); 42 | oldParams[newVal[0]] = newVal[1]; 43 | }); 44 | // 合并、去重参数 45 | newParams = { ...oldParams, ...params }; 46 | } 47 | let [paramsStr, i] = ['', 1]; 48 | for (const [key, val] of Object.entries(newParams)) { 49 | paramsStr += i > 1 ? `&${key}=${val}` : `${key}=${val}`; 50 | i++; 51 | } 52 | const baseUrl = `${hostStr}?${paramsStr}`; 53 | return encode ? encodeURIComponent(baseUrl) : baseUrl; 54 | } 55 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | /** 获取当前开发环境 */ 2 | export function getEnv() { 3 | return { 4 | isDev: process.env.ENV === 'dev', // 本地开发环境 5 | isProd: process.env.ENV === 'prod', // 生产环境 6 | isTest: process.env.ENV === 'test', //测试环境 7 | }; 8 | } 9 | 10 | export function classNames(...args: any) { 11 | const classes = []; 12 | for (let i = 0; i < args.length; i += 1) { 13 | const arg = args[i]; 14 | if (arg) { 15 | const argType = typeof arg; 16 | 17 | if (argType === 'string' || argType === 'number') { 18 | classes.push((this && this[arg]) || arg); 19 | } else if (Array.isArray(arg)) { 20 | classes.push(classNames(...arg)); 21 | } else if (argType === 'object') { 22 | const hasOwn = {}.hasOwnProperty; 23 | Object.keys(arg).forEach(key => { 24 | if (hasOwn.call(arg, key) && arg[key]) { 25 | classes.push((this && this[key]) || key); 26 | } 27 | }); 28 | } 29 | } 30 | } 31 | 32 | return classes.join(' '); 33 | } 34 | 35 | export function isMobile() { 36 | const flag = navigator.userAgent.match( 37 | /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i, 38 | ); 39 | if (flag) { 40 | return true; 41 | } 42 | return false; 43 | } 44 | 45 | export function setTimeout(fn: () => void, time: number) { 46 | const timer = window.setTimeout(fn, time); 47 | return () => { 48 | window.clearTimeout(timer); 49 | }; 50 | } 51 | 52 | export function setInterval(fn: () => void, time: number) { 53 | const timer = window.setInterval(fn, time); 54 | return () => { 55 | window.clearInterval(timer); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/util/stringUtil.ts: -------------------------------------------------------------------------------- 1 | /** 这个文件中封装了一些常用的工具函数 **/ 2 | 3 | /** 4 | 保留N位小数 5 | @param {Number|String} str 待处理数字 6 | @param {Number} x 保留几位小数点 7 | @return {Number|String} 处理成功返回字符串,处理失败返回原值 8 | */ 9 | export function pointX(str, x = 0) { 10 | if (!str && str !== 0) { 11 | return str; 12 | } 13 | const temp = Number(str); 14 | if (temp === 0) { 15 | return temp.toFixed(x); 16 | } 17 | return temp ? temp.toFixed(x) : str; 18 | } 19 | 20 | /** 21 | 去掉字符串两端空格 22 | @param {String} str 待处理字符串 23 | @return {String} 处理后的字符串 24 | */ 25 | export function trim(str) { 26 | const reg = /^\s*|\s*$/g; 27 | return str.replace(reg, ''); 28 | } 29 | 30 | /** 31 | 给字符串打马赛克 32 | 如:将123456转换为1****6,最多将字符串中间6个字符变成* 33 | 如果字符串长度小于等于2,将不会有效果 34 | @param {String} str 待处理字符串 35 | @return {String} 处理后的字符串 36 | */ 37 | export function addMosaic(str) { 38 | const s = String(str); 39 | const length = s.length; 40 | const howMuch = (() => { 41 | if (s.length <= 2) { 42 | return s.length; 43 | } 44 | const l = s.length - 2; 45 | if (l <= 6) { 46 | return l; 47 | } 48 | return 6; 49 | })(); 50 | const start = Math.floor((length - howMuch) / 2); 51 | const ret = s.split('').map((v, i) => { 52 | if (i >= start && i < start + howMuch) { 53 | return '*'; 54 | } 55 | return v; 56 | }); 57 | return ret.join(''); 58 | } 59 | 60 | /** 61 | 字符串加密 简单的加密方法 62 | @param {String} code 待处理字符串 63 | @return {String} 加密后的字符串 64 | */ 65 | export function compile(code) { 66 | let c = String.fromCharCode(code.charCodeAt(0) + code.length); 67 | for (let i = 1; i < code.length; i++) { 68 | c += String.fromCharCode(code.charCodeAt(i) + code.charCodeAt(i - 1)); 69 | } 70 | return c; 71 | } 72 | 73 | /** 74 | 字符串解谜 对应上面的字符串加密方法 75 | @param {String} code 加密的字符串 76 | @return {String} 解密后的字符串 77 | */ 78 | export function unCompile(code) { 79 | let c = String.fromCharCode(code.charCodeAt(0) - code.length); 80 | for (let i = 1; i < code.length; i++) { 81 | c += String.fromCharCode(code.charCodeAt(i) - c.charCodeAt(i - 1)); 82 | } 83 | return c; 84 | } 85 | -------------------------------------------------------------------------------- /src/util/variableTypeDetection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 检测变量类型 3 | * @param type 4 | */ 5 | function isType(type) { 6 | return function (value): boolean { 7 | return Object.prototype.toString.call(value) === `[object ${type}]`; 8 | }; 9 | } 10 | 11 | export const variableTypeDetection = { 12 | isNumber: isType('Number'), 13 | isString: isType('String'), 14 | isBoolean: isType('Boolean'), 15 | isNull: isType('Null'), 16 | isUndefined: isType('Undefined'), 17 | isSymbol: isType('Symbol'), 18 | isFunction: isType('Function'), 19 | isObject: isType('Object'), 20 | isArray: isType('Array'), 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | // extend your base config so you don't have to redefine your compilerOptions 3 | "extends": "./tsconfig.json", 4 | "include": [ 5 | "**/*.ts", 6 | "**/*.tsx", 7 | "**/*.js", 8 | "**/*.jsx" 9 | ], 10 | "exclude": [ 11 | "node_modules/**", 12 | "dist/**", 13 | "coverage/**" 14 | ] 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* 开启增量编译:TS 编译器在第一次编译的时候,会生成一个存储编译信息的文件,下一次编译的时候,会根据这个文件进行增量的编译,以此提高 TS 的编译速度 */ 4 | // "incremental": true, 5 | // 指定存储增量编译信息的文件位置 6 | // "tsBuildInfoFile": "./", 7 | // 显示诊断信息 8 | // "diagnostics": true, 9 | /* 指定模块代码的生成方式: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 10 | // "module": "commonjs", 11 | /* 指定 ECMAScript 的目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 12 | "rootDir": "./src", 13 | "target": "es5", // 指定 ECMAScript 目标版本 14 | "module": "ESNext", //指定生成那个模块的代码 15 | "strict": true, // 开启所有的严格检查配置 16 | "esModuleInterop": true, // 允许 export = xxx 导出 ,并使用 import xxx form "module-name" 导入 17 | "outDir": "dist", 18 | /* 指定要包含在编译中的库文件——引用类库——即申明文件,如果输出的模块方式是 es5,就会默认引入 "dom","es5","scripthost" 。如果在 TS 中想要使用一些 ES6 以上版本的语法,就需要引入相关的类库 */ 19 | "lib": [ 20 | "webworker", 21 | "dom", 22 | "es5", 23 | "es2015", 24 | "es2016", 25 | "es2015.promise", 26 | "dom.iterable", 27 | "scripthost", 28 | "esnext", 29 | ], // 要包含在编译中的依赖库文件列表 30 | "allowJs": true, // 允许编译 JavaScript 文件 31 | // 检查 JS 文件 32 | "checkJs": true, 33 | "skipLibCheck": true, // 跳过所有声明文件的类型检查 34 | "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入 35 | "resolveJsonModule": true, // 允许使用 .json 扩展名导入的模块 36 | "noEmit": true, // 不输出(意思是不编译代码,只执行类型检查) 37 | /* react 模式下:直接将 JSX 编译成 JS,会生成 React.createElement 的形式,在使用前不需要再进行转换操作了,输出文件的扩展名为 .js */ 38 | /* preserve 模式下:不会将 JSX 编译成 JS,生成代码中会保留 JSX,以供后续的转换操作使用(比如:Babel)。 另外,输出文件会带有 .jsx 扩展名 */ 39 | /* react-native 模式下:相当于 preserve,它也保留了所有的 JSX,但是输出文件的扩展名是 .js */ 40 | "jsx": "react", // 在.tsx文件中支持JSX 41 | "sourceMap": true, // 生成相应的.map文件 42 | "declaration": true, // 生成相应的.d.ts文件 43 | "allowUmdGlobalAccess": true, 44 | "experimentalDecorators": true, // 启用对ES装饰器的实验性支持 45 | "moduleResolution": "node", // 制定模块解析策略 46 | "baseUrl": "./", 47 | "incremental": true, // 通过从以前的编译中读取/写入信息到磁盘上的文件来启用增量编译 48 | "forceConsistentCasingInFileNames": true, 49 | /* 当目标是ES5或ES3的时候提供对for-of、扩展运算符和解构赋值中对于迭代器的完整支持 */ 50 | "downlevelIteration": true, 51 | "noUnusedLocals": true, 52 | "noUnusedParameters": true, 53 | "noImplicitReturns": true, 54 | "noFallthroughCasesInSwitch": true, 55 | // 不允许使用隐式的 any 类型 56 | "noImplicitAny": false, 57 | // 不允许 this 有隐式的 any 类型,即 this 必须有明确的指向 58 | "noImplicitThis": false, 59 | // 不允许把 null、undefined 赋值给其他类型变量 60 | "strictNullChecks": false, 61 | "paths": { 62 | //别名 63 | "@/*": [ 64 | "./src/*" 65 | ], 66 | "@images/*": [ 67 | "./src/assets/images/*" 68 | ], 69 | } 70 | }, 71 | "include": [ 72 | "src" 73 | ], 74 | "exclude": [ 75 | "node_modules", 76 | "dist" 77 | ] // *** 不进行类型检查的文件 *** 78 | } -------------------------------------------------------------------------------- /webpack/webpack.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const variable = require('./webpackUtils/variable'); 4 | const resolveConfig = require('./webpackUtils/resolve'); 5 | const plugins = require('./webpackUtils/plugins'); 6 | const { SRC_PATH, DIST_PATH, IS_DEV, IS_PRO, getCDNPath } = variable; 7 | 8 | const config = { 9 | entry: { 10 | index: path.join(SRC_PATH, 'index.tsx'), 11 | }, 12 | output: { 13 | path: DIST_PATH, 14 | filename: IS_DEV ? 'js/[name].bundle.js' : 'js/[name].[contenthash:8].bundle.js', 15 | publicPath: getCDNPath(), 16 | globalObject: 'this', 17 | chunkFilename: IS_DEV ? 'js/[name].chunk.js' : 'js/[name].[contenthash:8].chunk.js', 18 | assetModuleFilename: 'assets/[hash][ext][query]', 19 | clean: true, 20 | }, 21 | //loader的执行顺序默认从右到左,多个loader用[],字符串只用一个loader,也可以是对象的格式 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(tsx?|js)$/, 26 | include: [SRC_PATH], 27 | use: [ 28 | { 29 | loader: 'thread-loader', 30 | options: { 31 | workers: require('os').cpus().length * 2, 32 | parallel: true, 33 | }, 34 | }, 35 | { 36 | loader: 'babel-loader', // 这是一个webpack优化点,使用缓存 37 | options: { 38 | cacheDirectory: true, 39 | }, 40 | }, 41 | ], 42 | exclude: [/node_modules/, /public/, /(.|_)min\.js$/], 43 | }, 44 | { 45 | test: /\.css$|\.scss$/i, 46 | // include: [SRC_PATH], 47 | // exclude: /node_modules/, // 取消匹配node_modules里面的文件 48 | use: [ 49 | IS_DEV ? 'style-loader' : MiniCssExtractPlugin.loader, 50 | { 51 | loader: 'css-loader', 52 | options: { 53 | modules: false, 54 | sourceMap: !IS_PRO, 55 | }, 56 | }, 57 | { 58 | loader: 'postcss-loader', 59 | options: { 60 | postcssOptions: { 61 | plugins: [ 62 | [ 63 | 'postcss-preset-env', 64 | { 65 | // 其他选项 66 | }, 67 | ], 68 | ], 69 | }, 70 | }, 71 | }, 72 | 'sass-loader', 73 | { 74 | loader: 'style-resources-loader', 75 | options: { 76 | patterns: path.resolve(SRC_PATH, 'assets', 'css', 'core.scss'), 77 | }, 78 | }, 79 | ], 80 | }, 81 | { 82 | test: /\.(png|jpg|gif|jpeg|webp|svg)$/, 83 | type: 'asset/resource', 84 | generator: { 85 | // publicPath: '../', 86 | filename: 'assets/images/[hash][ext][query]', 87 | }, 88 | }, 89 | { 90 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 91 | type: 'asset/resource', 92 | generator: { 93 | // publicPath: '../', 94 | filename: 'assets/fonts/[hash][ext][query]', 95 | }, 96 | }, 97 | ], 98 | }, 99 | resolve: resolveConfig, 100 | plugins: plugins.getPlugins(), 101 | }; 102 | 103 | module.exports = config; 104 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const webpackMerge = require('webpack-merge'); 3 | const baseConfig = require('./webpack.base'); 4 | const variable = require('./webpackUtils/variable'); 5 | 6 | const { DIST_PATH } = variable; 7 | //引入 8 | 9 | const config = { 10 | mode: 'development', 11 | cache: { type: 'memory' }, 12 | devtool: 'eval-cheap-module-source-map', 13 | stats: 'errors-only', 14 | watchOptions: { 15 | aggregateTimeout: 500, 16 | poll: 1000, 17 | ignored: /node_modules/, 18 | }, 19 | devServer: { 20 | open: 'chrome', 21 | contentBase: DIST_PATH, 22 | compress: true, //是否启用gzip压缩 23 | publicPath: '/', 24 | host: 'localhost', 25 | port: 9093, 26 | // hot: true, 27 | disableHostCheck: true, 28 | stats: 'errors-only', 29 | proxy: { 30 | // "/service": { 31 | // target: "http://localhost:3000" 32 | // } 33 | }, 34 | }, 35 | }; 36 | const mergedConfig = webpackMerge.merge(baseConfig, config); 37 | 38 | mergedConfig.plugins = mergedConfig.plugins.filter(Boolean); 39 | 40 | module.exports = mergedConfig; 41 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpackMerge = require('webpack-merge'); 2 | const baseConfig = require('./webpack.base'); 3 | 4 | const config = { 5 | mode: 'production', 6 | cache: { type: 'filesystem', buildDependencies: { config: [__filename] } }, 7 | output: { 8 | pathinfo: false, //优化 9 | }, 10 | optimization: { 11 | minimize: true, //开启压缩 12 | moduleIds: 'deterministic', //单独模块id,模块内容变化再更新 13 | splitChunks: { 14 | chunks: 'all', // 匹配的块的类型:initial(初始块),async(按需加载的异步块),all(所有块) 15 | automaticNameDelimiter: '-', 16 | cacheGroups: { 17 | // 项目第三方组件 18 | vendor: { 19 | name: 'vendors', 20 | enforce: true, // ignore splitChunks.minSize, splitChunks.minChunks, splitChunks.maxAsyncRequests and splitChunks.maxInitialRequests 21 | test: /[\\/]node_modules[\\/]/, 22 | priority: 10, 23 | }, 24 | // 项目公共组件 25 | default: { 26 | minSize: 0, // 分离后的最小块文件大小默认3000 27 | name: 'common', // 用以控制分离后代码块的命名 28 | minChunks: 3, // 最小共用次数 29 | priority: 10, // 优先级,多个分组冲突时决定把代码放在哪块 30 | reuseExistingChunk: true, 31 | }, 32 | }, 33 | }, 34 | }, 35 | }; 36 | 37 | const mergedConfig = webpackMerge.merge(baseConfig, config); 38 | module.exports = mergedConfig; 39 | -------------------------------------------------------------------------------- /webpack/webpackUtils/plugins.js: -------------------------------------------------------------------------------- 1 | // const cleanWebpackPlugin = require("clean-webpack-plugin") ; 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | // const CopyPlugin = require("copy-webpack-plugin"); 5 | const variable = require('./variable'); 6 | const DotenvPlugin = require('dotenv-webpack'); 7 | const path = require('path'); 8 | 9 | // const {CleanWebpackPlugin}=cleanWebpackPlugin; 10 | const { PUBLIC_PATH, DIST_PATH, ENV_CONFIG_PATH, IS_DEV, SRC_PATH } = variable; 11 | 12 | function getHTMLPlugins() { 13 | const indexHtmlPlugin = new HtmlWebpackPlugin({ 14 | template: path.join(PUBLIC_PATH, 'index.html'), 15 | // filename: path.join(DIST_PATH, 'index.html'), 16 | filename: 'index.html', 17 | inject: true, //true 插入body底部,head:插入head标签,false:不生成js文件 18 | // hash: true, // 会在打包好的bundle.js后面加上hash串 19 | title: '', 20 | minify: { 21 | removeComments: true, // 删除注释 22 | collapseWhitespace: true, 23 | minifyCSS: true, // 压缩 HTML 中出现的 CSS 代码 24 | minifyJS: true, // 压缩 HTML 中出现的 JS 代码 25 | }, 26 | }); 27 | 28 | return [indexHtmlPlugin]; 29 | } 30 | 31 | function getPlugins() { 32 | // clean 33 | // const cleanPlugin = new CleanWebpackPlugin({ 34 | // cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*.*'] 35 | // }); 36 | 37 | const miniCssPlugin = new MiniCssExtractPlugin({ 38 | filename: IS_DEV ? 'css/[name].css' : 'css/[name].[contenthash:8].css', 39 | chunkFilename: IS_DEV ? 'css/[name].chunk.css' : 'css/[name].[contenthash:8].chunk.css', 40 | // 常遇到如下警告,Conflicting order. Following module has been added:…。 41 | // 此警告意思为在不同的js中引用相同的css时,先后顺序不一致。也就是说,在1.js中先后引入a.css和b.css,而在2.js中引入的却是b.css和a.css,此时会有这个warning。 42 | ignoreOrder: true, 43 | }); 44 | 45 | const dotenvPlugin = new DotenvPlugin({ 46 | path: ENV_CONFIG_PATH, 47 | }); 48 | 49 | // const copyPlugin=new CopyPlugin({ 50 | // patterns: [ 51 | // { from: path.resolve(SRC_PATH,"assets"), to: path.resolve(DIST_PATH,"assets") }, 52 | // ], 53 | // }); 54 | 55 | return [ 56 | // cleanPlugin, 57 | // copyPlugin, 58 | ...getHTMLPlugins(), 59 | dotenvPlugin, 60 | miniCssPlugin, 61 | ]; 62 | } 63 | 64 | module.exports = { 65 | getPlugins, 66 | }; 67 | -------------------------------------------------------------------------------- /webpack/webpackUtils/resolve.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const variable = require('./variable'); 3 | const { SRC_PATH, ROOT_PATH } = variable; 4 | 5 | module.exports = { 6 | extensions: ['.tsx', '.ts', '.js', '.json'], 7 | modules: [path.resolve(ROOT_PATH, 'node_modules')], 8 | // 查找 package.json main 9 | mainFields: ['main'], 10 | alias: { 11 | '@': SRC_PATH, 12 | '@images': path.resolve(SRC_PATH, 'assets/images'), 13 | '@fonts': path.resolve(SRC_PATH, 'assets/fonts'), 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /webpack/webpackUtils/util.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const packageConfig = require('../../package.json'); 3 | 4 | function readFile(curPath) { 5 | const content = fs.readFileSync(curPath, 'utf-8'); 6 | return content; 7 | } 8 | 9 | /** 10 | * 11 | * 获取版本 12 | * @export 13 | * @returns 14 | */ 15 | function getVersion() { 16 | //const version = readFile(path.join(__dirname, '../../version')).trim(); // return version; 17 | return packageConfig.version || '1.0.0'; 18 | } 19 | 20 | /** 21 | * 22 | * 获取测试版本 23 | * @export 24 | * @returns 25 | */ 26 | function getTestVersion() { 27 | return packageConfig.testVersion || '1.0.0'; 28 | } 29 | 30 | /** 31 | * 32 | * 获取env 33 | * @returns 34 | */ 35 | function getEnv() { 36 | return process.env.NODE_ENV || 'dev'; 37 | } 38 | 39 | module.exports = { 40 | readFile, 41 | getVersion, 42 | getTestVersion, 43 | getEnv, 44 | }; 45 | -------------------------------------------------------------------------------- /webpack/webpackUtils/variable.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpackUtils = require('./util'); 3 | const dotenv = require('dotenv'); 4 | 5 | const { config: loadConfig } = dotenv; 6 | //__dirname 为当前目录文件路径 7 | 8 | const NODE_ENV = webpackUtils.getEnv(); 9 | 10 | //构建目录 11 | const DIST_PATH = path.resolve(__dirname, '../../', 'dist'); 12 | //源码目录 13 | const SRC_PATH = path.resolve(__dirname, '../../', 'src'); 14 | //public 目录 15 | const PUBLIC_PATH = path.resolve(__dirname, '../../', 'public'); 16 | //根节点目录 17 | const ROOT_PATH = path.resolve(__dirname, '../../'); 18 | //是否是产线环境 19 | const IS_PRO = NODE_ENV === 'prod'; 20 | //是否是开发环境 21 | const IS_DEV = NODE_ENV === 'dev'; 22 | 23 | //当前构建的版本 24 | const version = webpackUtils.getVersion(); 25 | 26 | function getCDNPath() { 27 | return IS_PRO ? `${process.env.CDN_ROOT}/${version}/` : './'; 28 | } 29 | 30 | const ENV_CONFIG_PATH = path.resolve(ROOT_PATH, 'env', `${NODE_ENV}.env`); 31 | 32 | //webpack 读取env 配置 33 | loadConfig({ 34 | path: ENV_CONFIG_PATH, 35 | }); 36 | 37 | console.log('::NODE_ENV', NODE_ENV); 38 | 39 | console.log('version', version); 40 | 41 | module.exports = { 42 | DIST_PATH, 43 | SRC_PATH, 44 | PUBLIC_PATH, 45 | ROOT_PATH, 46 | IS_PRO, 47 | IS_DEV, 48 | getCDNPath, 49 | ENV_CONFIG_PATH, 50 | }; 51 | --------------------------------------------------------------------------------