├── .nvmrc ├── docs ├── PRINT.md ├── CHANGE_LOG.md ├── CHANGE_LOG_EN.md ├── Copy.md ├── PART_UPDATE.md └── PART_UPDATE_EN.md ├── server ├── src │ ├── static │ │ └── index.ts │ ├── router │ │ ├── Example │ │ │ └── index.ts │ │ └── index.ts │ ├── config │ │ ├── prod.ts │ │ ├── dev.ts │ │ └── index.ts │ ├── controllers │ │ └── example │ │ │ └── index.ts │ ├── services │ │ └── example │ │ │ └── content.ts │ ├── middleware │ │ ├── log.ts │ │ └── cors.ts │ ├── utils │ │ ├── redis │ │ │ ├── redisConnection.ts │ │ │ └── redisOperate.ts │ │ ├── logger │ │ │ └── globalLog.ts │ │ └── fileOperation │ │ │ └── index.ts │ └── server.ts ├── dockerfile ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── src ├── main │ ├── README.md │ ├── public │ │ ├── icon.ico │ │ ├── icon.png │ │ └── tray.png │ ├── config │ │ ├── config.js │ │ └── webpack.config.js │ ├── preload.js │ ├── dev-app-update.yml │ ├── app-update.yml │ ├── controls │ │ ├── AppTray.js │ │ ├── AppMainWindow.js │ │ └── AppAutoUpdater.js │ ├── script │ │ └── build.js │ ├── util │ │ └── utils.js │ ├── index.js │ └── loading.html └── render │ ├── README.md │ ├── components │ ├── FormCps │ │ ├── index.tsx │ │ └── readme.md │ ├── TableCps │ │ ├── index.tsx │ │ └── readme.md │ ├── readme.md │ └── AutoUpdate │ │ ├── style.less │ │ └── index.tsx │ ├── assets │ ├── image │ │ └── yay.jpg │ └── style │ │ ├── common.less │ │ └── bootstrap-part.less │ ├── .env │ ├── layouts │ ├── menu │ │ ├── index.less │ │ └── index.tsx │ ├── loading │ │ ├── Style.module.less │ │ └── index.tsx │ ├── index.less │ ├── header │ │ ├── index.tsx │ │ └── index.less │ ├── error │ │ └── index.tsx │ └── index.tsx │ ├── api │ └── api.list.ts │ ├── common │ ├── global.ts │ └── enum.ts │ ├── pages │ ├── Foo │ │ ├── services │ │ │ └── foo.ts │ │ ├── models │ │ │ └── foo.ts │ │ ├── components │ │ │ └── TableList.tsx │ │ └── index.tsx │ ├── home.normal.less │ ├── Home │ │ ├── Edge │ │ │ └── index.tsx │ │ └── WebSocket │ │ │ └── index.tsx │ └── index.tsx │ ├── utils │ ├── polyfill │ │ ├── electron-store.js │ │ ├── electron.js │ │ └── index.js │ ├── store │ │ └── index.ts │ ├── rightClickMenuFuc.tsx │ ├── axiosRequest.tsx │ ├── common.ts │ └── autoUpdate │ │ └── partUpdate.js │ ├── models │ └── xxStore.ts │ ├── global.less │ ├── app.ts │ ├── mock │ └── foo.ts │ ├── config │ ├── menus.tsx │ └── iconfont.ts │ └── .umirc.ts ├── .prettierignore ├── .gitlab-ci.yml ├── .editorconfig ├── .gitignore ├── typings.d.ts ├── tsconfig.json ├── .prettierrc.js ├── scripts ├── webpack.main.js └── main-build.js ├── package.json ├── README.md ├── README_EN.md └── .eslintrc.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.18.3 2 | -------------------------------------------------------------------------------- /docs/PRINT.md: -------------------------------------------------------------------------------- 1 | ## 打印相关使用文档 -------------------------------------------------------------------------------- /server/src/static/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/README.md: -------------------------------------------------------------------------------- 1 | # electron主进程 2 | -------------------------------------------------------------------------------- /src/render/README.md: -------------------------------------------------------------------------------- 1 | # electron渲染进程 2 | -------------------------------------------------------------------------------- /src/render/components/FormCps/index.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/render/components/TableCps/index.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/render/components/readme.md: -------------------------------------------------------------------------------- 1 | # 组件库写这儿 -------------------------------------------------------------------------------- /src/render/components/FormCps/readme.md: -------------------------------------------------------------------------------- 1 | # form组件 -------------------------------------------------------------------------------- /src/render/components/TableCps/readme.md: -------------------------------------------------------------------------------- 1 | # table组件 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | -------------------------------------------------------------------------------- /src/main/public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qld-cf/electron-react-tpl/HEAD/src/main/public/icon.ico -------------------------------------------------------------------------------- /src/main/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qld-cf/electron-react-tpl/HEAD/src/main/public/icon.png -------------------------------------------------------------------------------- /src/main/public/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qld-cf/electron-react-tpl/HEAD/src/main/public/tray.png -------------------------------------------------------------------------------- /src/main/config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 主进程全局配置 3 | */ 4 | 5 | export const DEV_ADDRESS = 'http://localhost:9090' 6 | -------------------------------------------------------------------------------- /src/render/assets/image/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qld-cf/electron-react-tpl/HEAD/src/render/assets/image/yay.jpg -------------------------------------------------------------------------------- /src/render/.env: -------------------------------------------------------------------------------- 1 | # 环境变量配置 2 | # https://umijs.org/zh-CN/docs/env-variables#host 3 | PORT=9090 4 | ESLINT=none 5 | BROWSER=none -------------------------------------------------------------------------------- /src/render/layouts/menu/index.less: -------------------------------------------------------------------------------- 1 | .menu { 2 | height: calc(100vh - var(--header-height)); 3 | overflow-y: auto; 4 | } 5 | -------------------------------------------------------------------------------- /src/render/api/api.list.ts: -------------------------------------------------------------------------------- 1 | 2 | // api 网关地址 3 | const apiList = { 4 | list: 'xxx', 5 | 6 | 7 | }; 8 | export default apiList; 9 | -------------------------------------------------------------------------------- /src/render/common/global.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局通用常量 3 | */ 4 | export const pageSizeOptions = ['10', '20', '30', '40', '50', '100'] 5 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project: "arch-foundation/cicd" 3 | ref: electron 4 | file: "/templates/Electron.gitlab-ci.yml" -------------------------------------------------------------------------------- /src/render/components/AutoUpdate/style.less: -------------------------------------------------------------------------------- 1 | // :global{ 2 | // .ant-btn-primary{ 3 | // margin-right: 10px; 4 | // } 5 | // } -------------------------------------------------------------------------------- /src/render/pages/Foo/services/foo.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export function getFoo(params: any) { 4 | return axios('/api/foo', { method: 'POST', params }) 5 | } 6 | -------------------------------------------------------------------------------- /src/render/layouts/loading/Style.module.less: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/render/utils/polyfill/electron-store.js: -------------------------------------------------------------------------------- 1 | module.exports = class Store { 2 | get(name) { 3 | return localStorage.getItem(name) 4 | } 5 | set(name, val) { 6 | localStorage.setItem(name, val) 7 | } 8 | } -------------------------------------------------------------------------------- /server/src/router/Example/index.ts: -------------------------------------------------------------------------------- 1 | import example from '../../controllers/example' 2 | import * as Router from 'koa-router'; 3 | const router = new Router(); 4 | 5 | router.get('*',example) 6 | 7 | module.exports = router -------------------------------------------------------------------------------- /src/render/utils/polyfill/electron.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ipcRenderer: { 3 | send: () => {}, 4 | on: () => {}, 5 | }, 6 | ipcMain: { 7 | send: () => {}, 8 | on: () => {}, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/preload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 为window挂载方法 3 | */ 4 | const { ipcRenderer } = require('electron') 5 | 6 | /** 挂载停止启动loading方法 */ 7 | window.stopLoading = function () { 8 | ipcRenderer.send('stop-loading-main') 9 | } 10 | -------------------------------------------------------------------------------- /docs/CHANGE_LOG.md: -------------------------------------------------------------------------------- 1 | # electron-react-umi-tpl change log 2 | 3 | [English Version](https://github.com/qld-cf/electron-react-tpl/blob/master/CHANGE_LOG_EN.md) 4 | 5 | ### 2020-06-14 6 | 7 | 1. 添加增量更新, 支持远程更新,无需下载新包覆盖安装 8 | 2. 操作: 文档待补充 -------------------------------------------------------------------------------- /src/render/layouts/index.less: -------------------------------------------------------------------------------- 1 | .content { 2 | // height: calc(100vh - var(--header-height) - 32px); // 32px margin 3 | height: 0; // 父元素 flex 布局,这里直接设置 0 4 | margin: 16px; 5 | background-color: #eff0fa; 6 | overflow: auto; 7 | } -------------------------------------------------------------------------------- /server/src/config/prod.ts: -------------------------------------------------------------------------------- 1 | const prodConfig = { 2 | baseUrl: '', 3 | baseHost: '', 4 | basePort: 3025, 5 | baseSocketPort: 8025, 6 | host_redis: '127.0.0.1', 7 | port_redis: 6379, 8 | } 9 | 10 | export default prodConfig -------------------------------------------------------------------------------- /server/src/config/dev.ts: -------------------------------------------------------------------------------- 1 | const devConfig = { 2 | baseUrl: '', 3 | baseHost: '127.0.0.1', 4 | basePort: 3002, 5 | baseSocketPort: 8024, 6 | host_redis: '127.0.0.1', 7 | port_redis: 6379, 8 | } 9 | 10 | export default devConfig -------------------------------------------------------------------------------- /src/main/dev-app-update.yml: -------------------------------------------------------------------------------- 1 | # 本地开发测试使用 2 | 3 | # 在dev-app-update.yml中配置检查更新的地址(全量更新) electron-builder会根据本地version和服务器yml文件version匹配 4 | # 如果远程包更新,则拉取后自动安装 5 | # 这个地址为开发环境下的地址 6 | # 需要放入根目录才能打包运行 7 | provider: 'generic' 8 | url: 'http://127.0.0.1:4000/download/' 9 | channel: 'latest' -------------------------------------------------------------------------------- /server/src/controllers/example/index.ts: -------------------------------------------------------------------------------- 1 | import * as koa from 'koa' 2 | import {getContent} from '../../services/example/content' 3 | 4 | const index = async(ctx:koa.Context,next:Function)=>{ 5 | ctx.body = getContent('koa') 6 | await next() 7 | } 8 | 9 | export default index -------------------------------------------------------------------------------- /src/render/layouts/loading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Spin } from 'antd' 3 | import styles from './Style.module.less' 4 | 5 | export default () => { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /server/src/services/example/content.ts: -------------------------------------------------------------------------------- 1 | const getContent = (code:T):string=>{ 2 | let res:any; 3 | if(typeof(code)!=='string'){ 4 | res = `${String(code)} is not string` 5 | }else{ 6 | res = `${code} is string` 7 | } 8 | return res 9 | } 10 | 11 | export {getContent} -------------------------------------------------------------------------------- /docs/CHANGE_LOG_EN.md: -------------------------------------------------------------------------------- 1 | # electron-react-umi-tpl change log 2 | 3 | [English Version](https://github.com/qld-cf/electron-react-tpl/blob/master/CHANGE_LOG_EN.md) 4 | 5 | ### 2020-06-14 6 | 7 | 1. ADD electron Incremental update ; 8 | 2. jUST update app.zip for update online; 9 | 3. NOT need to download exe or dmg 10 | -------------------------------------------------------------------------------- /src/render/common/enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局通用枚举 3 | */ 4 | 5 | export enum FormTypes { // 表单类型 6 | INPUT = 'COMMON_INPUT', 7 | NUMBER = 'COMMON_INPUT_NUMBER', 8 | DATE = 'COMMON_DATE', 9 | RANGE_DATE = 'COMMON_RANGE_DATE', 10 | SELECT = 'COMMON_SELECT', 11 | BUSINESS_SELECT='BUSINESS_SELECT' 12 | }; 13 | -------------------------------------------------------------------------------- /server/dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | RUN mkdir -p /home/Service 4 | 5 | 6 | WORKDIR /home/Service 7 | 8 | COPY . /home/Service 9 | 10 | RUN npm install -g typescript 11 | RUN npm install 12 | 13 | ENV NODE_ENV production 14 | 15 | #PORT 16 | EXPOSE 8025 17 | EXPOSE 3025 18 | 19 | CMD [ "npm","run","build" ] 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/main/app-update.yml: -------------------------------------------------------------------------------- 1 | # 正式更新 2 | 3 | # 在dev-update.yml中配置检查更新的地址(全量更新) electron-builder会根据本地version和服务器yml文件version匹配 4 | # 如果远程包更新,则拉取后自动安装 5 | # 这个地址为正式环境下的更新地址 6 | # 需要放入根目录才能打包运行 7 | # channel: 检查更新的渠道 beta | latest", 8 | # url: 服务器地址 9 | # provider: 默认参数 10 | provider: 'generic' 11 | url: 'your server exe/dmg url' 12 | channel: 'latest' -------------------------------------------------------------------------------- /src/render/utils/polyfill/index.js: -------------------------------------------------------------------------------- 1 | const dict = process.env.TARGET === 'web' 2 | ? { 3 | electron: require('./electron'), 4 | 'electron-store': require('./electron-store'), 5 | } 6 | : { 7 | electron: require('electron'), 8 | 'electron-store': require('electron-store'), 9 | } 10 | 11 | module.exports = function (name) { 12 | return dict[name] 13 | } -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | test/unit/coverage 10 | test/e2e/reports 11 | selenium-debug.log 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | 21 | package-lock.json 22 | 23 | /config/ 24 | config.rar 25 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | // "noImplicitAny": true, 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "noImplicitAny": false 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules" 15 | ] 16 | } -------------------------------------------------------------------------------- /src/render/pages/home.normal.less: -------------------------------------------------------------------------------- 1 | .container { 2 | background-repeat: no-repeat; 3 | background-size: cover; 4 | background-position: center center; 5 | } 6 | .homewrap { 7 | display: flex; 8 | flex-direction: column; 9 | margin: '30px 0 '; 10 | align-items: center; 11 | width: 100%; 12 | height: 100%; 13 | .bigFt { 14 | font-size: 36px; 15 | line-height: 50px; 16 | color: #474245; 17 | margin-top: 35px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/middleware/log.ts: -------------------------------------------------------------------------------- 1 | import * as koa from 'koa' 2 | const log = () => { 3 | return async (ctx:koa.Context, next:Function) => { 4 | // 记录请求开始的时间 5 | const start = Date.now(); 6 | await next(); 7 | // 记录完成的时间 作差 计算响应时间 8 | const responseTime = Date.now() - start; 9 | if((global as any).log){ 10 | (global as any).log.info(`响应时间为${responseTime / 1000}s`); 11 | } 12 | } 13 | }; 14 | 15 | export default log -------------------------------------------------------------------------------- /server/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as koa from 'koa'; 3 | import * as Route from 'koa-router' 4 | 5 | const routerMount = (app:koa) => { 6 | fs.readdirSync(__dirname).forEach((file:string) => { 7 | if(file === 'index.js'||file === 'index.js.map'){ 8 | return 9 | }; 10 | const router:Route = require(`./${file}/index.js`); 11 | app.use(router.routes()).use(router.allowedMethods()); 12 | }) 13 | }; 14 | 15 | export default routerMount -------------------------------------------------------------------------------- /src/render/pages/Home/Edge/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Card, Button } from 'antd' 3 | 4 | interface IProps { 5 | loading: boolean 6 | } 7 | 8 | const Edge = (props: IProps | any) => { 9 | 10 | return ( 11 |
12 | 13 | Edge 14 | 15 | 20 |
21 | ) 22 | } 23 | 24 | export default Edge 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | package-lock.json 9 | 10 | # production 11 | **/dist 12 | 13 | # misc 14 | .DS_Store 15 | .idea 16 | .vscode 17 | .svn 18 | .npmrc 19 | .yarnrc 20 | 21 | # umi 22 | .umi 23 | .umi-production 24 | 25 | # 一些临时文件 26 | **/.tmp 27 | **/.umi 28 | 29 | # Electron 构建文件 30 | release 31 | src/main/bundle.js 32 | src/main/bundle.js.map 33 | -------------------------------------------------------------------------------- /src/render/models/xxStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局store dva+redux+redux-saga 3 | */ 4 | export default { 5 | namespace: 'xxStore', 6 | state: { 7 | xxList: [] 8 | }, 9 | effects: { 10 | *updateList({ payload }: any, { put }: any) { 11 | // 开放使用方法 12 | yield put({ 13 | type: 'save', 14 | payload: { xxList: payload } 15 | }) 16 | } 17 | }, 18 | reducers: { 19 | save(state: any, { payload }: any) { 20 | return { ...state, ...payload } 21 | } 22 | }, 23 | subscriptions: {} 24 | } 25 | -------------------------------------------------------------------------------- /src/render/layouts/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Layout } from 'antd' 3 | import './index.less' 4 | 5 | 6 | const { Header } = Layout 7 | 8 | const headerProps = { 9 | style: { 10 | background: '#A14EFF' 11 | } 12 | } 13 | 14 | const HeaderComponent = (props: any) => { 15 | 16 | return ( 17 |
18 |
19 |
Hello Electron
20 |
21 |
22 | ) 23 | } 24 | 25 | export default HeaderComponent 26 | -------------------------------------------------------------------------------- /server/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import devConfig from './dev'; 2 | import prodConfig from './prod'; 3 | 4 | interface configDefault { 5 | baseUrl: string, 6 | baseHost: string, 7 | basePort: number 8 | baseSocketPort: number, 9 | host_redis: string, 10 | port_redis: number, 11 | } 12 | 13 | const getConfig = (type: string): configDefault => { 14 | let config: configDefault; 15 | if (typeof (type) !== 'string') { 16 | type = String(type) 17 | } 18 | if (type === 'production') { 19 | config = prodConfig 20 | } 21 | if (type === 'development') { 22 | config = devConfig 23 | } 24 | return config 25 | } 26 | 27 | export default getConfig -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | import { History } from 'umi' 2 | 3 | declare module '*.css' 4 | declare module '*.less' 5 | 6 | // declare module '*.module.less' { 7 | // const styles: { [className: string]: string } 8 | // export default styles 9 | // } 10 | 11 | declare module '*.png' 12 | declare module '*.jpg' 13 | declare module '*.jpeg' 14 | 15 | declare module 'umi' { 16 | /** react-router history 对象 */ 17 | export const history: History 18 | } 19 | 20 | declare global { 21 | /** 启动时候的 ENV */ 22 | const REACT_APP_MY_ENV: string 23 | 24 | interface Window { 25 | /** 浏览器下开发,关闭 electron 载入动画 */ 26 | stopLoading: () => void 27 | /** 是否开启自动更新 */ 28 | isOpenAutoUpdate: boolean 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/config/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * electron web端编译配置 3 | */ 4 | const path = require('path'); 5 | 6 | const pathResolve = (dir = '') => path.join(__dirname, '..', dir) // 指向 src/main 7 | 8 | module.exports = { 9 | mode: 'production', 10 | devtool: 'cheap-module-source-map', 11 | target: 'electron-main', 12 | entry: pathResolve('index.js'), 13 | output: { 14 | path: pathResolve(), 15 | filename: 'bundle.js', 16 | }, 17 | node: { 18 | __dirname: false, 19 | __filename: false 20 | }, 21 | resolve: { 22 | extensions: ['.js', '.ts', '.tsx', '.jsx', '.json'] 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.js$/, 28 | exclude: /node-modules/ 29 | }, 30 | ] 31 | } 32 | }; -------------------------------------------------------------------------------- /src/render/pages/Foo/models/foo.ts: -------------------------------------------------------------------------------- 1 | import * as api from '../services/foo' 2 | 3 | export interface DataItem { 4 | age: number 5 | name: string 6 | job: string 7 | id: number | string 8 | } 9 | 10 | export interface FooState { 11 | list: Array 12 | } 13 | 14 | export default { 15 | namespace: 'foo', 16 | state: { 17 | list: [] 18 | }, 19 | effects: { 20 | *fetch({ payload }: any, { call, put }: any) { 21 | const { data } = yield call(api.getFoo, payload) 22 | yield put({ 23 | type: 'save', 24 | payload: { list: data.list } 25 | }) 26 | } 27 | }, 28 | reducers: { 29 | save(state: FooState, { payload }: any) { 30 | return { ...state, ...payload } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/render/layouts/header/index.less: -------------------------------------------------------------------------------- 1 | .layout-top-eader { 2 | background-size: 100% 100%; 3 | background-repeat: no-repeat; 4 | padding: 0; 5 | color: white; 6 | 7 | .logo { 8 | width: 200px; 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | 13 | img { 14 | width: 44px; 15 | } 16 | 17 | h1 { 18 | color: white; 19 | font-size: 24px; 20 | margin: 0 0 0 15px; 21 | } 22 | } 23 | 24 | .header-shop-box { 25 | margin: 0 10px; 26 | .shop-name { 27 | width: 200px; 28 | padding: 0 9px; 29 | line-height: 34px; 30 | border-radius: 4px; 31 | box-shadow: 0 0 1px #ccc; 32 | } 33 | .ant-select { 34 | width: 200px; 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/render/utils/store/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Q: electron无法正常保存COOKIE STORAGE 3 | * 方案: electron客户端保存本地数据Store 4 | */ 5 | const Store = require('@utils/polyfill')('electron-store') 6 | 7 | const store = new Store() 8 | 9 | class GlobalStore { 10 | public get(name: string | string[]): any { 11 | if (Array.isArray(name)) { 12 | return name.map(n => store.get(n)) 13 | } else { 14 | return store.get(name) 15 | } 16 | } 17 | public save(key: string, val: any): void { 18 | store.set(key, val) 19 | } 20 | public delete(key: string | string[]): void { 21 | if (Array.isArray(key)) { 22 | key.forEach(k => store.delete(k)) 23 | } else { 24 | store.delete(key) 25 | } 26 | } 27 | } 28 | 29 | export default new GlobalStore() 30 | -------------------------------------------------------------------------------- /src/render/pages/Foo/components/TableList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TableProps, ColumnType } from 'antd/lib/table' 3 | import { Table } from 'antd' 4 | import { DataItem } from '../models/foo' 5 | 6 | const columns: Array> = [ 7 | { 8 | dataIndex: 'name', 9 | title: 'Name' 10 | }, 11 | { 12 | dataIndex: 'age', 13 | title: 'Age' 14 | }, 15 | { 16 | dataIndex: 'job', 17 | title: 'Job' 18 | } 19 | ] 20 | 21 | export interface IProps extends TableProps { 22 | loading: boolean 23 | } 24 | 25 | export default (props: IProps) => { 26 | const tableProps: TableProps = { 27 | ...props, 28 | columns, 29 | rowKey: (recode: DataItem) => recode.id 30 | } 31 | 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /src/render/global.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Umi 中约定 src/global.less 为全局样式 3 | */ 4 | @import './assets/style/bootstrap-part.less'; 5 | @import './assets/style/common.less'; 6 | 7 | :root { 8 | --header-height: 64px; 9 | } 10 | 11 | html, 12 | body, 13 | #root { 14 | height: 100%; 15 | } 16 | 17 | ul, 18 | li { 19 | list-style: none; 20 | margin: 0; 21 | padding: 0; 22 | } 23 | 24 | body { 25 | margin: 0; 26 | } 27 | 28 | // 滚动条美化 29 | * { 30 | &::-webkit-scrollbar { 31 | width: 7px; 32 | height: 7px; 33 | } 34 | 35 | &::-webkit-scrollbar-thumb { 36 | border-radius: 2px; 37 | // box-shadow: inset 0 0 5px rgba(99, 99, 99, .4); 38 | background: rgba(161, 78, 255, 0.24); // 滑动条 39 | } 40 | 41 | &::-webkit-scrollbar-track { 42 | box-shadow: inset 0 0 5px rgba(99, 99, 99, 0.24); 43 | border-radius: 2px; 44 | background: #ededed; // 条背景 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/render/pages/Home/WebSocket/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Card } from 'antd' 3 | import socketIOClient from "socket.io-client"; 4 | 5 | const ENDPOINT = "http://127.0.0.1:3002"; 6 | 7 | interface IProps { 8 | loading: boolean 9 | } 10 | 11 | const WebSocket = (props: IProps) => { 12 | const [response, setResponse] = useState(""); 13 | useEffect(() => { 14 | const socket = socketIOClient(ENDPOINT, { transport: ['websocket'] }); 15 | console.log('socket', socket) 16 | if (socket) { 17 | socket.on("FromAPI", (data: React.SetStateAction) => { 18 | setResponse(data); 19 | }); 20 | } 21 | }, []); 22 | return ( 23 | 24 | Settings 25 |

26 | WebSocket检测: 当前服务器返回时间: 27 |

28 |
29 | ) 30 | } 31 | 32 | export default WebSocket 33 | -------------------------------------------------------------------------------- /src/render/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义运行时配置 3 | * https://umijs.org/zh-CN/docs/runtime-config 4 | */ 5 | import React from 'react' 6 | import { ConfigProvider } from 'antd' 7 | import zhCN from 'antd/es/locale/zh_CN' 8 | import RightClickMenuFuc from './utils/rightClickMenuFuc' 9 | import ErrorBoundary from '@/layouts/error' 10 | 11 | // 注入右键复制黏贴 12 | RightClickMenuFuc() 13 | 14 | // 是否开启app自动更新, 默认关闭 15 | window.isOpenAutoUpdate = false 16 | 17 | // 定时器: 关闭electron的loading 18 | setTimeout(() => window.stopLoading(), 1000) 19 | 20 | // 覆写 render 21 | export function render(oldRender: () => void) { 22 | oldRender() 23 | } 24 | 25 | // 修改交给 react-dom 渲染时的根组件 26 | // https://umijs.org/zh-CN/docs/runtime-config 27 | export function rootContainer(container: React.ReactDOM) { 28 | return React.createElement( 29 | ErrorBoundary, 30 | {}, 31 | React.createElement(ConfigProvider, { locale: zhCN }, container) 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "allowJs": true, 13 | "outDir": "./dist", 14 | "paths": { 15 | "@/*": ["src/render/*"], 16 | "@@/*": ["src/render/.umi/*"], 17 | "@config/*": ["src/render/config/*"], 18 | "@components/*": ["src/render/components/*"], 19 | "@utils/*": ["src/render/utils/*"], 20 | "@typings/*": ["typings/*"], 21 | "@pages/*": ["src/render/pages/*"] 22 | }, 23 | "allowSyntheticDefaultImports": true 24 | }, 25 | // "include": ["./typings.d.ts"], 26 | // "typings": "./typings.d.ts", 27 | // "exclude": [ 28 | // "node_modules", 29 | // "dist", 30 | // "scripts", 31 | // "build", 32 | // ] 33 | } 34 | -------------------------------------------------------------------------------- /src/render/pages/Foo/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * redux实例 3 | */ 4 | import React from 'react' 5 | import { Card } from 'antd' 6 | import { connect } from 'umi' 7 | import { ConnectProps } from 'umi/types' 8 | import TableList from './components/TableList' 9 | import { FooState } from './models/foo' 10 | 11 | interface IProps extends ConnectProps { 12 | foo: FooState 13 | loading: boolean 14 | } 15 | 16 | const Foo = (props: IProps) => { 17 | const { dispatch = () => {}, foo } = props 18 | React.useEffect(() => { 19 | dispatch({ 20 | type: 'foo/fetch', 21 | payload: {} 22 | }) 23 | }, []) 24 | 25 | const data = React.useMemo(() => { 26 | return foo.list 27 | }, [foo.list]) 28 | 29 | return ( 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default connect(({ foo, loading }: any) => ({ 37 | foo, 38 | loading: loading.effects['foo/fetch'] 39 | }))(Foo) 40 | -------------------------------------------------------------------------------- /src/render/mock/foo.ts: -------------------------------------------------------------------------------- 1 | // 使用 Mock 2 | const Mock = require('mockjs') 3 | 4 | const barData = Mock.mock({ 5 | // 属性 list 的值是一个数组,其中含有 1 到 10 个元素 6 | 'list|1-10': [ 7 | { 8 | // 属性 id 是一个自增数,起始值为 1,每次增 1 9 | 'id|+1': 1 10 | } 11 | ] 12 | }) 13 | 14 | module.exports = { 15 | // 'GET /mock/api/goods/list': barData, 16 | 17 | 'POST /api/foo'(req: any, res: any) { 18 | res.json({ 19 | list: [ 20 | { id: 1, age: 29, name: 'anan', job: '前端组长' }, 21 | { id: 2, age: 36, name: 'andy', job: 'BOSS' }, 22 | { id: 3, age: 28, name: 'kevin', job: '前端开发' } 23 | ] 24 | }) 25 | }, 26 | 27 | 'GET /mock/api/data/goods/list': { 28 | success: true, // 网关层数据返回 29 | traceId: 'xxxxxxx', 30 | result: { 31 | success: true, 32 | result: { 33 | list: [ 34 | { 35 | id: 1, 36 | name: 'goods-1' 37 | } 38 | ], 39 | size: '10', 40 | pageSize: '1', 41 | total: '100' 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* const fabric = require('@umijs/fabric'); 2 | module.exports = { 3 | ...fabric.prettier, 4 | }; 5 | */ 6 | 7 | // .prettierrc.js 8 | module.exports = { 9 | // 一行最多 100 字符 10 | printWidth: 100, 11 | // 使用 2 个空格缩进 12 | tabWidth: 2, 13 | // 不使用缩进符,而使用空格 14 | useTabs: false, 15 | // 行尾需要有分号 16 | semi: false, 17 | // 使用单引号 18 | singleQuote: true, 19 | // 对象的 key 仅在必要时用引号 20 | quoteProps: 'as-needed', 21 | // jsx 使用单引号 22 | jsxSingleQuote: true, 23 | // 末尾不需要逗号 24 | trailingComma: 'none', 25 | // 大括号内的首尾需要空格 26 | bracketSpacing: true, 27 | // jsx 标签的反尖括号需要换行 28 | jsxBracketSameLine: false, 29 | // 箭头函数,只有一个参数的时候,也需要括号 30 | // arrowParens: 'always', 31 | // 每个文件格式化的范围是文件的全部内容 32 | rangeStart: 0, 33 | rangeEnd: Infinity, 34 | // 不需要写文件开头的 @prettier 35 | requirePragma: false, 36 | // 不需要自动在文件开头插入 @prettier 37 | insertPragma: false, 38 | // 使用默认的折行标准 39 | proseWrap: 'preserve', 40 | // 根据显示样式决定 html 要不要折行 41 | htmlWhitespaceSensitivity: 'css', 42 | // 换行符使用 lf 43 | endOfLine: 'lf' 44 | } 45 | -------------------------------------------------------------------------------- /server/src/utils/redis/redisConnection.ts: -------------------------------------------------------------------------------- 1 | import * as ioredis from 'ioredis'; 2 | import { resolve } from 'dns'; 3 | 4 | interface redisConfig { 5 | port:number, 6 | host:string, 7 | password?:string 8 | db?:number //连接的数据库id,默认为0,即第一个数据库 9 | family?:number //可填参数:4、6 (ipv4\ipv6) 10 | } 11 | 12 | let clientCreate = (config:redisConfig,callback_:Function) => { 13 | let redis:ioredis.Redis = new ioredis(config); 14 | redis.on('connect',()=>{ //根据 connect 事件判断连接成功 15 | callback_(null,redis) //链接成功, 返回 redis 连接对象 16 | }) 17 | redis.on('error',(err)=>{ //根据 error 事件判断连接失败 18 | callback_(err,null) //捕捉异常, 返回 error 19 | }) 20 | } 21 | 22 | let redisConn = (options?:redisConfig) => { 23 | let config = options 24 | return new Promise((resolve,reject) => { //返回API调用方 一个 promise 对象 25 | clientCreate(config,(err:any,conn:ioredis.Redis) => { 26 | if(err) { 27 | reject(err) 28 | } 29 | resolve(conn) //返回连接的redis对象 30 | }) 31 | }) 32 | } 33 | 34 | export default redisConn -------------------------------------------------------------------------------- /src/render/layouts/error/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ErrorInfo } from 'react' 2 | import { Spin } from 'antd' 3 | const log = require('electron-log'); 4 | 5 | export default class ErrorBoundary extends React.PureComponent { 6 | static getDerivedStateFromError() { 7 | return { hasError: true } 8 | } 9 | 10 | state = { hasError: false } 11 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 12 | const errorData = `${error.stack}\n${errorInfo.componentStack}` 13 | log.error(`ErrorBoundary-${new Date()}: `, errorData) 14 | if (process.env.NODE_ENV !== 'development') { // 生产环境才开启 15 | location.href = location.href.includes('index.html') ? `${location.origin}/index.html` : location.origin as string 16 | } 17 | } 18 | 19 | render() { 20 | const { children } = this.props 21 | const { hasError } = this.state 22 | 23 | if (hasError) { 24 | return ( 25 | 26 |
27 |
28 | ) 29 | } else { 30 | return children 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/render/config/menus.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 左侧菜单配置 3 | */ 4 | import React from 'react' 5 | import { AreaChartOutlined, createFromIconfontCN } from '@ant-design/icons' 6 | import { iconFonts, icons } from './iconfont' 7 | 8 | const isDev = process.env.APP_ENV === 'development' 9 | 10 | // iconfont.cn 11 | const IconFont = createFromIconfontCN({ 12 | // scriptUrl: '//at.alicdn.com/t/font_1492696_4ai9rbngxhe.js' 13 | scriptUrl: 'http://at.alicdn.com/t/font_1492696_4ai9rbngxhe.js' 14 | }) 15 | 16 | export interface IMenu { 17 | title: string 18 | path: string 19 | fullPath?: string 20 | icon?: React.ReactNode 21 | subs?: Array 22 | electron?: boolean 23 | } 24 | 25 | export default [ 26 | { 27 | title: 'Home', 28 | path: '/Home', 29 | icon: , 30 | subs: [ 31 | { 32 | title: 'WebSocket', 33 | path: '/WebSocket', 34 | fullPath: '/Home/WebSocket' 35 | }, 36 | { 37 | title: 'Edge', 38 | path: '/Edge', 39 | fullPath: '/Home/Edge' 40 | } 41 | ] 42 | } 43 | ] as Array 44 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa4typescript", 3 | "version": "1.0.0", 4 | "description": "use ts in koa2", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "dev": "nodemon --watch 'src/**/*' -e ts,tsx --exec npm run start ", 8 | "start": "tsc && cross-env NODE_ENV=development node dist/server.js", 9 | "build": "tsc", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "koa", 14 | "typescript" 15 | ], 16 | "author": "chrislee0211", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@types/ioredis": "^4.0.18", 20 | "@types/koa-bodyparser": "^4.3.0", 21 | "@types/log4js": "^2.3.5", 22 | "@types/socket.io": "^2.1.4", 23 | "@types/vfile-message": "^2.0.0", 24 | "cross-env": "^6.0.3", 25 | "ioredis": "^4.14.1", 26 | "koa": "^2.10.0", 27 | "koa-bodyparser": "^3.2.0", 28 | "koa-router": "^7.4.0", 29 | "log4js": "^5.3.0", 30 | "socket.io": "^2.3.0" 31 | }, 32 | "devDependencies": { 33 | "@types/koa": "^2.0.51", 34 | "@types/koa-router": "^7.0.42", 35 | "nodemon": "^1.19.4", 36 | "ts-node": "^8.4.1", 37 | "typescript": "^3.6.4" 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/controls/AppTray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * APP托盘 3 | */ 4 | 'use strict' 5 | 6 | const { app, Menu, Tray } = require('electron') 7 | const path = require('path') 8 | const trayIcon = `${path.join(__dirname, '/public/tray.png')}` 9 | 10 | module.exports = class AppTray { 11 | constructor(mainWindow) { 12 | this.mainWindow = mainWindow || null 13 | this.initTray() 14 | } 15 | 16 | initTray() { 17 | this.tray = new Tray(trayIcon) 18 | this.tray.setToolTip('我的托盘图标') 19 | 20 | const trayMenuTemplate = [ 21 | { 22 | label: '显示主页面', 23 | click: () => { 24 | this.mainWindow.show() 25 | } 26 | }, 27 | { 28 | label: '退出', 29 | click: () => { 30 | app.quit() 31 | } 32 | } 33 | ] 34 | const contextMenu = Menu.buildFromTemplate(trayMenuTemplate) 35 | // 设置此图标的上下文菜单 36 | this.tray.setContextMenu(contextMenu) 37 | // 单击右下角小图标显示应用 38 | this.tray.on('click', () => { 39 | this.mainWindow.isVisible() ? this.mainWindow.hide() : this.mainWindow.show() 40 | this.mainWindow.isVisible() 41 | ? this.mainWindow.setSkipTaskbar(false) 42 | : this.mainWindow.setSkipTaskbar(true) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # koa2 + typescript + websocket + redis + log4js 2 | 3 | ## 一、说明 4 | 5 | > - cors 跨域处理 6 | > - redis 7 | > - websocket 8 | > - log4js: 日志中间件 9 | 10 | ## 二、使用 11 | 12 | ``` 13 | npm install 14 | 15 | // 本地热加载开发 16 | npm run dev 17 | // 构建成js 18 | npm run build 19 | // 线上启动 pm2或run dev 20 | npm run pm2-start:watch 21 | 22 | 23 | ``` 24 | 25 | #### 前端使用 26 | 27 | ``` 28 | import React, { useEffect, useState } from 'react' 29 | import { Card } from 'antd' 30 | 31 | import socketIOClient from "socket.io-client"; 32 | 33 | 34 | const ENDPOINT = "http://127.0.0.1:8002"; 35 | 36 | interface IProps { 37 | loading: boolean 38 | } 39 | 40 | const WebSocket = (props: IProps) => { 41 | const [response, setResponse] = useState(""); 42 | useEffect(() => { 43 | const socket = socketIOClient(ENDPOINT, { transport: ['websocket'] }); 44 | console.log('socket', socket) 45 | if (socket) { 46 | socket.on("FromAPI", data => { 47 | setResponse(data); 48 | }); 49 | } 50 | }, []); 51 | return ( 52 | 53 | Settings 54 |

55 | WebSocket检测: 当前服务器返回时间: 56 |

57 |
58 | ) 59 | } 60 | 61 | export default WebSocket 62 | 63 | 64 | ``` 65 | -------------------------------------------------------------------------------- /src/render/utils/rightClickMenuFuc.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 右键复制黏贴 3 | */ 4 | const remote = require('electron').remote; 5 | const Menu = remote.Menu; 6 | const MenuItem = remote.MenuItem; 7 | 8 | 9 | const RightClickMenuFuc = () => { 10 | /** 11 | * 判断点击区域可编辑 12 | * @param {*} e 13 | */ 14 | function isEleEditable(e: any): any { 15 | if (!e) { 16 | return false; 17 | } 18 | // 为input标签或者contenteditable属性为true 19 | if (e.tagName === 'INPUT' || e.contentEditable === 'true') { 20 | return true; 21 | } else { 22 | // 递归查询父节点 23 | return isEleEditable(e.parentNode) 24 | } 25 | } 26 | 27 | const menu = new Menu(); 28 | menu.append(new MenuItem({ label: '粘贴', role: 'paste' })); 29 | 30 | const menu2 = new Menu(); 31 | menu2.append(new MenuItem({ label: '复制', role: 'copy' })); 32 | window.addEventListener('contextmenu', (e) => { // 上下文监听事件 33 | e.preventDefault(); 34 | if (isEleEditable(e.target)) { 35 | menu.popup(remote.getCurrentWindow()); 36 | } else { 37 | // 判断有文本选中 38 | let selectText = window?.getSelection()?.toString(); 39 | if (selectText) { 40 | menu2.popup(remote.getCurrentWindow()); 41 | } 42 | } 43 | 44 | }, false) 45 | } 46 | 47 | export default RightClickMenuFuc 48 | 49 | -------------------------------------------------------------------------------- /src/render/utils/axiosRequest.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * axios二次封装 3 | */ 4 | 5 | import axios from 'axios' 6 | 7 | const instance = axios.create({ 8 | baseURL: process.env.REACT_APP_URL, 9 | timeout: 10000 10 | }) 11 | 12 | // 请求拦截 13 | instance.interceptors.request.use( 14 | config => { 15 | // config.headers['X-Token'] = '' 16 | return config 17 | }, 18 | error => { 19 | console.error(error) 20 | return Promise.reject(error) 21 | } 22 | ) 23 | // 响应拦截 24 | instance.interceptors.response.use( 25 | response => { 26 | const res = response.data 27 | if (res.code === 200) { 28 | return res.data 29 | } else { 30 | console.error('INSTANCE_ERROR') 31 | } 32 | }, 33 | error => { 34 | return Promise.reject(error) 35 | } 36 | ) 37 | 38 | export const getRequest = (url: any) => { 39 | return instance({ 40 | method: 'get', 41 | url: url 42 | }); 43 | } 44 | 45 | export const postRequest = (url: any, params: any) => { 46 | return instance({ 47 | method: 'post', 48 | url: url, 49 | data: params, 50 | transformRequest: [(data) => { 51 | let ret = '' 52 | for (const it in data) { 53 | ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&' 54 | } 55 | return ret 56 | }] 57 | }); 58 | } -------------------------------------------------------------------------------- /server/src/utils/logger/globalLog.ts: -------------------------------------------------------------------------------- 1 | import * as log4js from 'log4js'; 2 | import default_redis from '../redis/redisOperate'; 3 | 4 | const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]; 5 | const contextLogger:any = {}; 6 | log4js.configure({ 7 | appenders: { 8 | console: { 9 | type: 'stdout', 10 | }, 11 | cheese: { 12 | type: 'dateFile', 13 | encoding: 'utf-8', 14 | filename: 'logs/globalLog', 15 | layout: { 16 | type: "pattern", 17 | pattern: '{"date":"%d","level":"%p","data":\'%m\'}' 18 | }, 19 | pattern: "-yyyy-MM-dd.log", 20 | alwaysIncludePattern: true, 21 | }, 22 | }, 23 | categories: {default: {appenders: ['cheese','console'], level: 'info'}} 24 | }); 25 | 26 | const bindLog = function () { 27 | 28 | const logger:any = log4js.getLogger('cheese'); 29 | // 循环 methods 将所有方法挂载到ctx 上 30 | methods.forEach((method:any) => { 31 | contextLogger[method] = (message:any) => { 32 | logger[method](message) 33 | } 34 | }); 35 | // 为 ctx 增加 log 方法 36 | Object.defineProperty(global, 'log', { 37 | value: contextLogger, 38 | writable: false, 39 | enumerable: true, 40 | configurable: false 41 | }); 42 | 43 | }; 44 | 45 | export default bindLog -------------------------------------------------------------------------------- /scripts/webpack.main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 主进程 webpack 配置 3 | */ 4 | const path = require('path'); 5 | 6 | const resolve = (dir = '') => path.join(__dirname, '../src/main', dir) // 指向 src/main 7 | 8 | module.exports = function (env) { 9 | process.env.NODE_ENV = env; 10 | const isDev = env === 'development'; 11 | 12 | return { 13 | mode: isDev ? 'development' : 'production', 14 | devtool: isDev ? undefined : 'cheap-module-source-map', 15 | target: 'electron-main', 16 | entry: resolve('index.js'), 17 | output: { 18 | path: resolve(), 19 | filename: 'bundle.js', 20 | }, 21 | node: { 22 | __dirname: false, 23 | __filename: false 24 | }, 25 | resolve: { 26 | extensions: ['.js', '.json', '.ts'], 27 | alias: { 28 | '@root': path.join(__dirname, '..'), 29 | }, 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.(js|ts)$/, 35 | exclude: /node-modules/, 36 | loader: 'babel-loader', 37 | options: { 38 | plugins: ["@babel/plugin-transform-typescript"], 39 | }, 40 | }, 41 | { 42 | test: /\.node$/, 43 | loader: isDev ? 'node-loader' : 'relative-loader', 44 | options: isDev ? undefined : { 45 | relativePath: '/aclas-addons/' 46 | } 47 | }, 48 | ] 49 | } 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /server/src/utils/fileOperation/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | /** 5 | * 写入文件工具,所有静态文件m默认存在static文件夹内 6 | * @param data 写入的内容 7 | * @param fileName 文件名称 8 | * @returns {string} 若写入成功则返回文件路径,否则返回undefined 9 | * @author chrislee 10 | * @Time 2020/4/11 11 | */ 12 | export const writeFile = async (data:any,fileName:string):Promise => { 13 | let fileAddr:string = ``; 14 | fileAddr = path.resolve(__dirname,`../../static/${fileName}`); 15 | return new Promise((resolve,reject) => { 16 | fs.writeFile(fileAddr,data,(err:NodeJS.ErrnoException) => { 17 | if(err){ 18 | reject(undefined) 19 | }else{ 20 | resolve(fileAddr) 21 | } 22 | }) 23 | }) 24 | } 25 | 26 | 27 | /** 28 | * 删除指定的文件名 29 | * @param fileName 文件名称 30 | * @returns {string} 若删除成功,返回文件名称,否则返回undefined 31 | * @author chrislee 32 | * @Time 2020/4/11 33 | */ 34 | export const removeFile = async (fileName:string):Promise => { 35 | let fileAddr:string = ``; 36 | fileAddr = path.resolve(__dirname,`../../static/${fileName}`); 37 | return new Promise((resolve,reject) => { 38 | fs.unlink(fileAddr,(err:NodeJS.ErrnoException)=>{ 39 | if(err){ 40 | reject(undefined) 41 | }else{ 42 | resolve(fileName) 43 | } 44 | }) 45 | }) 46 | } -------------------------------------------------------------------------------- /src/render/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './home.normal.less' 3 | import AutoUpdate from '@components/AutoUpdate' 4 | import { Button, message, Input } from 'antd' 5 | const Store = require('electron-store') 6 | const store = new Store() 7 | 8 | export default function () { 9 | const getLocalStoreData = () => { 10 | message.info(store.get('LOCAL_ELECTRON_STORE')) 11 | } 12 | return ( 13 |
14 |
Welcome to MapleChain Electron App
15 | 16 |
electron-store
17 |
18 | 26 | 29 |
30 |
RightClickMenuFuc
31 |
32 | Here is the copy msg | 选中任意文字后右键复制 33 | 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/render/layouts/menu/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 左侧菜单 3 | */ 4 | import React from 'react' 5 | import { Menu } from 'antd' 6 | import { useHistory, useLocation } from 'umi' 7 | import menus, { IMenu } from '@config/menus' 8 | import styles from './index.less' 9 | 10 | const { SubMenu, Item } = Menu 11 | 12 | export default (props: any) => { 13 | const history = useHistory() 14 | const location = useLocation() 15 | 16 | const clickMenu = (path: string) => { 17 | history.push(path) 18 | } 19 | 20 | const generate = (menus: Array, deep = [] as Array) => { 21 | return menus.map(menu => { 22 | const paths = [...deep, menu.path] 23 | const path = paths.join('') 24 | 25 | return Array.isArray(menu.subs) && menu.subs.length ? ( 26 | 31 | {generate(menu.subs, paths)} 32 | 33 | ) : ( 34 | clickMenu(path)} 37 | title={menu.title} 38 | icon={menu.icon} 39 | > 40 | {menu.title} 41 | 42 | ) 43 | }) 44 | } 45 | 46 | return ( 47 | 52 | {generate(menus)} 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/main/script/build.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webpack 构建 3 | */ 4 | const webpack = require('webpack'); 5 | const chalk = require('chalk'); 6 | const config = require('../config/webpack.config'); 7 | 8 | const compiler = webpack(config); 9 | const TAG = '[src/main/script/build.js]'; 10 | 11 | compiler.hooks.beforeCompile.tap(TAG, arg0 => { 12 | console.log(TAG, chalk.yellow('Electron webpack 开始构建')); 13 | }); 14 | 15 | compiler.run((err, stats) => { 16 | if (err) { 17 | // err 对象将只包含与webpack相关的问题,例如错误配置等 18 | console.log(TAG, chalk.red('💥 Electron webpack 相关报错')); 19 | } else if (stats.hasErrors()) { 20 | // webpack 编译报错 21 | const json = stats.toJson('errors-only'); 22 | console.log(TAG, filterLogs(json.errors)().join('\n')); 23 | console.log(TAG, chalk.red('💥 Electron 构建报错')); 24 | } else { 25 | console.log(TAG, chalk.green('Electron webpack 构建完成')); 26 | } 27 | }); 28 | 29 | /** 30 | * webpack 日志过滤 31 | */ 32 | function filterLogs(errors) { 33 | let tmp = []; 34 | return function (filter = true) { 35 | if (filter) { 36 | errors.forEach(err => { 37 | if (err.includes('Error: Child compilation failed:')) { 38 | // 忽略 webpack 内部调用错误栈 39 | return; 40 | } 41 | if (!tmp.find(_ => _.split('\n')[1] === err.split('\n')[1])) { 42 | // 一个错误,可能会被爆出多次,做下报错去重 43 | // 比如一个 loader 报错,那么 n 个文件经过 loader 就会报出 n 个错误 44 | tmp.push(err); 45 | } 46 | }); 47 | } else { 48 | tmp = errors; 49 | } 50 | return tmp; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa'; // koa框架 2 | import getConfig from './config'; 3 | import * as http from 'http'; 4 | import * as socketIO from 'socket.io' 5 | import globalLogger from './utils/logger/globalLog' 6 | import log from './middleware/log' 7 | // 路由分发 8 | import routerMount from './router/index'; 9 | 10 | 11 | // 中间件 12 | import cors from './middleware/cors'; 13 | import * as bodyParser from 'koa-bodyparser'; 14 | 15 | const app = new Koa(); // 新建一个koa应用 16 | const env = process.env.NODE_ENV 17 | const PORT: number | string = getConfig(env).basePort; 18 | const server = http.createServer(app.callback()); 19 | // const io = socketIO(server, { pingInterval: 20000 }) 20 | const io = socketIO(server) 21 | 22 | 23 | let interval; 24 | 25 | io.on("connection", (socket) => { 26 | console.log("New client connected"); 27 | if (interval) { 28 | clearInterval(interval); 29 | } 30 | // getApiAndEmit(socket) 31 | interval = setInterval(() => getApiAndEmit(socket), 3000); 32 | socket.on("disconnect", () => { 33 | console.log("Client disconnected"); 34 | clearInterval(interval); 35 | }); 36 | }); 37 | 38 | const getApiAndEmit = socket => { 39 | const response = new Date(); 40 | // 向客户端发送事件 41 | socket.emit("FromAPI", response); 42 | }; 43 | 44 | 45 | app.use(cors) 46 | app.use(log()) 47 | app.use(bodyParser()) 48 | routerMount(app) 49 | 50 | globalLogger() 51 | 52 | 53 | const _server = app.listen(PORT); // 监听应用端口 54 | 55 | server.listen(_server) // 监听socket端口 必须要监听该端口服务 不然405 56 | // server.listen(getConfig(env).baseSocketPort) // 监听socket端口 57 | 58 | 59 | console.log(`Server running on port ${PORT}`); -------------------------------------------------------------------------------- /src/main/util/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 工具集 4 | */ 5 | const fs = require('fs') 6 | const path = require('path') 7 | const log = require('electron-log'); 8 | const axios = require('axios').default; 9 | 10 | /** 11 | * 删除文件或文件夹 12 | * @param {string} dir 文件夹位置 13 | */ 14 | function deleteDirSync(dir) { 15 | let files = fs.readdirSync(dir) 16 | // eslint-disable-next-line @typescript-eslint/prefer-for-of 17 | for (let i = 0;i < files.length;i++) { 18 | let newPath = path.join(dir, files[i]); 19 | let stat = fs.statSync(newPath) 20 | if (stat.isDirectory()) { 21 | // 如果是文件夹就递归下去 22 | deleteDirSync(newPath); 23 | } else { 24 | // 删除文件 25 | fs.unlinkSync(newPath); 26 | } 27 | } 28 | fs.rmdirSync(dir)// 如果文件夹是空的,删除自身 29 | } 30 | 31 | 32 | /** 33 | * 通过main进程发送事件给renderer进程,提示更新信息 34 | * @param {string} text 信息 35 | * @param {object} mainWindow 实例 36 | */ 37 | function sendUpdateMessage(text, mainWindow) { 38 | log.info('enter sendUpdateMessage', text); 39 | mainWindow.webContents.send('message', text) 40 | } 41 | 42 | /** 43 | * 下载远程压缩包并写入指定文件 44 | * @param {string} 远程地址 45 | * @param {string} 文件名 46 | */ 47 | function downloadFile(uri, filename) { 48 | const writer = fs.createWriteStream(filename) 49 | axios.get(uri, { responseType: 'stream' }).then(res => { 50 | res.data.pipe(writer); 51 | }); 52 | return new Promise((resolve, reject) => { 53 | writer.on('finish', resolve); 54 | writer.on('error', reject); 55 | }); 56 | } 57 | 58 | module.exports = { 59 | deleteDirSync, 60 | downloadFile, 61 | sendUpdateMessage 62 | } -------------------------------------------------------------------------------- /src/render/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | import { Layout } from 'antd' 3 | import { useHistory, useLocation, useStore } from 'umi' 4 | import Header from './header' 5 | import SideMenu from './menu' 6 | import store from '@utils/store/' 7 | import styles from './index.less' 8 | 9 | const { Content, Sider } = Layout 10 | 11 | 12 | const BasicLayout: React.FC = (props: any) => { 13 | const location = useLocation() 14 | const history = useHistory() 15 | const [collapsed, setCollapsed] = useState(false) 16 | const [shopCurrent, setShopCurrent] = useState(store.get('')) 17 | 18 | const clickCollapse = useCallback(() => { 19 | setCollapsed(!collapsed) 20 | }, [collapsed]) 21 | const changeShop = useCallback(shop => { 22 | setShopCurrent(shop) 23 | }, [shopCurrent]) 24 | 25 | const LayoutMain = ( 26 | 27 |
32 | 33 | {/* 左侧菜单 */} 34 | 35 | 36 | 37 | 38 | {/* 右侧 content */} 39 | 40 | {props.children} 41 | 42 | 43 | 44 | ) 45 | 46 | const LayoutLogin = props.children 47 | 48 | const LayoutDict: any = { 49 | '/User/login': LayoutLogin 50 | } 51 | 52 | return LayoutDict[location.pathname] ? LayoutDict[location.pathname] : LayoutMain 53 | } 54 | 55 | export default BasicLayout 56 | -------------------------------------------------------------------------------- /src/render/utils/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * url参数查询 3 | * @param {string} [url=location.search] - url地址 4 | * @param {string} [query] - 查询参数 5 | * @param {boolean} [decode=true] - 返回的查询值是否需要解码 6 | * @returns {object|string} 7 | */ 8 | export const getParams = ({ 9 | url = window.location.search, 10 | query, 11 | decode = true 12 | }: { url?: string; query?: string; decode?: boolean } = {}) => { 13 | const paramStr = url.split('?')[1] 14 | const paramArr = (paramStr && paramStr.split('&')) || [] 15 | const params: any = {} 16 | paramArr.forEach((param, i) => { 17 | const paramData = param.split('=') 18 | params[paramData[0]] = decode ? decodeURIComponent(paramData[1]) : paramData[1] 19 | }) 20 | return query ? params[query] : params 21 | } 22 | 23 | /** 24 | * url添加参数 25 | * @param {string} url - 需要添加参数的url 26 | * @param {object} params - 添加的参数,参数是'key:value'形式 27 | * @param {boolean} [encode=false] - 返回的url是否需要编码 28 | * @returns {string} 29 | */ 30 | export function addParams({ 31 | url = '', 32 | params = {}, 33 | encode = false 34 | }: { 35 | url?: string 36 | params: object 37 | encode?: boolean 38 | }) { 39 | if (!Object.keys(params).length) { 40 | return url 41 | } 42 | url = decodeURIComponent(url) 43 | const [hostStr, searchStr] = url.split('?') 44 | if (url.includes('?')) { 45 | const oldParams: any = {} 46 | searchStr.split('&').forEach(val => { 47 | const newVal = val.split('=') 48 | oldParams[newVal[0]] = newVal[1] 49 | }) 50 | // 合并、去重参数 51 | params = { ...oldParams, ...params } 52 | } 53 | let [paramsStr, i] = ['', 1] 54 | for (const [key, val] of Object.entries(params)) { 55 | paramsStr += i > 1 ? `&${key}=${val}` : `${key}=${val}` 56 | i++ 57 | } 58 | const baseUrl = `${hostStr}?${paramsStr}` 59 | return encode ? encodeURIComponent(baseUrl) : baseUrl 60 | } 61 | -------------------------------------------------------------------------------- /src/render/.umirc.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const isWeb = process.env.TARGET === 'web' 4 | const isDev = process.env.APP_ENV === 'development' 5 | const isProd = process.env.APP_ENV === 'production' 6 | const resolvePath = dir => path.join(__dirname, dir) 7 | 8 | const webpack_dev = config => { 9 | return ( 10 | config 11 | // .devtool('eval-cheap-source-map') // 是否开启sourceMap 12 | .target('electron-renderer') 13 | ) 14 | } 15 | const webpack_dev_web = config => { 16 | // web端运行localhost:9090 一般不用 17 | return config 18 | .devtool('eval-cheap-source-map') 19 | .node.set('fs', 'empty') 20 | .set('worker_threads', 'empty') 21 | .set('electron', 'empty') 22 | .set('electron-is-dev', 'empty') 23 | .set('electron-store', 'empty') 24 | .set('electron-updater', 'empty') 25 | } 26 | const webpack_prod = config => { 27 | return config.target('electron-renderer') 28 | } 29 | 30 | const chainWebpack = config => { 31 | return ( 32 | config 33 | // .devtool('eval-cheap-source-map') // 是否开启sourceMap 34 | .target('electron-renderer') 35 | ) 36 | } 37 | 38 | export default { 39 | chainWebpack, // 热加载需要 40 | // 是否编译 node_modules 41 | nodeModulesTransform: { 42 | // 打包加速 43 | type: 'none' 44 | }, 45 | // 生成资源带 hash 尾缀 46 | // 开发模式下 umi 会忽略此选项,不然热重载会出问题(很贴心) 47 | hash: true, 48 | // url 格式 49 | history: { 50 | type: 'hash' 51 | }, 52 | // script、link 标签资源引入路径 53 | publicPath: './', 54 | // antd 主题配置 55 | theme: { 56 | '@primary-color': '#A14EFF', 57 | '@link-color': '#A14EFF', 58 | '@font-family': '"futura-pt", sans-serif', 59 | '@line-height-base': '1.3', 60 | '@border-radius-base': '6px' 61 | }, 62 | // 路径别名 63 | alias: { 64 | '@': resolvePath(''), 65 | '@components': resolvePath('components'), 66 | '@config': resolvePath('config'), 67 | '@utils': resolvePath('utils'), 68 | '@pages': resolvePath('pages') 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/src/middleware/cors.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa'; 2 | import * as URL from 'url'; 3 | 4 | const cors = async (ctx: Koa.Context, next: Function) => { 5 | const origin = URL.parse(ctx.get('origin') || ctx.get('referer') || ''); 6 | // 允许来自所有域名请求 7 | ctx.set("Access-Control-Allow-Origin", `${origin.protocol}//${origin.host}`); 8 | // ctx.set("Access-Control-Allow-Origin", `*`); 9 | // 这样就能只允许 http://localhost:8080 这个域名的请求了 10 | // ctx.set("Access-Control-Allow-Origin", "http://localhost:9090"); 11 | 12 | // 设置所允许的HTTP请求方法 13 | ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE"); 14 | 15 | // 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段. 16 | ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type"); 17 | 18 | // 服务器收到请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。 19 | 20 | // Content-Type表示具体请求中的媒体类型信息 21 | ctx.set("Content-Type", "application/json;charset=utf-8"); 22 | 23 | // 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。 24 | // 当设置成允许请求携带cookie时,需要保证"Access-Control-Allow-Origin"是服务器有的域名,而不能是"*"; 25 | ctx.set("Access-Control-Allow-Credentials", 'true'); 26 | 27 | // 该字段可选,用来指定本次预检请求的有效期,单位为秒。 28 | // 当请求方法是PUT或DELETE等特殊方法或者Content-Type字段的类型是application/json时,服务器会提前发送一次请求进行验证 29 | // 下面的的设置只本次验证的有效时间,即在该时间段内服务端可以不用进行验证 30 | ctx.set("Access-Control-Max-Age", '300'); 31 | 32 | /* 33 | CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段: 34 | Cache-Control、 35 | Content-Language、 36 | Content-Type、 37 | Expires、 38 | Last-Modified、 39 | Pragma。 40 | */ 41 | // 需要获取其他字段时,使用Access-Control-Expose-Headers, 42 | // getResponseHeader('myData')可以返回我们所需的值 43 | //https://www.rails365.net/articles/cors-jin-jie-expose-headers-wu 44 | ctx.set("Access-Control-Expose-Headers", "myData"); 45 | 46 | if (ctx.method !== 'OPTIONS') { 47 | // 如果请求类型为非预检请求,则进入下一个中间件(包括路由中间件等) 48 | await next(); 49 | } else { 50 | // 当为预检时,直接返回204,代表空响应体 51 | ctx.body = ''; 52 | ctx.status = 204; 53 | } 54 | } 55 | 56 | export default cors -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * electron 主进程入口 3 | */ 4 | 'use strict' 5 | 6 | const { app, ipcMain } = require('electron') 7 | const AppMainWindow = require('./controls/AppMainWindow') 8 | const AppTray = require('./controls/AppTray') 9 | const Store = require('electron-store') 10 | const store = new Store() 11 | const os = require('os') 12 | const electronDev = require('electron-is-dev') 13 | 14 | // 测试用 15 | store.set('LOCAL_ELECTRON_STORE', 'STORE-MSG: WELCOME TO MY TPL') 16 | // 渲染进程保证node api可用,9.0版本已经默认开启 17 | app.allowRendererProcessReuse = true 18 | // win7部分系统白屏优化: 下关闭硬件加速 19 | const isWin7 = os.release().startsWith('6.1') 20 | if (isWin7) app.disableHardwareAcceleration() 21 | 22 | class MainApp { 23 | constructor() { 24 | this.mainWindow = null 25 | this.tray = null 26 | } 27 | init() { 28 | this.initAppLife() 29 | this.initIPC() 30 | } 31 | 32 | // 创建右键托盘 33 | createTray() { 34 | this.tray = new AppTray(this.mainWindow) 35 | } 36 | 37 | // 创建主进程窗口 38 | createMainWindow() { 39 | this.mainWindow = new AppMainWindow() 40 | this.createTray() 41 | electronDev || this.handleKeepSingleApp() 42 | } 43 | 44 | handleKeepSingleApp() { 45 | // 限制只可以打开一个应用 46 | const gotTheLock = app.requestSingleInstanceLock() 47 | console.log('gotTheLock', gotTheLock) 48 | if (!gotTheLock) { 49 | app.quit() 50 | } else { 51 | app.on('second-instance', (event, commandLine, workingDirectory) => { 52 | // 当运行第二个实例时,将会聚焦到mainWindow这个窗口 53 | if (this.mainWindow) { 54 | if (this.mainWindow.isMinimized()) this.mainWindow.restore() 55 | this.mainWindow.focus() 56 | this.mainWindow.show() 57 | } 58 | }) 59 | } 60 | } 61 | 62 | // 生命周期 63 | initAppLife() { 64 | app.whenReady().then(() => { 65 | this.createMainWindow() 66 | }) 67 | 68 | app.on('window-all-closed', () => { 69 | console.log('window-all-closed') 70 | // 在 macOS 上,除非用户用 Cmd + Q 确定地退出,否则绝大部分应用及其菜单栏会保持激活。 71 | console.log('process.platform', process.platform) 72 | if (process.platform !== 'darwin') { 73 | console.log('quit') 74 | app.quit() 75 | } 76 | }) 77 | 78 | app.on('before-quit', () => { 79 | console.log('before-quit') 80 | this.mainWindow.destoryMainWindow() 81 | }) 82 | } 83 | 84 | // 可作为IPC通讯集合 85 | initIPC() { 86 | // 关闭启动loading层, 移除loading view视图 87 | ipcMain.on('stop-loading-main', () => { 88 | this.mainWindow.removeView() 89 | }) 90 | } 91 | } 92 | 93 | // 初始化 94 | new MainApp().init() 95 | -------------------------------------------------------------------------------- /scripts/main-build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | /** 3 | * 主进程 webpack 构建 4 | * electron 启动交给 electron-connect 5 | */ 6 | const path = require('path'); 7 | const child_process = require('child_process'); 8 | const electron = require('electron'); 9 | const webpack = require('webpack'); 10 | const chalk = require('chalk'); 11 | const ora = require('ora'); 12 | const wait_on = require('wait-on'); 13 | const argv = require('minimist')(process.argv.slice(2)); 14 | const config_factory = require('./webpack.main'); 15 | require('dotenv').config({ path: path.join(__dirname, '../src/render/.env') }); 16 | 17 | const root = path.join(__dirname, '..'); 18 | const main = process.env['npm_package_main']; 19 | const { env, port = process.env.PORT, watch } = argv; 20 | const spinner = ora('Electron webpack build...'); 21 | const TAG = 'scripts/main-build.js'; 22 | let watching = null; 23 | let child = null; 24 | 25 | const compiler = webpack(config_factory(env)); 26 | compiler.hooks.afterCompile.tap(TAG, () => { 27 | spinner.stop(); 28 | if (watch) { // 开发模式 29 | if (child) { process.kill(child.pid, 'SIGINT'); } 30 | child = child_process.spawn( 31 | electron, 32 | [path.join(root, main)], 33 | { stdio: 'inherit', } 34 | ); 35 | } 36 | }); 37 | compiler.hooks.beforeCompile.tap(TAG, () => { 38 | spinner.start(); 39 | }); 40 | 41 | function compileHandle(err, stats) { 42 | if (err) { 43 | // err 对象将只包含与webpack相关的问题,例如错误配置等 44 | console.log(TAG, chalk.red('💥 Electron webpack 相关报错')); 45 | console.log(err); 46 | 47 | // 关闭 wepback 监听 48 | watching && watching.close(() => console.log(TAG, 'Watching Ended.')); 49 | process.exit(1); 50 | } else if (stats.hasErrors()) { 51 | // webpack 编译报错 52 | const json = stats.toJson('errors-only'); 53 | console.log(TAG, json.errors); 54 | console.log(TAG, chalk.red('💥 Electron 构建报错')); 55 | } else { 56 | console.log(TAG, chalk.green('Electron webpack 构建完成')); 57 | } 58 | } 59 | 60 | if (watch) { // 开发模式 61 | const opts = { 62 | resources: [`http://localhost:${port}`], // PORT === port 63 | interval: 900, // poll interval in ms, default 250ms 64 | log: false, 65 | }; 66 | 67 | // 等待 webpack-dev-server 启动后启动 electron 68 | wait_on(opts, function (err) { 69 | if (err) { 70 | console.log(err); 71 | process.exit(1); 72 | } 73 | // once here, all resources are available 74 | watching = compiler.watch({ 75 | ignored: /bundle\.js(\.map)?/, 76 | }, compileHandle); 77 | }); 78 | } else { // 构建模式 79 | compiler.run(compileHandle); 80 | } -------------------------------------------------------------------------------- /src/render/components/AutoUpdate/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 组件控制app更新 3 | */ 4 | 5 | import React, { useEffect } from 'react' 6 | import { Modal, message, Button } from 'antd' 7 | import app, { ipcRenderer as ipc, remote } from 'electron' 8 | import styles from './style.less' 9 | import { checkForPartUpdates } from '@/utils/autoUpdate/partUpdate' 10 | const { confirm } = Modal 11 | interface IProps { 12 | loading?: boolean 13 | } 14 | 15 | const appPkgVersion = remote.app.getVersion() 16 | 17 | const Update = (props: IProps) => { 18 | console.log('mainApp', app) 19 | console.log('ipc', ipc) 20 | // 模拟 自动更新 21 | useEffect(() => { 22 | // 检测app自动更新 23 | // if (window.isOpenAutoUpdate) { 24 | const container = document.getElementById('container') // TODO: 自动更新进度条 25 | // console.log('ipc', ipc) 26 | if (ipc) { 27 | 28 | ipc.on('message', (event, text) => { 29 | console.log('text', text) 30 | if (text.indexOf('ERROR') !== -1) { 31 | if (text.indexOf('ERR_CONNECTION_REFUSED') !== -1) { 32 | message.info('获取服务器地址异常') 33 | } else { 34 | message.info(text) 35 | } 36 | return 37 | } 38 | 39 | message.info(text) 40 | const msg = document.createElement('div') 41 | msg.innerText = text 42 | console.log('message', msg) 43 | if (msg && container) { 44 | container.appendChild(msg) 45 | } 46 | }) 47 | 48 | // 因为如果安装文件过小的话,很快就下载完成,导致没能达到触发条件。 49 | ipc.on('downloadProgress', (event, { percent }) => { 50 | console.log(percent) 51 | }) 52 | 53 | // 接收到主进程有新的版本已经下载完成,询问是否更新。 54 | ipc.on('isUpdateNow', () => { 55 | confirm({ 56 | title: '确定要现在升级吗?', 57 | content: '更新内容:...', 58 | onOk() { 59 | ipc.send('updateNow') 60 | }, 61 | onCancel() { 62 | console.log('update canceled') 63 | } 64 | }) 65 | }) 66 | } 67 | // } 68 | }, [window.isOpenAutoUpdate]) 69 | 70 | const updateNewestVersion = () => { 71 | ipc && ipc.send('checkForUpdates') 72 | } 73 | 74 | const updateSpecifiedVersion = (v: string) => { 75 | const updateInfo = { // TODO: 接口返回 76 | isUpdate: true, 77 | // updateUrl: 'http://127.0.0.1:4002/download/' 78 | updateUrl: 'your server exe/dmg url' + v 79 | } 80 | ipc && updateInfo.isUpdate && ipc.send('checkForUpdates', updateInfo) // 如果强更才发起通知 81 | } 82 | return ( 83 |
84 |
Version: {appPkgVersion}
85 | {window.isOpenAutoUpdate &&
} 86 | {!window.isOpenAutoUpdate && 87 |
88 | 91 | 94 |
} 95 |
96 | ) 97 | } 98 | 99 | export default Update -------------------------------------------------------------------------------- /src/render/utils/autoUpdate/partUpdate.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * app更新验证 4 | */ 5 | const app = require('@utils/polyfill')('electron') 6 | const isElectronDev = require('electron-is-dev') 7 | const axios = require('axios').default; 8 | import { message, Modal } from 'antd' 9 | import { ExclamationCircleOutlined } from '@ant-design/icons'; 10 | const { confirm } = Modal; 11 | const { ipcRenderer: ipc, remote } = app 12 | 13 | let localYmlUrl = `` 14 | const remoteYmlURL = 'http://yourFileServer/app-update.yml' // 这里配置你的远程文件服务器 15 | 16 | if (!isElectronDev && process.platform === 'win32') { // win平台 17 | localYmlUrl = `./resources/app-update.yml` 18 | } 19 | if (!isElectronDev && process.platform === 'darwin') { // mac平台 20 | localYmlUrl = `/Applications/${remote.app.getName()}.app/Contents/Resources/resources/app-update.yml` 21 | } 22 | 23 | // 下载远程压缩包并写入指定文件 24 | function downloadFile(uri) { 25 | return new Promise((resolve, reject) => { 26 | axios.get(uri).then(res => { 27 | return resolve(res) 28 | }).catch(e => { 29 | console.error(e) 30 | message.info('获取远程版本失败') 31 | // eslint-disable-next-line prefer-promise-reject-errors 32 | return reject('获取远程版本失败') 33 | }); 34 | }); 35 | } 36 | 37 | function checkVersion(params) { 38 | return new Promise((resolve, reject) => { 39 | const currentVersion = remote.app.getVersion() 40 | // 获取最新版本号 41 | downloadFile(remoteYmlURL, localYmlUrl).then(res => { 42 | const remoteVersion = JSON.stringify(res.data).split('\\n')[0].split(' ')[1] 43 | const remoteVersionArr = remoteVersion.split('.') 44 | const currentVersionArr = currentVersion.split('.') 45 | // 0.1.1 Y和Z比较来开启增量更新 1.1.1 X比较来开启全量更新 46 | if (Number(remoteVersionArr[0]) > Number(currentVersionArr[0])) { 47 | // 开启全量更新 48 | return resolve('OPEN_ALL_UPDATE') 49 | } else if (Number(remoteVersionArr[2]) > Number(currentVersionArr[2]) || Number(remoteVersionArr[1]) > Number(currentVersionArr[1])) { 50 | // 开启增量更新 51 | return resolve('OPEN_PART_UPDATE') 52 | } else { 53 | console.log('无版本变动,不更新') 54 | } 55 | }).catch(e => { 56 | console.error(e) 57 | }) 58 | }) 59 | } 60 | 61 | /** 检查更新 */ 62 | export async function checkForPartUpdates() { 63 | try { 64 | // check version 检查版本 65 | const res = await checkVersion() 66 | if (res && res === 'OPEN_PART_UPDATE') { 67 | // 增量更新 68 | console.log('OPEN_PART_UPDATE') 69 | confirm({ 70 | title: '检测到更新', 71 | icon: , 72 | content: ( 73 |
74 |

是否更新?

75 |
76 | ), 77 | okText: '确认', 78 | cancelText: '取消', 79 | onOk() { 80 | ipc && ipc.send('checkForPartUpdates') 81 | message.info('请耐心等待几秒..') 82 | }, 83 | onCancel() { 84 | console.log('Cancel'); 85 | }, 86 | }); 87 | // partUpdates() 88 | } 89 | if (res && res === 'OPEN_ALL_UPDATE') { 90 | console.log('OPEN_ALL_UPDATE') 91 | // 全量更新 92 | } 93 | } catch (error) { 94 | console.error('checkVersionERROR', error) 95 | } 96 | } -------------------------------------------------------------------------------- /docs/Copy.md: -------------------------------------------------------------------------------- 1 | # electron-react-umi-tpl change log 2 | 3 | ### 选中右键复制黏贴 4 | 5 | ``` 6 | // app.ts 7 | 8 | // 注入右键复制黏贴 9 | import RightClickMenuFuc from './utils/rightClickMenuFuc' 10 | 11 | RightClickMenuFuc() 12 | 13 | ``` 14 | 15 | 16 | ``` 17 | // RightClickMenuFuc.ts 18 | 19 | /** 20 | * 右键复制黏贴 21 | */ 22 | const remote = require('electron').remote; 23 | const Menu = remote.Menu; 24 | const MenuItem = remote.MenuItem; 25 | 26 | 27 | const RightClickMenuFuc = () => { 28 | /** 29 | * 判断点击区域可编辑 30 | * @param {*} e 31 | */ 32 | function isEleEditable(e: any): any { 33 | if (!e) { 34 | return false; 35 | } 36 | // 为input标签或者contenteditable属性为true 37 | if (e.tagName == 'INPUT' || e.contentEditable == 'true') { 38 | return true; 39 | } else { 40 | // 递归查询父节点 41 | return isEleEditable(e.parentNode) 42 | } 43 | } 44 | 45 | const menu = new Menu(); 46 | menu.append(new MenuItem({ label: '粘贴', role: 'paste' })); 47 | 48 | const menu2 = new Menu(); 49 | menu2.append(new MenuItem({ label: '复制', role: 'copy' })); 50 | window.addEventListener('contextmenu', (e) => { // 上下文监听事件 51 | e.preventDefault(); 52 | if (isEleEditable(e.target)) { 53 | menu.popup(remote.getCurrentWindow()); 54 | } else { 55 | // 判断有文本选中 56 | let selectText = window.getSelection().toString(); 57 | if (!!selectText) { 58 | menu2.popup(remote.getCurrentWindow()); 59 | } 60 | } 61 | 62 | }, false) 63 | } 64 | 65 | export default RightClickMenuFuc 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | 73 | # electron-react-umi-tpl change log 74 | 75 | ### Right click to copy and paste 76 | 77 | ``` 78 | // app.ts 79 | 80 | // Inject right click to copy and paste 81 | import RightClickMenuFuc from'./utils/rightClickMenuFuc' 82 | 83 | RightClickMenuFuc() 84 | 85 | ``` 86 | 87 | 88 | ``` 89 | // RightClickMenuFuc.ts 90 | 91 | /** 92 | * Right click to copy and paste 93 | */ 94 | const remote = require('electron').remote; 95 | const Menu = remote.Menu; 96 | const MenuItem = remote.MenuItem; 97 | 98 | 99 | const RightClickMenuFuc = () => { 100 | /** 101 | * Judging that the click area can be edited 102 | * @param {*} e 103 | */ 104 | function isEleEditable(e: any): any { 105 | if (!e) { 106 | return false; 107 | } 108 | // is the input tag or the contenteditable attribute is true 109 | if (e.tagName =='INPUT' || e.contentEditable =='true') { 110 | return true; 111 | } else { 112 | // Query the parent node recursively 113 | return isEleEditable(e.parentNode) 114 | } 115 | } 116 | 117 | const menu = new Menu(); 118 | menu.append(new MenuItem({ label:'paste', role:'paste' })); 119 | 120 | const menu2 = new Menu(); 121 | menu2.append(new MenuItem({ label:'copy', role:'copy' })); 122 | window.addEventListener('contextmenu', (e) => {// Context listener event 123 | e.preventDefault(); 124 | if (isEleEditable(e.target)) { 125 | menu.popup(remote.getCurrentWindow()); 126 | } else { 127 | // Determine that there is text selected 128 | let selectText = window.getSelection().toString(); 129 | if (!!selectText) { 130 | menu2.popup(remote.getCurrentWindow()); 131 | } 132 | } 133 | 134 | }, false) 135 | } 136 | 137 | export default RightClickMenuFuc 138 | 139 | 140 | 141 | 142 | ``` -------------------------------------------------------------------------------- /src/main/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | electron主进程的loading过渡 8 | 117 | 118 | 119 |
120 |
121 | 122 | 123 | 124 |
125 |
126 | 127 | 128 | -------------------------------------------------------------------------------- /src/render/assets/style/common.less: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里写全局公共样式 3 | */ 4 | @commonColor: #a14eff; 5 | @commonBgColor: #eff0fa; 6 | 7 | // wrap 8 | .commonWrap { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | 13 | .layout-content { 14 | padding: 0 0 24px 0; 15 | margin: 0px; 16 | height: 100%; 17 | } 18 | 19 | .layout-content-withTab { 20 | padding: 24px 0; 21 | margin: 0px; 22 | height: 100%; 23 | } 24 | 25 | .ant-layout { 26 | background: @commonBgColor !important; 27 | } 28 | 29 | // settlement 30 | .settlement { 31 | background: @commonBgColor; 32 | } 33 | 34 | // other 35 | .mid { 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | height: 100%; 40 | } 41 | 42 | .status2 { 43 | padding: 5px 10px; 44 | background-color: #1890ff; 45 | border-radius: 5px; 46 | color: #fff; 47 | } 48 | 49 | .bsp { 50 | background-color: #1890ff; 51 | color: #fff; 52 | padding: 4px; 53 | border-radius: 3px; 54 | cursor: pointer; 55 | } 56 | 57 | .totalNumber { 58 | background: #e6f2ff; 59 | margin-top: 20px; 60 | border: 1px solid #b3e4ff; 61 | border-radius: 5px; 62 | height: 50px; 63 | line-height: 50px; 64 | padding-left: 20px; 65 | } 66 | 67 | .mRight10 { 68 | margin-right: 10px; 69 | } 70 | 71 | .mRight5 { 72 | margin-right: 5px; 73 | } 74 | 75 | .mTop16 { 76 | margin-top: 16px; 77 | } 78 | 79 | .mBottom16 { 80 | margin-bottom: 16px; 81 | } 82 | 83 | .alignCenter { 84 | text-align: center; 85 | } 86 | 87 | .mRight16 { 88 | margin-right: 16px; 89 | } 90 | 91 | .mLeft16 { 92 | margin-left: 16px; 93 | } 94 | 95 | // 单行省略号 96 | .ellipsis { 97 | overflow: hidden; 98 | text-overflow: ellipsis; 99 | white-space: nowrap; 100 | } 101 | 102 | .wrapStyle { 103 | padding: 16px 16px 10px; 104 | background-color: #ffffff; 105 | border-radius: 10px; 106 | } 107 | 108 | // 组件通用 109 | .commonHandleBack { 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | margin-top: 30px; 114 | 115 | Button:nth-of-type(1) { 116 | margin-right: 20px; 117 | } 118 | 119 | Button { 120 | width: 110px; 121 | height: 40px; 122 | } 123 | } 124 | 125 | .bg-white { 126 | background-color: white; 127 | } 128 | 129 | .cursor-pointer { 130 | cursor: pointer; 131 | } 132 | 133 | // 带左侧标识的 title 134 | @color-pirmary: #a14eff; 135 | 136 | .note-title { 137 | position: relative; 138 | padding-left: 12px; 139 | line-height: 44px; 140 | font-size: 16px; 141 | font-weight: bold; 142 | color: #474245; 143 | 144 | &::before { 145 | content: ''; 146 | position: absolute; 147 | top: 50%; 148 | left: 0; 149 | width: 2px; 150 | height: 14px; 151 | background-color: @color-pirmary; 152 | transform: translateY(-50%); 153 | } 154 | } 155 | 156 | .note-title-border { 157 | .note-title; 158 | 159 | &::after { 160 | content: ''; 161 | position: absolute; 162 | bottom: 0; 163 | left: 0; 164 | height: 1px; 165 | width: 100%; 166 | background-color: #f3f3f3; 167 | } 168 | } 169 | 170 | .optionBtnList { 171 | width: 100%; 172 | display: flex; 173 | justify-content: flex-start; 174 | align-items: center; 175 | margin: 10px 0; 176 | 177 | & > li > button { 178 | margin-right: 15px; 179 | } 180 | } 181 | 182 | .commonWrap{ 183 | .wrapStyle:nth-of-type(2){ 184 | min-height: 480px; 185 | } 186 | } 187 | 188 | .detailContent{ 189 | padding: 0 20px 20px 20px; 190 | background-color: #fff; 191 | height: auto; 192 | } 193 | -------------------------------------------------------------------------------- /server/src/utils/redis/redisOperate.ts: -------------------------------------------------------------------------------- 1 | import redisConnect from './redisConnection' 2 | import * as ioredis from 'ioredis' 3 | import getConfig from '../../config/index'; 4 | 5 | export interface redisTool { 6 | setString(key:string,value:any):Promise 7 | getString(key:any):Promise 8 | delString(key:string):Promise 9 | getDbSize():Promise 10 | connToRedis():Promise 11 | } 12 | 13 | interface redisConfig { 14 | port:number, 15 | host:string, 16 | password?:string 17 | db?:number 18 | family?:number 19 | } 20 | 21 | const env = process.env.NODE_ENV; 22 | let redisConfig:redisConfig = { 23 | port: getConfig(env).port_redis, 24 | host: getConfig(env).host_redis 25 | } 26 | 27 | class RedisTool implements redisTool { 28 | redis:ioredis.Redis 29 | config:redisConfig 30 | constructor(opt?:any){ 31 | this.redis = null; 32 | if(opt){ 33 | this.config = Object.assign(redisConfig,opt) 34 | }else{ 35 | this.config = redisConfig 36 | } 37 | // this.connToRedis() 38 | this.connToRedis().then(res=>{ 39 | if(res){ 40 | console.log('redis connet success') 41 | } 42 | }).catch(e=>{ 43 | console.error('The Redis Can not Connect:'+e) 44 | }) 45 | } 46 | 47 | /**连接redis */ 48 | connToRedis(){ 49 | return new Promise((resolve, reject) => { 50 | if(this.redis) { 51 | resolve(true) //已创建 52 | } else { 53 | redisConnect(this.config).then((resp:ioredis.Redis) => { 54 | this.redis = resp 55 | resolve(true) } 56 | ).catch(err => { reject(err) }) 57 | } 58 | }) 59 | } 60 | 61 | /**存储string类型的key-value */ 62 | async setString(key:string,value:any){ 63 | let val:string = typeof(value)!=='string'?JSON.stringify(value):value; 64 | let k:string = typeof(value)!=='string'?JSON.stringify(key):key; 65 | try { 66 | const res = await this.redis.set(k, val); 67 | return res; 68 | } 69 | catch (e) { 70 | console.error(e); 71 | } 72 | } 73 | 74 | /**获取string类型的key-value */ 75 | async getString(key:any){ 76 | let id:string = typeof(key)!=='string'?JSON.stringify(key):key; 77 | try{ 78 | const res = await this.redis.get(id); 79 | return res 80 | }catch(e){ 81 | console.error(e); 82 | return null 83 | } 84 | } 85 | 86 | /**删除string类型的key-value */ 87 | async delString(key:string){ 88 | let id:string = typeof(key)!=='string'?JSON.stringify(key):key; 89 | try{ 90 | const res = await this.redis.del(id); 91 | return res 92 | }catch(e){ 93 | console.error(e); 94 | return null 95 | } 96 | } 97 | 98 | /**获取当前数据库key的数量 */ 99 | async getDbSize(){ 100 | try{ 101 | const res = await this.redis.dbsize(); 102 | return res 103 | }catch(e){ 104 | console.error(e); 105 | return null 106 | } 107 | } 108 | 109 | } 110 | 111 | export const default_redis = new RedisTool(); 112 | export const redis_db1 = new RedisTool({db:1}) 113 | // export const redis_db2 = new RedisTool({db:2}) 114 | // export const redis_db3 = new RedisTool({db:3}) 115 | // export const redis_db4 = new RedisTool({db:4}) 116 | 117 | export default default_redis -------------------------------------------------------------------------------- /src/main/controls/AppMainWindow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * electron窗口初始化 3 | */ 4 | 'use strict' 5 | 6 | const { BrowserView, BrowserWindow } = require('electron') 7 | const isDevEnv = require('electron-is-dev') 8 | const path = require('path') 9 | const { 10 | default: installExtension, 11 | REACT_DEVELOPER_TOOLS, 12 | REDUX_DEVTOOLS 13 | } = require('electron-devtools-installer') 14 | const AppAutoUpdater = require('../controls/AppAutoUpdater') 15 | const { DEV_ADDRESS } = require('../config/config') 16 | const log = require('electron-log') 17 | 18 | module.exports = class AppMainWindow extends BrowserWindow { 19 | constructor() { 20 | const config = { 21 | width: 1010, 22 | height: 716, 23 | minWidth: 800, 24 | minHeight: 600, 25 | autoHideMenuBar: false, 26 | fullscreen: false, 27 | webPreferences: { 28 | nodeIntegration: true, 29 | webviewTag: true, 30 | preload: path.join(__dirname, 'preload.js'), 31 | contextIsolation: false, // required flag 32 | enableRemoteModule: true 33 | } 34 | } 35 | 36 | super(config) 37 | this.mainWindow = this 38 | this.browserView = null 39 | this.initMainWindow() 40 | this.initEvents() 41 | } 42 | 43 | initMainWindow() { 44 | // 必须在主进程塞入文件前配置 loading 45 | this.windowLoading() 46 | this.loadURL( 47 | isDevEnv ? DEV_ADDRESS : `file://${path.join(__dirname, '../render/dist/index.html')}` 48 | ) 49 | if (isDevEnv) { 50 | // 打开开发者工具 51 | this.mainWindow.openDevTools() 52 | } 53 | // 异步安装插件 54 | installExtension(REACT_DEVELOPER_TOOLS) 55 | .then(name => console.log(`Added Extension REDUX_DEVTOOLS: ${name}`)) 56 | .catch(err => console.log('An error occurred: ', err)) 57 | installExtension(REDUX_DEVTOOLS) 58 | .then(name => console.log(`Added Extension REDUX_DEVTOOLS: ${name}`)) 59 | .catch(err => console.log('An error occurred: ', err)) 60 | } 61 | 62 | // 主进程加载时的loading过渡,避免白屏 63 | windowLoading() { 64 | console.log(`file://${path.join(__dirname, 'loading.html')}`) 65 | this.browserView = new BrowserView() 66 | this.mainWindow.setBrowserView(this.browserView) 67 | this.browserView.setBounds({ x: 0, y: 0, width: 1010, height: 716 }) 68 | this.browserView.webContents.loadURL(`file://${path.join(__dirname, 'loading.html')}`) 69 | this.browserView.webContents.on('dom-ready', () => { 70 | this.mainWindow.show() 71 | }) 72 | } 73 | 74 | initEvents() { 75 | // 窗口关闭的监听 76 | this.mainWindow.on('closed', () => { 77 | console.log('closed') 78 | this.mainWindow = null 79 | }) 80 | 81 | this.mainWindow.on('close', e => { 82 | console.log('close windows') 83 | // 兼容不同平台关闭 84 | if (process.platform === 'win32' && this.mainWindow['hide']) { 85 | this.mainWindow.hide() 86 | e.preventDefault() 87 | } 88 | }) 89 | this.mainWindow.once('ready-to-show', () => { 90 | // 加入loading.html后, 此处updateHandle无效 91 | // 检查自动更新 92 | // log.info('enter ready-to-show') 93 | }) 94 | 95 | this.mainWindow.once('show', () => { 96 | log.info('enter show') 97 | // 检查自动更新 98 | AppAutoUpdater.updateHandle(this.mainWindow) 99 | }) 100 | 101 | // 隐藏默认菜单 102 | this.mainWindow.webContents.once('did-finish-load', () => { 103 | this.mainWindow.setMenuBarVisibility(false) 104 | }) 105 | } 106 | 107 | destoryMainWindow() { 108 | this.mainWindow = null 109 | } 110 | 111 | removeView() { 112 | this.mainWindow.removeBrowserView(this.browserView) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-app", 3 | "description": "{{description}}", 4 | "author": "app", 5 | "version": "0.0.1", 6 | "scripts": { 7 | "start": "npm run dev", 8 | "dev": "concurrently -n=umi,electron -c=blue,green -p=[{name}] \"cross-env APP_ROOT=src/render umi dev\" \"npm run electron:dev\"", 9 | "electron:dev": "node scripts/main-build --env=development --watch", 10 | "---- webpack 构建 bundle.js electron-webpack ----": "---- webpack 构建 bundle.js ----", 11 | "electron:build": "node scripts/main-build --env=production", 12 | "---- 辅助命令 ----": "---- DIST=dist 指向根目录 dist ----", 13 | "build:umi": "cross-env DIST=dist APP_ENV=test APP_ROOT=src/render umi build", 14 | "---- close flex routes ----": "---- 开发关闭动态路由,可选 ----", 15 | "flex": "concurrently -n=umi,electron -c=blue,green \"cross-env closeFlexRoute=true APP_ENV=test APP_ROOT=src/render umi dev\" \"npm run electron:dev\"", 16 | "---- 自动根据操作系统平台构建包 electron进程环境变量:APP_ENV=production electron-package ----": "---- 自动根据操作系统平台构建包 ----", 17 | "pack": "npm run electron:build && cross-env APP_ENV=production APP_ROOT=src/render umi build && electron-builder", 18 | "---- 不同接口环境 APP_ENV=test ----": "---- 自动根据操作系统平台构建包 ----", 19 | "pack-env-test": "npm run electron:build && cross-env APP_ENV=test APP_ROOT=src/render umi build && electron-builder", 20 | "---- 构建三个操作系统平台包 electron-package-all ----": "---- 构建三个操作系统平台包 ----", 21 | "pack-all": "npm run electron:build && cross-env APP_ENV=production APP_ROOT=src/render umi build && electron-builder -mwl", 22 | "---- 构建 mac 操作系统平台包 electron-package-mac ----": "---- 构建 mac 操作系统平台包 ----", 23 | "pack-mac": "npm run electron:build && cross-env APP_ENV=production APP_ROOT=src/render umi build && electron-builder -m", 24 | "---- 构建 windows 操作系统平台包 electron-package-windows ----": "---- 构建 windows 操作系统平台包 ----", 25 | "pack-windows": "npm run electron:build && cross-env APP_ENV=production APP_ROOT=src/render umi build && electron-builder -w" 26 | }, 27 | "main": "src/main/bundle.js", 28 | "build": { 29 | "extends": null, 30 | "productName": "APP", 31 | "appId": "yourAppId", 32 | "directories": { 33 | "output": "release/${version}_setup" 34 | }, 35 | "files": [ 36 | "!node_modules/**/*", 37 | "src/main/**/*", 38 | "src/main/public", 39 | "src/render/dist/**/*" 40 | ], 41 | "mac": { 42 | "target": [ 43 | "dmg", 44 | "zip" 45 | ] 46 | }, 47 | "win": { 48 | "target": [ 49 | { 50 | "target": "nsis", 51 | "arch": [ 52 | "x64", 53 | "ia32" 54 | ] 55 | } 56 | ], 57 | "artifactName": "${productName}_setup_${version}.${ext}", 58 | "icon": "src/main/public/icon.ico" 59 | }, 60 | "publish": [ 61 | { 62 | "provider": "generic", 63 | "url": "" 64 | } 65 | ], 66 | "nsis": { 67 | "oneClick": false, 68 | "perMachine": false, 69 | "allowToChangeInstallationDirectory": true, 70 | "deleteAppDataOnUninstall": false 71 | } 72 | }, 73 | "dependencies": { 74 | "@ant-design/icons": "^4.0.6", 75 | "adm-zip": "^0.4.14", 76 | "antd": "^4.2.2", 77 | "axios": "^0.19.2", 78 | "classnames": "^2.2.6", 79 | "dayjs": "^1.8.25", 80 | "electron-is-dev": "^1.2.0", 81 | "electron-log": "^4.1.1", 82 | "js-cookie": "^2.2.1", 83 | "react": "^16.12.0", 84 | "react-dom": "^16.12.0", 85 | "socket.io-client": "^2.3.0", 86 | "umi": "^3.2.17" 87 | }, 88 | "devDependencies": { 89 | "@types/classnames": "^2.2.10", 90 | "@types/js-cookie": "^2.2.6", 91 | "@typescript-eslint/eslint-plugin": "^2.27.0", 92 | "@typescript-eslint/parser": "^2.27.0", 93 | "@umijs/fabric": "^2.0.8", 94 | "@umijs/preset-react": "1.x", 95 | "@umijs/test": "^3.1.1", 96 | "babel-eslint": "^10.1.0", 97 | "babel-loader": "^8.2.2", 98 | "chalk": "^4.0.0", 99 | "concurrently": "^5.1.0", 100 | "cross-env": "^7.0.2", 101 | "dotenv": "^8.2.0", 102 | "electron": "^12.2.3", 103 | "electron-builder": "^22.5.1", 104 | "electron-devtools-installer": "^3.0.0", 105 | "electron-is-dev": "^1.2.0", 106 | "electron-log": "^4.1.1", 107 | "electron-store": "^5.1.1", 108 | "electron-updater": "^4.2.5", 109 | "eslint-plugin-html": "^6.0.2", 110 | "eslint-plugin-react": "^7.19.0", 111 | "minimist": "^1.2.5", 112 | "mockjs": "^1.1.0", 113 | "node-loader": "^0.6.0", 114 | "ora": "^4.0.4", 115 | "prettier": "^1.19.1", 116 | "wait-on": "^4.0.2", 117 | "webpack": "^5.28.0" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /docs/PART_UPDATE.md: -------------------------------------------------------------------------------- 1 | ## 增量更新说明文档 2 | 3 | [English Version](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/PART_UPDATE_EN.md) 4 | 5 | 6 | 7 | #### 提前准备 8 | 9 | 10 | 1. 准备本地或者远程服务器或者远程静态文件url 11 | ``` 12 | npm i -g http-server 13 | cd yourFileFolder // 进入任意文件夹 14 | http-server -p 4000 // 快速开启本地服务,用于存储更新文件 15 | ``` 16 | 2. 配置和打包,拿到更新文件内容并压缩 17 | ``` 18 | // package.json 19 | // 关闭asar模式 20 | "asar": false, 21 | // 打包 22 | npm run pack-windows 23 | // 进入打好的windows包 24 | cd release\0.x.x_setup\win-unpacked\resources 25 | // 压缩app文件夹 => app.zip, 拷贝app-update.yml和app.zip 26 | 27 | ``` 28 | 29 | 3. app.zip app-update.yml 传到服务器 30 | 31 | 3. 本地流程,启动客户端,点击增量更新 32 | 33 | ``` 34 | // 入口 35 | // src\render\components\AutoUpdate\index.tsx 36 | 39 | 40 | ``` 41 | 42 | ``` 43 | // 本地检查与服务器的version版本比较 44 | // 如果找到新版本,则向主进程通信,通知checkForPartUpdates开始更新 45 | 46 | // src\render\utils\autoUpdate\partUpdate.js 47 | /** 检查更新 */ 48 | export async function checkForPartUpdates() { 49 | try { 50 | // check version 检查版本 51 | const res = await checkVersion() 52 | if (res && res === 'OPEN_PART_UPDATE') { 53 | // 增量更新 54 | console.log('OPEN_PART_UPDATE') 55 | confirm({ 56 | title: '检测到更新', 57 | icon: , 58 | content: ( 59 |
60 |

是否更新?

61 |
62 | ), 63 | okText: '确认', 64 | cancelText: '取消', 65 | onOk() { 66 | ipc && ipc.send('checkForPartUpdates') 67 | message.info('请耐心等待几秒..') 68 | }, 69 | onCancel() { 70 | console.log('Cancel'); 71 | }, 72 | }); 73 | // partUpdates() 74 | } 75 | if (res && res === 'OPEN_ALL_UPDATE') { 76 | console.log('OPEN_ALL_UPDATE') 77 | // 全量更新 78 | } 79 | } catch (error) { 80 | console.error('checkVersionERROR', error) 81 | } 82 | } 83 | 84 | function checkVersion(params) { 85 | return new Promise((resolve, reject) => { 86 | const currentVersion = remote.app.getVersion() 87 | // 获取最新版本号 88 | downloadFile(remoteYmlURL, localYmlUrl).then(res => { 89 | const remoteVersion = JSON.stringify(res.data).split('\\n')[0].split(' ')[1] 90 | const remoteVersionArr = remoteVersion.split('.') 91 | const currentVersionArr = currentVersion.split('.') 92 | // 0.1.1 Y和Z比较来开启增量更新 1.1.1 X比较来开启全量更新 93 | if (Number(remoteVersionArr[0]) > Number(currentVersionArr[0])) { 94 | // 开启全量更新 95 | return resolve('OPEN_ALL_UPDATE') 96 | } else if (Number(remoteVersionArr[2]) > Number(currentVersionArr[2]) || Number(remoteVersionArr[1]) > Number(currentVersionArr[1])) { 97 | // 开启增量更新 98 | return resolve('OPEN_PART_UPDATE') 99 | } else { 100 | console.log('无版本变动,不更新') 101 | } 102 | }).catch(e => { 103 | console.error(e) 104 | }) 105 | }) 106 | } 107 | 108 | ``` 109 | 110 | 111 | ``` 112 | // src\main\controls\AppAutoUpdater.js 113 | // 下载服务器文件包 114 | // 本地解压和备份,替换,重启客户端即可完成更新 115 | 116 | // 增量更新 117 | ipcMain.on('checkForPartUpdates', async (e, msg) => { 118 | console.log('checkForPartUpdates', msg) 119 | // if (isElectronDev) { 120 | // console.log('开发模式不支持') 121 | // return 122 | // } 123 | try { 124 | if (fs.existsSync(`${localresourcePath}.back`)) { // 删除旧备份 125 | deleteDirSync(`${localresourcePath}.back`) 126 | } 127 | if (fs.existsSync(localresourcePath)) { 128 | fs.renameSync(localresourcePath, `${localresourcePath}.back`); // 备份目录 129 | } 130 | await downloadFile(remoteAppURL, appZipPath) 131 | console.log('app.asar.unpacked.zip 下载完成') 132 | fs.mkdirSync(localresourcePath) // 创建app来解压用 133 | try { 134 | // 同步解压缩 135 | const unzip = new AdmZip(appZipPath) 136 | unzip.extractAllTo(resourcePath, true) 137 | console.log('app.asar.unpacked.zip 解压缩完成') 138 | console.log('更新完成,正在重启...') 139 | mainWindow.webContents.send('partUpdateReady') 140 | setTimeout(() => { 141 | app.relaunch(); // 重启 142 | app.exit(0); 143 | }, 1800); 144 | } catch (error) { 145 | console.error(`extractAllToERROR: ${error}`); 146 | } 147 | // 更新窗口 148 | // BrowserWindow.getAllWindows().forEach((win: any) => { 149 | // win.webContents.reload() 150 | // // remote.app.relaunch(); // 重启 151 | // // remote.app.exit(0); 152 | // }) 153 | console.log('webContents reload完成') 154 | } catch (error) { 155 | console.error(`checkForPartUpdatesERROR`, error) 156 | if (fs.existsSync(`${localresourcePath}.back`)) { 157 | fs.renameSync(`${localresourcePath}.back`, localresourcePath); 158 | } 159 | } 160 | }) 161 | 162 | ``` 163 | 164 | -------------------------------------------------------------------------------- /docs/PART_UPDATE_EN.md: -------------------------------------------------------------------------------- 1 | ## Incremental update documentation 2 | #### Prepare ahead of time 3 | 1. Prepare local or remote server or remote static file URL 4 | ``` 5 | npm i -g http-server 6 | // enter any folder of filer folder 7 | Http-server - P 4000 // quickly start the local service to store update files 8 | ``` 9 | 2. Configure and package, get the updated file content and compress it 10 | ``` 11 | // package.json 12 | //Turn off ASAR mode 13 | "asar": false, 14 | //Packing 15 | npm run pack 16 | //Enter the windows package 17 | cd release\0.x.x_ setup\win-unpacked\resources 18 | //Compressed app folder = > app.zip , copy app- update.yml and app.zip 19 | ``` 20 | Three app.zip Pass to server 21 | 3. Local process, start the client and click incremental update 22 | ``` 23 | //Entrance 24 | // src\render\components\AutoUpdate\ index.tsx 25 | 28 | ``` 29 | 30 | ``` 31 | //Version version comparison between local check and server 32 | //If a new version is found, it communicates with the main process and notifies checkforpartupdates to start updating 33 | // src\render\utils\autoUpdate\ partUpdate.js 34 | /**Check for updates*/ 35 | export async function checkForPartUpdates() { 36 | try { 37 | //Check version 38 | const res = await checkVersion() 39 | if (res && res === 'OPEN_ PART_ UPDATE') { 40 | //Incremental update 41 | console.log ('OPEN_ PART_ UPDATE') 42 | confirm({ 43 | Title: 'update detected', 44 | icon: , 45 | content: ( 46 |
47 |

Update

48 |
49 | ) 50 | Oktext: 'confirm', 51 | Canceltext: 'Cancel', 52 | onOk() { 53 | ipc && ipc.send ('checkForPartUpdates') 54 | message.info ('please wait a few seconds..') 55 | } 56 | onCancel() { 57 | console.log ('Cancel'); 58 | } 59 | }; 60 | // partUpdates() 61 | } 62 | if (res && res === 'OPEN_ ALL_ UPDATE') { 63 | console.log ('OPEN_ ALL_ UPDATE') 64 | //Full update 65 | } 66 | } catch (error) { 67 | console.error ('checkVersionERROR', error) 68 | } 69 | } 70 | function checkVersion(params) { 71 | return new Promise((resolve, reject) => { 72 | const currentVersion = remote.app.getVersion () 73 | //Get the latest version number 74 | downloadFile(remoteYmlURL, localYmlUrl).then(res => { 75 | const remoteVersion = JSON.stringify ( res.data ).split('\\n')[0].split(' ')[1] 76 | const remoteVersionArr = remoteVersion.split ('.') 77 | const currentVersionArr = currentVersion.split ('.') 78 | //0.1.1 y and Z comparison to turn on incremental update 1.1.1 x comparison to turn on full update 79 | if (Number(remoteVersionArr[0]) > Number(currentVersionArr[0])) { 80 | //Open full update 81 | return resolve('OPEN_ ALL_ UPDATE') 82 | } else if (Number(remoteVersionArr[2]) > Number(currentVersionArr[2]) || Number(remoteVersionArr[1]) > Number(currentVersionArr[1])) { 83 | //Turn on incremental update 84 | return resolve('OPEN_ PART_ UPDATE') 85 | } else { 86 | console.log ('No version change, no update ') 87 | } 88 | }).catch(e => { 89 | console.error (E) 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | 96 | 97 | ``` 98 | // src\main\controls\ AppAutoUpdater.js 99 | //Download Server package 100 | //Local decompression and backup, replacement, restart the client to complete the update 101 | //Incremental update 102 | ipcMain.on ('checkForPartUpdates', async (e, msg) => { 103 | console.log ('checkForPartUpdates', msg) 104 | // if (isElectronDev) { 105 | // console.log ('development mode not supported ') 106 | // return 107 | // } 108 | try { 109 | if ( fs.existsSync ('${localresourcepath}. Back') {// delete the old backup 110 | deleteDirSync(`${localresourcePath}.back`) 111 | } 112 | if ( fs.existsSync (localresourcePath)) { 113 | fs.renameSync (localresourcepath, '${localresourcepath}. Back'); // backup directory 114 | } 115 | await downloadFile(remoteAppURL, appZipPath) 116 | console.log () app.asar.unpacked . zip download complete ') 117 | fs.mkdirSync (localresourcepath) // create an app to unzip the 118 | try { 119 | //Synchronous decompression 120 | const unzip = new AdmZip(appZipPath) 121 | unzip.extractAllTo (resourcePath, true) 122 | console.log () app.asar.unpacked . zip decompression complete ') 123 | console.log ('update complete, restarting...') 124 | mainWindow.webContents.send ('partUpdateReady') 125 | setTimeout(() => { 126 | app.relaunch (); // restart 127 | app.exit (0); 128 | }, 1800); 129 | } catch (error) { 130 | console.error (`extractAllToERROR: ${error}`); 131 | } 132 | //Update window 133 | // BrowserWindow.getAllWindows ().forEach((win: any) => { 134 | // win.webContents.reload () 135 | // // remote.app.relaunch (); // restart 136 | // // remote.app.exit (0); 137 | // }) 138 | console.log ('webcontents reload completed ') 139 | } catch (error) { 140 | console.error (`checkForPartUpdatesERROR`, error) 141 | if ( fs.existsSync (`${localresourcePath}.back`)) { 142 | fs.renameSync (`${localresourcePath}.back`, localresourcePath); 143 | } 144 | } 145 | } 146 | ``` -------------------------------------------------------------------------------- /src/main/controls/AppAutoUpdater.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 自动更新 3 | * https://www.electron.build/auto-update 4 | */ 5 | 6 | const { autoUpdater } = require('electron-updater') 7 | const { ipcMain, app } = require('electron') 8 | const log = require('electron-log'); 9 | autoUpdater.logger = require("electron-log") 10 | autoUpdater.logger.transports.file.level = "info" 11 | const isElectronDev = require('electron-is-dev') 12 | const path = require('path') 13 | const AdmZip = require('adm-zip') 14 | const fs = require('fs') 15 | const electronMainUtils = require('../util/utils') 16 | 17 | 18 | let localresourcePath = '' // 本地resource/app路径 19 | let resourcePath = '' // 本地resource 路径 20 | let appZipPath = '' // app压缩包位置 21 | let yourFileServer = '' // 文件服务器 22 | const remoteAppURL = `https://${yourFileServer}/app.zip` // yourFileServer: 你的远程文件服务器 23 | // windows 本地测试 admin:改为你的用户名 24 | if (isElectronDev && process.platform === 'win32') { 25 | // win 本地安装包路径 26 | // YOURAPP: app名称,一般取自package.json 27 | localresourcePath = `C:/Users/admin/AppData/Local/Programs/YOURAPP/resources/app` 28 | resourcePath = `C:/Users/admin/AppData/Local/Programs/YOURAPP/resources` 29 | appZipPath = `C:/Users/admin/AppData/Local/Programs/YOURAPP/resources/app.zip` 30 | } 31 | if (!isElectronDev && process.platform === 'win32') { // win平台 32 | localresourcePath = `./resources/app` 33 | resourcePath = `./resources` 34 | appZipPath = `./resources/app.zip` 35 | } 36 | if (!isElectronDev && process.platform === 'darwin') { // mac平台 37 | localresourcePath = `/Applications/YOURAPP.app/Contents/Resources/app` 38 | resourcePath = `/Applications/YOURAPP.app/Contents/Resources` 39 | appZipPath = `/Applications/YOURAPP.app/Contents/Resources/app.zip` 40 | } 41 | 42 | /** 43 | * autoUpdater - 更新操作 44 | * @param {object} mainWindow 实例 45 | */ 46 | function updateHandle(mainWindow) { 47 | log.info('enter updateHandle') 48 | log.info(`mainWindow: ${mainWindow}`) 49 | autoUpdater.on('error', function (error) { 50 | electronMainUtils.sendUpdateMessage(`ERROR: 检查更新出错:${error}`, mainWindow) 51 | }); 52 | autoUpdater.on('checking-for-update', function () { 53 | log.info(`enter checking-for-update`) 54 | log.info(`正在检查更新…`) 55 | }); 56 | autoUpdater.on('update-available', function (UpdateInfo) { 57 | log.info('UpdateInfo', UpdateInfo) 58 | electronMainUtils.sendUpdateMessage('检测到新版本,正在下载…', mainWindow) 59 | }); 60 | autoUpdater.on('update-not-available', function (info) { 61 | log.info(`现在使用的已经是最新版本`) 62 | electronMainUtils.sendUpdateMessage('现在使用的已经是最新版本', mainWindow) 63 | }); 64 | 65 | /** 66 | * 更新下载进度事件 67 | */ 68 | autoUpdater.on('download-progress', function (progressObj) { 69 | mainWindow.webContents.send('downloadProgress', progressObj) 70 | }); 71 | 72 | /** 73 | * 下载更新包完成 74 | */ 75 | autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) { 76 | // 渲染层回复立即更新,则自动退出当前程序,然后进行程序更新 77 | ipcMain.on('updateNow', (e, arg) => { 78 | log.info('开始更新'); 79 | autoUpdater.quitAndInstall(); 80 | }); 81 | // 询问渲染层是否立即更新 82 | mainWindow.webContents.send('isUpdateNow') 83 | }); 84 | 85 | /** 86 | * 增量更新 87 | */ 88 | ipcMain.on('checkForPartUpdates', async (e, msg) => { 89 | try { 90 | if (fs.existsSync(`${localresourcePath}.back`)) { // 删除旧备份 91 | electronMainUtils.deleteDirSync(`${localresourcePath}.back`) 92 | } 93 | if (fs.existsSync(localresourcePath)) { 94 | fs.renameSync(localresourcePath, `${localresourcePath}.back`); // 备份目录 95 | } 96 | await electronMainUtils.downloadFile(remoteAppURL, appZipPath) 97 | if (!fs.existsSync(`${localresourcePath}`)) { // 删除旧备份 98 | fs.mkdirSync(localresourcePath) // 创建app来解压用 99 | } 100 | try { 101 | // 同步解压缩 102 | const unzip = new AdmZip(appZipPath) 103 | unzip.extractAllTo(resourcePath, true) 104 | console.log('app.asar.unpacked.zip 解压缩完成') 105 | console.log('更新完成,正在重启...') 106 | mainWindow.webContents.send('partUpdateReady') // 此处可以: 通知渲染进程,进行指定操作 107 | setTimeout(() => { 108 | app.relaunch(); // 重启 109 | app.exit(0); 110 | }, 1800); 111 | } catch (error) { 112 | console.error(`extractAllToERROR: ${error}`); 113 | } 114 | console.log('webContents reload完成') 115 | } catch (error) { 116 | console.error(`checkForPartUpdatesERROR`, error) 117 | } finally { 118 | if (fs.existsSync(`${localresourcePath}.back`)) { 119 | fs.renameSync(`${localresourcePath}.back`, localresourcePath); 120 | } 121 | } 122 | }) 123 | 124 | } 125 | 126 | if (process.platform === 'win32') { 127 | autoUpdater.updateConfigPath = path.join(__dirname, 'app-win-update.yml') // 远程包配置-win 128 | } else { 129 | autoUpdater.updateConfigPath = path.join(__dirname, 'app-mac-update.yml') // 远程包配置-mac 130 | } 131 | 132 | /** 133 | * 检查更新 134 | */ 135 | function checkForUpdates() { 136 | console.log('enter checkForUpdates') 137 | if (isElectronDev) { 138 | autoUpdater.checkForUpdates() 139 | } 140 | if (!isElectronDev) { 141 | autoUpdater.checkForUpdatesAndNotify() 142 | } 143 | return; 144 | } 145 | 146 | // 通知更新检查(远程版本号大于本地即可拉去更新) 147 | ipcMain.on('checkForUpdates', (e, msg) => { 148 | console.log('checkForUpdatesMsg', msg) 149 | checkForUpdates() 150 | }) 151 | 152 | module.exports = { 153 | updateHandle, 154 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-react-umi-tpl 2 | 3 | [github](https://github.com/qld-cf/electron-react-tpl) 4 | [English Version](https://github.com/qld-cf/electron-react-tpl/blob/master/README_EN.md) 5 | 6 | 体验: 7 | [下载](https://www.yuque.com/docs/share/c453798a-12b3-4eb7-a725-4b960732eab1?#《资源1》) 8 | 9 | 10 | ##### 相对纯净模板,请[戳这里(待维护)](https://github.com/qld-cf/electron-common-react-tpl),无umi更自由,热加载更快 11 | 12 | 13 | ##### TODO 14 | 1. web+cdn版本,便于web服务器部署; 15 | 16 | 17 | 更新日志: 18 | 19 | 1. 2020-06-08 添加[全量更新](https://segmentfault.com/a/1190000016674982)功能 20 | 2. 2020-06-29 添加[远程增量更新功能](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/PART_UPDATE.md),无需下载包来重新安装更新; 21 | 3. 2020-07-27 优化初始化客户端loading等待页面,优化页面 22 | 4. 2020-08-18 添加[选中复制右键黏贴 | 自定义功能](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/Copy.md) 23 | 5. 2020-08-24 优化win7部分系统白屏(win7关闭硬件加速) 24 | 6. 2020-08-28 添加`koa2 + typescript + websocket + redis + log4js`服务端websocket|redis功能,客户端socket.io 25 | 7. 2020-09-20 新增错误边界识别和处理(页面中遇到React语法等报错后,自动捕捉记录错误日志,页面返回首页,开发模式默认关闭,生产环境开启) 26 | 8. 2020-10-16 新增 [限制只允许应用单开/单实例](https://www.electronjs.org/docs/api/app#%E4%BA%8B%E4%BB%B6-second-instance) 27 | 9. 2020-11-05 修复增量更新问题,优化部分代码,移除旧版打印,新增cli工具命令行启动 28 | 10. 2021-02-02 修复eslint, tslint问题 29 | 11. 2021-03-29 新增主进程HRM功能,修改主进程可以重新reload客户端 30 | 12. 2022-01-19 升级electron版本到12,修复升级引出的问题(又是瞎忙的一年..) 31 | 13. 2022-02-21 加入本地数据持久化sqlite + sequelize,分支:[feature/add_sqlite_20220209](https://github.com/qld-cf/electron-react-tpl/tree/feature/add_sqlite_20220209) 32 | 33 | 34 | --- 35 | 36 | [中文](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/CHANGE_LOG.md) 37 | [English Version](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/CHANGE_LOG_EN.md) 38 | 39 | 40 | 41 | `electron 12` + `umi 3.2` + `typescript` + `react 16.12` + `redux` + `antDesign 4.0` + `eslint tslint react-tslint`脚手架, 下载即用,已经为你做好了基座设施 42 | 43 | #### 客户端集成: 44 | - [x] 自动更新(electron-builder) 45 | - [x] 托盘菜单 app启动loading加载条 46 | - [x] electron-log 本地日志 electron-store 本地存储 47 | - [x] app打包图标 添加增量更新 48 | - [x] 添加redux-devtools插件 49 | - [x] 右键复制黏贴 50 | - [ ] app崩溃信息采集 51 | - [ ] app消息通知,快捷键等 52 | 53 | 54 | 55 | #### web端: 56 | 1. 基于[umi](https://umijs.org/zh-CN)脚手架,基础配置已集成,开发者关注业务代码编写即可 57 | 2. 本地存储redux(redux-saga) 58 | 3. antDesign >= 4.0 59 | 4. iconfont图标 60 | 61 | - 菜单配置 `src/layouts/menu/config.tsx` 62 | 63 | 64 | #### Fix: 65 | - [x] 升级到electron9.1.0,同步官方 66 | - [x] 部分win7白屏解决方案 67 | - [ ] 待修复sqlite打包问题 68 | - [ ] node Api功能封装与渲染进程业务解耦 69 | 70 | 71 | #### Next Feature 72 | - [x] 基于create-react-app的版本 73 | 74 | 75 | ### 工具命令行启动 76 | 77 | ``` 78 | > npm i -g maple-react-cli // 全局安装cli工具 79 | > maple-react-cli // 初始化 80 | 81 | ? 请选择您接下来的操作 选择模板类型 82 | ? 选一个项目模板来初始化您的项目~ 83 | ✔ electron-react-tpl 84 | ✔ 初始化中.. 85 | ✔ 准备拉取代码... 86 | ? 请输入您本地初始化的项目名~ 输入自定义项目名,如project 87 | ✔ 拉取代码成功 88 | ✔ 安装依赖成功~ 89 | ? 是否运行项目? yes 90 | 恭喜~项目启动成功~请稍候... 91 | ``` 92 | 93 | [cli工具](https://github.com/qld-cf/maple-react-cli) 94 | 95 | 96 | 97 | ### 本地开启 98 | 99 | ``` 100 | npm i // 安装不成功请用yarn 101 | npm start 102 | npm run pack // 默认根据当前系统打包 103 | npm run pack-mac // 打包mac平台 104 | npm run pack-windows // 打包windows平台 105 | npm run pack-all // 打包所有平台 106 | 107 | // 如果需要开启websocket 108 | 109 | cd ./server 110 | npm run dev 111 | ``` 112 | 113 | 114 | ### 目录树 115 | ``` 116 | |-- project 117 | |-- .editorconfig 118 | |-- .eslintrc.js 119 | |-- .gitignore 120 | |-- .gitlab-ci.yml 121 | |-- .prettierignore 122 | |-- .prettierrc.js 123 | |-- directoryList.md 124 | |-- package-lock.json 125 | |-- package.json 126 | |-- README.md 127 | |-- tsconfig.json 128 | |-- typings.d.ts 129 | |-- eslint-rules 自定义eslint配置 130 | | |-- base.js 131 | | |-- react.js 132 | | |-- ts.js 133 | |-- src 134 | |-- main 主进程 135 | | |-- app-update.yml 生产环境自动更新配置 136 | | |-- bundle.js 自动生成 137 | | |-- bundle.js.map 138 | | |-- dev-app-update.yml 开发环境自动更新配置 139 | | |-- index.js 入口 140 | | |-- loading.html 141 | | |-- preload.js 142 | | |-- README.md 143 | | |-- config 编译配置 144 | | | |-- config.js 145 | | | |-- webpack.config.js 146 | | |-- controls 控制集 147 | | | |-- AppAutoUpdater.js 148 | | | |-- AppMainWindow.js 149 | | | |-- AppTray.js 150 | | | |-- electron-helper.js 151 | | |-- public 附件 152 | | | |-- icon.ico 153 | | | |-- icon.png 154 | | | |-- tray.png 155 | | |-- script 编译脚本 156 | | |-- build.js 157 | |-- render 渲染进程 158 | |-- .env 159 | |-- .umirc.ts 160 | |-- app.ts 161 | |-- global.less 162 | |-- README.md 163 | |-- .umi umi自动生成配置和插件等 164 | | |-- umi.ts 165 | | |-- core 166 | | |-- plugin-dva 167 | | |-- plugin-initial-state 168 | | |-- plugin-model 169 | | |-- plugin-request 170 | |-- api 接口集合 171 | | |-- api.list.ts 172 | |-- assets 附件 173 | | |-- image 174 | | | |-- yay.jpg 175 | | |-- style 176 | | |-- bootstrap-part.less 177 | | |-- common.less 178 | |-- common 通用 179 | | |-- enum.ts 180 | | |-- global.ts 181 | |-- components 组件 182 | | |-- readme.md 183 | | |-- AutoUpdate 184 | | | |-- index.tsx 185 | | | |-- style.less 186 | | |-- FormCps 187 | | | |-- index.tsx 188 | | | |-- readme.md 189 | | |-- TableCps 190 | | |-- index.tsx 191 | | |-- readme.md 192 | |-- config 配置 193 | | |-- iconfont.ts 194 | | |-- menus.tsx 195 | |-- dist 本地打包生成文件 196 | |-- layouts 布局 197 | | |-- index.less 198 | | |-- index.tsx 199 | | |-- header 200 | | | |-- index.less 201 | | | |-- index.tsx 202 | | |-- loading 203 | | | |-- index.less 204 | | | |-- index.tsx 205 | | |-- menu 206 | | |-- index.less 207 | | |-- index.tsx 208 | |-- mock 209 | | |-- foo.ts 210 | |-- models redux 211 | | |-- xxStore.ts 212 | |-- pages 213 | | |-- home.normal.less 214 | | |-- index.tsx 215 | | |-- Foo 示例 216 | | | |-- index.tsx 217 | | | |-- components 218 | | | | |-- TableList.tsx 219 | | | |-- models 220 | | | | |-- foo.ts 221 | | | |-- services 222 | | | |-- foo.ts 223 | | |-- Home 业务 224 | | |-- Edge 225 | | | |-- index.tsx 226 | | |-- WebSocket 227 | | |-- index.tsx 228 | |-- utils 工具集 229 | 230 | ``` 231 | 232 | ### log 233 | 234 | - 本地调试日志 235 | 236 | ```js 237 | const log = require('electron-log'); 238 | // log.transports.file.file = 'xx/record.log' 本地可指定文件 239 | // 默认日志存放 240 | // on Linux: ~/.config/{appName}/log.log 241 | // on macOS: ~/Library/Logs/{appName}/log.log 242 | // on Windows: user\AppData\Roaming\{appName}\log.log 243 | log.info('Hello, log'); 244 | log.warn('Some problem appears'); 245 | ``` 246 | 247 | ### 注意事项 248 | 249 | 1. 下载依赖和打包运行错误,请用cnpm或者配置npm config的electron ERROR路径 250 | 2. 任何地方的component文件夹名不可首字母大写 会被umi识别为路由而影响热加载等 251 | 3. 卡在node install.js : npm config edit 添加:electron_mirror="https://npm.taobao.org/mirrors/electron/" 252 | 4. 下载electron 一直失败,请删除包,然后安装全局的12版本的electron即可 253 | 5. umi版本随着业务量增大,tsx数量暴涨后,热加载效率会变低,可以尝试配置路由而不选用动态路由 254 | ``` 255 | // .umirc.ts 256 | const routes = [] // 自定义路由,来自src/render/.umi/core/routes.ts 257 | routes: closeFlexRoute ? routes : undefined, 258 | ``` 259 | 260 | ### 参考 261 | 262 | (官方electron文档)[https://www.electronjs.org/docs] 263 | (官方umi文档)[https://umijs.org/] 264 | (electron9版本升级到12)[https://www.cnblogs.com/mapleChain/p/15823267.html] 265 | 266 | ###### 能提供开发思路或者学习的话,麻烦给一颗卑微的星星star~谢谢 补充或者建议请提issue 267 | 268 | 269 | 270 | [github](https://github.com/qld-cf/electron-react-tpl) 271 | 272 | 273 | ### 附录 274 | 275 | ![update](https://cdn.nlark.com/yuque/0/2020/png/2166813/1600571961820-5b9ccc9e-f3dc-46f9-8f4f-5c8dde92fe12.png) 276 | ![update1](https://cdn.nlark.com/yuque/0/2020/jpeg/2166813/1600573119535-9fbb6b11-8ad8-4d65-a66d-17cc32ad7732.jpeg) 277 | -------------------------------------------------------------------------------- /src/render/assets/style/bootstrap-part.less: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap@4 部分样式 3 | */ 4 | 5 | .m-0 { 6 | margin: 0 !important; 7 | } 8 | 9 | .mt-0, 10 | .my-0 { 11 | margin-top: 0 !important; 12 | } 13 | 14 | .mr-0, 15 | .mx-0 { 16 | margin-right: 0 !important; 17 | } 18 | 19 | .mb-0, 20 | .my-0 { 21 | margin-bottom: 0 !important; 22 | } 23 | 24 | .ml-0, 25 | .mx-0 { 26 | margin-left: 0 !important; 27 | } 28 | 29 | .m-1 { 30 | margin: 0.25rem !important; 31 | } 32 | 33 | .mt-1, 34 | .my-1 { 35 | margin-top: 0.25rem !important; 36 | } 37 | 38 | .mr-1, 39 | .mx-1 { 40 | margin-right: 0.25rem !important; 41 | } 42 | 43 | .mb-1, 44 | .my-1 { 45 | margin-bottom: 0.25rem !important; 46 | } 47 | 48 | .ml-1, 49 | .mx-1 { 50 | margin-left: 0.25rem !important; 51 | } 52 | 53 | .m-2 { 54 | margin: 0.5rem !important; 55 | } 56 | 57 | .mt-2, 58 | .my-2 { 59 | margin-top: 0.5rem !important; 60 | } 61 | 62 | .mr-2, 63 | .mx-2 { 64 | margin-right: 0.5rem !important; 65 | } 66 | 67 | .mb-2, 68 | .my-2 { 69 | margin-bottom: 0.5rem !important; 70 | } 71 | 72 | .ml-2, 73 | .mx-2 { 74 | margin-left: 0.5rem !important; 75 | } 76 | 77 | .m-3 { 78 | margin: 1rem !important; 79 | } 80 | 81 | .mt-3, 82 | .my-3 { 83 | margin-top: 1rem !important; 84 | } 85 | 86 | .mr-3, 87 | .mx-3 { 88 | margin-right: 1rem !important; 89 | } 90 | 91 | .mb-3, 92 | .my-3 { 93 | margin-bottom: 1rem !important; 94 | } 95 | 96 | .ml-3, 97 | .mx-3 { 98 | margin-left: 1rem !important; 99 | } 100 | 101 | .m-4 { 102 | margin: 1.5rem !important; 103 | } 104 | 105 | .mt-4, 106 | .my-4 { 107 | margin-top: 1.5rem !important; 108 | } 109 | 110 | .mr-4, 111 | .mx-4 { 112 | margin-right: 1.5rem !important; 113 | } 114 | 115 | .mb-4, 116 | .my-4 { 117 | margin-bottom: 1.5rem !important; 118 | } 119 | 120 | .ml-4, 121 | .mx-4 { 122 | margin-left: 1.5rem !important; 123 | } 124 | 125 | .m-5 { 126 | margin: 3rem !important; 127 | } 128 | 129 | .mt-5, 130 | .my-5 { 131 | margin-top: 3rem !important; 132 | } 133 | 134 | .mr-5, 135 | .mx-5 { 136 | margin-right: 3rem !important; 137 | } 138 | 139 | .mb-5, 140 | .my-5 { 141 | margin-bottom: 3rem !important; 142 | } 143 | 144 | .ml-5, 145 | .mx-5 { 146 | margin-left: 3rem !important; 147 | } 148 | 149 | .p-0 { 150 | padding: 0 !important; 151 | } 152 | 153 | .pt-0, 154 | .py-0 { 155 | padding-top: 0 !important; 156 | } 157 | 158 | .pr-0, 159 | .px-0 { 160 | padding-right: 0 !important; 161 | } 162 | 163 | .pb-0, 164 | .py-0 { 165 | padding-bottom: 0 !important; 166 | } 167 | 168 | .pl-0, 169 | .px-0 { 170 | padding-left: 0 !important; 171 | } 172 | 173 | .p-1 { 174 | padding: 0.25rem !important; 175 | } 176 | 177 | .pt-1, 178 | .py-1 { 179 | padding-top: 0.25rem !important; 180 | } 181 | 182 | .pr-1, 183 | .px-1 { 184 | padding-right: 0.25rem !important; 185 | } 186 | 187 | .pb-1, 188 | .py-1 { 189 | padding-bottom: 0.25rem !important; 190 | } 191 | 192 | .pl-1, 193 | .px-1 { 194 | padding-left: 0.25rem !important; 195 | } 196 | 197 | .p-2 { 198 | padding: 0.5rem !important; 199 | } 200 | 201 | .pt-2, 202 | .py-2 { 203 | padding-top: 0.5rem !important; 204 | } 205 | 206 | .pr-2, 207 | .px-2 { 208 | padding-right: 0.5rem !important; 209 | } 210 | 211 | .pb-2, 212 | .py-2 { 213 | padding-bottom: 0.5rem !important; 214 | } 215 | 216 | .pl-2, 217 | .px-2 { 218 | padding-left: 0.5rem !important; 219 | } 220 | 221 | .p-3 { 222 | padding: 1rem !important; 223 | } 224 | 225 | .pt-3, 226 | .py-3 { 227 | padding-top: 1rem !important; 228 | } 229 | 230 | .pr-3, 231 | .px-3 { 232 | padding-right: 1rem !important; 233 | } 234 | 235 | .pb-3, 236 | .py-3 { 237 | padding-bottom: 1rem !important; 238 | } 239 | 240 | .pl-3, 241 | .px-3 { 242 | padding-left: 1rem !important; 243 | } 244 | 245 | .p-4 { 246 | padding: 1.5rem !important; 247 | } 248 | 249 | .pt-4, 250 | .py-4 { 251 | padding-top: 1.5rem !important; 252 | } 253 | 254 | .pr-4, 255 | .px-4 { 256 | padding-right: 1.5rem !important; 257 | } 258 | 259 | .pb-4, 260 | .py-4 { 261 | padding-bottom: 1.5rem !important; 262 | } 263 | 264 | .pl-4, 265 | .px-4 { 266 | padding-left: 1.5rem !important; 267 | } 268 | 269 | .p-5 { 270 | padding: 3rem !important; 271 | } 272 | 273 | .pt-5, 274 | .py-5 { 275 | padding-top: 3rem !important; 276 | } 277 | 278 | .pr-5, 279 | .px-5 { 280 | padding-right: 3rem !important; 281 | } 282 | 283 | .pb-5, 284 | .py-5 { 285 | padding-bottom: 3rem !important; 286 | } 287 | 288 | .pl-5, 289 | .px-5 { 290 | padding-left: 3rem !important; 291 | } 292 | 293 | .h-100 { 294 | height: 100%; 295 | } 296 | 297 | .w-100 { 298 | width: 100%; 299 | } 300 | 301 | .d-flex { 302 | display: flex !important; 303 | } 304 | 305 | 306 | .flex-row { 307 | -ms-flex-direction: row !important; 308 | flex-direction: row !important; 309 | } 310 | 311 | .flex-column { 312 | -ms-flex-direction: column !important; 313 | flex-direction: column !important; 314 | } 315 | 316 | .flex-row-reverse { 317 | -ms-flex-direction: row-reverse !important; 318 | flex-direction: row-reverse !important; 319 | } 320 | 321 | .flex-column-reverse { 322 | -ms-flex-direction: column-reverse !important; 323 | flex-direction: column-reverse !important; 324 | } 325 | 326 | .flex-wrap { 327 | -ms-flex-wrap: wrap !important; 328 | flex-wrap: wrap !important; 329 | } 330 | 331 | .flex-nowrap { 332 | -ms-flex-wrap: nowrap !important; 333 | flex-wrap: nowrap !important; 334 | } 335 | 336 | .flex-wrap-reverse { 337 | -ms-flex-wrap: wrap-reverse !important; 338 | flex-wrap: wrap-reverse !important; 339 | } 340 | 341 | .flex-fill { 342 | -ms-flex: 1 1 auto !important; 343 | flex: 1 1 auto !important; 344 | } 345 | 346 | .flex-grow-0 { 347 | -ms-flex-positive: 0 !important; 348 | flex-grow: 0 !important; 349 | } 350 | 351 | .flex-grow-1 { 352 | -ms-flex-positive: 1 !important; 353 | flex-grow: 1 !important; 354 | } 355 | 356 | .flex-shrink-0 { 357 | -ms-flex-negative: 0 !important; 358 | flex-shrink: 0 !important; 359 | } 360 | 361 | .flex-shrink-1 { 362 | -ms-flex-negative: 1 !important; 363 | flex-shrink: 1 !important; 364 | } 365 | 366 | .justify-content-start { 367 | -ms-flex-pack: start !important; 368 | justify-content: flex-start !important; 369 | } 370 | 371 | .justify-content-end { 372 | -ms-flex-pack: end !important; 373 | justify-content: flex-end !important; 374 | } 375 | 376 | .justify-content-center { 377 | -ms-flex-pack: center !important; 378 | justify-content: center !important; 379 | } 380 | 381 | .justify-content-between { 382 | -ms-flex-pack: justify !important; 383 | justify-content: space-between !important; 384 | } 385 | 386 | .justify-content-around { 387 | -ms-flex-pack: distribute !important; 388 | justify-content: space-around !important; 389 | } 390 | 391 | .align-items-start { 392 | -ms-flex-align: start !important; 393 | align-items: flex-start !important; 394 | } 395 | 396 | .align-items-end { 397 | -ms-flex-align: end !important; 398 | align-items: flex-end !important; 399 | } 400 | 401 | .align-items-center { 402 | -ms-flex-align: center !important; 403 | align-items: center !important; 404 | } 405 | 406 | .align-items-baseline { 407 | -ms-flex-align: baseline !important; 408 | align-items: baseline !important; 409 | } 410 | 411 | .align-items-stretch { 412 | -ms-flex-align: stretch !important; 413 | align-items: stretch !important; 414 | } 415 | 416 | .align-content-start { 417 | -ms-flex-line-pack: start !important; 418 | align-content: flex-start !important; 419 | } 420 | 421 | .align-content-end { 422 | -ms-flex-line-pack: end !important; 423 | align-content: flex-end !important; 424 | } 425 | 426 | .align-content-center { 427 | -ms-flex-line-pack: center !important; 428 | align-content: center !important; 429 | } 430 | 431 | .align-content-between { 432 | -ms-flex-line-pack: justify !important; 433 | align-content: space-between !important; 434 | } 435 | 436 | .align-content-around { 437 | -ms-flex-line-pack: distribute !important; 438 | align-content: space-around !important; 439 | } 440 | 441 | .align-content-stretch { 442 | -ms-flex-line-pack: stretch !important; 443 | align-content: stretch !important; 444 | } 445 | 446 | .align-self-auto { 447 | -ms-flex-item-align: auto !important; 448 | align-self: auto !important; 449 | } 450 | 451 | .align-self-start { 452 | -ms-flex-item-align: start !important; 453 | align-self: flex-start !important; 454 | } 455 | 456 | .align-self-end { 457 | -ms-flex-item-align: end !important; 458 | align-self: flex-end !important; 459 | } 460 | 461 | .align-self-center { 462 | -ms-flex-item-align: center !important; 463 | align-self: center !important; 464 | } 465 | 466 | .align-self-baseline { 467 | -ms-flex-item-align: baseline !important; 468 | align-self: baseline !important; 469 | } 470 | 471 | .align-self-stretch { 472 | -ms-flex-item-align: stretch !important; 473 | align-self: stretch !important; 474 | } 475 | 476 | .text-center { 477 | text-align: center; 478 | } 479 | 480 | .text-left { 481 | text-align: left; 482 | } 483 | 484 | .text-right { 485 | text-align: right; 486 | } 487 | 488 | .cursor-pointer { 489 | cursor: pointer; 490 | } 491 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # electron-react-umi-tpl 2 | 3 | [github](https://github.com/qld-cf/electron-react-tpl) 4 | [English Version](https://github.com/qld-cf/electron-react-tpl/blob/master/README_EN.md) 5 | 6 | Experience: 7 | [Download](https://www.yuque.com/docs/share/c453798a-12b3-4eb7-a725-4b960732eab1?#《Resource 1》) 8 | 9 | 10 | ##### Relatively pure template, please [click here (to be maintained)](https://github.com/qld-cf/electron-common-react-tpl), no umi is more free, hot loading is faster 11 | 12 | 13 | ##### TODO 14 | 1. The web+cdn version is convenient for web server deployment; 15 | 16 | Changelog: 17 | 18 | 1. 2020-06-08 Added [full update](https://segmentfault.com/a/1190000016674982) function 19 | 2. 2020-06-29 Add [remote incremental update function](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/PART_UPDATE.md), no need to download the package to renew install updates; 20 | 3. 2020-07-27 Optimize the initialization client loading waiting page, optimize the page 21 | 4. 2020-08-18 Added [select copy, right-click and paste | custom function](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/Copy.md) 22 | 5. 2020-08-24 Optimize the white screen of some win7 systems (win7 turns off hardware acceleration) 23 | 6. 2020-08-28 Add `koa2 + typescript + websocket + redis + log4js` server websocket|redis function, client socket.io 24 | 7. 2020-09-20 Added error boundary recognition and processing (after encountering errors such as React syntax on the page, the error log is automatically captured and recorded, the page returns to the home page, the development mode is turned off by default, and the production environment is turned on) 25 | 8. 2020-10-16 Added [Restriction to allow only single application/single instance](https://www.electronjs.org/docs/api/app#%E4%BA%8B%E4%BB%B6- second-instance) 26 | 9. 2020-11-05 Fix the incremental update problem, optimize some codes, remove the old version of printing, and add the cli tool to start the command line 27 | 10. 2021-02-02 Fix eslint, tslint issues 28 | 11. 2021-03-29 Added the main process HRM function, modifying the main process can reload the client 29 | 12. 2022-01-19 Upgrade the electron version to 12, fix the problems caused by the upgrade (another busy year..) 30 | 13. 2022-02-21 Add local data persistence sqlite + sequelize, branch: [feature/add_sqlite_20220209](https://github.com/qld-cf/electron-react-tpl/tree/feature/add_sqlite_20220209) 31 | 32 | --- 33 | 34 | [Chinese](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/CHANGE_LOG.md) 35 | [English Version](https://github.com/qld-cf/electron-react-tpl/blob/master/docs/CHANGE_LOG_EN.md) 36 | 37 | 38 | 39 | `electron 12` + `umi 3.2` + `typescript` + `react 16.12` + `redux` + `antDesign 4.0` + `eslint tslint react-tslint` scaffolding, download and use, the infrastructure has been prepared for you 40 | 41 | #### Client Integration: 42 | - [x] Auto update (electron-builder) 43 | - [x] Tray menu app start loading loading bar 44 | - [x] electron-log local log electron-store local storage 45 | - [x] app packaging icon added incremental update 46 | - [x] Add redux-devtools plugin 47 | - [x] right click copy paste 48 | - [ ] App crash information collection 49 | - [ ] App message notification, shortcut keys, etc. 50 | 51 | 52 | #### web side: 53 | 1. Based on [umi](https://umijs.org/zh-CN) scaffolding, the basic configuration has been integrated, and developers can focus on writing business code 54 | 2. Local storage redux (redux-saga) 55 | 3. antDesign >= 4.0 56 | 4. iconfont icon 57 | 58 | - Menu configuration `src/layouts/menu/config.tsx` 59 | 60 | 61 | #### Fix: 62 | - [x] Upgrade to electron9.1.0, sync official 63 | - [x] Some win7 white screen solutions 64 | - [ ] To be fixed sqlite packaging problem 65 | - [ ] Node Api function encapsulation is decoupled from rendering process business 66 | 67 | 68 | #### Next Feature 69 | - [x] version based on create-react-app 70 | 71 | 72 | ### Tool command line startup 73 | 74 | ```` 75 | > npm i -g maple-react-cli // Install the cli tool globally 76 | > maple-react-cli // initialization 77 | 78 | ? Please select your next action Select template type 79 | ? Choose a project template to initialize your project~ 80 | ✔ electron-react-tpl 81 | ✔ Initializing.. 82 | ✔ Ready to pull code... 83 | ? Please enter your local initialized project name~ Enter a custom project name, such as project 84 | ✔ Pull code successfully 85 | ✔ Installed dependencies successfully~ 86 | ? Run the project? yes 87 | Congratulations~The project started successfully~Please wait... 88 | ```` 89 | 90 | [cli tool](https://github.com/qld-cf/maple-react-cli) 91 | 92 | 93 | ### local start 94 | 95 | ```` 96 | npm i // If the installation is unsuccessful, please use yarn 97 | npm start 98 | npm run pack // The default is to pack according to the current system 99 | npm run pack-mac // Pack the mac platform 100 | npm run pack-windows // package windows platform 101 | npm run pack-all // pack all platforms 102 | 103 | // If you need to open websocket 104 | 105 | cd ./server 106 | npm run dev 107 | ```` 108 | 109 | 110 | ### directory tree 111 | ```` 112 | |-- project 113 | |-- .editorconfig 114 | |-- .eslintrc.js 115 | |-- .gitignore 116 | |-- .gitlab-ci.yml 117 | |-- .prettierignore 118 | |-- .prettierrc.js 119 | |-- directoryList.md 120 | |-- package-lock.json 121 | |-- package.json 122 | |-- README.md 123 | |-- tsconfig.json 124 | |-- typings.d.ts 125 | |-- eslint-rules Customize eslint configuration 126 | | |-- base.js 127 | | |-- react.js 128 | | |-- ts.js 129 | |-- src 130 | |-- main main process 131 | |-- app-update.yml Automatic update configuration for production environment 132 | | |-- bundle.js is automatically generated 133 | | |-- bundle.js.map 134 | | |-- dev-app-update.yml The development environment automatically updates the configuration 135 | | |-- index.js entry 136 | | |-- loading.html 137 | | |-- preload.js 138 | | |-- README.md 139 | | |-- config compile configuration 140 | | | |-- config.js 141 | | | |-- webpack.config.js 142 | | |-- controls control set 143 | | | |-- AppAutoUpdater.js 144 | | | |-- AppMainWindow.js 145 | | | |-- AppTray.js 146 | | | |-- electron-helper.js 147 | | |-- public attachments 148 | | | |-- icon.ico 149 | | | |-- icon.png 150 | | | |-- tray.png 151 | | |-- script compile script 152 | | |-- build.js 153 | |-- render rendering process 154 | |-- .env 155 | |-- .umirc.ts 156 | |-- app.ts 157 | |-- global.less 158 | |-- README.md 159 | |-- .umi umi automatically generates configuration and plugins, etc. 160 | | |-- umi.ts 161 | | |-- core 162 | | |-- plugin-dva 163 | |-- plugin-initial-state 164 | |-- plugin-model 165 | | |-- plugin-request 166 | |-- api interface collection 167 | | |-- api.list.ts 168 | |-- assets attachment 169 | | |-- image 170 | | | |-- yay.jpg 171 | | |-- style 172 | | |-- bootstrap-part.less 173 | | |-- common.less 174 | |-- common 175 | | |-- enum.ts 176 | | |-- global.ts 177 | |-- components component 178 | | |-- readme.md 179 | | |-- AutoUpdate 180 | | | |-- index.tsx 181 | | | |-- style.less 182 | | |-- FormCps 183 | | | |-- index.tsx 184 | | | |-- readme.md 185 | | |-- TableCps 186 | | |-- index.tsx 187 | | |-- readme.md 188 | |-- config configuration 189 | | |-- iconfont.ts 190 | | |-- menus.tsx 191 | |-- dist local packaging generated files 192 | |-- layouts layout 193 | | |-- index.less 194 | | |-- index.tsx 195 | | |-- header 196 | | | |-- index.less 197 | | | |-- index.tsx 198 | | |-- loading 199 | | | |-- index.less 200 | | | |-- index.tsx 201 | | |-- menu 202 | | |-- index.less 203 | | |-- index.tsx 204 | |-- mock 205 | | |-- foo.ts 206 | |-- models redux 207 | | |-- xxStore.ts 208 | |-- pages 209 | | |-- home.normal.less 210 | | |-- index.tsx 211 | | |-- Foo example 212 | | | |-- index.tsx 213 | | | |-- components 214 | | | | |-- TableList.tsx 215 | | | |-- models 216 | | | | |-- foo.ts 217 | | | |-- services 218 | | | |-- foo.ts 219 | | |-- Home Business 220 | | |-- Edge 221 | | | |-- index.tsx 222 | | |-- WebSocket 223 | | |-- index.tsx 224 | |-- utils toolset 225 | 226 | ```` 227 | 228 | ### log 229 | 230 | - Local debug log 231 | 232 | ````js 233 | const log = require('electron-log'); 234 | // log.transports.file.file = 'xx/record.log' local specifiable file 235 | // default log storage 236 | // on Linux: ~/.config/{appName}/log.log 237 | // on macOS: ~/Library/Logs/{appName}/log.log 238 | // on Windows: user\AppData\Roaming\{appName}\log.log 239 | log.info('Hello, log'); 240 | log.warn('Some problem appears'); 241 | ```` 242 | 243 | ### Precautions 244 | 245 | 1. Download dependencies and package running errors, please use cnpm or configure the electron ERROR path of npm config 246 | 2. The name of the component folder in any place cannot be capitalized. It will be recognized by umi as a route and affect hot loading, etc. 247 | 3. Stuck in node install.js : npm config edit add: electron_mirror="https://npm.taobao.org/mirrors/electron/" 248 | 4. The download of electron has been failing, please delete the package, and then install the global version 12 of electron. 249 | 5. As the business volume of the umi version increases and the number of tsx surges, the hot loading efficiency will become lower. You can try to configure routing instead of dynamic routing. 250 | ```` 251 | // .umirc.ts 252 | const routes = [] // custom routes, from src/render/.umi/core/routes.ts 253 | routes: closeFlexRoute ? routes : undefined, 254 | ```` 255 | 256 | ### refer to 257 | 258 | (Official electron documentation)[https://www.electronjs.org/docs] 259 | (Official umi documentation) [https://umijs.org/] 260 | (electron9 version upgraded to 12)[https://www.cnblogs.com/mapleChain/p/15823267.html] 261 | 262 | ###### Can be used or easy to use, please give a humble star~ Thank you, please file an issue for supplements or suggestions 263 | 264 | 265 | 266 | [github](https://github.com/qld-cf/electron-react-tpl) 267 | 268 | 269 | ### Appendix 270 | 271 | ![update](https://cdn.nlark.com/yuque/0/2020/png/2166813/1600571961820-5b9ccc9e-f3dc-46f9-8f4f-5c8dde92fe12.png) 272 | ![update1](https://cdn.nlark.com/yuque/0/2020/jpeg/2166813/1600573119535-9fbb6b11-8ad8-4d65-a66d-17cc32ad7732.jpeg) -------------------------------------------------------------------------------- /src/render/config/iconfont.ts: -------------------------------------------------------------------------------- 1 | const icons = [ 2 | 'step-backward', 3 | 'step-forward', 4 | 'fast-backward', 5 | 'fast-forward', 6 | 'shrink', 7 | 'arrows-alt', 8 | 'down', 9 | 'up', 10 | 'left', 11 | 'right', 12 | 'caret-up', 13 | 'caret-down', 14 | 'caret-left', 15 | 'caret-right', 16 | 'up-circle', 17 | 'down-circle', 18 | 'left-circle', 19 | 'right-circle', 20 | 'double-right', 21 | 'double-left', 22 | 'vertical-left', 23 | 'vertical-right', 24 | 'vertical-align-top', 25 | 'vertical-align-middle', 26 | 'vertical-align-bottom', 27 | 'forward', 28 | 'backward', 29 | 'rollback', 30 | 'enter', 31 | 'retweet', 32 | 'swap', 33 | 'swap-left', 34 | 'swap-right', 35 | 'arrow-up', 36 | 'arrow-down', 37 | 'arrow-left', 38 | 'arrow-right', 39 | 'play-circle', 40 | 'up-square', 41 | 'down-square', 42 | 'left-square', 43 | 'right-square', 44 | 'login', 45 | 'logout', 46 | 'menu-fold', 47 | 'menu-unfold', 48 | 'border-bottom', 49 | 'border-horizontal', 50 | 'border-inner', 51 | 'border-outer', 52 | 'border-left', 53 | 'border-right', 54 | 'border-top', 55 | 'border-verticle', 56 | 'pic-center', 57 | 'pic-left', 58 | 'pic-right', 59 | 'radius-bottomleft', 60 | 'radius-bottomright', 61 | 'radius-upleft', 62 | 'radius-upright', 63 | 'fullscreen', 64 | 'fullscreen-exit', 65 | 'question', 66 | 'question-circle', 67 | 'plus', 68 | 'plus-circle', 69 | 'pause', 70 | 'pause-circle', 71 | 'minus', 72 | 'minus-circle', 73 | 'plus-square', 74 | 'minus-square', 75 | 'info', 76 | 'info-circle', 77 | 'exclamation', 78 | 'exclamation-circle', 79 | 'close', 80 | 'close-circle', 81 | 'close-square', 82 | 'check', 83 | 'check-circle', 84 | 'check-square', 85 | 'clock-circle', 86 | 'warning', 87 | 'issues-close', 88 | 'stop', 89 | 'edit', 90 | 'form', 91 | 'copy', 92 | 'scissor', 93 | 'delete', 94 | 'snippets', 95 | 'diff', 96 | 'highlight', 97 | 'align-center', 98 | 'align-left', 99 | 'align-right', 100 | 'bg-colors', 101 | 'bold', 102 | 'italic', 103 | 'underline', 104 | 'strikethrough', 105 | 'redo', 106 | 'undo', 107 | 'zoom-in', 108 | 'zoom-out', 109 | 'font-colors', 110 | 'font-size', 111 | 'line-height', 112 | 'dash', 113 | 'small-dash', 114 | 'sort-ascending', 115 | 'sort-descending', 116 | 'drag', 117 | 'ordered-list', 118 | 'unordered-list', 119 | 'radius-setting', 120 | 'column-width', 121 | 'column-height', 122 | 'area-chart', 123 | 'pie-chart', 124 | 'bar-chart', 125 | 'dot-chart', 126 | 'line-chart', 127 | 'radar-chart', 128 | 'heat-map', 129 | 'fall', 130 | 'rise', 131 | 'stock', 132 | 'box-plot', 133 | 'fund', 134 | 'sliders', 135 | 'android', 136 | 'apple', 137 | 'windows', 138 | 'ie', 139 | 'chrome', 140 | 'github', 141 | 'aliwangwang', 142 | 'dingding', 143 | 'weibo-square', 144 | 'weibo-circle', 145 | 'taobao-circle', 146 | 'html5', 147 | 'weibo', 148 | 'twitter', 149 | 'wechat', 150 | 'youtube', 151 | 'alipay-circle', 152 | 'taobao', 153 | 'skype', 154 | 'qq', 155 | 'medium-workmark', 156 | 'gitlab', 157 | 'medium', 158 | 'linkedin', 159 | 'google-plus', 160 | 'dropbox', 161 | 'facebook', 162 | 'codepen', 163 | 'code-sandbox', 164 | 'amazon', 165 | 'google', 166 | 'codepen-circle', 167 | 'alipay', 168 | 'ant-design', 169 | 'ant-cloud', 170 | 'aliyun', 171 | 'zhihu', 172 | 'slack', 173 | 'slack-square', 174 | 'behance', 175 | 'behance-square', 176 | 'dribbble', 177 | 'dribbble-square', 178 | 'instagram', 179 | 'yuque', 180 | 'alibaba', 181 | 'yahoo', 182 | 'reddit', 183 | 'sketch', 184 | 'account-book', 185 | 'alert', 186 | 'api', 187 | 'appstore', 188 | 'audio', 189 | 'bank', 190 | 'bell', 191 | 'book', 192 | 'bug', 193 | 'bulb', 194 | 'calculator', 195 | 'build', 196 | 'calendar', 197 | 'camera', 198 | 'car', 199 | 'carry-out', 200 | 'cloud', 201 | 'code', 202 | 'compass', 203 | 'contacts', 204 | 'container', 205 | 'control', 206 | 'credit-card', 207 | 'crown', 208 | 'customer-service', 209 | 'dashboard', 210 | 'database', 211 | 'dislike', 212 | 'environment', 213 | 'experiment', 214 | 'eye-invisible', 215 | 'eye', 216 | 'file-add', 217 | 'file-excel', 218 | 'file-exclamation', 219 | 'file-image', 220 | 'file-markdown', 221 | 'file-pdf', 222 | 'file-ppt', 223 | 'file-text', 224 | 'file-unknown', 225 | 'file-word', 226 | 'file-zip', 227 | 'file', 228 | 'filter', 229 | 'fire', 230 | 'flag', 231 | 'folder-add', 232 | 'folder', 233 | 'folder-open', 234 | 'frown', 235 | 'funnel-plot', 236 | 'gift', 237 | 'hdd', 238 | 'heart', 239 | 'home', 240 | 'hourglass', 241 | 'idcard', 242 | 'insurance', 243 | 'interaction', 244 | 'layout', 245 | 'like', 246 | 'lock', 247 | 'mail', 248 | 'medicine-box', 249 | 'meh', 250 | 'message', 251 | 'mobile', 252 | 'money-collect', 253 | 'pay-circle', 254 | 'notification', 255 | 'phone', 256 | 'picture', 257 | 'play-square', 258 | 'printer', 259 | 'profile', 260 | 'project', 261 | 'pushpin', 262 | 'property-safety', 263 | 'read', 264 | 'reconciliation', 265 | 'red-envelope', 266 | 'rest', 267 | 'rocket', 268 | 'safety-certificate', 269 | 'save', 270 | 'schedule', 271 | 'security-scan', 272 | 'setting', 273 | 'shop', 274 | 'shopping', 275 | 'skin', 276 | 'smile', 277 | 'sound', 278 | 'star', 279 | 'switcher', 280 | 'tablet', 281 | 'tag', 282 | 'tags', 283 | 'tool', 284 | 'thunderbolt', 285 | 'trophy', 286 | 'unlock', 287 | 'usb', 288 | 'video-camera', 289 | 'wallet', 290 | 'apartment', 291 | 'audit', 292 | 'barcode', 293 | 'bars', 294 | 'block', 295 | 'border', 296 | 'branches', 297 | 'ci', 298 | 'cloud-download', 299 | 'cloud-server', 300 | 'cloud-sync', 301 | 'cloud-upload', 302 | 'cluster', 303 | 'coffee', 304 | 'copyright', 305 | 'deployment-unit', 306 | 'desktop', 307 | 'disconnect', 308 | 'dollar', 309 | 'download', 310 | 'ellipsis', 311 | 'euro', 312 | 'exception', 313 | 'export', 314 | 'file-done', 315 | 'file-jpg', 316 | 'file-protect', 317 | 'file-sync', 318 | 'file-search', 319 | 'fork', 320 | 'gateway', 321 | 'global', 322 | 'gold', 323 | 'history', 324 | 'import', 325 | 'inbox', 326 | 'key', 327 | 'laptop', 328 | 'link', 329 | 'line', 330 | 'loading-3-quarters', 331 | 'loading', 332 | 'man', 333 | 'menu', 334 | 'monitor', 335 | 'more', 336 | 'number', 337 | 'percentage', 338 | 'paper-clip', 339 | 'pound', 340 | 'poweroff', 341 | 'pull-request', 342 | 'qrcode', 343 | 'reload', 344 | 'safety', 345 | 'robot', 346 | 'scan', 347 | 'search', 348 | 'select', 349 | 'shake', 350 | 'share-alt', 351 | 'shopping-cart', 352 | 'solution', 353 | 'sync', 354 | 'table', 355 | 'team', 356 | 'to-top', 357 | 'trademark', 358 | 'transaction', 359 | 'upload', 360 | 'user-add', 361 | 'user-delete', 362 | 'usergroup-add', 363 | 'user', 364 | 'usergroup-delete', 365 | 'wifi', 366 | 'woman' 367 | ]; 368 | 369 | const iconfontIcons = [ 370 | '两季品', 371 | '物流', 372 | '预约', 373 | '定位', 374 | '素材库', 375 | '上门取货', 376 | '超级服务', 377 | '素材中心', 378 | '全链路可售', 379 | '权益发放工具', 380 | 'S&OP', 381 | '单仓发全国', 382 | '增值服务', 383 | '活动监控', 384 | '活动全景', 385 | '活动协同', 386 | 'WBR', 387 | '目标监控', 388 | '赚翻乐', 389 | 'ANS', 390 | '价值评估', 391 | '活动复盘', 392 | '活动协同', 393 | '目标制定', 394 | '目标管理', 395 | '营销流量配置', 396 | 'GMV目标', 397 | '选品系统', 398 | '运营工具', 399 | '营销流量配置', 400 | '盘货系统', 401 | '货品调拨监控', 402 | '数据风控预警平台', 403 | '运营中枢', 404 | '结算效率分析', 405 | '销量实时看板', 406 | '实时看板', 407 | '订单履约看板', 408 | '实时预测', 409 | '销量预测', 410 | '进货参谋', 411 | '多维度看板', 412 | '财务分析', 413 | '前端商品毛利', 414 | '经营分析', 415 | '数据分析', 416 | '商家数据', 417 | '准确率分析', 418 | '数据管理', 419 | '分销商结算', 420 | '结算工具箱', 421 | '成本查询', 422 | '后端商品毛利', 423 | '供应商结算', 424 | '补货计划', 425 | '协同计划', 426 | '销量计划', 427 | '清仓计划', 428 | '经销计划', 429 | '优化计划', 430 | '清退计划', 431 | '管理计划', 432 | '退仓计划', 433 | '商品计划', 434 | '销售计划', 435 | '贴纸计划', 436 | '运营管理', 437 | '会场管理', 438 | '会场', 439 | '菜鸟运营指标', 440 | '配置管理', 441 | '权益管理', 442 | '生命周期', 443 | '公告管理', 444 | '排期', 445 | '销售管理', 446 | '排期管理', 447 | '生命周期管理', 448 | '品牌授权管理', 449 | '招商管理', 450 | '品类管理', 451 | '品控', 452 | '绩效', 453 | '销售', 454 | '岗位授权', 455 | '品牌授权', 456 | '活动管理', 457 | '风控', 458 | '绩效管理', 459 | '岗位授权管理', 460 | '风控管理', 461 | '合同管理', 462 | '品控管理', 463 | '智慧营销管理', 464 | '智慧销售管理', 465 | '合同', 466 | '智慧销售', 467 | '智慧营销', 468 | '品类规划', 469 | '货品分层', 470 | '智能选品', 471 | '新品发布', 472 | '询货管理', 473 | '货品择仓', 474 | '进货管理', 475 | '进货', 476 | '备货管理', 477 | '备货', 478 | '货品调拨', 479 | '询货', 480 | '盘货', 481 | '补货管理', 482 | '盘或管理', 483 | '补货', 484 | '货品调拨管理', 485 | '退仓管理', 486 | '仓库健康指数管理', 487 | '路由仓管理', 488 | '清仓管理', 489 | '出入库管理', 490 | '库存保障管理', 491 | '库存健康管理', 492 | '库存健康指数', 493 | '退仓', 494 | '库存健康', 495 | '清仓', 496 | '路由仓', 497 | '库存保障', 498 | '渠道库存', 499 | '出入库', 500 | '入库管理', 501 | '渠道库存管理', 502 | '库存', 503 | '库存管理', 504 | '入库', 505 | '调拨执行', 506 | '调拨差异', 507 | '调拨管理', 508 | '渠道退货', 509 | '渠道销售', 510 | '渠道分货', 511 | '渠道管理', 512 | '退货单管理', 513 | '在途报表管理', 514 | '进销存报表', 515 | '进销存报表管理', 516 | '在途报表', 517 | '报废单管理', 518 | '报表管理', 519 | '退货单', 520 | '调拨单管理', 521 | '报废单', 522 | '调拨单', 523 | '头程单管理', 524 | '退仓单管理', 525 | '退仓单', 526 | '头程单', 527 | '单据管理', 528 | '月结管理', 529 | '返利规则管理', 530 | '结算报表', 531 | '结算单管理', 532 | '月结', 533 | '返利规则', 534 | '结算管理', 535 | '结算单', 536 | '结算报表', 537 | '结算', 538 | '定价管理', 539 | '定价', 540 | '采购计划管理', 541 | '预约采购', 542 | '资源订购管理', 543 | '采购计划', 544 | '预约采购管理', 545 | '采购单管理', 546 | '资源订购', 547 | '采购价管理', 548 | '采购管理', 549 | '采购价', 550 | '采购单', 551 | '选品', 552 | '商品备案管理', 553 | '爆款商品', 554 | '商品复制', 555 | '商品复制管理', 556 | '商品发布管理', 557 | '商品备案', 558 | '商品管理', 559 | '供应商管理', 560 | '商家', 561 | '分销商管理', 562 | '商家管理', 563 | '人群管理', 564 | '人群', 565 | '用户管理', 566 | '分销商', 567 | '供应商', 568 | '货', 569 | '资源', 570 | '配置', 571 | '权益', 572 | '目录', 573 | '服务', 574 | '中枢', 575 | '经销', 576 | '运营', 577 | '图片', 578 | '平台', 579 | '列表', 580 | '活动', 581 | '看板', 582 | '进/补', 583 | '工具', 584 | '对话', 585 | '公告', 586 | '结构', 587 | '指标', 588 | '券', 589 | '删除', 590 | '解锁', 591 | '进出', 592 | '工具箱', 593 | '发布', 594 | '上升', 595 | '日历', 596 | '仓库', 597 | '价格', 598 | '采购', 599 | '渠道', 600 | '报表', 601 | '锁', 602 | '单据', 603 | '商品', 604 | '招商', 605 | '智慧', 606 | '正向', 607 | '目标', 608 | '关闭', 609 | '逆向', 610 | '加', 611 | '健康', 612 | '减', 613 | '协同', 614 | '价格', 615 | '设置/管理', 616 | '监控', 617 | '收起', 618 | '拒绝', 619 | '流量', 620 | '注意', 621 | '下拉', 622 | '调拨', 623 | '能力', 624 | '疑问', 625 | '数据', 626 | '安全', 627 | '用户', 628 | '消息', 629 | '搜索', 630 | '完成', 631 | '关闭', 632 | '减' 633 | ]; 634 | 635 | const iconfontIconValues = [ 636 | 'icon-liangjipin', 637 | 'icon-RectangleCopy258', 638 | 'icon-RectangleCopy257', 639 | 'icon-dingwei', 640 | 'icon-RectangleCopy256', 641 | 'icon-RectangleCopy255', 642 | 'icon-RectangleCopy254', 643 | 'icon-RectangleCopy253', 644 | 'icon-RectangleCopy252', 645 | 'icon-RectangleCopy251', 646 | 'icon-RectangleCopy250', 647 | 'icon-RectangleCopy249', 648 | 'icon-RectangleCopy248', 649 | 'icon-RectangleCopy247', 650 | 'icon-RectangleCopy246', 651 | 'icon-RectangleCopy245', 652 | 'icon-RectangleCopy244', 653 | 'icon-RectangleCopy243', 654 | 'icon-RectangleCopy242', 655 | 'icon-RectangleCopy241', 656 | 'icon-RectangleCopy240', 657 | 'icon-RectangleCopy239', 658 | 'icon-RectangleCopy238', 659 | 'icon-RectangleCopy237', 660 | 'icon-RectangleCopy236', 661 | 'icon-RectangleCopy235', 662 | 'icon-RectangleCopy234', 663 | 'icon-RectangleCopy233', 664 | 'icon-RectangleCopy232', 665 | 'icon-RectangleCopy231', 666 | 'icon-RectangleCopy230', 667 | 'icon-RectangleCopy229', 668 | 'icon-RectangleCopy228', 669 | 'icon-RectangleCopy227', 670 | 'icon-RectangleCopy226', 671 | 'icon-RectangleCopy225', 672 | 'icon-RectangleCopy224', 673 | 'icon-RectangleCopy223', 674 | 'icon-RectangleCopy222', 675 | 'icon-RectangleCopy221', 676 | 'icon-RectangleCopy220', 677 | 'icon-RectangleCopy219', 678 | 'icon-RectangleCopy218', 679 | 'icon-RectangleCopy217', 680 | 'icon-RectangleCopy216', 681 | 'icon-RectangleCopy215', 682 | 'icon-RectangleCopy214', 683 | 'icon-RectangleCopy213', 684 | 'icon-RectangleCopy212', 685 | 'icon-RectangleCopy211', 686 | 'icon-RectangleCopy210', 687 | 'icon-RectangleCopy209', 688 | 'icon-RectangleCopy208', 689 | 'icon-RectangleCopy207', 690 | 'icon-RectangleCopy206', 691 | 'icon-RectangleCopy205', 692 | 'icon-RectangleCopy204', 693 | 'icon-RectangleCopy203', 694 | 'icon-RectangleCopy202', 695 | 'icon-RectangleCopy201', 696 | 'icon-RectangleCopy200', 697 | 'icon-RectangleCopy199', 698 | 'icon-RectangleCopy198', 699 | 'icon-RectangleCopy197', 700 | 'icon-RectangleCopy196', 701 | 'icon-RectangleCopy195', 702 | 'icon-RectangleCopy194', 703 | 'icon-RectangleCopy193', 704 | 'icon-RectangleCopy192', 705 | 'icon-RectangleCopy191', 706 | 'icon-RectangleCopy190', 707 | 'icon-RectangleCopy189', 708 | 'icon-RectangleCopy188', 709 | 'icon-RectangleCopy187', 710 | 'icon-RectangleCopy186', 711 | 'icon-RectangleCopy185', 712 | 'icon-RectangleCopy184', 713 | 'icon-RectangleCopy183', 714 | 'icon-RectangleCopy182', 715 | 'icon-RectangleCopy181', 716 | 'icon-RectangleCopy180', 717 | 'icon-RectangleCopy179', 718 | 'icon-RectangleCopy178', 719 | 'icon-RectangleCopy177', 720 | 'icon-RectangleCopy176', 721 | 'icon-RectangleCopy175', 722 | 'icon-RectangleCopy174', 723 | 'icon-RectangleCopy173', 724 | 'icon-RectangleCopy172', 725 | 'icon-RectangleCopy171', 726 | 'icon-RectangleCopy170', 727 | 'icon-RectangleCopy169', 728 | 'icon-RectangleCopy168', 729 | 'icon-RectangleCopy167', 730 | 'icon-RectangleCopy166', 731 | 'icon-RectangleCopy165', 732 | 'icon-RectangleCopy164', 733 | 'icon-RectangleCopy163', 734 | 'icon-RectangleCopy162', 735 | 'icon-RectangleCopy161', 736 | 'icon-RectangleCopy160', 737 | 'icon-RectangleCopy159', 738 | 'icon-RectangleCopy158', 739 | 'icon-RectangleCopy157', 740 | 'icon-RectangleCopy156', 741 | 'icon-RectangleCopy155', 742 | 'icon-RectangleCopy154', 743 | 'icon-RectangleCopy153', 744 | 'icon-RectangleCopy152', 745 | 'icon-RectangleCopy151', 746 | 'icon-RectangleCopy150', 747 | 'icon-RectangleCopy149', 748 | 'icon-RectangleCopy148', 749 | 'icon-RectangleCopy147', 750 | 'icon-RectangleCopy146', 751 | 'icon-RectangleCopy145', 752 | 'icon-RectangleCopy144', 753 | 'icon-RectangleCopy143', 754 | 'icon-RectangleCopy142', 755 | 'icon-RectangleCopy141', 756 | 'icon-RectangleCopy140', 757 | 'icon-RectangleCopy139', 758 | 'icon-RectangleCopy138', 759 | 'icon-RectangleCopy137', 760 | 'icon-RectangleCopy136', 761 | 'icon-RectangleCopy135', 762 | 'icon-RectangleCopy134', 763 | 'icon-RectangleCopy133', 764 | 'icon-RectangleCopy132', 765 | 'icon-RectangleCopy131', 766 | 'icon-RectangleCopy130', 767 | 'icon-RectangleCopy129', 768 | 'icon-RectangleCopy128', 769 | 'icon-RectangleCopy127', 770 | 'icon-RectangleCopy126', 771 | 'icon-RectangleCopy125', 772 | 'icon-RectangleCopy124', 773 | 'icon-RectangleCopy123', 774 | 'icon-RectangleCopy122', 775 | 'icon-RectangleCopy121', 776 | 'icon-RectangleCopy120', 777 | 'icon-RectangleCopy119', 778 | 'icon-RectangleCopy118', 779 | 'icon-RectangleCopy117', 780 | 'icon-RectangleCopy116', 781 | 'icon-RectangleCopy115', 782 | 'icon-RectangleCopy114', 783 | 'icon-RectangleCopy113', 784 | 'icon-RectangleCopy112', 785 | 'icon-RectangleCopy111', 786 | 'icon-RectangleCopy110', 787 | 'icon-RectangleCopy109', 788 | 'icon-RectangleCopy108', 789 | 'icon-RectangleCopy107', 790 | 'icon-RectangleCopy106', 791 | 'icon-RectangleCopy105', 792 | 'icon-RectangleCopy104', 793 | 'icon-RectangleCopy103', 794 | 'icon-RectangleCopy102', 795 | 'icon-RectangleCopy101', 796 | 'icon-RectangleCopy100', 797 | 'icon-RectangleCopy99', 798 | 'icon-RectangleCopy98', 799 | 'icon-RectangleCopy97', 800 | 'icon-RectangleCopy96', 801 | 'icon-RectangleCopy95', 802 | 'icon-RectangleCopy94', 803 | 'icon-RectangleCopy93', 804 | 'icon-RectangleCopy92', 805 | 'icon-RectangleCopy91', 806 | 'icon-RectangleCopy90', 807 | 'icon-RectangleCopy89', 808 | 'icon-RectangleCopy88', 809 | 'icon-RectangleCopy87', 810 | 'icon-RectangleCopy86', 811 | 'icon-RectangleCopy85', 812 | 'icon-RectangleCopy84', 813 | 'icon-RectangleCopy83', 814 | 'icon-RectangleCopy82', 815 | 'icon-RectangleCopy81', 816 | 'icon-RectangleCopy80', 817 | 'icon-RectangleCopy79', 818 | 'icon-RectangleCopy78', 819 | 'icon-RectangleCopy77', 820 | 'icon-RectangleCopy76', 821 | 'icon-RectangleCopy75', 822 | 'icon-RectangleCopy74', 823 | 'icon-RectangleCopy73', 824 | 'icon-RectangleCopy72', 825 | 'icon-RectangleCopy71', 826 | 'icon-RectangleCopy70', 827 | 'icon-RectangleCopy69', 828 | 'icon-RectangleCopy68', 829 | 'icon-RectangleCopy67', 830 | 'icon-RectangleCopy66', 831 | 'icon-RectangleCopy65', 832 | 'icon-RectangleCopy64', 833 | 'icon-RectangleCopy63', 834 | 'icon-RectangleCopy62', 835 | 'icon-RectangleCopy61', 836 | 'icon-RectangleCopy60', 837 | 'icon-RectangleCopy59', 838 | 'icon-RectangleCopy58', 839 | 'icon-RectangleCopy57', 840 | 'icon-RectangleCopy56', 841 | 'icon-RectangleCopy55', 842 | 'icon-RectangleCopy54', 843 | 'icon-RectangleCopy53', 844 | 'icon-RectangleCopy52', 845 | 'icon-RectangleCopy51', 846 | 'icon-RectangleCopy50', 847 | 'icon-RectangleCopy49', 848 | 'icon-RectangleCopy48', 849 | 'icon-RectangleCopy47', 850 | 'icon-RectangleCopy46', 851 | 'icon-RectangleCopy45', 852 | 'icon-RectangleCopy44', 853 | 'icon-RectangleCopy43', 854 | 'icon-RectangleCopy42', 855 | 'icon-RectangleCopy41', 856 | 'icon-RectangleCopy40', 857 | 'icon-RectangleCopy39', 858 | 'icon-RectangleCopy38', 859 | 'icon-RectangleCopy37', 860 | 'icon-RectangleCopy36', 861 | 'icon-RectangleCopy35', 862 | 'icon-RectangleCopy34', 863 | 'icon-RectangleCopy33', 864 | 'icon-RectangleCopy32', 865 | 'icon-RectangleCopy31', 866 | 'icon-RectangleCopy30', 867 | 'icon-RectangleCopy29', 868 | 'icon-RectangleCopy28', 869 | 'icon-RectangleCopy27', 870 | 'icon-RectangleCopy26', 871 | 'icon-RectangleCopy25', 872 | 'icon-RectangleCopy24', 873 | 'icon-RectangleCopy23', 874 | 'icon-RectangleCopy22', 875 | 'icon-RectangleCopy21', 876 | 'icon-RectangleCopy20', 877 | 'icon-RectangleCopy19', 878 | 'icon-Rectangle', 879 | 'icon-RectangleCopy18', 880 | 'icon-RectangleCopy17', 881 | 'icon-RectangleCopy16', 882 | 'icon-RectangleCopy15', 883 | 'icon-RectangleCopy14', 884 | 'icon-RectangleCopy13', 885 | 'icon-RectangleCopy12', 886 | 'icon-RectangleCopy11', 887 | 'icon-RectangleCopy10', 888 | 'icon-RectangleCopy9', 889 | 'icon-RectangleCopy8', 890 | 'icon-RectangleCopy7', 891 | 'icon-RectangleCopy6', 892 | 'icon-RectangleCopy5', 893 | 'icon-RectangleCopy4', 894 | 'icon-RectangleCopy3', 895 | 'icon-RectangleCopy2', 896 | 'icon-RectangleCopy1', 897 | 'icon-RectangleCopy', 898 | 'icon-jian' 899 | ]; 900 | 901 | const iconFonts = iconfontIcons.map((item, index) => ({ 902 | value: iconfontIconValues[index], 903 | label: item 904 | })); 905 | 906 | export { icons, iconFonts }; 907 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | root: true, 4 | env: { 5 | browser: true, 6 | es6: true 7 | }, 8 | settings: { 9 | "version": "detect" 10 | }, 11 | extends: [ 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:react/recommended' 14 | ], 15 | globals: { 16 | Atomics: 'readonly', 17 | SharedArrayBuffer: 'readonly' 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaFeatures: { 22 | experimentalObjectRestSpread: true, // es6对象的扩展运算符 23 | jsx: true // //指定ESLint可以解析JSX语法 24 | }, 25 | ecmaVersion: 2019, 26 | sourceType: 'module' 27 | }, 28 | plugins: [ 29 | 'html', 30 | 'react', 31 | '@typescript-eslint', 32 | 'react-hooks' 33 | ], 34 | rules: { 35 | /** ********************************** js相关配置 ******************************************/ 36 | 'default-case-last': 'off', 37 | 'no-promise-executor-return': 'off', 38 | 'no-useless-backreference': 'off', 39 | /** 40 | * setter 必须有对应的 getter,getter 可以没有对应的 setter 41 | */ 42 | 'accessor-pairs': [ 43 | 'error', 44 | { 45 | setWithoutGet: true, 46 | getWithoutSet: false 47 | } 48 | ], 49 | /** 50 | * 数组的方法除了 forEach 之外,回调函数必须有返回值 51 | */ 52 | 'array-callback-return': 'error', 53 | /** 54 | * 将 var 定义的变量视为块作用域,禁止在块外使用 55 | * @reason 已经禁止使用 var 了 56 | */ 57 | 'block-scoped-var': 'off', 58 | /** 59 | * callback 之后必须立即 return 60 | */ 61 | 'callback-return': 'off', 62 | /** 63 | * 变量名必须是 camelcase 风格的 64 | * @reason 很多 api 或文件名都不是 camelcase 风格的 65 | */ 66 | camelcase: 'off', 67 | /** 68 | * 注释的首字母必须大写 69 | */ 70 | 'capitalized-comments': 'off', 71 | /** 72 | * 在类的非静态方法中,必须存在对 this 的引用 73 | */ 74 | 'class-methods-use-this': 'off', 75 | /** 76 | * 禁止函数的循环复杂度超过 20 77 | * @reason https://en.wikipedia.org/wiki/Cyclomatic_complexity 78 | */ 79 | complexity: [ 80 | 'error', 81 | { 82 | max: 20 83 | } 84 | ], 85 | /** 86 | * 禁止函数在不同分支返回不同类型的值 87 | * @reason 缺少 TypeScript 的支持,类型判断是不准确的 88 | */ 89 | 'consistent-return': 'off', 90 | /** 91 | * 限制 this 的别名 92 | */ 93 | 'consistent-this': 'off', 94 | /** 95 | * constructor 中必须有 super 96 | */ 97 | 'constructor-super': 'error', 98 | /** 99 | * switch 语句必须有 default 100 | */ 101 | 'default-case': 'off', 102 | /** 103 | * 有默认值的参数必须放在函数参数的末尾 104 | */ 105 | 'default-param-last': 'off', 106 | /** 107 | * 禁止使用 foo['bar'],必须写成 foo.bar 108 | * @reason 当需要写一系列属性的时候,可以更统一 109 | */ 110 | 'dot-notation': 'off', 111 | /** 112 | * 必须使用 === 或 !==,禁止使用 == 或 != 113 | */ 114 | eqeqeq: ['error', 'always'], 115 | /** 116 | * 禁止方向错误的 for 循环 117 | */ 118 | 'for-direction': 'error', 119 | /** 120 | * 函数赋值给变量的时候,函数名必须与变量名一致 121 | */ 122 | 'func-name-matching': [ 123 | 'error', 124 | 'always', 125 | { 126 | includeCommonJSModuleExports: false 127 | } 128 | ], 129 | /** 130 | * 函数必须有名字 131 | */ 132 | 'func-names': 'off', 133 | /** 134 | * 必须只使用函数声明或只使用函数表达式 135 | */ 136 | 'func-style': 'off', 137 | /** 138 | * getter 必须有返回值,并且禁止返回空 139 | */ 140 | 'getter-return': 'error', 141 | /** 142 | * require 必须在全局作用域下 143 | */ 144 | 'global-require': 'off', 145 | /** 146 | * setter 和 getter 必须写在一起 147 | */ 148 | 'grouped-accessor-pairs': 'off', 149 | /** 150 | * for in 内部必须有 hasOwnProperty 151 | */ 152 | 'guard-for-in': 'off', 153 | /** 154 | * callback 中的 err 必须被处理 155 | * @reason 它是通过字符串匹配来判断函数参数 err 的,不准确 156 | */ 157 | 'handle-callback-err': 'off', 158 | /** 159 | * 禁止使用指定的标识符 160 | */ 161 | 'id-blacklist': 'off', 162 | /** 163 | * 限制变量名长度 164 | */ 165 | 'id-length': 'off', 166 | /** 167 | * 限制变量名必须匹配指定的正则表达式 168 | */ 169 | 'id-match': 'off', 170 | /** 171 | * 变量必须在定义的时候赋值 172 | */ 173 | 'init-declarations': 'off', 174 | /** 175 | * 单行注释必须写在上一行 176 | */ 177 | 'line-comment-position': 'off', 178 | /** 179 | * 类的成员之间是否需要空行 180 | * @reason 有时为了紧凑需要挨在一起,有时为了可读性需要空一行 181 | */ 182 | 'lines-between-class-members': 'off', 183 | /** 184 | * 限制一个文件中类的数量 185 | */ 186 | 'max-classes-per-file': 'off', 187 | /** 188 | * 代码块嵌套的深度禁止超过 5 层 189 | */ 190 | 'max-depth': ['error', 5], 191 | /** 192 | * 限制一个文件最多的行数 193 | */ 194 | 'max-lines': 'off', 195 | /** 196 | * 限制函数块中的代码行数 197 | */ 198 | 'max-lines-per-function': 'off', 199 | /** 200 | * 回调函数嵌套禁止超过 3 层,多了请用 async await 替代 201 | */ 202 | 'max-nested-callbacks': ['error', 3], 203 | /** 204 | * 函数的参数禁止超过 3 个 205 | */ 206 | 'max-params': ['error', 8], 207 | /** 208 | * 限制函数块中的语句数量 209 | */ 210 | 'max-statements': 'off', 211 | /** 212 | * 限制一行中的语句数量 213 | */ 214 | 'max-statements-per-line': 'off', 215 | /** 216 | * 约束多行注释的格式 217 | * @reason 能写注释已经不容易了,不需要限制太多 218 | */ 219 | 'multiline-comment-style': 'off', 220 | /** 221 | * new 后面的类名必须首字母大写 222 | */ 223 | 'new-cap': [ 224 | 'error', 225 | { 226 | newIsCap: true, 227 | capIsNew: false, 228 | properties: true 229 | } 230 | ], 231 | /** 232 | * 禁止使用 alert 233 | */ 234 | 'no-alert': 'off', 235 | /** 236 | * 禁止使用 Array 构造函数时传入的参数超过一个 237 | * @reason 参数为一个时表示创建一个指定长度的数组,比较常用 238 | * 参数为多个时表示创建一个指定内容的数组,此时可以用数组字面量实现,不必使用构造函数 239 | */ 240 | 'no-array-constructor': 'error', 241 | /** 242 | * 禁止将 async 函数做为 new Promise 的回调函数 243 | * @reason 出现这种情况时,一般不需要使用 new Promise 实现异步了 244 | */ 245 | 'no-async-promise-executor': 'error', 246 | /** 247 | * 禁止将 await 写在循环里,因为这样就无法同时发送多个异步请求了 248 | * @reason 要求太严格了,有时需要在循环中写 await 249 | */ 250 | 'no-await-in-loop': 'off', 251 | /** 252 | * 禁止使用位运算 253 | */ 254 | 'no-bitwise': 'off', 255 | /** 256 | * 禁止直接使用 Buffer 257 | * @reason Buffer 构造函数是已废弃的语法 258 | */ 259 | 'no-buffer-constructor': 'error', 260 | /** 261 | * 禁止使用 caller 或 callee 262 | * @reason 它们是已废弃的语法 263 | */ 264 | 'no-caller': 'error', 265 | /** 266 | * switch 的 case 内有变量定义的时候,必须使用大括号将 case 内变成一个代码块 267 | */ 268 | 'no-case-declarations': 'error', 269 | /** 270 | * 禁止对已定义的 class 重新赋值 271 | */ 272 | 'no-class-assign': 'error', 273 | /** 274 | * 禁止与负零进行比较 275 | */ 276 | 'no-compare-neg-zero': 'error', 277 | /** 278 | * 禁止在测试表达式中使用赋值语句,除非这个赋值语句被括号包起来了 279 | */ 280 | 'no-cond-assign': ['error', 'except-parens'], 281 | /** 282 | * 禁止使用 console 283 | */ 284 | 'no-console': 'off', 285 | /** 286 | * 禁止对使用 const 定义的常量重新赋值 287 | */ 288 | 'no-const-assign': 'error', 289 | /** 290 | * 禁止将常量作为分支条件判断中的测试表达式,但允许作为循环条件判断中的测试表达式 291 | */ 292 | 'no-constant-condition': [ 293 | 'error', 294 | { 295 | checkLoops: false 296 | } 297 | ], 298 | /** 299 | * 禁止在构造函数中返回值 300 | */ 301 | 'no-constructor-return': 'off', 302 | /** 303 | * 禁止使用 continue 304 | */ 305 | 'no-continue': 'off', 306 | /** 307 | * 禁止在正则表达式中出现 Ctrl 键的 ASCII 表示,即禁止使用 /\x1f/ 308 | * @reason 几乎不会遇到这种场景 309 | */ 310 | 'no-control-regex': 'off', 311 | /** 312 | * 禁止使用 debugger 313 | */ 314 | 'no-debugger': 'error', 315 | /** 316 | * 禁止对一个变量使用 delete 317 | * @reason 编译阶段就会报错了 318 | */ 319 | 'no-delete-var': 'off', 320 | /** 321 | * 禁止在正则表达式中出现形似除法操作符的开头,如 let a = /=foo/ 322 | * @reason 有代码高亮的话,在阅读这种代码时,也完全不会产生歧义或理解上的困难 323 | */ 324 | 'no-div-regex': 'off', 325 | /** 326 | * 禁止在函数参数中出现重复名称的参数 327 | * @reason 编译阶段就会报错了 328 | */ 329 | 'no-dupe-args': 'off', 330 | /** 331 | * 禁止重复定义类的成员 332 | */ 333 | 'no-dupe-class-members': 'error', 334 | /** 335 | * 禁止 if else 的条件判断中出现重复的条件 336 | */ 337 | 'no-dupe-else-if': 'off', 338 | /** 339 | * 禁止在对象字面量中出现重复的键名 340 | */ 341 | 'no-dupe-keys': 'error', 342 | /** 343 | * 禁止在 switch 语句中出现重复测试表达式的 case 344 | */ 345 | 'no-duplicate-case': 'error', 346 | /** 347 | * 禁止重复导入模块 348 | */ 349 | 'no-duplicate-imports': 'error', 350 | /** 351 | * 禁止在 else 内使用 return,必须改为提前结束 352 | * @reason else 中使用 return 可以使代码结构更清晰 353 | */ 354 | 'no-else-return': 'off', 355 | /** 356 | * 禁止出现空代码块,允许 catch 为空代码块 357 | */ 358 | 'no-empty': [ 359 | 'error', 360 | { 361 | allowEmptyCatch: true 362 | } 363 | ], 364 | /** 365 | * 禁止在正则表达式中使用空的字符集 [] 366 | */ 367 | 'no-empty-character-class': 'error', 368 | /** 369 | * 不允许有空函数 370 | * @reason 有时需要将一个空函数设置为某个项的默认值 371 | */ 372 | 'no-empty-function': 'off', 373 | /** 374 | * 禁止解构赋值中出现空 {} 或 [] 375 | */ 376 | 'no-empty-pattern': 'error', 377 | /** 378 | * 禁止使用 foo == null,必须使用 foo === null 379 | */ 380 | 'no-eq-null': 'error', 381 | /** 382 | * 禁止使用 eval 383 | */ 384 | 'no-eval': 'error', 385 | /** 386 | * 禁止将 catch 的第一个参数 error 重新赋值 387 | */ 388 | 'no-ex-assign': 'error', 389 | /** 390 | * 禁止修改原生对象 391 | * @reason 修改原生对象可能会与将来版本的 js 冲突 392 | */ 393 | 'no-extend-native': 'error', 394 | /** 395 | * 禁止出现没必要的 bind 396 | */ 397 | 'no-extra-bind': 'error', 398 | /** 399 | * 禁止不必要的布尔类型转换 400 | */ 401 | 'no-extra-boolean-cast': 'error', 402 | /** 403 | * 禁止出现没必要的 label 404 | * @reason 已经禁止使用 label 了 405 | */ 406 | 'no-extra-label': 'off', 407 | /** 408 | * switch 的 case 内必须有 break, return 或 throw,空的 case 除外 409 | */ 410 | 'no-fallthrough': 'error', 411 | /** 412 | * 禁止将一个函数声明重新赋值 413 | */ 414 | 'no-func-assign': 'error', 415 | /** 416 | * 禁止对全局变量赋值 417 | */ 418 | 'no-global-assign': 'error', 419 | /** 420 | * 禁止使用 ~+ 等难以理解的类型转换,仅允许使用 !! 421 | */ 422 | 'no-implicit-coercion': [ 423 | 'error', 424 | { 425 | allow: ['!!'] 426 | } 427 | ], 428 | /** 429 | * 禁止在全局作用域下定义变量或申明函数 430 | * @reason 模块化之后,不会出现这种在全局作用域下定义变量的情况 431 | */ 432 | 'no-implicit-globals': 'off', 433 | /** 434 | * 禁止在 setTimeout 或 setInterval 中传入字符串 435 | */ 436 | 'no-implied-eval': 'error', 437 | /** 438 | * 禁止对导入的模块进行赋值 439 | */ 440 | 'no-import-assign': 'off', 441 | /** 442 | * 禁止在代码后添加单行注释 443 | */ 444 | 'no-inline-comments': 'off', 445 | /** 446 | * 禁止在 if 代码块内出现函数声明 447 | */ 448 | 'no-inner-declarations': ['error', 'both'], 449 | /** 450 | * 禁止在 RegExp 构造函数中出现非法的正则表达式 451 | */ 452 | 'no-invalid-regexp': 'error', 453 | /** 454 | * 禁止在类之外的地方使用 this 455 | * @reason 只允许在 class 中使用 this 456 | */ 457 | 'no-invalid-this': 'error', 458 | /** 459 | * 禁止使用特殊空白符(比如全角空格),除非是出现在字符串、正则表达式或模版字符串中 460 | */ 461 | 'no-irregular-whitespace': [ 462 | 'error', 463 | { 464 | skipStrings: true, 465 | skipComments: false, 466 | skipRegExps: true, 467 | skipTemplates: true 468 | } 469 | ], 470 | /** 471 | * 禁止使用 __iterator__ 472 | * @reason __iterator__ 是一个已废弃的属性 473 | * 使用 [Symbol.iterator] 替代它 474 | */ 475 | 'no-iterator': 'error', 476 | /** 477 | * 禁止 label 名称与已定义的变量重复 478 | * @reason 已经禁止使用 label 了 479 | */ 480 | 'no-label-var': 'off', 481 | /** 482 | * 禁止使用 label 483 | */ 484 | 'no-labels': 'error', 485 | /** 486 | * 禁止使用没必要的 {} 作为代码块 487 | */ 488 | 'no-lone-blocks': 'error', 489 | /** 490 | * 禁止 else 中只有一个单独的 if 491 | * @reason 单独的 if 可以把逻辑表达的更清楚 492 | */ 493 | 'no-lonely-if': 'off', 494 | /** 495 | * 禁止在循环内的函数内部出现循环体条件语句中定义的变量 496 | * @reason 使用 let 就已经解决了这个问题了 497 | */ 498 | 'no-loop-func': 'off', 499 | /** 500 | * 禁止使用 magic numbers 501 | */ 502 | 'no-magic-numbers': 'off', 503 | /** 504 | * 禁止正则表达式中使用肉眼无法区分的特殊字符 505 | * @reason 某些特殊字符很难看出差异,最好不要在正则中使用 506 | */ 507 | 'no-misleading-character-class': 'error', 508 | /** 509 | * 相同类型的 require 必须放在一起 510 | */ 511 | 'no-mixed-requires': 'off', 512 | /** 513 | * 禁止连续赋值,比如 foo = bar = 1 514 | */ 515 | 'no-multi-assign': 'off', 516 | /** 517 | * 禁止使用 \ 来换行字符串 518 | */ 519 | 'no-multi-str': 'error', 520 | /** 521 | * 禁止 if 里有否定的表达式 522 | * @reason 否定的表达式可以把逻辑表达的更清楚 523 | */ 524 | 'no-negated-condition': 'off', 525 | /** 526 | * 禁止使用嵌套的三元表达式,比如 a ? b : c ? d : e 527 | */ 528 | 'no-nested-ternary': 'off', 529 | /** 530 | * 禁止直接 new 一个类而不赋值 531 | * @reason new 应该作为创建一个类的实例的方法,所以不能不赋值 532 | */ 533 | 'no-new': 'error', 534 | /** 535 | * 禁止使用 new Function 536 | * @reason 这和 eval 是等价的 537 | */ 538 | 'no-new-func': 'error', 539 | /** 540 | * 禁止直接 new Object 541 | */ 542 | 'no-new-object': 'error', 543 | /** 544 | * 禁止直接 new require('foo') 545 | */ 546 | 'no-new-require': 'error', 547 | /** 548 | * 禁止使用 new 来生成 Symbol 549 | */ 550 | 'no-new-symbol': 'error', 551 | /** 552 | * 禁止使用 new 来生成 String, Number 或 Boolean 553 | */ 554 | 'no-new-wrappers': 'error', 555 | /** 556 | * 禁止将 Math, JSON 或 Reflect 直接作为函数调用 557 | */ 558 | 'no-obj-calls': 'error', 559 | /** 560 | * 禁止使用 0 开头的数字表示八进制数 561 | * @reason 编译阶段就会报错了 562 | */ 563 | 'no-octal': 'off', 564 | /** 565 | * 禁止使用八进制的转义符 566 | * @reason 编译阶段就会报错了 567 | */ 568 | 'no-octal-escape': 'off', 569 | /** 570 | * 禁止对函数的参数重新赋值 571 | */ 572 | 'no-param-reassign': 'off', 573 | /** 574 | * 禁止对 __dirname 或 __filename 使用字符串连接 575 | * @reason 不同平台下的路径符号不一致,建议使用 path 处理平台差异性 576 | */ 577 | 'no-path-concat': 'error', 578 | /** 579 | * 禁止使用 ++ 或 -- 580 | */ 581 | 'no-plusplus': 'off', 582 | /** 583 | * 禁止使用 process.env.NODE_ENV 584 | */ 585 | 'no-process-env': 'off', 586 | /** 587 | * 禁止使用 process.exit(0) 588 | */ 589 | 'no-process-exit': 'off', 590 | /** 591 | * 禁止使用 __proto__ 592 | * @reason __proto__ 是已废弃的语法 593 | */ 594 | 'no-proto': 'error', 595 | /** 596 | * 禁止使用 hasOwnProperty, isPrototypeOf 或 propertyIsEnumerable 597 | * @reason hasOwnProperty 比较常用 598 | */ 599 | 'no-prototype-builtins': 'off', 600 | /** 601 | * 禁止重复定义变量 602 | * @reason 禁用 var 之后,编译阶段就会报错了 603 | */ 604 | 'no-redeclare': 'off', 605 | /** 606 | * 禁止在正则表达式中出现连续的空格 607 | */ 608 | 'no-regex-spaces': 'error', 609 | /** 610 | * 禁止使用指定的全局变量 611 | */ 612 | 'no-restricted-globals': 'off', 613 | /** 614 | * 禁止导入指定的模块 615 | */ 616 | 'no-restricted-imports': 'off', 617 | /** 618 | * 禁止使用指定的模块 619 | */ 620 | 'no-restricted-modules': 'off', 621 | /** 622 | * 禁止使用指定的对象属性 623 | */ 624 | 'no-restricted-properties': 'off', 625 | /** 626 | * 禁止使用指定的语法 627 | */ 628 | 'no-restricted-syntax': 'off', 629 | /** 630 | * 禁止在 return 语句里赋值 631 | */ 632 | 'no-return-assign': ['error', 'always'], 633 | /** 634 | * 禁止在 return 语句里使用 await 635 | */ 636 | 'no-return-await': 'error', 637 | /** 638 | * 禁止出现 location.href = 'javascript:void(0)'; 639 | * @reason 有些场景下还是需要用到这个 640 | */ 641 | 'no-script-url': 'off', 642 | /** 643 | * 禁止将自己赋值给自己 644 | */ 645 | 'no-self-assign': 'error', 646 | /** 647 | * 禁止将自己与自己比较 648 | */ 649 | 'no-self-compare': 'error', 650 | /** 651 | * 禁止使用逗号操作符 652 | */ 653 | 'no-sequences': 'error', 654 | /** 655 | * 禁止 setter 有返回值 656 | */ 657 | 'no-setter-return': 'off', 658 | /** 659 | * 禁止变量名与上层作用域内的已定义的变量重复 660 | * @reason 很多时候函数的形参和传参是同名的 661 | */ 662 | 'no-shadow': 'off', 663 | /** 664 | * 禁止使用保留字作为变量名 665 | */ 666 | 'no-shadow-restricted-names': 'error', 667 | /** 668 | * 禁止在数组中出现连续的逗号 669 | */ 670 | 'no-sparse-arrays': 'error', 671 | /** 672 | * 禁止使用 node 中的同步的方法,比如 fs.readFileSync 673 | */ 674 | 'no-sync': 'off', 675 | /** 676 | * 禁止在普通字符串中出现模版字符串里的变量形式 677 | */ 678 | 'no-template-curly-in-string': 'error', 679 | /** 680 | * 禁止使用三元表达式 681 | */ 682 | 'no-ternary': 'off', 683 | /** 684 | * 禁止在 super 被调用之前使用 this 或 super 685 | */ 686 | 'no-this-before-super': 'error', 687 | /** 688 | * 禁止 throw 字面量,必须 throw 一个 Error 对象 689 | */ 690 | 'no-throw-literal': 'error', 691 | /** 692 | * 禁止使用未定义的变量 693 | */ 694 | // 'no-undef': 'error', 695 | /** 696 | * 禁止将 undefined 赋值给变量 697 | */ 698 | // 'no-undef-init': 'error', 699 | /** 700 | * 禁止使用 undefined 701 | */ 702 | // 'no-undefined': 'off', 703 | /** 704 | * 禁止变量名出现下划线 705 | */ 706 | 'no-underscore-dangle': 'off', 707 | /** 708 | * 循环内必须对循环条件中的变量有修改 709 | */ 710 | 'no-unmodified-loop-condition': 'error', 711 | /** 712 | * 必须使用 !a 替代 a ? false : true 713 | * @reason 后者表达的更清晰 714 | */ 715 | 'no-unneeded-ternary': 'off', 716 | /** 717 | * 禁止在 return, throw, break 或 continue 之后还有代码 718 | */ 719 | 'no-unreachable': 'error', 720 | /** 721 | * 禁止在 finally 中出现 return, throw, break 或 continue 722 | * @reason finally 中的语句会在 try 之前执行 723 | */ 724 | 'no-unsafe-finally': 'error', 725 | /** 726 | * 禁止在 in 或 instanceof 操作符的左侧变量前使用感叹号 727 | */ 728 | 'no-unsafe-negation': 'error', 729 | /** 730 | * 禁止无用的表达式 731 | */ 732 | 'no-unused-expressions': [ 733 | 'error', 734 | { 735 | allowShortCircuit: true, 736 | allowTernary: true, 737 | allowTaggedTemplates: true 738 | } 739 | ], 740 | /** 741 | * 禁止出现没用到的 label 742 | * @reason 已经禁止使用 label 了 743 | */ 744 | 'no-unused-labels': 'off', 745 | /** 746 | * 已定义的变量必须使用 747 | */ 748 | 'no-unused-vars': 'off', 749 | // [ 750 | // 'warn', 751 | // { 752 | // vars: 'all', 753 | // args: 'none', 754 | // ignoreRestSiblings: false, 755 | // caughtErrors: 'none' 756 | // } 757 | // ], 758 | /** 759 | * 变量必须先定义后使用 760 | */ 761 | 'no-use-before-define': 'off', 762 | // [ 763 | // 'error', 764 | // { 765 | // variables: false, 766 | // functions: false, 767 | // classes: false 768 | // } 769 | // ], 770 | /** 771 | * 禁止出现没必要的 call 或 apply 772 | */ 773 | 'no-useless-call': 'error', 774 | /** 775 | * 禁止在 catch 中仅仅只是把错误 throw 出去 776 | * @reason 这样的 catch 是没有意义的,等价于直接执行 try 里的代码 777 | */ 778 | 'no-useless-catch': 'error', 779 | /** 780 | * 禁止出现没必要的计算键名 781 | */ 782 | 'no-useless-computed-key': 'error', 783 | /** 784 | * 禁止出现没必要的字符串连接 785 | */ 786 | 'no-useless-concat': 'error', 787 | /** 788 | * 禁止出现没必要的 constructor 789 | */ 790 | 'no-useless-constructor': 'error', 791 | /** 792 | * 禁止出现没必要的转义 793 | * @reason 转义可以使代码更易懂 794 | */ 795 | 'no-useless-escape': 'off', 796 | /** 797 | * 禁止解构赋值时出现同样名字的的重命名,比如 let { foo: foo } = bar; 798 | */ 799 | 'no-useless-rename': 'error', 800 | /** 801 | * 禁止没必要的 return 802 | */ 803 | 'no-useless-return': 'off', 804 | /** 805 | * 禁止使用 var 806 | */ 807 | 'no-var': 'warn', 808 | /** 809 | * 禁止使用 void 810 | */ 811 | 'no-void': 'error', 812 | /** 813 | * 禁止注释中出现 TODO 和 FIXME 814 | */ 815 | 'no-warning-comments': 'off', 816 | /** 817 | * 禁止使用 with 818 | * @reason 编译阶段就会报错了 819 | */ 820 | 'no-with': 'off', 821 | /** 822 | * 必须使用 a = {b} 而不是 a = {b: b} 823 | * @reason 有时后者可以使代码结构更清晰 824 | */ 825 | 'object-shorthand': 'off', 826 | /** 827 | * 禁止变量申明时用逗号一次申明多个 828 | */ 829 | 'one-var': ['error', 'never'], 830 | /** 831 | * 必须使用 x = x + y 而不是 x += y 832 | */ 833 | 'operator-assignment': 'off', 834 | /** 835 | * 限制语句之间的空行规则,比如变量定义完之后必须要空行 836 | */ 837 | 'padding-line-between-statements': 'off', 838 | /** 839 | * 申明后不再被修改的变量必须使用 const 来申明 840 | */ 841 | 'prefer-const': 'off', 842 | /** 843 | * 必须使用解构赋值 844 | */ 845 | 'prefer-destructuring': 'off', 846 | /** 847 | * 使用 ES2016 的语法 ** 替代 Math.pow 848 | */ 849 | 'prefer-exponentiation-operator': 'off', 850 | /** 851 | * 使用 ES2018 中的正则表达式命名组 852 | * @reason 正则表达式已经较难理解了,没必要强制加上命名组 853 | */ 854 | 'prefer-named-capture-group': 'off', 855 | /** 856 | * 必须使用 0b11111011 而不是 parseInt() 857 | */ 858 | 'prefer-numeric-literals': 'off', 859 | /** 860 | * 必须使用 ... 而不是 Object.assign,除非 Object.assign 的第一个参数是一个变量 861 | */ 862 | 'prefer-object-spread': 'error', 863 | /** 864 | * Promise 的 reject 中必须传入 Error 对象,而不是字面量 865 | */ 866 | 'prefer-promise-reject-errors': 'error', 867 | /** 868 | * 优先使用正则表达式字面量,而不是 RegExp 构造函数 869 | */ 870 | 'prefer-regex-literals': 'off', 871 | /** 872 | * 必须使用 ...args 而不是 arguments 873 | */ 874 | 'prefer-rest-params': 'off', 875 | /** 876 | * 必须使用 ... 而不是 apply,比如 foo(...args) 877 | */ 878 | 'prefer-spread': 'off', 879 | /** 880 | * 必须使用模版字符串而不是字符串连接 881 | */ 882 | 'prefer-template': 'off', 883 | /** 884 | * parseInt 必须传入第二个参数 885 | */ 886 | radix: 'error', 887 | /** 888 | * 禁止将 await 或 yield 的结果做为运算符的后面项 889 | * @reason 这样会导致不符合预期的结果 890 | * https://github.com/eslint/eslint/issues/11899 891 | * 在上面 issue 修复之前,关闭此规则 892 | */ 893 | 'require-atomic-updates': 'off', 894 | /** 895 | * async 函数中必须存在 await 语句 896 | */ 897 | 'require-await': 'off', 898 | /** 899 | * 正则表达式中必须要加上 u 标志 900 | */ 901 | 'require-unicode-regexp': 'off', 902 | /** 903 | * generator 函数内必须有 yield 904 | */ 905 | 'require-yield': 'error', 906 | /** 907 | * 导入必须按规则排序 908 | */ 909 | 'sort-imports': 'off', 910 | /** 911 | * 对象字面量的键名必须排好序 912 | */ 913 | 'sort-keys': 'off', 914 | /** 915 | * 变量申明必须排好序 916 | */ 917 | 'sort-vars': 'off', 918 | /** 919 | * 注释的斜线或 * 后必须有空格 920 | */ 921 | 'spaced-comment': [ 922 | 'error', 923 | 'always', 924 | { 925 | block: { 926 | exceptions: ['*'], 927 | balanced: true 928 | } 929 | } 930 | ], 931 | /** 932 | * 禁止使用 'strict'; 933 | */ 934 | strict: ['off', 'never'], 935 | /** 936 | * 创建 Symbol 时必须传入参数 937 | */ 938 | 'symbol-description': 'error', 939 | /** 940 | * 必须使用 isNaN(foo) 而不是 foo === NaN 941 | */ 942 | 'use-isnan': 'error', 943 | /** 944 | * typeof 表达式比较的对象必须是 'undefined', 'object', 'boolean', 'number', 'string', 'function', 'symbol', 或 'bigint' 945 | */ 946 | 'valid-typeof': 'error', 947 | /** 948 | * var 必须在作用域的最前面 949 | */ 950 | 'vars-on-top': 'off', 951 | /** 952 | * 必须使用 if (foo === 5) 而不是 if (5 === foo) 953 | */ 954 | yoda: [ 955 | 'error', 956 | 'never', 957 | { 958 | onlyEquality: true 959 | } 960 | ], 961 | 962 | /** ********************************** react/jsx相关配置 ******************************************/ 963 | /** 964 | * 布尔值类型的 propTypes 的 name 必须为 is 或 has 开头 965 | * @reason 类型相关的约束交给 TypeScript 966 | */ 967 | 'react/boolean-prop-naming': 'off', 968 | /** 969 | *