diff --git a/.eslintrc.js b/.eslintrc.js index 974bbfd..406430b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,16 +6,11 @@ module.exports = { 'plugin:react-hooks/recommended', 'plugin:prettier/recommended', ], - plugins: ['jest'], - env: { - 'jest/globals': true, - }, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], // 定义了该eslint文件所依赖的插件 globals: { // 这里填入你的项目需要的全局变量 // 这里值为 false 表示这个全局变量不允许被重新赋值,比如: - // - // React: false, - // ReactDOM: false __dirname: false, }, rules: { @@ -44,5 +39,6 @@ module.exports = { 'react/no-unsafe': 'off', '@typescript-eslint/no-invalid-this': 'off', 'react/jsx-key': 0, + 'no-undef': 0, }, }; diff --git a/README.md b/README.md index 877b2d5..5682204 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,60 @@ -# VisResumeMook +## Electron + React Hooks + TS 实战开发:从 0 到 1 实现简历平台 -React + TS + Electron 打造可视化在线简历平台 +### 开发 -![](https://img.shields.io/badge/visResumeMook--resume-1.0.2-red.svg) -![](https://img.shields.io/badge/react-16.12.0-blue.svg) -![](https://img.shields.io/badge/electron-11.1.1-green.svg) -![](https://img.shields.io/badge/react_redux-7.1.3-orange.svg) -![](https://img.shields.io/badge/typescript-3.7.2-blue.svg) -![](https://img.shields.io/badge/license-MIT-orange.svg) +> 由于网络问题,往往安装 Electron 会很慢,此时可以考虑换个淘宝源 -# 开发 - -```bash -git clone https://github.com/PDKSophia/visResumeMook.git -cd visResumeMook -npm run webpack:main -npm run webpack:render +```js +// 1.下载 +npm run install +// 2. 运行 +npm run start:render +npm run start:main ``` -推荐使用 `vscode-beehive-extension` 插件辅助项目脚本运行~ +### 更多分支 + +- 环境分支 + + - [electron-init](https://github.com/PDKSophia/visResumeMook/tree/electron-init):关于`Electron`相关的初始化配置代码,只涉及到 Electron + - [electron-react-init](https://github.com/PDKSophia/visResumeMook/tree/electron-react-init):将 `Electron + React` 结合起来的初始化配置代码 + +- 完整项目 + - [master](https://github.com/PDKSophia/visResumeMook):简历平台应用完整项目地址 + - [init-cli](https://github.com/PDKSophia/visResumeMook/tree/init-cli):完整配置的分支代码,可以基于此分支直接进行项目开发 + +> 支持配套学习分支,章节名对应分支名,如有疑问,请提 issues,同时欢迎提 Merge Request + +### 小册章节 + + + +- [x] [开篇-小册介绍](https://juejin.cn/book/6950646725295996940) +- [x] [开篇-技术选型和项目结构](https://juejin.cn/book/6950646725295996940/section/6962895331667230727) +- [x] [基础篇-Electron 初步认识并掌握基础知识](https://juejin.cn/book/6950646725295996940/section/6961585436967829516) +- [x] [设计篇-需求功能设计与数据存储方案设计](https://juejin.cn/book/6950646725295996940/section/6962435230061821952) +- [x] [环境篇-动手搭建我们的简历平台](https://juejin.cn/book/6950646725295996940/section/6961586491285831720) +- [x] [🏆 500 米里程碑|环境搭建篇完成](https://juejin.cn/book/6950646725295996940/section/6962898545577820198) +- [x] [业务篇-首页开发,好的印象能加分](https://juejin.cn/book/6950646725295996940/section/6962938228357726241) +- [x] [业务篇-如何写我们的 Redux 与 jsonFile](https://juejin.cn/book/6950646725295996940/section/6962906314565484551) +- [x] [业务篇-简历制作之常用组件设计与简历数据设计](https://juejin.cn/book/6950646725295996940/section/6962895451875966989) +- [x] [业务篇-简历制作之入口页面开发](https://juejin.cn/book/6950646725295996940/section/6964550757375246366) +- [x] [业务篇-简历制作之工具条模块与简历模版之间通信](https://juejin.cn/book/6950646725295996940/section/6965876539833450503) +- [x] [业务篇-简历制作之数据的录入与展示](https://juejin.cn/book/6950646725295996940/section/6962940365221396511) +- [x] [业务篇-简历制作之导出 PDF](https://juejin.cn/book/6950646725295996940/section/6962940108383191048) +- [x] [🏆 1000 米里程碑 |简历主流程完成](https://juejin.cn/book/6950646725295996940/section/6962940484008280077) +- [x] [业务篇-首页主题换肤功能实现](https://juejin.cn/book/6950646725295996940/section/6962761759404851239) +- [x] [业务篇-简历模版列表实现与侧边栏交互效果](https://juejin.cn/book/6950646725295996940/section/6962940426999300109) +- [x] [业务篇-简历数据存档且自定义存储路径](https://juejin.cn/book/6950646725295996940/section/6962940676258398222) +- [x] [🏆 1500 米里程碑 |丰富功能](https://juejin.cn/book/6950646725295996940/section/6962939774650810380) +- [x] [优化篇-如何采用 Hooks 优化主题换肤逻辑](https://juejin.cn/book/6950646725295996940/section/6962940589528580132) +- [x] [优化篇-公共弹窗拆解优化,让职能更加单一](https://juejin.cn/book/6950646725295996940/section/6962941125426413599) +- [x] [优化篇-采用 Hooks 优化简历 Form 组件](https://juejin.cn/book/6950646725295996940/section/6962941213401939998) +- [x] [定制篇-自定义 Electron 原生应用菜单](https://juejin.cn/book/6950646725295996940/section/6962938070312157184) +- [x] [打包篇-Webpack 打包优化](https://juejin.cn/book/6950646725295996940/section/6962941321325576226) +- [x] [打包篇-Electron 打包体积优化](https://juejin.cn/book/6950646725295996940/section/6962941003858706436) +- [x] [🏆 到达目的地-应用程序发布](https://juejin.cn/book/6950646725295996940/section/6962941268389265415) +- [x] [结尾篇-行而不辍,未来可期](https://juejin.cn/book/6950646725295996940/section/6962941492159578143) +- [x] [彩蛋篇-Webpack 基础介绍与两大利器](https://juejin.cn/book/6950646725295996940/section/6962895331730620423) +- [x] [彩蛋篇-RcReduxModel 中间件开发设计](https://juejin.cn/book/6950646725295996940/section/6953057493043904549) +- [x] [期望篇-可视化自定义独特的简历模版](https://juejin.cn/editor/book/6950646725295996940/section/6953057711445671943) diff --git a/app/electron.ts b/app/main/electron.ts similarity index 55% rename from app/electron.ts rename to app/main/electron.ts index d182a64..3cc336b 100644 --- a/app/electron.ts +++ b/app/main/electron.ts @@ -2,9 +2,10 @@ * @desc electron 主入口 */ import path from 'path'; -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, ipcMain } from 'electron'; function isDev() { + // 👉 还记得我们配置中通过 webpack.DefinePlugin 定义的构建变量吗 return process.env.NODE_ENV === 'development'; } @@ -15,24 +16,27 @@ function createWindow() { height: 800, webPreferences: { devTools: true, - webSecurity: true, nodeIntegration: true, }, }); - // 加载应用的 index.html。 if (isDev()) { + // 👇 看到了吗,在开发环境下,我们加载的是运行在 7001 端口的 React mainWindow.loadURL(`http://127.0.0.1:7001`); } else { - mainWindow.loadURL(`file://${path.join(__dirname, '../src/index.html')}`); + mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`); } } app.whenReady().then(() => { createWindow(); app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); + +const ROOT_PATH = path.join(app.getAppPath(), '../'); + +ipcMain.on('get-root-path', (event, arg) => { + event.reply('reply-root-path', ROOT_PATH); +}); diff --git a/app/renderer/app.tsx b/app/renderer/app.tsx new file mode 100644 index 0000000..4403461 --- /dev/null +++ b/app/renderer/app.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Router from './router'; +import store from './store'; +import { Provider } from 'react-redux'; + +function App() { + return ( + + + + ); +} + +ReactDOM.render(, document.getElementById('root')); diff --git a/app/renderer/common/components/MyButton/index.less b/app/renderer/common/components/MyButton/index.less new file mode 100644 index 0000000..9b412e6 --- /dev/null +++ b/app/renderer/common/components/MyButton/index.less @@ -0,0 +1,36 @@ +.es-button { + display: inline-block; + cursor: pointer; + box-sizing: border-box; + border: 1px solid #ddd; + border-radius: 16px; + font-size: 14px; + color: rgba(0, 0, 0, 0.6); + text-align: center; + user-select: none; + opacity: 1; + &:hover { + opacity: 0.9; + } +} +.es-button-small { + width: 64px; + height: 32px; + line-height: 30px; +} +.es-button-middle { + width: 90px; + height: 36px; + line-height: 34px; +} +.es-button-big { + width: 118px; + height: 42px; + line-height: 40px; +} +.es-button-disabled { + cursor: not-allowed; +} +.es-button-border { + border: 1px solid rgba(0, 0, 0, 0.07); +} diff --git a/app/renderer/common/components/MyButton/index.tsx b/app/renderer/common/components/MyButton/index.tsx new file mode 100644 index 0000000..06ee024 --- /dev/null +++ b/app/renderer/common/components/MyButton/index.tsx @@ -0,0 +1,64 @@ +/** + * @description 按钮组件 + */ +import React from 'react'; +import classnames from 'classnames'; +import './index.less'; + +export interface Button { + /** + * @description 按钮大小 + */ + size?: 'middle' | 'big' | 'small'; + /** + * @description 宽度 + */ + width?: number; + /** + * @description 自定义样式 + */ + style?: React.CSSProperties; + /** + * @description 子组件 + */ + children?: React.ReactNode | any; + /** + * @description 禁止 + */ + disabled?: boolean; + /** + * @description 样式名 + */ + className?: string; + /** + * @description 点击事件 + */ + onClick?: Function; + /** + * @description 是否显示边框 + */ + border?: boolean; +} +function MyButton({ size = 'small', style, width, children, disabled, className, onClick, border = true }: Button) { + return ( +
{ + onClick && onClick(); + }} + > + {children} +
+ ); +} + +export default MyButton; diff --git a/app/renderer/common/components/MyInput/index.less b/app/renderer/common/components/MyInput/index.less new file mode 100644 index 0000000..861d8e1 --- /dev/null +++ b/app/renderer/common/components/MyInput/index.less @@ -0,0 +1,119 @@ +.my-input { + display: inline-flex; + width: 100%; + height: auto; + line-height: 16px; + font-size: 14px; + box-sizing: border-box; + color: rgba(0, 0, 0, 0.8); + border: 1px solid transparent; + + &.normal { + border-radius: 8px; + background: #f8f8f8; + + &:hover { + background: #f5f5f5; + } + + &:active { + background: #eeeeee; + } + + &.focus { + background: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.07); + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.02); + } + } + + .my-input-center { + display: flex; + align-items: center; + justify-content: center; + } + + .my-input-input { + position: relative; + width: 100%; + padding: 0 15px; + line-height: 46px; + + &.large { + line-height: 62px; + + .my-input-clear { + top: 20px; + } + } + + input { + border: none; + outline: none; + background: transparent; + width: 100%; + font-family: Arial, Helvetica, sans-serif; + &::-webkit-input-placeholder { + color: rgba(0, 0, 0, 0.2); + } + } + } + + .my-input-textarea { + position: relative; + width: 100%; + padding: 12px 15px 32px 15px; + box-sizing: content-box; + + &.count-true { + padding-bottom: 27px; + } + + textarea { + padding: 0; + width: 100%; + height: 100%; + line-height: 1.5; + border: none; + outline: none; + resize: none; + background: transparent; + font-family: Arial, Helvetica, sans-serif; + &::-webkit-input-placeholder { + color: rgba(0, 0, 0, 0.2); + } + } + + .my-input-textarea-footer { + position: absolute; + right: 16px; + bottom: 4px; + font-size: 12px; + color: rgba(0, 0, 0, 0.4); + line-height: 20px; + + .max-length-text { + color: #ffab30; + } + } + } + + .my-input-clear { + display: block; + position: absolute; + top: 15px; + right: 15px; + width: 16px; + height: 16px; + cursor: pointer; + background: url(../../../../../assets/icon/cancel.png) center; + background-size: cover; + } + + &.allow-clear { + .my-input-input, + .my-input-textarea { + padding-right: 46px; + } + } +} diff --git a/app/renderer/common/components/MyInput/index.tsx b/app/renderer/common/components/MyInput/index.tsx new file mode 100644 index 0000000..913e7e8 --- /dev/null +++ b/app/renderer/common/components/MyInput/index.tsx @@ -0,0 +1,248 @@ +import React from 'react'; +import './index.less'; +import classnames from 'classnames'; + +const TYPE = { + text: 'text', + textarea: 'textarea', +}; + +export type SizeType = 'normal' | 'large'; +export type Type = 'text' | 'textarea' | ''; + +export interface InputProps extends Omit, 'size'> { + /** + * @description 自动获取焦点 + */ + autoFocus?: boolean; + /** + * @description 控件类型 + */ + type?: Type; + /** + * @description 控件大小 + */ + size?: SizeType; + /** + * @description 是否禁用 + */ + disabled?: boolean; + /** + * @description 设置前置标签 + */ + addonBefore?: React.ReactNode; + /** + * @description 设置后置标签 + */ + addonAfter?: React.ReactNode; + /** + * @description 可以计数 + */ + allowCount?: boolean; + /** + * @description 可以点击清除图标删除内容 + */ + allowClear?: boolean; + /** + * @description textarea行数,默认3 + */ + rows?: number; + /** + * @description 动态样式 + */ + style?: React.CSSProperties; + /** + * @description 输入框内容 + */ + value?: string | number | undefined; + /** + * @description 输入框占位符 + */ + placeholder?: string; + /** + * @description 输入框id + */ + id?: string; + /** + * @description 最大长度 + */ + maxLength?: number; + /** + * @description 是否背景透明 + */ + bgTransparent?: boolean; + /** + * @description 毁掉函数 + */ + onChange?: (event: React.ChangeEvent) => void; +} + +interface InputState { + focus: boolean; + text: string | number; +} + +export default class MyInput extends React.PureComponent { + input: HTMLInputElement | HTMLTextAreaElement | undefined; + constructor(props: InputProps) { + super(props); + this.state = { + focus: false, + text: props?.value || '', + }; + } + + componentDidMount() { + if (this.props.value) { + this.setState({ + text: this.props.value, + }); + } + } + + UNSAFE_componentWillReceiveProps(nextProps: InputProps) { + this.setState({ + text: nextProps.value || '', + }); + } + + saveInput = (input: HTMLInputElement | HTMLTextAreaElement) => { + this.input = input; + }; + + // 模拟change事件 + actionChange(e: any) { + const target = this.input as any; + const event = Object.create(e); + // 如果是点击清除按钮,则需要改target指向input,value清空 + if (e.type === 'click') { + target.value = ''; + event.target = target; + event.currentTarget = target; + } + this.props.onChange && this.props.onChange(event); + } + + focus = () => { + this.input && this.input.focus(); + }; + + blur = () => { + this.input && this.input.blur(); + }; + + onFocus = () => { + this.setState({ + focus: true, + }); + }; + + onBlur = () => { + this.setState({ + focus: false, + }); + }; + + onInput = (e: any) => { + this.setState({ text: e.target.value }); + this.actionChange(e); + }; + + onClear = (e: any) => { + this.setState({ + text: '', + }); + this.actionChange(e); + }; + + renderBefore() { + const { addonBefore } = this.props; + return !!addonBefore &&
{addonBefore}
; + } + + renderAfter() { + const { addonAfter } = this.props; + return !!addonAfter &&
{addonAfter}
; + } + + renderClear() { + const { allowClear } = this.props; + return !!allowClear && this.state.text && ; + } + + renderInput() { + const { placeholder, size = 'normal', maxLength, id, disabled, autoFocus } = this.props; + return ( +
+ + {this.renderClear()} +
+ ); + } + + renderTextarea() { + const { placeholder, maxLength = 1000, id, disabled, allowCount = true, autoFocus, rows } = this.props; + const _rows = rows || 3; + const text = this.state.text; + return ( +
+