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 打造可视化在线简历平台
+### 开发
-
-
-
-
-
-
+> 由于网络问题,往往安装 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 (
+
+
+ {this.renderClear()}
+ {allowCount && (
+
+ = maxLength,
+ })}
+ >
+ {String(text).length}
+
+ {!!maxLength && (
+ <>
+ /
+ {maxLength}
+ >
+ )}
+
+ )}
+
+ );
+ }
+
+ render() {
+ const { bgTransparent = false, style, type, allowClear } = this.props;
+ return (
+
+ {this.renderBefore()}
+ {TYPE.textarea === type ? this.renderTextarea() : this.renderInput()}
+ {this.renderAfter()}
+
+ );
+ }
+}
diff --git a/app/renderer/common/components/MyModal/MyConfirm/index.less b/app/renderer/common/components/MyModal/MyConfirm/index.less
new file mode 100644
index 0000000..010c013
--- /dev/null
+++ b/app/renderer/common/components/MyModal/MyConfirm/index.less
@@ -0,0 +1,80 @@
+.vis-mask {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.78);
+ height: 100%;
+ z-index: 1000;
+ .center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ .vis-confirm-box {
+ width: 440px;
+ padding: 40px 40px 24px;
+ box-sizing: border-box;
+ border-radius: 12px;
+ background-color: #ffffff;
+ z-index: 3;
+ .vis-confirm-content {
+ p.vis-confirm-content-title {
+ font-weight: 700;
+ font-size: 18px;
+ color: rgba(0, 0, 0, 0.8);
+ line-height: 28px;
+ }
+ p.vis-confirm-content-desc {
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.6);
+ line-height: 22px;
+ margin-top: 8px;
+ max-height: 44px;
+ }
+ }
+ .vis-confirm-footer {
+ margin-top: 24px;
+ text-align: right;
+ :global {
+ .vis-confirm-footer-btn {
+ border: 1px solid #ddd;
+ border-radius: 24px;
+ font-size: 14px;
+ height: 40px;
+ line-height: 40px;
+ margin-right: 16px;
+ text-align: center;
+ user-select: none;
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ .vis-confirm-footer-cancel-btn {
+ color: rgba(0, 0, 0, 0.6);
+ &:hover {
+ background-color: #f6f8fa;
+ }
+ }
+ .vis-confirm-footer-submit-btn {
+ color: #ffffff;
+ background-color: #198fff;
+ border: none;
+ &:hover {
+ background-color: #3b9efa;
+ }
+ }
+ .vis-confirm-footer-delete-btn {
+ color: #ffffff;
+ background-color: #e65c5c;
+ border: none;
+ &:hover {
+ background-color: #f06b6b;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/common/components/MyModal/MyConfirm/index.tsx b/app/renderer/common/components/MyModal/MyConfirm/index.tsx
new file mode 100644
index 0000000..3adfec6
--- /dev/null
+++ b/app/renderer/common/components/MyModal/MyConfirm/index.tsx
@@ -0,0 +1,62 @@
+/**
+ * @description 弹窗组件
+ */
+import React from 'react';
+import './index.less';
+import MyButton from '@common/components/MyButton';
+import { IConfirmModal } from '../types';
+
+function MyConfirm({ title, width, className, description, renderFooter, config = {}, eleRef }: IConfirmModal) {
+ const { deleteBtn = { isShow: false }, cancelBtn = { isShow: true }, submitBtn = { isShow: true } } = config;
+ return (
+
+
+
+
+
{title || '友情提示'}
+ {description &&
{description}
}
+
+ {renderFooter || (
+
+ {cancelBtn?.isShow && (
+ {
+ cancelBtn?.callback && cancelBtn.callback();
+ }}
+ >
+ {cancelBtn?.text || '取消'}
+
+ )}
+ {submitBtn?.isShow && (
+ {
+ submitBtn?.callback && submitBtn.callback();
+ }}
+ >
+ {submitBtn?.text || '确定'}
+
+ )}
+ {deleteBtn?.isShow && (
+ {
+ deleteBtn?.callback && deleteBtn.callback();
+ }}
+ >
+ {deleteBtn?.text || '退出'}
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+export default MyConfirm;
diff --git a/app/renderer/common/components/MyModal/MyDialog/index.less b/app/renderer/common/components/MyModal/MyDialog/index.less
new file mode 100644
index 0000000..99fba53
--- /dev/null
+++ b/app/renderer/common/components/MyModal/MyDialog/index.less
@@ -0,0 +1,91 @@
+.vis-mask {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.78);
+ height: 100%;
+ z-index: 1000;
+ .center {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ .vis-dialog-box {
+ width: 760px;
+ box-sizing: border-box;
+ border-radius: 12px;
+ background-color: #ffffff;
+ z-index: 3;
+ .vis-dialog-header {
+ height: 56px;
+ line-height: 56px;
+ font-size: 18px;
+ color: rgba(0, 0, 0, 0.8);
+ padding: 0 24px;
+ font-weight: bold;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.07);
+ box-sizing: border-box;
+ }
+ .vis-dialog-close {
+ position: absolute;
+ top: 18px;
+ right: 24px;
+ width: 20px;
+ height: 20px;
+ background-image: url('../../../../../../assets/icon/cancel.png');
+ background-size: cover;
+ cursor: pointer;
+ }
+ .vis-dialog-content {
+ padding: 24px;
+ }
+ .vis-dialog-footer {
+ height: 60px;
+ line-height: 60px;
+ padding: 0 24px;
+ text-align: right;
+ border-top: 1px solid rgba(0, 0, 0, 0.07);
+ box-sizing: border-box;
+ :global {
+ .vis-dialog-footer-btn {
+ border: 1px solid #ddd;
+ border-radius: 12px;
+ font-size: 14px;
+ height: 36px;
+ line-height: 36px;
+ margin-right: 16px;
+ text-align: center;
+ user-select: none;
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ .vis-dialog-footer-cancel-btn {
+ color: rgba(0, 0, 0, 0.6);
+ &:hover {
+ background-color: #f6f8fa;
+ }
+ }
+ .vis-dialog-footer-submit-btn {
+ color: #ffffff;
+ background-color: #198fff;
+ border: none;
+ &:hover {
+ background-color: #3b9efa;
+ }
+ }
+ .vis-modal-delete-btn {
+ color: #ffffff;
+ background-color: #e65c5c;
+ border: none;
+ &:hover {
+ background-color: #f06b6b;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/common/components/MyModal/MyDialog/index.tsx b/app/renderer/common/components/MyModal/MyDialog/index.tsx
new file mode 100644
index 0000000..b610513
--- /dev/null
+++ b/app/renderer/common/components/MyModal/MyDialog/index.tsx
@@ -0,0 +1,68 @@
+/**
+ * @description 弹窗组件
+ */
+import React from 'react';
+import './index.less';
+import MyButton from '@common/components/MyButton';
+import { IDialogModal } from '../types';
+
+function MyDialog({
+ title,
+ width,
+ className,
+ showFooter,
+ renderFooter,
+ config = {},
+ eleRef,
+ children,
+ childStyle,
+}: IDialogModal) {
+ const { cancelBtn = { isShow: true }, submitBtn = { isShow: true } } = config;
+ return (
+
+
+
+
{title || 'VisResumeMook 提示您'}
+
{
+ cancelBtn?.callback && cancelBtn.callback();
+ }}
+ />
+
+ {children}
+
+ {showFooter &&
+ (renderFooter || (
+
+ {cancelBtn?.isShow && (
+ {
+ cancelBtn?.callback && cancelBtn.callback();
+ }}
+ >
+ {cancelBtn?.text || '取消'}
+
+ )}
+ {submitBtn?.isShow && (
+ {
+ submitBtn?.callback && submitBtn.callback();
+ }}
+ >
+ {submitBtn?.text || '确定'}
+
+ )}
+
+ ))}
+
+
+
+ );
+}
+
+export default MyDialog;
diff --git a/app/renderer/common/components/MyModal/index.tsx b/app/renderer/common/components/MyModal/index.tsx
new file mode 100644
index 0000000..c49be09
--- /dev/null
+++ b/app/renderer/common/components/MyModal/index.tsx
@@ -0,0 +1,20 @@
+/**
+ * @description 所有弹窗组件集合
+ * 方式一:
+ * import MyModal from '@components/MyModal';
+ *
+ *
+ * 方式二:
+ * import { Confirm } from '@components/MyModal';
+ *
+ */
+import MyDialog from './MyDialog';
+import MyConfirm from './MyConfirm';
+
+export const Dialog = MyDialog;
+export const Confirm = MyConfirm;
+
+export default {
+ Dialog: MyDialog,
+ Confirm: MyConfirm,
+};
diff --git a/app/renderer/common/components/MyModal/types.ts b/app/renderer/common/components/MyModal/types.ts
new file mode 100644
index 0000000..78b889a
--- /dev/null
+++ b/app/renderer/common/components/MyModal/types.ts
@@ -0,0 +1,74 @@
+type Position = 'top' | 'bottom' | 'center';
+
+interface BtnConfig {
+ /**
+ * @description 按钮文本
+ */
+ text?: string;
+ /**
+ * @description 是否显示
+ */
+ isShow?: boolean;
+ callback?: () => void;
+}
+interface IModal {
+ /**
+ * @description 标题
+ */
+ title: string;
+ /**
+ * @description 宽度
+ */
+ width?: number;
+ /**
+ * @description 类名
+ */
+ className?: string;
+ /**
+ * @description 描述
+ */
+ description?: string;
+ /**
+ * @description 弹窗位置
+ */
+ position?: Position;
+ /**
+ * @description 是否需要显示底部
+ */
+ showFooter?: boolean;
+ /**
+ * @description 底部按钮自定义
+ */
+ renderFooter?: React.ReactNode;
+ /**
+ * @description 配置
+ */
+ config?: {
+ /**
+ * @description 取消按钮
+ */
+ cancelBtn?: BtnConfig;
+ /**
+ * @description 确定按钮
+ */
+ submitBtn?: BtnConfig;
+ /**
+ * @description 删除按钮
+ */
+ deleteBtn?: BtnConfig;
+ };
+ eleRef?: React.LegacyRef
| undefined;
+ /**
+ * @description 子组件
+ */
+ children?: React.ReactNode;
+}
+
+export type IConfirmModal = IModal;
+
+export interface IDialogModal extends IModal {
+ /**
+ * @description 内容区域的样式
+ */
+ childStyle?: React.CSSProperties;
+}
diff --git a/common/components/MyScrollBox/index.less b/app/renderer/common/components/MyScrollBox/index.less
similarity index 67%
rename from common/components/MyScrollBox/index.less
rename to app/renderer/common/components/MyScrollBox/index.less
index 027d35f..f51ae7f 100644
--- a/common/components/MyScrollBox/index.less
+++ b/app/renderer/common/components/MyScrollBox/index.less
@@ -1,32 +1,32 @@
:global {
- .MyScrollBox_outer {
+ .scroll-box-outer {
width: 100%;
max-height: 200px;
position: relative;
overflow: hidden;
box-sizing: border-box;
- .MyScrollBox_hidden_scroll {
+ .scroll-box-hidden {
overflow-y: scroll;
overflow-x: hidden;
height: 100%;
- // width: calc(100% + 17px);
+ width: calc(100% + 17px);
// margin-right: -17px;
- .MyScrollBox_inner {
- // width: calc(100% - 17px);
+ .scroll-box-inter {
+ width: calc(100% - 17px);
display: inline-block;
}
}
- .MyScrollBox_hidden_scroll::-webkit-scrollbar {
+ .scroll-box-hidden::-webkit-scrollbar {
width: 0 !important; /* remove scrollbar space */
background: transparent; /* optional: just make scrollbar invisible */
}
/* 兼容IE10+ */
- .MyScrollBox_hidden_scroll {
+ .scroll-box-hidden {
-ms-overflow-style: none;
}
/* 但是firefox代码无效 */
- .MyScrollBox_hidden_scroll {
+ .scroll-box-hidden {
overflow: -moz-scrollbars-none;
}
}
diff --git a/app/renderer/common/components/MyScrollBox/index.tsx b/app/renderer/common/components/MyScrollBox/index.tsx
new file mode 100644
index 0000000..65a825a
--- /dev/null
+++ b/app/renderer/common/components/MyScrollBox/index.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import './index.less';
+
+interface IProps {
+ /**
+ * @description 子组件
+ */
+ children: React.ReactNode | any;
+ /**
+ * @description 最大高度,默认200
+ */
+ maxHeight?: number;
+ /**
+ * @description 根div样式
+ */
+ style?: React.CSSProperties;
+ /**
+ * @description 最内部的div样式
+ */
+ innerStyle?: React.CSSProperties;
+ /**
+ * @description 开启了滚动事件之后,回调得到滚动的top
+ */
+ onScrollTop?: (scrollTop: number) => void;
+}
+function MyScrollBox({ children, maxHeight = 200, style = {}, innerStyle = {}, onScrollTop }: IProps) {
+ function onScroll(e: any) {
+ const _event = e.target || e.currentTarget;
+ onScrollTop && onScrollTop(_event.scrollTop);
+ }
+ let _style = { ...style };
+ if (maxHeight) {
+ _style = { ..._style, maxHeight: `${maxHeight}px` };
+ }
+ return (
+
+ );
+}
+
+export default MyScrollBox;
diff --git a/app/renderer/common/components/MyUpload/ImageUpload/index.less b/app/renderer/common/components/MyUpload/ImageUpload/index.less
new file mode 100644
index 0000000..1abf933
--- /dev/null
+++ b/app/renderer/common/components/MyUpload/ImageUpload/index.less
@@ -0,0 +1,48 @@
+:global {
+ .my_input_image_upload_wrapper {
+ width: 112px;
+ height: 152px;
+ margin: 14px auto;
+ position: relative;
+ display: inline-block;
+ vertical-align: text-bottom;
+ &:hover {
+ .my_input_image_upload_box {
+ .my_input_image_upload__icon {
+ transform: scale(1.2);
+ }
+ }
+ }
+
+ .my_input_image_upload_input {
+ position: absolute;
+ opacity: 0;
+ z-index: 2;
+ height: 100%;
+ width: 100%;
+ cursor: pointer;
+ }
+ .my_input_image_upload_box {
+ position: absolute;
+ z-index: 1;
+ display: flex;
+ height: 100%;
+ width: 100%;
+ cursor: pointer;
+ align-items: center;
+ flex-direction: column;
+ justify-content: center;
+ font-size: 12px;
+ line-height: 18px;
+ color: rgba(0, 0, 0, 0.6);
+ cursor: pointer;
+ border: 1px dashed #ddd;
+ .my_input_image_upload__icon {
+ color: #bec1c5;
+ width: 24px;
+ height: 24px;
+ margin-bottom: 8px;
+ }
+ }
+ }
+}
diff --git a/app/renderer/common/components/MyUpload/ImageUpload/index.tsx b/app/renderer/common/components/MyUpload/ImageUpload/index.tsx
new file mode 100644
index 0000000..38c6894
--- /dev/null
+++ b/app/renderer/common/components/MyUpload/ImageUpload/index.tsx
@@ -0,0 +1,46 @@
+/**
+ * @description 图片上传组件,基于 Input 二次封装
+ * @author {pengdaokuan}
+ */
+import React from 'react';
+import './index.less';
+import Upload from '../Upload';
+
+interface IProps extends TSUpload.Input {
+ /**
+ * @description 图标
+ */
+ icon?: any;
+ /**
+ * @description 文本
+ */
+ text?: any;
+ /**
+ * @description 是否阻止input点击之后显示文件管理器
+ */
+ preventInputManager?: boolean;
+}
+
+const ImageUpload: React.FC = ({
+ icon,
+ text = '上传头像',
+ preventInputManager = false,
+ onAfterClick = () => {},
+ ...otherProps
+}) => {
+ return (
+
+ {!preventInputManager && (
+
+ {}} style={{ width: '112px', height: '152px' }} />
+
+ )}
+
+

+
{text}
+
+
+ );
+};
+
+export default ImageUpload;
diff --git a/app/renderer/common/components/MyUpload/Upload/index.less b/app/renderer/common/components/MyUpload/Upload/index.less
new file mode 100644
index 0000000..7f83d15
--- /dev/null
+++ b/app/renderer/common/components/MyUpload/Upload/index.less
@@ -0,0 +1,25 @@
+:global {
+ .my_input_selector {
+ height: 32px;
+ width: 120px;
+ cursor: pointer;
+ padding: 4px 15px;
+ font-size: 14px;
+ border-radius: 2px;
+ color: rgba(0, 0, 0, 0.65);
+ border-color: #d9d9d9;
+ border: 1px solid transparent;
+ background-color: #ffffff;
+ box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015);
+ &:hover {
+ color: #40a9ff;
+ border-color: #40a9ff;
+ }
+ }
+ .my_input_selector_hidden {
+ visibility: hidden;
+ }
+ .my_input_selector_visible {
+ visibility: visible;
+ }
+}
diff --git a/app/renderer/common/components/MyUpload/Upload/index.tsx b/app/renderer/common/components/MyUpload/Upload/index.tsx
new file mode 100644
index 0000000..a00bd88
--- /dev/null
+++ b/app/renderer/common/components/MyUpload/Upload/index.tsx
@@ -0,0 +1,43 @@
+/**
+ * @desc 上传控件,默认自带的input样式
+ * @author {pengdaokuan}
+ */
+import React, { useRef } from 'react';
+import './index.less';
+import classnames from 'classnames';
+import FileEvent from '../fileEvent';
+
+function Upload({ style, accept, multiple = false, visible = true, onAfterClick, onAfterChange }: TSUpload.Input) {
+ const inputSelectorRef = useRef(null);
+
+ function onChange(e: any) {
+ const fileList: any = e?.target?.files || [];
+ if (e.target.value === '') {
+ return;
+ }
+ let instance: TSUpload.File[] = [];
+ // fileList 属于 [object FileList] 类型
+ for (let file of fileList) {
+ instance.push(new FileEvent(file));
+ }
+ onAfterChange && onAfterChange(instance);
+ }
+
+ return (
+
+ );
+}
+
+export default Upload;
diff --git a/app/renderer/common/components/MyUpload/fileEvent.ts b/app/renderer/common/components/MyUpload/fileEvent.ts
new file mode 100644
index 0000000..3e1586f
--- /dev/null
+++ b/app/renderer/common/components/MyUpload/fileEvent.ts
@@ -0,0 +1,31 @@
+import { createUID } from '@common/utils';
+
+class FileEvent {
+ public uuid: string;
+ public file: any;
+ public fileType: string;
+ public base64URL: string;
+
+ public constructor(file: any) {
+ this.file = file;
+ this.uuid = createUID();
+ const types = file?.type?.split('/') || [];
+ this.fileType = types.length ? types[0] : '';
+ /**
+ * @description 本地预览地址
+ */
+ this.base64URL = window.URL.createObjectURL(file);
+ }
+
+ // 释放创建过的URL,不然会存在性能问题
+ // 详情可见 : https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
+ public revokeFileBase64URL(base64URL: string) {
+ window.URL.revokeObjectURL(base64URL);
+ }
+
+ public upload() {}
+ public cancel() {}
+ public retry() {}
+}
+
+export default FileEvent;
diff --git a/app/renderer/common/constants/resume.ts b/app/renderer/common/constants/resume.ts
new file mode 100644
index 0000000..425e838
--- /dev/null
+++ b/app/renderer/common/constants/resume.ts
@@ -0,0 +1,83 @@
+export const RESUME_TOOLBAR_MAPS = {
+ personal: 'personal', // 个人信息
+ contact: 'contact', // 联系方式
+ education: 'education', // 教育信息
+ workPrefer: 'workPrefer', // 工作期望
+ schoolExperience: 'schoolExperience', // 在校经历
+ projectExperience: 'projectExperience', // 项目经验
+ workExperience: 'workExperience', // 工作经历
+ certificate: 'certificate', // 获奖证书
+ evaluation: 'evaluation', // 个人评价
+ skill: 'skill', // 技能清单
+ test: 'test', // 测试用例
+};
+
+const RESUME_TOOLBAR_LIST: TSResume.SliderItem[] = [
+ {
+ key: RESUME_TOOLBAR_MAPS.personal,
+ name: '个人信息',
+ require: true,
+ summary: '更好介绍自己,机会会更多',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.education,
+ name: '教育信息',
+ require: false,
+ summary: '介绍你的学校和专业信息',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.contact,
+ name: '联系方式',
+ require: false,
+ summary: '少侠,请留下你的联系方式',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.workPrefer,
+ name: '工作期望',
+ require: false,
+ summary: '聊聊你所期望的宜人办公城市',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.schoolExperience,
+ name: '在校经历',
+ require: false,
+ summary: '展示在校任职成果和人际关系',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.projectExperience,
+ name: '项目经验',
+ require: false,
+ summary: '展示研究过什么优秀项目和成果',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.workExperience,
+ name: '工作经历',
+ require: false,
+ summary: '申请岗位的相关经验和能力',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.certificate,
+ name: '获奖证书',
+ require: false,
+ summary: '得过什么奖项值得炫耀',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.evaluation,
+ name: '个人评价',
+ require: false,
+ summary: '低调夸一夸自己有什么亮点',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.skill,
+ name: '技能清单',
+ require: false,
+ summary: '展示具备的技能,突出你的能力',
+ },
+ {
+ key: RESUME_TOOLBAR_MAPS.test,
+ name: '测试用例',
+ require: true,
+ summary: '测试用例,一键覆盖',
+ },
+];
+export default RESUME_TOOLBAR_LIST;
diff --git a/app/renderer/common/constants/router.ts b/app/renderer/common/constants/router.ts
new file mode 100644
index 0000000..8fa3cd2
--- /dev/null
+++ b/app/renderer/common/constants/router.ts
@@ -0,0 +1,30 @@
+// 模块路径
+const ROUTER = {
+ root: '/',
+ resume: '/resume',
+};
+export default ROUTER;
+
+export const ROUTER_KEY = {
+ root: 'root',
+ resume: 'resume',
+};
+
+// 入口模块
+export const ROUTER_ENTRY = [
+ {
+ url: 'https://github.com/PDKSophia/visResumeMook',
+ key: 'intro',
+ text: '介绍',
+ },
+ {
+ url: ROUTER.resume,
+ key: ROUTER_KEY.resume,
+ text: '简历',
+ },
+ {
+ url: 'https://github.com/PDKSophia/visResumeMook',
+ key: 'code',
+ text: '源码',
+ },
+];
diff --git a/app/renderer/common/constants/skill.ts b/app/renderer/common/constants/skill.ts
new file mode 100644
index 0000000..00ae786
--- /dev/null
+++ b/app/renderer/common/constants/skill.ts
@@ -0,0 +1,183 @@
+export interface IRecommendSkill {
+ uid: string;
+ label: string;
+ styles: {
+ bg: string;
+ font: string;
+ };
+}
+
+const colors = [
+ {
+ // 绿色
+ bg: '#f0f8ec',
+ font: '#78c74f',
+ },
+ {
+ // 灰色
+ bg: '#f4f4f4',
+ font: '#a3a7ab',
+ },
+ {
+ // 橙色
+ bg: '#fdf6ec',
+ font: '#f0c588',
+ },
+ {
+ // 蓝色
+ bg: '#ecf5ff',
+ font: '#67afff',
+ },
+ {
+ // 红色
+ bg: '#fef0ef',
+ font: '#f88c8b',
+ },
+];
+
+import { createUID } from '../utils';
+
+const RecommendSkill: IRecommendSkill[] = [
+ {
+ uid: createUID(),
+ label: 'Vue.js',
+ styles: colors[0],
+ },
+ {
+ uid: createUID(),
+ label: '数据双向绑定原理',
+ styles: colors[2],
+ },
+ {
+ uid: createUID(),
+ label: 'React.js',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: 'VScode',
+ styles: colors[2],
+ },
+ {
+ uid: createUID(),
+ label: 'Angular.js',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: 'Webpack',
+ styles: colors[3],
+ },
+ {
+ uid: createUID(),
+ label: 'React Hooks',
+ styles: colors[2],
+ },
+ {
+ uid: createUID(),
+ label: '开源',
+ styles: colors[0],
+ },
+ {
+ uid: createUID(),
+ label: '了解 MYSQL',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: '微信小程序',
+ styles: colors[0],
+ },
+ {
+ uid: createUID(),
+ label: 'Taro',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: '微信公众号开发',
+ styles: colors[3],
+ },
+ {
+ uid: createUID(),
+ label: 'Babel',
+ styles: colors[1],
+ },
+ {
+ uid: createUID(),
+ label: 'TypeScript',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: 'Electron',
+ styles: colors[2],
+ },
+ {
+ uid: createUID(),
+ label: 'Server',
+ styles: colors[2],
+ },
+ {
+ uid: createUID(),
+ label: 'ESLint',
+ styles: colors[3],
+ },
+ {
+ uid: createUID(),
+ label: '跨域解决',
+ styles: colors[0],
+ },
+ {
+ uid: createUID(),
+ label: '自动化测试',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: 'Linux',
+ styles: colors[2],
+ },
+ {
+ uid: createUID(),
+ label: 'Git',
+ styles: colors[0],
+ },
+ {
+ uid: createUID(),
+ label: '设计模式',
+ styles: colors[1],
+ },
+ {
+ uid: createUID(),
+ label: 'Redis',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: '数据库优化',
+ styles: colors[3],
+ },
+ {
+ uid: createUID(),
+ label: '正则表达式',
+ styles: colors[1],
+ },
+ {
+ uid: createUID(),
+ label: '具备良好的网络基础知识',
+ styles: colors[0],
+ },
+ {
+ uid: createUID(),
+ label: '数据存储',
+ styles: colors[4],
+ },
+ {
+ uid: createUID(),
+ label: 'Echarts',
+ styles: colors[3],
+ },
+];
+
+export default RecommendSkill;
diff --git a/app/renderer/common/messager/index.ts b/app/renderer/common/messager/index.ts
new file mode 100644
index 0000000..5099f41
--- /dev/null
+++ b/app/renderer/common/messager/index.ts
@@ -0,0 +1,43 @@
+export const MESSAGE_EVENT_NAME_MAPS = {
+ OPEN_FORM_MODAL: 'open_form_modal', // 简历模块选择
+};
+
+/**
+ * @description 发送事件
+ * @summary 简历选择工具-选中模块
+ * // 注册事件并发送数据
+ * Messager.send(MESSAGE_EVENT_NAME_MAPS.xxx, {
+ data: 'xxx'
+ });
+ // 在具体组件监听此事件并接收数据
+ useEffect(() => {
+ document.addEventListener(MESSAGE_EVENT_NAME_MAPS.xxx, onReceive);
+ return () => {
+ document.removeEventListener(MESSAGE_EVENT_NAME_MAPS.xxx, onReceive);
+ };
+ }, []);
+ const onReceive = (e: any) => {
+ Messager.receive(e, (data: any) => {
+ console.log(data);
+ });
+ };
+ */
+class Messager {
+ send = (eventName: string, payload: any) => {
+ document.dispatchEvent(
+ new CustomEvent(eventName, {
+ detail: {
+ payload: payload,
+ },
+ })
+ );
+ };
+ receive = (e: any, messageHandler: Function) => {
+ if (messageHandler) {
+ const payload = e?.detail?.payload;
+ messageHandler(payload);
+ }
+ };
+}
+
+export default new Messager();
diff --git a/app/renderer/common/types/rc-redux-model.d.ts b/app/renderer/common/types/rc-redux-model.d.ts
new file mode 100644
index 0000000..0aa0a6a
--- /dev/null
+++ b/app/renderer/common/types/rc-redux-model.d.ts
@@ -0,0 +1,27 @@
+declare namespace TSRcReduxModel {
+ interface D {
+ [key: string]: any;
+ }
+ export interface Props {
+ /**
+ * @description 命名空间,唯一,必须
+ */
+ namespace: string;
+ /**
+ * @description 数据状态,必须
+ */
+ state: S;
+ /**
+ * @description action,非必须
+ */
+ action?: A;
+ /**
+ * @description action,非必须
+ */
+ reducers?: R;
+ /**
+ * @description 是否开启 Immutable,非必须
+ */
+ openSeamlessImmutable?: boolean;
+ }
+}
diff --git a/app/renderer/common/types/resume.d.ts b/app/renderer/common/types/resume.d.ts
new file mode 100644
index 0000000..377ff2d
--- /dev/null
+++ b/app/renderer/common/types/resume.d.ts
@@ -0,0 +1,227 @@
+declare namespace TSResume {
+ /**
+ * @description 基本信息
+ */
+ export interface Base {
+ /**
+ * @description 头像
+ */
+ avatar?: string;
+ /**
+ * @description 姓名
+ */
+ username: string;
+ /**
+ * @description 地区
+ */
+ area?: string;
+ /**
+ * @description 学校
+ */
+ school?: string;
+ /**
+ * @description 学年
+ */
+ onSchoolTime?: {
+ /**
+ * @description 入校时间
+ */
+ beginTime: string | number | null;
+ /**
+ * @description 离校时间
+ */
+ endTime: string | number | null;
+ };
+ /**
+ * @description 专业
+ */
+ major?: string;
+ /**
+ * @description 学位
+ */
+ degree?: string;
+ /**
+ * @description 籍贯
+ */
+ hometown?: string;
+ /**
+ * @description 政治面貌
+ */
+ political?: string;
+ }
+
+ /**
+ * @description 联系方式
+ */
+ export interface Contact {
+ /**
+ * @description 电话号码
+ */
+ phone?: string;
+ /**
+ * @description 邮箱
+ */
+ email?: string;
+ /**
+ * @description github
+ */
+ github?: string;
+ /**
+ * @description 掘金
+ */
+ juejin?: string;
+ }
+
+ /**
+ * @description 求职信息
+ */
+ export interface Work {
+ /**
+ * @description 意愿岗位
+ */
+ job?: string;
+ /**
+ * @description 意愿城市
+ */
+ city?: string;
+ cityList?: string[];
+ }
+
+ /**
+ * @description 在校经验
+ */
+ export interface SchoolExperience extends Experience {
+ /**
+ * @description 部门
+ */
+ department?: string;
+ /**
+ * @description 职位
+ */
+ post?: string;
+ /**
+ * @description 主要工作
+ */
+ content?: string;
+ parseContent?: string[];
+ }
+
+ /**
+ * @description 工作经验
+ */
+ export interface WorkExperience extends Experience {
+ /**
+ * @description 部门
+ */
+ department?: string;
+ /**
+ * @description 职位
+ */
+ post?: string;
+ /**
+ * @description 主要工作
+ */
+ content?: string;
+ parseContent?: string[];
+ }
+
+ /**
+ * @description 项目经验
+ */
+ export interface ProjectExperience extends Experience {
+ /**
+ * @description 项目名
+ */
+ projectName?: string;
+ /**
+ * @description 职位
+ */
+ post?: string;
+ /**
+ * @description 主要工作
+ */
+ content?: string;
+ parseContent?: string[];
+ }
+
+ /**
+ * @description 经验
+ */
+ interface Experience {
+ /**
+ * @description 开始时间
+ */
+ beginTime?: number | string | undefined;
+ /**
+ * @description 结束时间
+ */
+ endTime?: number | string | undefined;
+ /**
+ * @description 额外补充内容
+ */
+ supplement?: string;
+ /**
+ * @description 最后修改时间
+ */
+ date?: number;
+ }
+
+ /**
+ * @description 一份完整的简历信息
+ */
+ export interface IntactResume {
+ base: Base;
+ skill: string;
+ skillList: string[];
+ hobby: string;
+ evaluation: string;
+ evaluationList: string[];
+ certificate: string;
+ certificateList: string[];
+ contact: Contact;
+ work: Work;
+ workExperience?: WorkExperience[];
+ schoolExperience?: SchoolExperience[];
+ projectExperience?: ProjectExperience[];
+ }
+
+ /**
+ * @description 简历模版
+ */
+ export interface TemplateItem {
+ /**
+ * @description 唯一标识
+ */
+ id: string;
+ /**
+ * @description 模版名
+ */
+ name: string;
+ /**
+ * @description 模版封面
+ */
+ cover: string;
+ }
+
+ /**
+ * @description 简历工具条模块
+ */
+ export interface SliderItem {
+ /**
+ * @description 唯一标识
+ */
+ key: string;
+ /**
+ * @description 模块名
+ */
+ name: string;
+ /**
+ * @description 描述
+ */
+ summary: string;
+ /**
+ * @description 是否必须
+ */
+ require?: boolean;
+ }
+}
diff --git a/app/renderer/common/types/router.d.ts b/app/renderer/common/types/router.d.ts
new file mode 100644
index 0000000..b6f4985
--- /dev/null
+++ b/app/renderer/common/types/router.d.ts
@@ -0,0 +1,18 @@
+// router.d.ts
+// 路由类型约束
+declare namespace TSRouter {
+ export interface Item {
+ /**
+ * @description 路由跳转链接
+ */
+ url: string;
+ /**
+ * @description 关键词
+ */
+ key: string;
+ /**
+ * @description 文本
+ */
+ text: string;
+ }
+}
diff --git a/app/renderer/common/types/upload.d.ts b/app/renderer/common/types/upload.d.ts
new file mode 100644
index 0000000..d5c2804
--- /dev/null
+++ b/app/renderer/common/types/upload.d.ts
@@ -0,0 +1,69 @@
+declare namespace TSUpload {
+ /**
+ * @description 上传框
+ */
+ export interface Input {
+ /**
+ * @description 样式
+ */
+ style?: React.CSSProperties;
+ /**
+ * @description 上传的文件类型
+ */
+ accept?: string;
+ /**
+ * @description 支持多选文件
+ */
+ multiple?: boolean;
+ /**
+ * @description input 输入框是否可见
+ */
+ visible?: boolean;
+ /**
+ * @description input 点击后回调事件
+ */
+ onAfterClick?: () => void;
+ /**
+ * @description input change 回调事件,回传选中的文件列表
+ */
+ onAfterChange?: (fileList: File[]) => void;
+ }
+
+ /**
+ * @description 文件
+ */
+ export interface File {
+ /**
+ * @description 文件标识
+ */
+ uuid: string;
+ /**
+ * @description 文件
+ */
+ file: any;
+ /**
+ * @description 文件类型
+ */
+ fileType: string;
+ /**
+ * @description 文件本地预览
+ */
+ base64URL: string;
+ /**
+ * @description 释放本地预览URL
+ */
+ revokeFileBase64URL: (base64URL: string) => void;
+ /**
+ * @description 文件上传
+ */
+ upload?: () => void;
+ /**
+ * @description 取消上传
+ */
+ cancel?: () => void;
+ /**
+ * @description 重传
+ */
+ retry?: () => void;
+ }
+}
diff --git a/app/renderer/common/utils/appPath.ts b/app/renderer/common/utils/appPath.ts
new file mode 100644
index 0000000..234fc71
--- /dev/null
+++ b/app/renderer/common/utils/appPath.ts
@@ -0,0 +1,19 @@
+// 监听主进程与渲染进程通信
+import { ipcRenderer } from 'electron';
+
+/**
+ * @description 获取项目绝对路径
+ * @returns {Promise}
+ */
+export function getAppPath(): Promise {
+ return new Promise((resolve: (value: string) => void, reject: (value: Error) => void) => {
+ ipcRenderer.send('get-root-path', '');
+ ipcRenderer.on('reply-root-path', (event, arg: string) => {
+ if (arg) {
+ resolve(arg);
+ } else {
+ reject(new Error('项目路径错误'));
+ }
+ });
+ });
+}
diff --git a/app/renderer/common/utils/file.ts b/app/renderer/common/utils/file.ts
new file mode 100644
index 0000000..2dcd5a3
--- /dev/null
+++ b/app/renderer/common/utils/file.ts
@@ -0,0 +1,63 @@
+import fs, { promises as fsPromiseAPIs } from 'fs';
+
+const fileAction = {
+ /**
+ * @description 读取文件内容
+ * @param path 路径
+ * @returns {Promise}
+ */
+ read: (path: string): Promise => {
+ return fsPromiseAPIs.readFile(path, { encoding: 'utf8' });
+ },
+ /**
+ * @description 写入文件内容
+ * @param path 路径
+ * @returns {Promise}
+ */
+ write: (path: string, content: string): Promise => {
+ return fsPromiseAPIs.writeFile(path, content, { encoding: 'utf8' });
+ },
+ /**
+ * @description 重命名文件
+ * @param {string} oldPath 旧地址
+ * @param {string} newPath 新地址
+ * @returns {Promise}
+ */
+ rename: (oldPath: string, newPath: string): Promise => {
+ return fsPromiseAPIs.rename(oldPath, newPath);
+ },
+ /**
+ * @description 删除文件
+ * @param path 路径
+ * @returns {Promise}
+ */
+ delete: (path: string): Promise => {
+ return fsPromiseAPIs.unlink(path);
+ },
+ /**
+ * @description 是否存在文件
+ * @param path 路径
+ * @returns {Promise}
+ */
+ hasFile: (path: string): Promise => {
+ return fsPromiseAPIs.access(path, fs.constants.F_OK);
+ },
+ /**
+ * @description 是否可写入此文件
+ * @param path 路径
+ * @returns {Promise}
+ */
+ canWrite: (path: string): Promise => {
+ return fsPromiseAPIs.access(path, fs.constants.W_OK);
+ },
+ /**
+ * @description 是否可读此文件
+ * @param path 路径
+ * @returns {Promise}
+ */
+ canRead: (path: string): Promise => {
+ return fsPromiseAPIs.access(path, fs.constants.R_OK);
+ },
+};
+
+export default fileAction;
diff --git a/app/renderer/common/utils/htmlToPdf.ts b/app/renderer/common/utils/htmlToPdf.ts
new file mode 100644
index 0000000..5bd2ee1
--- /dev/null
+++ b/app/renderer/common/utils/htmlToPdf.ts
@@ -0,0 +1,45 @@
+import JsPdf from 'jspdf';
+import html2canvas from 'html2canvas';
+const A4Width = 592.28;
+const A4Height = 841.89;
+
+/**
+ * @description 导出PDF文件,暂时支持单页
+ * @param {string} resumeName 导出文件名
+ */
+export function toPrintPdf(resumeName?: string) {
+ let name = resumeName || '未命名文件';
+ const dom: HTMLElement | any = document.querySelector('#visPdf');
+ if (dom) {
+ html2canvas(dom, {
+ allowTaint: true,
+ }).then((canvas) => {
+ let contentWidth = canvas.width;
+ let contentHeight = canvas.height;
+ // 一页pdf显示html页面生成的canvas高度,等比缩放
+ let pageHeight = (contentWidth / A4Width) * A4Height;
+ let leftHeight = contentHeight;
+ // 页面偏移
+ let position = 0;
+ let imgWidth = A4Width;
+ let imgHeight = (A4Width / contentWidth) * contentHeight;
+ let pageData = canvas.toDataURL('image/jpeg', 1.0);
+ // 这里的第一个参数表示方向,这里一定要选择 portrait
+ // 具体可看文档 https://artskydj.github.io/jsPDF/docs/jsPDF.html
+ let PDF = new JsPdf('portrait', 'pt', 'a4');
+ if (leftHeight < pageHeight) {
+ PDF.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight);
+ } else {
+ while (leftHeight > 0) {
+ PDF.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight);
+ leftHeight -= pageHeight;
+ position -= imgHeight;
+ if (leftHeight > 0) {
+ PDF.addPage();
+ }
+ }
+ }
+ PDF.save(name + '.pdf');
+ });
+ }
+}
diff --git a/app/renderer/common/utils/index.ts b/app/renderer/common/utils/index.ts
new file mode 100644
index 0000000..5599f0e
--- /dev/null
+++ b/app/renderer/common/utils/index.ts
@@ -0,0 +1,28 @@
+// 创建uid
+export function createUID() {
+ return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'
+ .replace(/[xy]/g, (c) => {
+ let r = (Math.random() * 16) | 0;
+ let v = c === 'x' ? r : (r & 3) | 8;
+ return v.toString(16);
+ })
+ .toUpperCase();
+}
+
+/**
+ * 将字符串数字转成整型数字
+ * @param {string} value
+ * @returns {number}
+ */
+export function transformStringToNumber(value: string): number {
+ return Number(value);
+}
+
+/**
+ * 剔除px
+ */
+export function reducePX(value: string | number | undefined): string {
+ if (!value) return '';
+ const _value = String(value);
+ return _value.replace('px', '');
+}
diff --git a/app/renderer/common/utils/router.ts b/app/renderer/common/utils/router.ts
new file mode 100644
index 0000000..cc4c70e
--- /dev/null
+++ b/app/renderer/common/utils/router.ts
@@ -0,0 +1,8 @@
+/**
+ * @desc 判断是否属于外部连接
+ * @param {string} url - 链接
+ */
+export function isHttpOrHttpsUrl(url: string): boolean {
+ let regRule = /(http|https):\/\/([\w.]+\/?)\S*/;
+ return regRule.test(url.toLowerCase());
+}
diff --git a/app/renderer/common/utils/time.ts b/app/renderer/common/utils/time.ts
new file mode 100644
index 0000000..48cd484
--- /dev/null
+++ b/app/renderer/common/utils/time.ts
@@ -0,0 +1,49 @@
+/**
+ * 时间戳变成类似 2021/03/09 这样的字符串
+ * @param {number} num 时间戳整数
+ * @returns {string} date
+ */
+export function intToDateString(num: number, unit = '/'): string {
+ let date;
+ if (!num) {
+ date = new Date();
+ } else {
+ date = new Date(num);
+ }
+ const year = date.getFullYear();
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return `${year}${unit}${month >= 10 ? month : '0' + month}${unit}${day >= 10 ? day : '0' + day}`;
+}
+
+/**
+ * 时间戳变成类似14:28:19这样的字符串
+ * @param {number} num 时间戳整数
+ * @returns {string} date
+ */
+export function intToTimeString(num?: number): string {
+ let date;
+ if (!num) {
+ date = new Date();
+ } else {
+ date = new Date(num);
+ }
+ const hour = date.getHours();
+ const minute = date.getMinutes();
+ const second = date.getSeconds();
+ return `${hour >= 10 ? hour : '0' + hour}:${minute >= 10 ? minute : '0' + minute}:${
+ second >= 10 ? second : '0' + second
+ }
+ `;
+}
+
+/**
+ * 时间戳变成类似2018/01/12 12:32:21这样的字符串
+ * @param {Number} num 时间戳整数
+ */
+export function formatToString(num: number | undefined, unit = '/') {
+ if (!num) return '';
+ const date1 = intToDateString(num, unit);
+ const date2 = intToTimeString(num);
+ return `${date1} ${date2}`;
+}
diff --git a/app/renderer/container/resume/ResumeAction/index.less b/app/renderer/container/resume/ResumeAction/index.less
new file mode 100644
index 0000000..c9dff30
--- /dev/null
+++ b/app/renderer/container/resume/ResumeAction/index.less
@@ -0,0 +1,21 @@
+.actions {
+ box-sizing: border-box;
+ padding: 12px;
+ border-radius: 6px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #ffffff;
+ .back {
+ color: rgba(0, 0, 0, 0.6);
+ font-size: 16px;
+ cursor: pointer;
+ }
+ :global {
+ .export-btn {
+ text-align: center;
+ background-color: #27292c;
+ color: #ffffff;
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeAction/index.tsx b/app/renderer/container/resume/ResumeAction/index.tsx
new file mode 100644
index 0000000..c71336e
--- /dev/null
+++ b/app/renderer/container/resume/ResumeAction/index.tsx
@@ -0,0 +1,53 @@
+/**
+ * @description 制作简历-操作区
+ */
+import React, { useState } from 'react';
+import './index.less';
+import { useHistory } from 'react-router';
+import ROUTER from '@common/constants/router';
+import MyButton from '@common/components/MyButton';
+import { toPrintPdf } from '@common/utils/htmlToPdf';
+import { useSelector } from 'react-redux';
+import MyModal from '@common/components/MyModal';
+
+function ResumeAction() {
+ const history = useHistory();
+ const [showModal, setShowModal] = useState(false);
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+ const work: TSResume.Work = useSelector((state: any) => state.resumeModel.work);
+
+ // 返回首页
+ const onBack = () => history.push(ROUTER.root);
+
+ return (
+
+
+ 返回
+
+
setShowModal(true)}>
+ 导出PDF
+
+ {showModal && (
+
setShowModal(false),
+ },
+ submitBtn: {
+ isShow: true,
+ callback: () => {
+ toPrintPdf(`${base?.username}+${base?.school}+${work?.job}`);
+ setShowModal(false);
+ },
+ },
+ }}
+ />
+ )}
+
+ );
+}
+
+export default ResumeAction;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Certificate/index.less b/app/renderer/container/resume/ResumeContent/UseForm/Certificate/index.less
new file mode 100644
index 0000000..07e2381
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Certificate/index.less
@@ -0,0 +1,26 @@
+.form {
+ font-size: 14px;
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ .tips {
+ color: rgb(240, 70, 70);
+ margin-top: 8px;
+ font-size: 14px;
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Certificate/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/Certificate/index.tsx
new file mode 100644
index 0000000..e260390
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Certificate/index.tsx
@@ -0,0 +1,53 @@
+/**
+ * @description 荣誉证书Form
+ */
+import React from 'react';
+import './index.less';
+import MyModal from '@common/components/MyModal';
+import MyInput from '@common/components/MyInput';
+import { useSelector } from 'react-redux';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+
+function Certificate({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const certificate: string = useSelector((state: any) => state.resumeModel.certificate);
+
+ return (
+
+
+
+
+ *证 书 :
+
+
+
{
+ updateResumeHook('certificate', e.target.value);
+ }}
+ rows={5}
+ value={certificate || ''}
+ placeholder="互联网+大赛一等奖|掘金大学骰王|互联网喝酒大赛进步奖"
+ allowClear={true}
+ />
+ * 多个证书以 | 分割
+
+
+
+
+ );
+}
+
+export default Certificate;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Contact/index.less b/app/renderer/container/resume/ResumeContent/UseForm/Contact/index.less
new file mode 100644
index 0000000..59a714e
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Contact/index.less
@@ -0,0 +1,21 @@
+.form {
+ font-size: 14px;
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ .left {
+ margin-top: 12px;
+ width: 80px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Contact/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/Contact/index.tsx
new file mode 100644
index 0000000..be8721f
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Contact/index.tsx
@@ -0,0 +1,100 @@
+/**
+ * @description 联系方式Form
+ */
+import React from 'react';
+import './index.less';
+import MyModal from '@common/components/MyModal';
+import MyInput from '@common/components/MyInput';
+import { useSelector } from 'react-redux';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function Contact({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const contact: TSResume.Contact = useSelector((state: any) => state.resumeModel.contact);
+
+ return (
+
+
+
+
+ *电 话 :
+
+
+ {
+ updateResumeHook('contact/phone', e.target.value);
+ }}
+ value={contact?.phone || ''}
+ placeholder="请输入电话号码"
+ allowClear={true}
+ />
+
+
+
+
+ *邮 箱 :
+
+
+ {
+ updateResumeHook('contact/email', e.target.value);
+ }}
+ value={contact?.email || ''}
+ placeholder="请输入邮箱"
+ allowClear={true}
+ />
+
+
+
+
+
+ *
+
+ Github :
+
+
+ {
+ updateResumeHook('contact/github', e.target.value);
+ }}
+ value={contact?.github || ''}
+ placeholder="Github 地址"
+ allowClear={true}
+ />
+
+
+
+
+
+ *
+
+ Juejin :
+
+
+ {
+ updateResumeHook('contact/juejin', e.target.value);
+ }}
+ value={contact?.juejin || ''}
+ placeholder="掘金地址"
+ allowClear={true}
+ />
+
+
+
+
+ );
+}
+
+export default Contact;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Education/index.less b/app/renderer/container/resume/ResumeContent/UseForm/Education/index.less
new file mode 100644
index 0000000..8e8d62e
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Education/index.less
@@ -0,0 +1,25 @@
+.form {
+ font-size: 14px;
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ span.line {
+ margin: 0 11px 0 11px;
+ color: rgba(0, 0, 0, 0.6);
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Education/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/Education/index.tsx
new file mode 100644
index 0000000..0b708de
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Education/index.tsx
@@ -0,0 +1,113 @@
+/**
+ * @description 教育信息Form
+ */
+import React from 'react';
+import './index.less';
+import MyModal from '@common/components/MyModal';
+import MyInput from '@common/components/MyInput';
+import { useSelector } from 'react-redux';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function Education({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+
+ return (
+
+
+
+
+ *学 校 :
+
+
+ {
+ updateResumeHook('base/school', e.target?.value || '');
+ }}
+ value={base?.school || ''}
+ placeholder="请输入贵校"
+ allowClear={true}
+ />
+
+
+
+
+ *专 业 :
+
+
+ {
+ updateResumeHook('base/major', e.target?.value || '');
+ }}
+ value={base?.major || ''}
+ placeholder="请输入专业"
+ allowClear={true}
+ />
+
+
+
+
+ *学 位 :
+
+
+ {
+ updateResumeHook('base/degree', e.target?.value || '');
+ }}
+ value={base?.degree || ''}
+ placeholder="学士?硕士?博士?"
+ allowClear={true}
+ />
+
+
+
+
+ *学 年 :
+
+
+ {
+ const nextTime = {
+ ...base?.onSchoolTime,
+ beginTime: e.target.value,
+ };
+ updateResumeHook
+
+
+
+ );
+}
+
+export default Education;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Evaluation/index.less b/app/renderer/container/resume/ResumeContent/UseForm/Evaluation/index.less
new file mode 100644
index 0000000..bcb4582
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Evaluation/index.less
@@ -0,0 +1,26 @@
+.form {
+ font-size: 14px;
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ .tips {
+ color: rgb(240, 70, 70);
+ margin-top: 8px;
+ font-size: 12px;
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Evaluation/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/Evaluation/index.tsx
new file mode 100644
index 0000000..6c290e5
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Evaluation/index.tsx
@@ -0,0 +1,53 @@
+/**
+ * @description 个人评价Form
+ */
+import React from 'react';
+import './index.less';
+import MyModal from '@common/components/MyModal';
+import MyInput from '@common/components/MyInput';
+import { useSelector } from 'react-redux';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+
+function Evaluation({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const evaluation: string = useSelector((state: any) => state.resumeModel.evaluation);
+
+ return (
+
+
+
+
+ *评 价 :
+
+
+
{
+ updateResumeHook('evaluation', e.target.value);
+ }}
+ rows={5}
+ value={evaluation || ''}
+ placeholder="夸一夸自己有什么亮点"
+ allowClear={true}
+ />
+ * 可通过 | 分割
+
+
+
+
+ );
+}
+
+export default Evaluation;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Personal/index.less b/app/renderer/container/resume/ResumeContent/UseForm/Personal/index.less
new file mode 100644
index 0000000..888876b
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Personal/index.less
@@ -0,0 +1,21 @@
+.form {
+ font-size: 14px;
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Personal/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/Personal/index.tsx
new file mode 100644
index 0000000..6aa4778
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Personal/index.tsx
@@ -0,0 +1,84 @@
+/**
+ * @description 个人信息Form
+ */
+import React from 'react';
+import './index.less';
+import MyModal from '@common/components/MyModal';
+import MyInput from '@common/components/MyInput';
+import { useSelector } from 'react-redux';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function Personal({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const hobby: string = useSelector((state: any) => state.resumeModel.hobby);
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+ return (
+
+
+
+
+ *姓 名 :
+
+
+ {
+ updateResumeHook('base/username', e.target?.value || '');
+ }}
+ value={base?.username || ''}
+ placeholder="请输入姓名"
+ allowClear={true}
+ />
+
+
+
+
+ *籍 贯 :
+
+
+ {
+ updateResumeHook('base/hometown', e.target?.value || '');
+ }}
+ value={base?.hometown || ''}
+ placeholder="请输入籍贯"
+ allowClear={true}
+ />
+
+
+
+
+
+ *
+
+ 爱 好 :
+
+
+ {
+ updateResumeHook('hobby', e.target?.value || '');
+ }}
+ rows={5}
+ value={hobby || ''}
+ placeholder="你有什么特长爱好呢"
+ allowClear={true}
+ />
+
+
+
+
+ );
+}
+
+export default Personal;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/Form.tsx b/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/Form.tsx
new file mode 100644
index 0000000..df9bcfe
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/Form.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import './index.less';
+import MyInput from '@common/components/MyInput';
+import { AdapterExperienceType } from '../WrapperExperience/adapter';
+
+interface IProps {
+ isDisable?: boolean;
+ currentItem?: AdapterExperienceType;
+ onChangeCurrentItem?: (newItem: AdapterExperienceType) => void;
+}
+function Form({ isDisable, currentItem, onChangeCurrentItem }: IProps) {
+ const onChangeValue = (key: string, value: string) => {
+ let newItem = { ...currentItem, [key]: value };
+ onChangeCurrentItem && onChangeCurrentItem(newItem);
+ };
+
+ return (
+
+
+
+ *项目名 :
+
+
+ onChangeValue('title', e.target.value)}
+ value={currentItem?.title}
+ placeholder="请输入项目名"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+
+ *职 位 :
+
+
+ onChangeValue('post', e.target.value)}
+ value={currentItem?.post}
+ placeholder="在项目中担任什么职位"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+
+ *时 间 :
+
+
+ onChangeValue('beginTime', e.target.value)}
+ value={currentItem?.beginTime}
+ placeholder="2015.09.01"
+ allowClear={!isDisable}
+ style={{ width: 290 }}
+ disabled={isDisable}
+ />
+ -
+ onChangeValue('endTime', e.target.value)}
+ value={currentItem?.endTime}
+ placeholder="2015.09.01"
+ allowClear={!isDisable}
+ style={{ width: 290 }}
+ disabled={isDisable}
+ />
+
+
+
+
+ *内 容 :
+
+
+ onChangeValue('content', e.target.value)}
+ rows={5}
+ value={currentItem?.content}
+ placeholder="你在项目中的主要工作是什么呢?"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+ );
+}
+export default Form;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/index.less b/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/index.less
new file mode 100644
index 0000000..903849c
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/index.less
@@ -0,0 +1,25 @@
+.wrapper {
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ font-size: 14px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ span.line {
+ margin: 0 12px 0 11px;
+ color: rgba(0, 0, 0, 0.6);
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/index.tsx
new file mode 100644
index 0000000..50a34ff
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/ProjectExperience/index.tsx
@@ -0,0 +1,44 @@
+/**
+ * @description 项目经验Form
+ */
+import React from 'react';
+import MyModal from '@common/components/MyModal';
+import Form from './Form';
+import { useSelector } from 'react-redux';
+import Wrapper from '../WrapperExperience';
+import AdapterExperience, { AdapterExperienceType } from '../WrapperExperience/adapter';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function ProjectExperience({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const projectExperience: TSResume.ProjectExperience[] = useSelector(
+ (state: any) => state.resumeModel.projectExperience
+ );
+
+ const updateDataList = (newDataList: AdapterExperienceType[]) => {
+ updateResumeHook('projectExperience', newDataList);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default ProjectExperience;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/Form.tsx b/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/Form.tsx
new file mode 100644
index 0000000..e8a93cd
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/Form.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import './index.less';
+import MyInput from '@common/components/MyInput';
+import { AdapterExperienceType } from '../WrapperExperience/adapter';
+
+interface IProps {
+ isDisable?: boolean;
+ currentItem?: AdapterExperienceType;
+ onChangeCurrentItem?: (newItem: AdapterExperienceType) => void;
+}
+function Form({ isDisable, currentItem, onChangeCurrentItem }: IProps) {
+ const onChangeValue = (key: string, value: string) => {
+ let newItem = { ...currentItem, [key]: value };
+ onChangeCurrentItem && onChangeCurrentItem(newItem);
+ };
+
+ return (
+
+
+
+ *部门 :
+
+
+ onChangeValue('title', e.target.value)}
+ value={currentItem?.title}
+ placeholder="请输入在校时的部门"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+
+ *职 位 :
+
+
+ onChangeValue('post', e.target.value)}
+ value={currentItem?.post}
+ placeholder="在部门中担任什么职位"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+
+ *时 间 :
+
+
+ onChangeValue('beginTime', e.target.value)}
+ value={currentItem?.beginTime}
+ placeholder="2015.09.01"
+ allowClear={!isDisable}
+ style={{ width: 290 }}
+ disabled={isDisable}
+ />
+ -
+ onChangeValue('endTime', e.target.value)}
+ value={currentItem?.endTime}
+ placeholder="2015.09.01"
+ allowClear={!isDisable}
+ style={{ width: 290 }}
+ disabled={isDisable}
+ />
+
+
+
+
+ *内 容 :
+
+
+ onChangeValue('content', e.target.value)}
+ rows={5}
+ value={currentItem?.content}
+ placeholder="任职期间主要工作是什么呢?"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+ );
+}
+export default Form;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/index.less b/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/index.less
new file mode 100644
index 0000000..903849c
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/index.less
@@ -0,0 +1,25 @@
+.wrapper {
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ font-size: 14px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ span.line {
+ margin: 0 12px 0 11px;
+ color: rgba(0, 0, 0, 0.6);
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/index.tsx
new file mode 100644
index 0000000..be67b7a
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/SchoolExperience/index.tsx
@@ -0,0 +1,42 @@
+/**
+ * @description 在校经历Form
+ */
+import React from 'react';
+import MyModal from '@common/components/MyModal';
+import Form from './Form';
+import { useSelector } from 'react-redux';
+import Wrapper from '../WrapperExperience';
+import AdapterExperience, { AdapterExperienceType } from '../WrapperExperience/adapter';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function SchoolExperience({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const schoolExperience: TSResume.SchoolExperience[] = useSelector((state: any) => state.resumeModel.schoolExperience);
+
+ const updateDataList = (newDataList: AdapterExperienceType[]) => {
+ updateResumeHook('schoolExperience', newDataList);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default SchoolExperience;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Skill/index.less b/app/renderer/container/resume/ResumeContent/UseForm/Skill/index.less
new file mode 100644
index 0000000..f53d0c7
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Skill/index.less
@@ -0,0 +1,44 @@
+.form {
+ font-size: 14px;
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ .left {
+ width: 80px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ .tips {
+ color: rgb(240, 70, 70);
+ margin-top: 8px;
+ font-size: 14px;
+ }
+ .action {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ align-items: center;
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.6);
+ .label {
+ padding: 4px 8px;
+ border-radius: 4px;
+ margin-right: 8px;
+ margin-bottom: 12px;
+ border: 1px solid;
+ cursor: pointer;
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Skill/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/Skill/index.tsx
new file mode 100644
index 0000000..4bf3882
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Skill/index.tsx
@@ -0,0 +1,77 @@
+/**
+ * @description 技能清单Form
+ */
+import React from 'react';
+import './index.less';
+import MyModal from '@common/components/MyModal';
+import MyInput from '@common/components/MyInput';
+import { useSelector } from 'react-redux';
+import RecommendSkill, { IRecommendSkill } from '@common/constants/skill';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function Skill({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const skill: string = useSelector((state: any) => state.resumeModel.skill);
+
+ return (
+
+
+
+
+
+ *
+
+ 技 能 :
+
+
+
+ {RecommendSkill.map((recommend: IRecommendSkill) => {
+ return (
+
{
+ const value = `${skill}${skill ? '|' : ''}${recommend.label}`;
+ updateResumeHook('skill', value);
+ }}
+ >
+ {recommend.label}
+
+ );
+ })}
+
+
{
+ updateResumeHook('skill', e.target.value);
+ }}
+ rows={5}
+ value={skill}
+ placeholder="例如 Vue、React"
+ allowClear={true}
+ />
+ * 多个技能以 | 分割
+
+
+
+
+ );
+}
+
+export default Skill;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Work/index.less b/app/renderer/container/resume/ResumeContent/UseForm/Work/index.less
new file mode 100644
index 0000000..07e2381
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Work/index.less
@@ -0,0 +1,26 @@
+.form {
+ font-size: 14px;
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ .tips {
+ color: rgb(240, 70, 70);
+ margin-top: 8px;
+ font-size: 14px;
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/Work/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/Work/index.tsx
new file mode 100644
index 0000000..8e2d12f
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/Work/index.tsx
@@ -0,0 +1,65 @@
+/**
+ * @description 工作期望Form
+ */
+import React from 'react';
+import './index.less';
+import MyModal from '@common/components/MyModal';
+import MyInput from '@common/components/MyInput';
+import { useSelector } from 'react-redux';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function Work({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const work: TSResume.Work = useSelector((state: any) => state.resumeModel.work);
+
+ return (
+
+
+
+
+ *职 位 :
+
+
+ {
+ updateResumeHook('work/job', e.target.value);
+ }}
+ value={work?.job || ''}
+ placeholder="求职岗位"
+ allowClear={true}
+ />
+
+
+
+
+ *城 市 :
+
+
+
{
+ updateResumeHook('work/city', e.target.value);
+ }}
+ value={work?.city || ''}
+ placeholder="请输入意愿城市"
+ allowClear={true}
+ />
+ * 多个地点以|分割
+
+
+
+
+ );
+}
+
+export default Work;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/Form.tsx b/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/Form.tsx
new file mode 100644
index 0000000..126a7b5
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/Form.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import './index.less';
+import MyInput from '@common/components/MyInput';
+import { AdapterExperienceType } from '../WrapperExperience/adapter';
+
+interface IProps {
+ isDisable?: boolean;
+ currentItem?: AdapterExperienceType;
+ onChangeCurrentItem?: (newItem: AdapterExperienceType) => void;
+}
+function Form({ isDisable, currentItem, onChangeCurrentItem }: IProps) {
+ const onChangeValue = (key: string, value: string) => {
+ let newItem = { ...currentItem, [key]: value };
+ onChangeCurrentItem && onChangeCurrentItem(newItem);
+ };
+
+ return (
+
+
+
+ *公司 :
+
+
+ onChangeValue('title', e.target.value)}
+ value={currentItem?.title}
+ placeholder="请输入之前就职的公司"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+
+ *职 位 :
+
+
+ onChangeValue('post', e.target.value)}
+ value={currentItem?.post}
+ placeholder="期间担任什么职位"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+
+ *时 间 :
+
+
+ onChangeValue('beginTime', e.target.value)}
+ value={currentItem?.beginTime}
+ placeholder="2015.09.01"
+ allowClear={!isDisable}
+ style={{ width: 290 }}
+ disabled={isDisable}
+ />
+ -
+ onChangeValue('endTime', e.target.value)}
+ value={currentItem?.endTime}
+ placeholder="2015.09.01"
+ allowClear={!isDisable}
+ style={{ width: 290 }}
+ disabled={isDisable}
+ />
+
+
+
+
+ *内 容 :
+
+
+ onChangeValue('content', e.target.value)}
+ rows={5}
+ value={currentItem?.content}
+ placeholder="任职期间主要工作是什么呢?"
+ allowClear={!isDisable}
+ disabled={isDisable}
+ />
+
+
+
+ );
+}
+export default Form;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/index.less b/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/index.less
new file mode 100644
index 0000000..903849c
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/index.less
@@ -0,0 +1,25 @@
+.wrapper {
+ .flex {
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
+ margin-bottom: 12px;
+ font-size: 14px;
+ .left {
+ width: 80px;
+ margin-top: 12px;
+ color: rgba(0, 0, 0, 0.6);
+ span.require {
+ color: rgb(240, 70, 70);
+ margin-right: 6px;
+ }
+ }
+ .right {
+ flex: 1;
+ span.line {
+ margin: 0 12px 0 11px;
+ color: rgba(0, 0, 0, 0.6);
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/index.tsx
new file mode 100644
index 0000000..1f6ad52
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WorkExperience/index.tsx
@@ -0,0 +1,41 @@
+/**
+ * @description 工作经历Form
+ */
+import React from 'react';
+import MyModal from '@common/components/MyModal';
+import Form from './Form';
+import { useSelector } from 'react-redux';
+import Wrapper from '../WrapperExperience';
+import AdapterExperience, { AdapterExperienceType } from '../WrapperExperience/adapter';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+interface IProps {
+ onClose: () => void;
+}
+function WorkExperience({ onClose }: IProps) {
+ const updateResumeHook = useUpdateResumeHook();
+ const workExperience: TSResume.WorkExperience[] = useSelector((state: any) => state.resumeModel.workExperience);
+ const updateDataList = (newDataList: AdapterExperienceType[]) => {
+ updateResumeHook('workExperience', newDataList);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default WorkExperience;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/List/index.less b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/List/index.less
new file mode 100644
index 0000000..e620ef4
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/List/index.less
@@ -0,0 +1,72 @@
+.experience-list {
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ background: #fff;
+ transition: all 0.5s ease;
+ .experience-item {
+ height: 60px;
+ color: #464646;
+ position: relative;
+ padding: 8px 16px 0 12px;
+ box-sizing: border-box;
+ &:hover {
+ .experience-item-action {
+ display: block;
+ }
+ }
+ &.is-select {
+ border-left: 3px solid #fed634 !important;
+ }
+ .experience-item-box {
+ cursor: pointer;
+ height: 100%;
+ box-sizing: border-box;
+ border-bottom: 1px solid #ebebeb;
+ .experience-item-title {
+ height: 24px;
+ font-size: 14px;
+ line-height: 24px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: left;
+ color: rgba(0, 0, 0, 0.6);
+ font-weight: 700;
+ }
+ .experience-item-date {
+ position: relative;
+ margin-right: 16px;
+ width: 100%;
+ font-size: 12px;
+ color: rgba(0, 0, 0, 0.4);
+ line-height: 24px;
+ }
+ }
+ .experience-item-action {
+ position: absolute;
+ bottom: 2px;
+ right: -16px;
+ width: 48px;
+ display: none;
+ img {
+ width: 16px;
+ height: 16px;
+ }
+ .experience-edit,
+ .experience-delete {
+ cursor: pointer;
+ margin: 0 8px;
+ display: inline-block;
+ }
+ }
+ }
+ .empty {
+ text-align: center;
+ .empty-tips {
+ margin-top: 128px;
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.4);
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/List/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/List/index.tsx
new file mode 100644
index 0000000..c8e3f30
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/List/index.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import './index.less';
+import { formatToString } from '@common/utils/time';
+import DeleteIcon from '@assets/icon/delete.png';
+import { AdapterExperienceType } from '../../adapter';
+
+export interface IListProps {
+ /**
+ * @description 当前操作索引下标
+ */
+ index: number;
+ /**
+ * @description 条目列表
+ */
+ experienceList: AdapterExperienceType[];
+ /**
+ * @description 删除回调
+ */
+ onDelete: (index: number) => void;
+ /**
+ * @description 选择条目回调
+ */
+ onChange: (index: number) => void;
+}
+
+function List({ index, experienceList, onDelete, onChange }: IListProps) {
+ return (
+
+ {experienceList &&
+ experienceList.length > 0 &&
+ experienceList.map((item: AdapterExperienceType, i: number) => {
+ return (
+
{
+ onChange(i);
+ }}
+ >
+
+
{item.title}
+
{formatToString(item?.date)}
+
+
+
{
+ e.stopPropagation && e.stopPropagation();
+ onDelete(i);
+ }}
+ >
+

+
+
+
+ );
+ })}
+ {!experienceList ||
+ (!experienceList.length && (
+
+ ))}
+
+ );
+}
+
+export default List;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/index.less b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/index.less
new file mode 100644
index 0000000..de0bbff
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/index.less
@@ -0,0 +1,24 @@
+.layout-left {
+ position: relative;
+ height: 100%;
+ .action {
+ height: 60px;
+ line-height: 60px;
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ text-align: center;
+ }
+ .empty {
+ text-align: center;
+ .empty-tips {
+ margin-top: 128px;
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.4);
+ }
+ .empty-action {
+ margin-top: 12px;
+ text-align: center;
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/index.tsx
new file mode 100644
index 0000000..0871d02
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Left/index.tsx
@@ -0,0 +1,43 @@
+/**
+ * @description 专门服务于经验弹窗左侧
+ */
+import React from 'react';
+import './index.less';
+import MyButton from '@common/components/MyButton';
+import MyScrollBox from '@common/components/MyScrollBox';
+import List, { IListProps } from './List';
+
+interface IProps extends IListProps {
+ onAdd: () => void;
+}
+
+function Left({ index, experienceList = [], onDelete, onAdd, onChange }: IProps) {
+ return (
+
+ {experienceList.length > 0 && (
+ <>
+
+
+
+
+
+ 添加条目
+
+
+ >
+ )}
+ {experienceList.length === 0 && (
+
+
还没有内容,快添加一下吧~
+
+
+ 添加条目
+
+
+
+ )}
+
+ );
+}
+
+export default Left;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/Menu/index.less b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/Menu/index.less
new file mode 100644
index 0000000..309c65e
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/Menu/index.less
@@ -0,0 +1,46 @@
+.menu {
+ height: 60px;
+ display: flex;
+ justify-content: space-between;
+ box-sizing: border-box;
+ align-items: center;
+ position: relative;
+ flex: 1;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.07);
+ .left {
+ .title {
+ width: 420px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ .right {
+ .btn {
+ height: 28px;
+ line-height: 28px;
+ font-size: 12px;
+ width: 60px;
+ border-radius: 24px;
+ text-align: center;
+ display: inline-block;
+ margin-right: 12px;
+ cursor: pointer;
+ &:last-child {
+ margin-right: 0px;
+ }
+ }
+ .save {
+ background-color: #fed634;
+ &:hover {
+ background-color: #fbdc5d;
+ }
+ }
+ .cancel {
+ border: 1px solid rgba(0, 0, 0, 0.07);
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.04);
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/Menu/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/Menu/index.tsx
new file mode 100644
index 0000000..96ffd94
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/Menu/index.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import './index.less';
+import { AdapterExperienceType } from '../../adapter';
+
+interface IProps {
+ /**
+ * @description 当前是否处于编辑状态
+ */
+ isEdit: boolean;
+ /**
+ * @description 当前的条目
+ */
+ currentItem: AdapterExperienceType;
+ /**
+ * @description 点击取消
+ */
+ onCancelEditValue: () => void;
+ /**
+ * @description 点击保存
+ */
+ onSaveEditValue: () => void;
+ /**
+ * @description 点击编辑
+ */
+ onChangeEditStatus: () => void;
+}
+function Menu({ currentItem, isEdit, onCancelEditValue, onSaveEditValue, onChangeEditStatus }: IProps) {
+ return (
+
+
+
+ {isEdit && (
+ <>
+
+ 取消
+
+
+ 保存
+
+ >
+ )}
+ {!isEdit && (
+
+ 编辑
+
+ )}
+
+
+ );
+}
+
+export default Menu;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/index.less b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/index.less
new file mode 100644
index 0000000..b3feeb2
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/index.less
@@ -0,0 +1,7 @@
+.header {
+ height: 60px;
+ padding: 0 12px;
+}
+.content {
+ padding: 24px 12px 0;
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/index.tsx
new file mode 100644
index 0000000..659c203
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/Right/index.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import './index.less';
+import MyScrollBox from '@src/common/components/MyScrollBox';
+
+interface IProps {
+ children: React.ReactNode;
+}
+function Right({ children }: IProps) {
+ const getChild = () => {
+ const menuElement = (children as any)[0];
+ const formElement = (children as any)[1][0];
+
+ return [
+ React.cloneElement(menuElement, {
+ ...(children as any)[0],
+ key: 'menuElement',
+ }),
+ React.cloneElement(formElement, {
+ ...(children as any)[0],
+ key: 'formElement',
+ }),
+ ];
+ };
+ return (
+ <>
+ {getChild()[0]}
+
+ {getChild()[1]}
+
+ >
+ );
+}
+
+export default Right;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/adapter.ts b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/adapter.ts
new file mode 100644
index 0000000..480d3a8
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/adapter.ts
@@ -0,0 +1,79 @@
+/**
+ * @description 适配器模块
+ * @summary 将不同数据适配成 List 组件所需的数据格式
+ */
+export interface AdapterExperienceType {
+ /**
+ * @description 标题
+ */
+ title?: string;
+ /**
+ * @description 职位
+ */
+ post?: string;
+ /**
+ * @description 主要工作
+ */
+ content?: string;
+ parseContent?: string[];
+ /**
+ * @description 开始时间
+ */
+ beginTime?: number | string | undefined;
+ /**
+ * @description 结束时间
+ */
+ endTime?: number | string | undefined;
+ /**
+ * @description 额外补充内容
+ */
+ supplement?: string;
+ /**
+ * @description 最后编辑时间
+ */
+ date?: number;
+}
+
+const AdapterExperience = {
+ /**
+ * @description 项目经验
+ */
+ project(list: TSResume.ProjectExperience[]): AdapterExperienceType[] {
+ if (!list || list.length === 0) return [];
+ const experienceList: AdapterExperienceType[] = list.map((item: TSResume.ProjectExperience) => {
+ return {
+ ...item,
+ title: item.projectName,
+ };
+ });
+ return experienceList;
+ },
+ /**
+ * @description 工作经验
+ */
+ work(list: TSResume.WorkExperience[]): AdapterExperienceType[] {
+ if (!list || list.length === 0) return [];
+ const experienceList: AdapterExperienceType[] = list.map((item: TSResume.WorkExperience) => {
+ return {
+ ...item,
+ title: item.department,
+ };
+ });
+ return experienceList;
+ },
+ /**
+ * @description 在校经验
+ */
+ school(list: TSResume.SchoolExperience[]): AdapterExperienceType[] {
+ if (!list || list.length === 0) return [];
+ const experienceList: AdapterExperienceType[] = list.map((item: TSResume.SchoolExperience) => {
+ return {
+ ...item,
+ title: item.department,
+ };
+ });
+ return experienceList;
+ },
+};
+
+export default AdapterExperience;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/index.less b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/index.less
new file mode 100644
index 0000000..3e21a9c
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/index.less
@@ -0,0 +1,14 @@
+.form {
+ display: flex;
+ height: 480px;
+ .left-box {
+ width: 240px;
+ height: 100%;
+ box-sizing: border-box;
+ border-right: 1px solid rgba(0, 0, 0, 0.07);
+ }
+ .right-box {
+ flex: 1;
+ height: 100%;
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/index.tsx b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/index.tsx
new file mode 100644
index 0000000..70df923
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/index.tsx
@@ -0,0 +1,247 @@
+import React, { useEffect, useState, useMemo, useCallback } from 'react';
+import './index.less';
+import Left from './Left';
+import Right from './Right';
+import Menu from './Right/Menu';
+import { onAddExperience, onDeleteExperience } from './utils';
+import { AdapterExperienceType } from './adapter';
+import MyModal from '@common/components/MyModal';
+
+interface IProps {
+ dataList: any[];
+ updateDataList: (newDataList: any[]) => void;
+ children: React.ReactNode;
+}
+
+function WrapperExperience({ children, dataList, updateDataList }: IProps) {
+ const [currentIndex, setCurrentIndex] = useState(-1);
+ const [currentItem, setCurrentItem] = useState({});
+ const [experienceList, setExperienceList] = useState([]);
+
+ // 删除状态
+ const [deleteModal, setDeleteModal] = useState({
+ show: false,
+ deleteIndex: -1,
+ });
+ // 编辑状态
+ const [editModal, setEditModal] = useState({
+ showByCancel: false, // 编辑下的取消弹窗
+ showBySave: false, // 编辑下的保存弹窗
+ status: false, // 编辑的状态
+ tempSaveItem: {}, // 暂时保存的数据
+ onAfterFn: () => {}, // 操作之后的执行方法
+ });
+
+ // 1. 初次当条目列表不为空,默认选中第一条
+ useEffect(() => {
+ if (dataList && dataList?.length > 0) {
+ setCurrentIndex(0);
+ }
+ }, []);
+
+ // 2. 当条目数据列表修改更新,则更新数据
+ useEffect(() => {
+ if (dataList && dataList?.length > 0) {
+ setExperienceList(dataList || []);
+ } else {
+ setExperienceList([]);
+ }
+ }, [dataList]);
+
+ // 3. 当条目索引发生改变,更新当前选中的条目数据
+ useEffect(() => {
+ if (currentIndex >= 0) {
+ setCurrentItem(experienceList[currentIndex]);
+ }
+ }, [currentIndex, experienceList]);
+
+ // 4. 删除条目
+ const onDeleteItem = (index: number) => {
+ setDeleteModal({
+ show: true,
+ deleteIndex: index,
+ });
+ };
+ const onDeleteCancel = useCallback(() => {
+ setDeleteModal({
+ show: false,
+ deleteIndex: -1,
+ });
+ }, [currentIndex, deleteModal]);
+ const onDeleteOk = useCallback(() => {
+ const newList = onDeleteExperience(deleteModal.deleteIndex, experienceList);
+ if (newList.length > 0) setCurrentIndex(0);
+ else setCurrentIndex(-1);
+ setDeleteModal({
+ show: false,
+ deleteIndex: -1,
+ });
+ setExperienceList(newList);
+ updateDataList && updateDataList(newList);
+ }, [currentIndex, deleteModal]);
+
+ // 5. 修改选中的条目
+ const onChangeItem = useCallback(
+ (index: number) => {
+ // 5.1 当前正在编辑状态
+ if (editModal.status) {
+ onToggleEditModal({
+ showByCancel: true,
+ onAfterFn: () => {
+ setCurrentIndex(index);
+ },
+ });
+ } else {
+ setCurrentIndex(index);
+ }
+ },
+ [editModal]
+ );
+
+ // 6. 添加条目
+ const onAddItem = () => {
+ if (editModal.status) {
+ onToggleEditModal({
+ showByCancel: true,
+ onAfterFn: () => {
+ const newList = onAddExperience(experienceList);
+ if (newList.length > 0) {
+ // 定位激活刚添加的这条数据
+ setCurrentIndex(0);
+ setExperienceList(newList);
+ updateDataList && updateDataList(newList);
+ }
+ },
+ });
+ } else {
+ const newList = onAddExperience(experienceList);
+ if (newList.length > 0) {
+ // 定位激活刚添加的这条数据
+ setCurrentIndex(0);
+ setExperienceList(newList);
+ updateDataList && updateDataList(newList);
+ }
+ }
+ };
+
+ // 修改编辑状态
+ const onToggleEditModal = useCallback(
+ (config) => {
+ setEditModal((prev) => {
+ return {
+ ...prev,
+ ...config,
+ };
+ });
+ },
+ [editModal]
+ );
+
+ const onSaveEditValue = useCallback(() => {
+ let newList = [...experienceList];
+ let item = editModal?.tempSaveItem ? { ...editModal?.tempSaveItem } : { ...currentItem };
+ newList[currentIndex] = item;
+ setExperienceList(newList);
+ updateDataList && updateDataList(newList);
+ onToggleEditModal({
+ status: false,
+ });
+ }, [editModal?.tempSaveItem, currentIndex, onToggleEditModal]);
+
+ const onChangeCurrentItem = useCallback(
+ (newItem: AdapterExperienceType) => {
+ onToggleEditModal({
+ tempSaveItem: { ...newItem },
+ });
+ // 这里是为了保证Form表单显示的数据实时性和一致性
+ setCurrentItem(newItem);
+ },
+ [children, onToggleEditModal]
+ );
+ const newForm = useMemo(() => {
+ return React.Children.map(children, (child) => {
+ if (React.isValidElement(child)) {
+ return React.cloneElement(child, {
+ isDisable: !editModal?.status,
+ currentItem,
+ onChangeCurrentItem,
+ });
+ }
+ return child;
+ });
+ }, [children, currentItem, editModal?.status, onChangeCurrentItem]);
+
+ return (
+
+
+
+
+
+ {experienceList.length > 0 && (
+
+
+ )}
+
+ {deleteModal.show && (
+
+ )}
+ {editModal.showByCancel && (
+
{
+ onToggleEditModal({
+ showByCancel: false,
+ });
+ },
+ },
+ submitBtn: {
+ isShow: true,
+ callback: () => {
+ onToggleEditModal({
+ status: false,
+ showByCancel: false,
+ tempSaveItem: {},
+ });
+ editModal?.onAfterFn && editModal?.onAfterFn();
+ setCurrentItem(experienceList[currentIndex]);
+ },
+ },
+ }}
+ />
+ )}
+
+ );
+}
+
+export default WrapperExperience;
diff --git a/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/utils.ts b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/utils.ts
new file mode 100644
index 0000000..9772804
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseForm/WrapperExperience/utils.ts
@@ -0,0 +1,23 @@
+import { AdapterExperienceType } from './adapter';
+
+export function onAddExperience(prevList: AdapterExperienceType[]) {
+ let nextList: AdapterExperienceType[] = prevList ? Array.from(prevList) : [];
+ const newAddItem: AdapterExperienceType = {
+ title: '未命名条目',
+ date: new Date().valueOf(),
+ post: '',
+ content: '',
+ parseContent: [],
+ beginTime: '',
+ endTime: '',
+ supplement: '',
+ };
+ nextList.unshift(newAddItem);
+ return nextList;
+}
+
+export function onDeleteExperience(deleteIndex: number, prevList: AdapterExperienceType[]) {
+ let nextList: AdapterExperienceType[] = prevList ? Array.from(prevList) : [];
+ nextList.splice(deleteIndex, 1);
+ return nextList;
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/index.ts b/app/renderer/container/resume/ResumeContent/UseTemplate/index.ts
new file mode 100644
index 0000000..3e9804d
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/index.ts
@@ -0,0 +1,2 @@
+// 简历模版仓库
+export { default as TemplateOne } from './templateOne';
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/styles/template-one.less b/app/renderer/container/resume/ResumeContent/UseTemplate/styles/template-one.less
new file mode 100644
index 0000000..9e7e59b
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/styles/template-one.less
@@ -0,0 +1,31 @@
+p,
+ul {
+ margin: 0;
+}
+ul {
+ list-style-type: none;
+ margin: 0;
+ display: block;
+ margin-block-start: 0;
+ margin-block-end: 0;
+ margin-inline-start: 0px;
+ margin-inline-end: 0px;
+ padding-inline-start: 0px;
+}
+.container {
+ padding-top: 48px;
+ p.title {
+ text-align: center;
+ font-weight: bold;
+ font-size: 20px;
+ margin: 0;
+ }
+ .content {
+ padding: 0 16px 0 20px;
+ padding-top: 2rem;
+ font-size: 13px;
+ li {
+ padding-bottom: 12px;
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Avatar/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Avatar/index.less
new file mode 100644
index 0000000..efa3a4c
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Avatar/index.less
@@ -0,0 +1,39 @@
+.box {
+ width: 100%;
+ height: 100%;
+ .avatar {
+ position: relative;
+ height: 100%;
+ &:hover {
+ .mask {
+ display: block;
+ }
+ }
+ img {
+ position: absolute;
+ top: 14px;
+ left: 55px;
+ width: 112px;
+ height: 152px;
+ z-index: 1;
+ }
+ .mask {
+ display: none;
+ position: absolute;
+ top: 14px;
+ left: 55px;
+ width: 112px;
+ height: 152px;
+ z-index: 2;
+ background-color: rgba(0, 0, 0, 0.7);
+ :global {
+ .btn-change {
+ color: #ffffff;
+ font-size: 12px;
+ margin-top: 50%;
+ border-color: #ffffff;
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Avatar/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Avatar/index.tsx
new file mode 100644
index 0000000..56f6e70
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Avatar/index.tsx
@@ -0,0 +1,47 @@
+/**
+ * @desc 头像
+ * @author pengdaokuan
+ */
+import React from 'react';
+import './index.less';
+import { useSelector } from 'react-redux';
+import uploadIcon from '@assets/icon/upload.png';
+import MyButton from '@common/components/MyButton';
+import ImageUpload from '@common/components/MyUpload/ImageUpload';
+import useUpdateResumeHook from '@src/container/resume/ResumeContent/useUpdateResumeHook';
+
+function Avatar() {
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+ const updateResumeHook = useUpdateResumeHook();
+
+ const onUpdateUserAvatar = (avatarUrl: string) => {
+ updateResumeHook('base/avatar', avatarUrl);
+ };
+
+ return (
+
+ {!base?.avatar && (
+
{
+ onUpdateUserAvatar(files[0]?.base64URL);
+ }}
+ />
+ )}
+ {base?.avatar && (
+
+

+
+ onUpdateUserAvatar('')}>
+ 更换
+
+
+
+ )}
+
+ );
+}
+
+export default Avatar;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/BaseInfo/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/BaseInfo/index.tsx
new file mode 100644
index 0000000..67d7c41
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/BaseInfo/index.tsx
@@ -0,0 +1,30 @@
+/**
+ * @desc 基本信息
+ * @author pengdaokuan
+ */
+import React from 'react';
+import '../../../styles/template-one.less';
+import { useSelector } from 'react-redux';
+
+function BaseInfo() {
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+
+ return (
+
+
基本信息 Basic
+
+ {base?.school && - 院校:{base?.school}
}
+ {base?.major && - 专业:{base?.major}
}
+ {base?.degree && - 学历:{base?.degree}
}
+ {base?.onSchoolTime && base?.onSchoolTime?.beginTime && base?.onSchoolTime?.endTime && (
+ -
+ 学年:{base?.onSchoolTime?.beginTime} - {base?.onSchoolTime?.endTime}
+
+ )}
+ {base?.hometown && - 籍贯:{base?.hometown}
}
+
+
+ );
+}
+
+export default BaseInfo;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Certificate/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Certificate/index.tsx
new file mode 100644
index 0000000..9b682ff
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Certificate/index.tsx
@@ -0,0 +1,26 @@
+/**
+ * @desc 荣誉奖励
+ * @author pengdaokuan
+ */
+import React from 'react';
+import { useSelector } from 'react-redux';
+import '../../../styles/template-one.less';
+
+function Certificate() {
+ const certificate: string = useSelector((state: any) => state.resumeModel.certificate);
+ const certificateList: string[] = useSelector((state: any) => state.resumeModel.certificateList);
+ return (
+
+
荣誉奖励 Certificate
+
+ {certificate &&
+ certificateList.length > 0 &&
+ certificateList?.map((value: string, index: number) => {
+ return - {value}
;
+ })}
+
+
+ );
+}
+
+export default Certificate;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Contact/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Contact/index.tsx
new file mode 100644
index 0000000..b5c4097
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Contact/index.tsx
@@ -0,0 +1,23 @@
+/**
+ * @desc 联系方式
+ * @author pengdaokuan
+ */
+import React from 'react';
+import '../../../styles/template-one.less';
+import { useSelector } from 'react-redux';
+
+function Contact() {
+ const contact: TSResume.Contact = useSelector((state: any) => state.resumeModel.contact);
+
+ return (
+
+
联系方式 Contact
+
+ {contact?.phone && - 电话:{contact?.phone}
}
+ {contact?.email && - 邮箱:{contact?.email}
}
+
+
+ );
+}
+
+export default Contact;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Job/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Job/index.less
new file mode 100644
index 0000000..602d5a5
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Job/index.less
@@ -0,0 +1,3 @@
+.line {
+ margin: 0 6px;
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Job/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Job/index.tsx
new file mode 100644
index 0000000..c5bb0f7
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Job/index.tsx
@@ -0,0 +1,36 @@
+/**
+ * @desc 求职意向
+ * @author pengdaokuan
+ */
+import React from 'react';
+import '../../../styles/template-one.less';
+import './index.less';
+import { useSelector } from 'react-redux';
+
+function Job() {
+ const work: TSResume.Work = useSelector((state: any) => state.resumeModel.work);
+ const cityList = (work && work?.cityList) || [];
+ return (
+
+
求职意向 Work
+
+ {work?.job && - 职位:{work?.job}
}
+ {work?.city && cityList?.length > 0 && (
+ -
+ 城市:
+ {cityList?.map((city: string, index: number) => {
+ return (
+
+ {city}
+ {cityList.length - 1 !== index && |}
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
+
+export default Job;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Post/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Post/index.less
new file mode 100644
index 0000000..2191a53
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Post/index.less
@@ -0,0 +1,51 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 0px;
+ border-bottom: 1px solid #eee;
+ }
+ .list {
+ padding-top: 12px;
+ padding-bottom: 32px;
+ font-size: 13px;
+ p {
+ margin-bottom: 0px;
+ line-height: 22px;
+ }
+ .flex {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+ flex-wrap: wrap;
+ }
+ .left {
+ width: 25%;
+ color: rgba(0, 0, 0, 0.8);
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ .right {
+ width: 75%;
+ color: rgba(0, 0, 0, 0.8);
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Post/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Post/index.tsx
new file mode 100644
index 0000000..bcf5b7c
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Post/index.tsx
@@ -0,0 +1,38 @@
+/**
+ * @desc 在校经历
+ * @author pengdaokuan
+ */
+import './index.less';
+import React from 'react';
+import { useSelector } from 'react-redux';
+
+function Post() {
+ const schoolExperience: TSResume.SchoolExperience[] = useSelector((state: any) => state.resumeModel.schoolExperience);
+
+ return (
+
+
在校经历 Post
+
+ {!!schoolExperience?.length &&
+ schoolExperience?.map((experience: TSResume.SchoolExperience, index: number) => {
+ return (
+ -
+
+
+ {experience?.beginTime}-{experience?.endTime}
+
+
{experience?.post}
+
+
+
{experience?.department}
+
{experience?.content}
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default Post;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Project/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Project/index.less
new file mode 100644
index 0000000..e52ec91
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Project/index.less
@@ -0,0 +1,65 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 0px;
+ border-bottom: 1px solid #eee;
+ }
+ .list {
+ padding-top: 12px;
+ padding-bottom: 32px;
+ font-size: 13px;
+ .flex {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+ flex-wrap: wrap;
+
+ .left {
+ width: 25%;
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+
+ .right {
+ width: 75%;
+ color: rgba(0, 0, 0, 0.8);
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ .text {
+ width: 100%;
+ ul.item-box {
+ line-height: 20px;
+ li.item-content {
+ list-style: outside;
+ margin-left: 18px;
+ color: #345975;
+ span {
+ color: rgba(0, 0, 0, 0.8);
+ }
+ }
+ }
+ }
+ }
+ p {
+ margin-bottom: 0px;
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Project/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Project/index.tsx
new file mode 100644
index 0000000..a36357b
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Project/index.tsx
@@ -0,0 +1,70 @@
+/**
+ * @desc 项目经历
+ * @author pengdaokuan
+ * @createTime 2021-03-22
+ * @lastModify 2021-03-22
+ */
+import React from 'react';
+import { useSelector } from 'react-redux';
+import './index.less';
+
+function Project() {
+ const projectExperience: TSResume.ProjectExperience[] = useSelector(
+ (state: any) => state.resumeModel.projectExperience
+ );
+
+ return (
+
+
项目经历 Project
+
+ {!!projectExperience?.length &&
+ projectExperience?.map((experience: TSResume.ProjectExperience, index: number) => {
+ return (
+ -
+
+ {(experience?.beginTime || experience?.endTime) && (
+
+ {experience?.beginTime && !experience?.endTime && {experience?.beginTime}}
+ {!experience?.beginTime && experience?.endTime && {experience?.endTime}}
+ {experience?.beginTime && experience?.endTime && (
+
+ {experience?.endTime} - {experience?.endTime}
+
+ )}
+
+ )}
+
+
+
+ {experience?.projectName && !experience?.post && {experience?.projectName}}
+ {!experience?.projectName && experience?.post && {experience?.post}}
+ {experience?.projectName && experience?.post && (
+
+ {experience?.post} - {experience?.post}
+
+ )}
+
+
+
+
+ {experience?.content &&
+ experience?.parseContent &&
+ experience?.parseContent.length > 0 &&
+ experience?.parseContent?.map((content: string, idx: number) => {
+ return (
+ -
+ {content}
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default Project;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Skill/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Skill/index.less
new file mode 100644
index 0000000..afc7e4e
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Skill/index.less
@@ -0,0 +1,23 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ border-bottom: 1px solid #eee;
+ margin-bottom: 0px;
+ }
+ .skill {
+ padding: 12px 0 32px;
+ .item {
+ list-style: outside;
+ margin-left: 24px;
+ line-height: 24px;
+ font-size: 13px;
+ color: #345975;
+ font-weight: bold;
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Skill/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Skill/index.tsx
new file mode 100644
index 0000000..7dcc5e2
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Skill/index.tsx
@@ -0,0 +1,31 @@
+/**
+ * @desc 技能
+ * @author pengdaokuan
+ */
+import React from 'react';
+import { useSelector } from 'react-redux';
+import './index.less';
+
+function Skill() {
+ const skill: string = useSelector((state: any) => state.resumeModel.skill);
+ const skillList: string[] = useSelector((state: any) => state.resumeModel.skillList);
+
+ return (
+
+
技能证书 Skill
+
+ {skill &&
+ skillList?.length > 0 &&
+ skillList?.map((skill: string, index: number) => {
+ return (
+ -
+ {skill}
+
+ );
+ })}
+
+
+ );
+}
+
+export default Skill;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Synopsis/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Synopsis/index.less
new file mode 100644
index 0000000..8350d0b
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Synopsis/index.less
@@ -0,0 +1,21 @@
+.content {
+ padding-top: 20px;
+ p.name {
+ margin-bottom: 8px;
+ color: #01426f;
+ font-size: 28px;
+ font-weight: bold;
+ }
+ p.job {
+ margin-bottom: 8px;
+ color: #01426f;
+ font-size: 18px;
+ font-weight: bold;
+ }
+ p.summary {
+ margin-top: 1rem;
+ color: rgba(0, 0, 0, 0.8);
+ line-height: 22px;
+ font-size: 13px;
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Synopsis/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Synopsis/index.tsx
new file mode 100644
index 0000000..d81ac04
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Synopsis/index.tsx
@@ -0,0 +1,24 @@
+/**
+ * @desc 简单介绍
+ * @author pengdaokuan
+ */
+import React from 'react';
+import { useSelector } from 'react-redux';
+import './index.less';
+
+function Synopsis() {
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+ const work: TSResume.Work = useSelector((state: any) => state.resumeModel.work);
+ const evaluation: string = useSelector((state: any) => state.resumeModel.evaluation);
+ const evaluationList: string[] = useSelector((state: any) => state.resumeModel.evaluationList);
+
+ return (
+
+ {base?.username &&
{base?.username}
}
+ {work?.job &&
{work?.job}
}
+ {evaluation &&
{evaluationList?.join(',')}
}
+
+ );
+}
+
+export default Synopsis;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Work/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Work/index.less
new file mode 100644
index 0000000..da9dca7
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Work/index.less
@@ -0,0 +1,47 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 0px;
+ border-bottom: 1px solid #eee;
+ }
+ .list {
+ padding-top: 12px;
+ padding-bottom: 32px;
+ font-size: 13px;
+ p {
+ margin-bottom: 0px;
+ line-height: 20px;
+ }
+ .flex {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+ flex-wrap: wrap;
+ .left {
+ width: 25%;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ .right {
+ width: 75%;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Work/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Work/index.tsx
new file mode 100644
index 0000000..84a971c
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/components/Work/index.tsx
@@ -0,0 +1,38 @@
+/**
+ * @desc 工作经历
+ * @author pengdaokuan
+ */
+import './index.less';
+import React from 'react';
+import { useSelector } from 'react-redux';
+
+function Work() {
+ const workExperience: TSResume.WorkExperience[] = useSelector((state: any) => state.resumeModel.workExperience);
+
+ return (
+
+
在校经历 Post
+
+ {!!workExperience?.length &&
+ workExperience?.map((experience: TSResume.WorkExperience, index: number) => {
+ return (
+ -
+
+
+ {experience?.beginTime}-{experience?.endTime}
+
+
{experience?.post}
+
+
+
{experience?.department}
+
{experience?.content}
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default Work;
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/index.less b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/index.less
new file mode 100644
index 0000000..9c31506
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/index.less
@@ -0,0 +1,44 @@
+.flex {
+ display: flex;
+ flex-direction: row;
+}
+.a4-box {
+ width: 820px;
+ height: 1160px;
+ background-color: #fff;
+
+ .container {
+ width: 100%;
+ height: 100%;
+ background-color: #ffffff;
+ .left {
+ width: 27%;
+ .avatar {
+ width: 100%;
+ height: 180px;
+ text-align: center;
+ background-color: #eee;
+ }
+ .fillColor {
+ width: 100%;
+ height: 32px;
+ background-color: rgb(250, 225, 4);
+ }
+ .baseData {
+ width: 100%;
+ height: calc(100% - 180px - 32px);
+ color: #fff;
+ background-color: #01426f;
+ }
+ }
+ /* 内容区域 */
+ .center {
+ width: 72%;
+ padding: 0 20px;
+ box-sizing: border-box;
+ .listData {
+ margin-top: 32px;
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/index.tsx b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/index.tsx
new file mode 100644
index 0000000..df46aa4
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/UseTemplate/templateOne/index.tsx
@@ -0,0 +1,55 @@
+/**
+ * @desc 模板1
+ * @author pengdaokuan
+ */
+import React from 'react';
+import './index.less';
+import Avatar from './components/Avatar';
+import BaseInfo from './components/BaseInfo';
+import Contact from './components/Contact';
+import Job from './components/Job';
+import Certificate from './components/Certificate';
+import Synopsis from './components/Synopsis';
+import Skill from './components/Skill';
+import Post from './components/Post';
+import Project from './components/Project';
+import Work from './components/Work';
+import { useSelector } from 'react-redux';
+import { RESUME_TOOLBAR_MAPS } from '@common/constants/resume';
+
+function TemplateOne() {
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+ const resumeToolbarKeys: string[] = useSelector((state: any) => state.templateModel.resumeToolbarKeys);
+ // 必须带有id,以方便导出时获取DOM元素内容
+ return (
+
+
+ {/* 左侧 */}
+
+
+
+
+
+ {resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.contact) && }
+ {resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.workPrefer) && }
+ {resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.certificate) && }
+
+
+ {/* 内容 */}
+
+ {(resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.evaluation) || base?.username) &&
}
+
+ {resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.skill) &&
}
+ {resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.schoolExperience) &&
}
+ {resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.projectExperience) &&
}
+ {resumeToolbarKeys.includes(RESUME_TOOLBAR_MAPS.workExperience) &&
}
+
+
+
+
+ );
+}
+
+export default TemplateOne;
diff --git a/app/renderer/container/resume/ResumeContent/index.tsx b/app/renderer/container/resume/ResumeContent/index.tsx
new file mode 100644
index 0000000..130f063
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/index.tsx
@@ -0,0 +1,63 @@
+import React, { useEffect, useState } from 'react';
+import * as UseTemplateList from './UseTemplate';
+import MyScrollBox from '@common/components/MyScrollBox';
+import Messager, { MESSAGE_EVENT_NAME_MAPS } from '@common/messager';
+import { RESUME_TOOLBAR_MAPS } from '@common/constants/resume';
+import CertificateForm from './UseForm/Certificate';
+import ContactForm from './UseForm/Contact';
+import EducationForm from './UseForm/Education';
+import EvaluationForm from './UseForm/Evaluation';
+import PersonalForm from './UseForm/Personal';
+import SkillForm from './UseForm/Skill';
+import WorkForm from './UseForm/Work';
+import ProjectExperience from './UseForm/ProjectExperience';
+import SchoolExperience from './UseForm/SchoolExperience';
+import WorkExperience from './UseForm/WorkExperience';
+
+function ResumeContent() {
+ const HEADER_ACTION_HEIGHT = 92;
+ const height = document.body.clientHeight;
+ const [formName, setFormName] = useState('');
+ const [showFormModal, setShowFormModal] = useState(false);
+
+ useEffect(() => {
+ document.addEventListener(MESSAGE_EVENT_NAME_MAPS.OPEN_FORM_MODAL, onReceive);
+ return () => {
+ document.removeEventListener(MESSAGE_EVENT_NAME_MAPS.OPEN_FORM_MODAL, onReceive);
+ };
+ }, []);
+ /**
+ * @description 接收订阅事件的传参
+ */
+ const onReceive = (e: any) => {
+ Messager.receive(e, (data: any) => {
+ setShowFormModal(true);
+ setFormName(data?.form_name);
+ });
+ };
+
+ const onClose = () => {
+ setShowFormModal(false);
+ setFormName('');
+ };
+ return (
+
+
+ {showFormModal && (
+ <>
+ {formName === RESUME_TOOLBAR_MAPS.certificate && }
+ {formName === RESUME_TOOLBAR_MAPS.contact && }
+ {formName === RESUME_TOOLBAR_MAPS.education && }
+ {formName === RESUME_TOOLBAR_MAPS.evaluation && }
+ {formName === RESUME_TOOLBAR_MAPS.personal && }
+ {formName === RESUME_TOOLBAR_MAPS.skill && }
+ {formName === RESUME_TOOLBAR_MAPS.workPrefer && }
+ {formName === RESUME_TOOLBAR_MAPS.projectExperience && }
+ {formName === RESUME_TOOLBAR_MAPS.schoolExperience && }
+ {formName === RESUME_TOOLBAR_MAPS.workExperience && }
+ >
+ )}
+
+ );
+}
+export default ResumeContent;
diff --git a/app/renderer/container/resume/ResumeContent/useUpdateResumeHook.ts b/app/renderer/container/resume/ResumeContent/useUpdateResumeHook.ts
new file mode 100644
index 0000000..a9d084b
--- /dev/null
+++ b/app/renderer/container/resume/ResumeContent/useUpdateResumeHook.ts
@@ -0,0 +1,263 @@
+import { useSelector, useDispatch } from 'react-redux';
+import { AdapterExperienceType } from './UseForm/WrapperExperience/adapter';
+
+/**
+ * @description 更新简历信息
+ * @param {string[]} stateKey 关键key,如路径为 [base/username] 表示修改 base 对象下的 username
+ * @param {string} stateValue
+ */
+function useUpdateResumeHook() {
+ const updatePersonalHook = useUpdatePersonalHook();
+ const updateContactHook = useUpdateContactHook();
+ const updateWorkHook = useUpdateWorkHook();
+ const updateEvaluationHook = useUpdateEvaluationHook();
+ const updateHobbyHook = useUpdateHobbyHook();
+ const updateCertificateHook = useUpdateCertificateHook();
+ const updateSkillHook = useUpdateSkillHook();
+ const updateProjectExperience = useUpdateProjectExperience();
+ const updateSchoolExperience = useUpdateSchoolExperience();
+ const updateWorkExperience = useUpdateWorkExperience();
+
+ return (stateKey: string, stateValue: T) => {
+ const keys = stateKey.split('/') || [];
+ if (keys[0]) {
+ if (keys[0] === 'base') updatePersonalHook(keys[1], stateValue);
+ if (keys[0] === 'contact') updateContactHook(keys[1], stateValue);
+ if (keys[0] === 'work') updateWorkHook(keys[1], stateValue);
+ if (keys[0] === 'evaluation') updateEvaluationHook(keys[0], stateValue);
+ if (keys[0] === 'hobby') updateHobbyHook(keys[0], stateValue);
+ if (keys[0] === 'certificate') updateCertificateHook(keys[0], stateValue);
+ if (keys[0] === 'skill') updateSkillHook(keys[0], stateValue);
+ if (keys[0] === 'projectExperience') updateProjectExperience(keys[0], stateValue);
+ if (keys[0] === 'schoolExperience') updateSchoolExperience(keys[0], stateValue);
+ if (keys[0] === 'workExperience') updateWorkExperience(keys[0], stateValue);
+ }
+ };
+}
+
+/**
+ * @description 修改个人信息(base)
+ */
+function useUpdatePersonalHook() {
+ const dispatch = useDispatch();
+ const base: TSResume.Base = useSelector((state: any) => state.resumeModel.base);
+
+ return (stateKey: string, stateValue: T) => {
+ dispatch({
+ type: 'resumeModel/setStore',
+ payload: {
+ key: 'base',
+ values: {
+ ...base,
+ [stateKey]: stateValue,
+ },
+ },
+ });
+ };
+}
+
+/**
+ * @description 修改联系方式(contact)
+ */
+function useUpdateContactHook() {
+ const dispatch = useDispatch();
+ const contact: TSResume.Contact = useSelector((state: any) => state.resumeModel.contact);
+
+ return (stateKey: string, stateValue: T) => {
+ dispatch({
+ type: 'resumeModel/setStore',
+ payload: {
+ key: 'contact',
+ values: {
+ ...contact,
+ [stateKey]: stateValue,
+ },
+ },
+ });
+ };
+}
+
+/**
+ * @description 修改工作期望(work)
+ */
+function useUpdateWorkHook() {
+ const dispatch = useDispatch();
+ const work: TSResume.Work = useSelector((state: any) => state.resumeModel.work);
+
+ return (stateKey: string, stateValue: T) => {
+ let cityList = work?.cityList ? [...work.cityList] : [];
+ if (stateKey === 'city') {
+ cityList = (stateValue as any).split('|');
+ }
+ dispatch({
+ type: 'resumeModel/setStore',
+ payload: {
+ key: 'work',
+ values: {
+ ...work,
+ cityList,
+ [stateKey]: stateValue,
+ },
+ },
+ });
+ };
+}
+
+/**
+ * @description 修改个人评价(evaluation)
+ */
+function useUpdateEvaluationHook() {
+ const dispatch = useDispatch();
+ return (stateKey: string, stateValue: T) => {
+ let evaluationList = stateValue ? (stateValue as any).split('|') : [];
+ dispatch({
+ type: 'resumeModel/setStoreList',
+ payload: [
+ {
+ key: stateKey,
+ values: stateValue,
+ },
+ {
+ key: 'evaluationList',
+ values: evaluationList,
+ },
+ ],
+ });
+ };
+}
+
+/**
+ * @description 修改个人特长爱好(hobby)
+ */
+function useUpdateHobbyHook() {
+ const dispatch = useDispatch();
+ return (stateKey: string, stateValue: T) => {
+ dispatch({
+ type: 'resumeModel/setStore',
+ payload: {
+ key: stateKey,
+ values: stateValue,
+ },
+ });
+ };
+}
+
+/**
+ * @description 修改荣誉证书(certificate)
+ */
+function useUpdateCertificateHook() {
+ const dispatch = useDispatch();
+ return (stateKey: string, stateValue: T) => {
+ let certificateList = stateValue ? (stateValue as any).split('|') : [];
+ dispatch({
+ type: 'resumeModel/setStoreList',
+ payload: [
+ {
+ key: stateKey,
+ values: stateValue,
+ },
+ {
+ key: 'certificateList',
+ values: certificateList,
+ },
+ ],
+ });
+ };
+}
+
+/**
+ * @description 修改技能清单(skill)
+ */
+function useUpdateSkillHook() {
+ const dispatch = useDispatch();
+ return (stateKey: string, stateValue: T) => {
+ let skillList = stateValue ? (stateValue as any).split('|') : [];
+ dispatch({
+ type: 'resumeModel/setStoreList',
+ payload: [
+ {
+ key: stateKey,
+ values: stateValue,
+ },
+ {
+ key: 'skillList',
+ values: skillList,
+ },
+ ],
+ });
+ };
+}
+
+/**
+ * @description 修改项目经验(Project)
+ */
+function useUpdateProjectExperience() {
+ const dispatch = useDispatch();
+ return (stateKey: string, stateValue: T) => {
+ let newList = (stateValue as any)?.map((s: AdapterExperienceType) => {
+ let parseContent = s.content ? s.content.split('|') : [];
+ return {
+ ...s,
+ projectName: s?.title,
+ parseContent,
+ };
+ });
+ dispatch({
+ type: 'resumeModel/setStore',
+ payload: {
+ key: stateKey,
+ values: newList,
+ },
+ });
+ };
+}
+
+/**
+ * @description 修改在校经历(School)
+ */
+function useUpdateSchoolExperience() {
+ const dispatch = useDispatch();
+ return (stateKey: string, stateValue: T) => {
+ let newList = (stateValue as any)?.map((s: AdapterExperienceType) => {
+ let parseContent = s.content ? s.content.split('|') : [];
+ return {
+ ...s,
+ department: s?.title,
+ parseContent,
+ };
+ });
+ dispatch({
+ type: 'resumeModel/setStore',
+ payload: {
+ key: stateKey,
+ values: newList,
+ },
+ });
+ };
+}
+
+/**
+ * @description 修改在校经历(School)
+ */
+function useUpdateWorkExperience() {
+ const dispatch = useDispatch();
+ return (stateKey: string, stateValue: T) => {
+ let newList = (stateValue as any)?.map((s: AdapterExperienceType) => {
+ let parseContent = s.content ? s.content.split('|') : [];
+ return {
+ ...s,
+ department: s?.title,
+ parseContent,
+ };
+ });
+ dispatch({
+ type: 'resumeModel/setStore',
+ payload: {
+ key: stateKey,
+ values: newList,
+ },
+ });
+ };
+}
+
+export default useUpdateResumeHook;
diff --git a/app/renderer/container/resume/ResumeToolbar/index.less b/app/renderer/container/resume/ResumeToolbar/index.less
new file mode 100644
index 0000000..3b637f3
--- /dev/null
+++ b/app/renderer/container/resume/ResumeToolbar/index.less
@@ -0,0 +1,123 @@
+.slider {
+ width: 280px;
+ font-size: 14px;
+ box-sizing: border-box;
+ background-color: #fff;
+ border-radius: 6px;
+ padding: 16px;
+ .module {
+ font-size: 14px;
+ .title {
+ font-size: 14px;
+ line-height: 22px;
+ color: #505667;
+ position: relative;
+ padding-left: 12px;
+ margin-bottom: 16px;
+ &.un-first {
+ margin-top: 24px;
+ }
+ .line {
+ position: absolute;
+ top: 4px;
+ left: 0;
+ width: 3px;
+ background-color: rgb(39, 41, 44);
+ height: 14px;
+ }
+ }
+ .content {
+ .box {
+ height: 72px;
+ margin-bottom: 12px;
+ background-color: #fbfbfb;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ padding: 0 22px;
+ border-radius: 8px;
+ cursor: pointer;
+ position: relative;
+ user-select: none;
+ &:hover {
+ .info {
+ .tips {
+ display: block;
+ }
+ .action {
+ display: inline-flex;
+ }
+ }
+ }
+ .info {
+ display: flex;
+ .icon {
+ width: 16px;
+ height: 16px;
+ background: url('../../../../../assets/icon/work.png') no-repeat;
+ background-size: cover;
+ margin-top: 2px;
+ margin-right: 8px;
+ }
+ .text {
+ .name {
+ font-size: 14px;
+ color: #505667;
+ }
+ .summary {
+ font-size: 12px;
+ color: #aeb2bd;
+ margin-top: 4px;
+ word-break: keep-all;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+ .tips {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 60px;
+ height: 20px;
+ line-height: 20px;
+ background-color: #505667;
+ font-size: 12px;
+ color: #ffffff;
+ text-align: center;
+ display: none;
+ border-top-right-radius: 8px;
+ border-bottom-left-radius: 8px;
+ }
+ .action {
+ position: absolute;
+ right: 0;
+ top: 18px;
+ width: 40px;
+ display: none;
+ height: 20px;
+ .edit {
+ width: 16px;
+ height: 16px;
+ background: url('../../../../../assets/icon/edit.png') no-repeat;
+ background-size: cover;
+ margin-right: 8px;
+ }
+ .delete {
+ width: 16px;
+ height: 16px;
+ background: url('../../../../../assets/icon/delete.png') no-repeat;
+ background-size: cover;
+ }
+ .add {
+ width: 16px;
+ height: 16px;
+ background: url('../../../../../assets/icon/add.png') no-repeat;
+ background-size: cover;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/resume/ResumeToolbar/index.tsx b/app/renderer/container/resume/ResumeToolbar/index.tsx
new file mode 100644
index 0000000..f188402
--- /dev/null
+++ b/app/renderer/container/resume/ResumeToolbar/index.tsx
@@ -0,0 +1,137 @@
+/**
+ * @description 编辑简历-工具条模块
+ */
+import React, { useEffect, useState } from 'react';
+import './index.less';
+import MyScrollBox from '@common/components/MyScrollBox';
+import RESUME_TOOLBAR_LIST from '@common/constants/resume';
+import { onAddToolbar, onDeleteToolbar } from './utils';
+import { useDispatch } from 'react-redux';
+import Messager, { MESSAGE_EVENT_NAME_MAPS } from '@common/messager';
+
+function ResumeToolbar() {
+ const dispatch = useDispatch();
+ const height = document.body.clientHeight;
+ const [addToolbarList, setAddToolbarList] = useState([]);
+ const [unAddToolbarList, setUnAddToolbarList] = useState([]);
+
+ useEffect(() => {
+ if (RESUME_TOOLBAR_LIST.length > 0) {
+ let _addToolbarList: TSResume.SliderItem[] = [];
+ let _unAddToolbarList: TSResume.SliderItem[] = [];
+ RESUME_TOOLBAR_LIST.forEach((s: TSResume.SliderItem) => {
+ if (s.require) _addToolbarList.push(s);
+ if (!s.require) _unAddToolbarList.push(s);
+ });
+ setAddToolbarList(_addToolbarList);
+ setUnAddToolbarList(_unAddToolbarList);
+ changeResumeToolbarKeys(_addToolbarList.map((s) => s.key));
+ }
+ }, []);
+
+ const changeResumeToolbarKeys = (moduleKeys: string[]) => {
+ if (moduleKeys.length > 0) {
+ dispatch({
+ type: 'templateModel/setStore',
+ payload: {
+ key: 'resumeToolbarKeys',
+ values: moduleKeys,
+ },
+ });
+ }
+ };
+
+ // 添加模块
+ const onAddSliderAction = (moduleToolbar: TSResume.SliderItem) => {
+ const nextAddSliderList = onAddToolbar(addToolbarList, moduleToolbar);
+ setAddToolbarList(nextAddSliderList);
+ const nextUnAddSliderList = onDeleteToolbar(unAddToolbarList, moduleToolbar);
+ setUnAddToolbarList(nextUnAddSliderList);
+ changeResumeToolbarKeys(nextAddSliderList.map((s: TSResume.SliderItem) => s.key));
+ };
+
+ // 删除模块
+ const onDeleteSliderAction = (moduleSlider: TSResume.SliderItem) => {
+ const nextAddSliderList = onDeleteToolbar(addToolbarList, moduleSlider);
+ setAddToolbarList(nextAddSliderList);
+ const nextUnAddSliderList = onAddToolbar(unAddToolbarList, moduleSlider);
+ setUnAddToolbarList(nextUnAddSliderList);
+ changeResumeToolbarKeys(nextAddSliderList.map((s: TSResume.SliderItem) => s.key));
+ };
+
+ return (
+
+
+ {!!addToolbarList.length && (
+
+
+
+ 已添加模块
+
+
+ {addToolbarList.map((addSlider: TSResume.SliderItem) => {
+ return (
+
{
+ Messager.send(MESSAGE_EVENT_NAME_MAPS.OPEN_FORM_MODAL, {
+ form_name: addSlider.key,
+ });
+ }}
+ >
+
+
+
+
{addSlider.name}
+
{addSlider.summary}
+
+ {addSlider.require &&
必选项
}
+ {!addSlider.require && (
+
+ {}} />
+ {
+ e.stopPropagation && e.stopPropagation();
+ onDeleteSliderAction(addSlider);
+ }}
+ />
+
+ )}
+
+
+ );
+ })}
+
+
+ )}
+ {!!unAddToolbarList.length && (
+
+
+
+ 未添加模块
+
+
+ {unAddToolbarList.map((unAddSlider: TSResume.SliderItem) => {
+ return (
+
onAddSliderAction(unAddSlider)}>
+
+
+
+
{unAddSlider.name}
+
{unAddSlider.summary}
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+}
+
+export default ResumeToolbar;
diff --git a/app/renderer/container/resume/ResumeToolbar/utils.ts b/app/renderer/container/resume/ResumeToolbar/utils.ts
new file mode 100644
index 0000000..569c3e1
--- /dev/null
+++ b/app/renderer/container/resume/ResumeToolbar/utils.ts
@@ -0,0 +1,35 @@
+/**
+ * @description 添加工具条模块
+ * @param {TSResume.SliderItem[]} prevToolbarList 上一轮
+ * @param {TSResume.SliderItem} currentToolbar 当前目标模块
+ * @returns {TSResume.SliderItem[]} nextToolbarList 下一轮
+ */
+export const onAddToolbar = (
+ prevToolbarList: TSResume.SliderItem[],
+ currentToolbar: TSResume.SliderItem
+): TSResume.SliderItem[] => {
+ const addKeys = prevToolbarList.map((s: TSResume.SliderItem) => s.key);
+ let nextToolbarList = [...Array.from(prevToolbarList)];
+ if (!addKeys.includes(currentToolbar.key)) {
+ nextToolbarList.push(currentToolbar);
+ }
+ return nextToolbarList;
+};
+
+/**
+ * @description 删除工具条模块
+ * @param {TSResume.SliderItem[]} prevToolbarList 上一轮
+ * @param {TSResume.SliderItem} currentToolbar 当前目标模块
+ * @returns {TSResume.SliderItem[]} nextToolbarList 下一轮
+ */
+export const onDeleteToolbar = (
+ prevToolbarList: TSResume.SliderItem[],
+ currentToolbar: TSResume.SliderItem
+): TSResume.SliderItem[] => {
+ const nextToolbarList = [...Array.from(prevToolbarList)];
+ const findIndex = nextToolbarList.findIndex((s: TSResume.SliderItem) => s.key === currentToolbar.key);
+ if (findIndex > -1) {
+ nextToolbarList.splice(findIndex, 1);
+ }
+ return nextToolbarList;
+};
diff --git a/app/renderer/container/resume/index.less b/app/renderer/container/resume/index.less
new file mode 100644
index 0000000..40b8110
--- /dev/null
+++ b/app/renderer/container/resume/index.less
@@ -0,0 +1,28 @@
+.container {
+ height: 100vh;
+ width: 100vw;
+ box-sizing: border-box;
+ background-color: #27292c;
+ // color: #fff;
+ .header {
+ height: 60px;
+ width: 820px;
+ padding-top: 16px;
+ margin-left: 32px;
+ }
+ .content {
+ margin-left: 32px;
+ width: 820px;
+ margin-top: 16px;
+ height: calc(100vh - 76px - 16px);
+ }
+ .toolbar {
+ width: 280px;
+ position: absolute;
+ top: 120px;
+ left: 888px;
+ border-radius: 6px;
+ box-sizing: border-box;
+ background-color: #fff;
+ }
+}
diff --git a/app/renderer/container/resume/index.tsx b/app/renderer/container/resume/index.tsx
new file mode 100644
index 0000000..2fe26e1
--- /dev/null
+++ b/app/renderer/container/resume/index.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import './index.less';
+import ResumeAction from './ResumeAction';
+import ResumeContent from './ResumeContent';
+import ResumeToolbar from './ResumeToolbar';
+
+function Resume() {
+ return (
+
+ );
+}
+export default Resume;
diff --git a/app/renderer/container/root/index.less b/app/renderer/container/root/index.less
new file mode 100644
index 0000000..f321539
--- /dev/null
+++ b/app/renderer/container/root/index.less
@@ -0,0 +1,54 @@
+.root {
+ height: 100vh;
+ width: 100vw;
+ text-align: center;
+ background-color: #27292c;
+ .container {
+ width: 100%;
+ color: #ffffff;
+ padding-top: calc(8vh + 60px);
+ img {
+ width: 112px;
+ height: 112px;
+ }
+ .title {
+ font-size: 24px;
+ line-height: 36px;
+ }
+ .tips {
+ font-size: 16px;
+ line-height: 24px;
+ margin-top: 24px;
+ }
+ .theme {
+ margin: 24px 0;
+ height: 30px;
+ }
+ .action {
+ width: 300px;
+ margin: 0 auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 24px;
+ .item {
+ width: 25%;
+ cursor: pointer;
+ }
+ }
+ .copyright {
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ .footer {
+ height: 56px;
+ line-height: 56px;
+ .copyright {
+ opacity: 0.6;
+ font-size: 14px;
+ text-align: center;
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/root/index.tsx b/app/renderer/container/root/index.tsx
new file mode 100644
index 0000000..82bfbb9
--- /dev/null
+++ b/app/renderer/container/root/index.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import './index.less';
+import { shell } from 'electron';
+import { useHistory } from 'react-router';
+import Logo from '@assets/logo.png';
+import { ROUTER_ENTRY, ROUTER_KEY } from '@common/constants/router';
+import { isHttpOrHttpsUrl } from '@common/utils/router';
+
+function Root() {
+ const history = useHistory();
+
+ const onRouterToLink = (router: TSRouter.Item) => {
+ if (isHttpOrHttpsUrl(router.url)) {
+ shell.openExternal(router.url);
+ } else {
+ if (router.key !== ROUTER_KEY.resume) {
+ history.push(router.url);
+ } else {
+ history.push(router.url);
+ }
+ }
+ };
+ return (
+
+
+

+
VisResumeMook
+
一个模板简历制作平台, 让你的简历更加出众 ~
+
+ {ROUTER_ENTRY.map((router: TSRouter.Item) => {
+ return (
+
{
+ onRouterToLink(router);
+ }}
+ >
+ {router.text}
+
+ );
+ })}
+
+
+
+
+ Copyright © 2018-{new Date().getFullYear()} All Rights Reserved. Copyright By pengdaokuan
+
+
+
+
+
+ );
+}
+export default Root;
diff --git a/app/renderer/container/templates/index.ts b/app/renderer/container/templates/index.ts
new file mode 100644
index 0000000..3e9804d
--- /dev/null
+++ b/app/renderer/container/templates/index.ts
@@ -0,0 +1,2 @@
+// 简历模版仓库
+export { default as TemplateOne } from './templateOne';
diff --git a/app/renderer/container/templates/styles/template-one.less b/app/renderer/container/templates/styles/template-one.less
new file mode 100644
index 0000000..9e7e59b
--- /dev/null
+++ b/app/renderer/container/templates/styles/template-one.less
@@ -0,0 +1,31 @@
+p,
+ul {
+ margin: 0;
+}
+ul {
+ list-style-type: none;
+ margin: 0;
+ display: block;
+ margin-block-start: 0;
+ margin-block-end: 0;
+ margin-inline-start: 0px;
+ margin-inline-end: 0px;
+ padding-inline-start: 0px;
+}
+.container {
+ padding-top: 48px;
+ p.title {
+ text-align: center;
+ font-weight: bold;
+ font-size: 20px;
+ margin: 0;
+ }
+ .content {
+ padding: 0 16px 0 20px;
+ padding-top: 2rem;
+ font-size: 13px;
+ li {
+ padding-bottom: 12px;
+ }
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/components/Avatar/index.less b/app/renderer/container/templates/templateOne/components/Avatar/index.less
new file mode 100644
index 0000000..efa3a4c
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Avatar/index.less
@@ -0,0 +1,39 @@
+.box {
+ width: 100%;
+ height: 100%;
+ .avatar {
+ position: relative;
+ height: 100%;
+ &:hover {
+ .mask {
+ display: block;
+ }
+ }
+ img {
+ position: absolute;
+ top: 14px;
+ left: 55px;
+ width: 112px;
+ height: 152px;
+ z-index: 1;
+ }
+ .mask {
+ display: none;
+ position: absolute;
+ top: 14px;
+ left: 55px;
+ width: 112px;
+ height: 152px;
+ z-index: 2;
+ background-color: rgba(0, 0, 0, 0.7);
+ :global {
+ .btn-change {
+ color: #ffffff;
+ font-size: 12px;
+ margin-top: 50%;
+ border-color: #ffffff;
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/components/Avatar/index.tsx b/app/renderer/container/templates/templateOne/components/Avatar/index.tsx
new file mode 100644
index 0000000..80cfab3
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Avatar/index.tsx
@@ -0,0 +1,19 @@
+/**
+ * @desc 头像
+ * @author pengdaokuan
+ */
+import React from 'react';
+import './index.less';
+import AvatarImage from '@assets/avatar.jpg';
+
+function Avatar() {
+ return (
+
+
+

+
+
+ );
+}
+
+export default Avatar;
diff --git a/app/renderer/container/templates/templateOne/components/BaseInfo/index.tsx b/app/renderer/container/templates/templateOne/components/BaseInfo/index.tsx
new file mode 100644
index 0000000..986a419
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/BaseInfo/index.tsx
@@ -0,0 +1,23 @@
+/**
+ * @desc 基本信息
+ * @author pengdaokuan
+ */
+import React from 'react';
+import '../../../styles/template-one.less';
+
+function BaseInfo() {
+ return (
+
+
基本信息 Basic
+
+ - 院校:湖南瞎说大学
+ - 专业:软件工程
+ - 学历:本科
+ - 学年:2015.09 - 2019.06
+ - 籍贯:海南·海口
+
+
+ );
+}
+
+export default BaseInfo;
diff --git a/app/renderer/container/templates/templateOne/components/Certificate/index.tsx b/app/renderer/container/templates/templateOne/components/Certificate/index.tsx
new file mode 100644
index 0000000..f0a3d2f
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Certificate/index.tsx
@@ -0,0 +1,22 @@
+/**
+ * @desc 荣誉奖励
+ * @author pengdaokuan
+ */
+import React from 'react';
+import '../../../styles/template-one.less';
+
+function Certificate() {
+ return (
+
+
荣誉奖励 Certificate
+
+ - 全国英语四级证书
+ - 全国计算机二级证书
+ - 湖南瞎说大学自封骰王
+ - 广州第一届酒王大赛参与奖
+
+
+ );
+}
+
+export default Certificate;
diff --git a/app/renderer/container/templates/templateOne/components/Contact/index.tsx b/app/renderer/container/templates/templateOne/components/Contact/index.tsx
new file mode 100644
index 0000000..8656057
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Contact/index.tsx
@@ -0,0 +1,20 @@
+/**
+ * @desc 联系方式
+ * @author pengdaokuan
+ */
+import React from 'react';
+import '../../../styles/template-one.less';
+
+function Contact() {
+ return (
+
+
联系方式 Contact
+
+ - 电话:176****2612
+ - 邮箱:1063137960@qq.com
+
+
+ );
+}
+
+export default Contact;
diff --git a/app/renderer/container/templates/templateOne/components/Job/index.less b/app/renderer/container/templates/templateOne/components/Job/index.less
new file mode 100644
index 0000000..602d5a5
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Job/index.less
@@ -0,0 +1,3 @@
+.line {
+ margin: 0 6px;
+}
diff --git a/app/renderer/container/templates/templateOne/components/Job/index.tsx b/app/renderer/container/templates/templateOne/components/Job/index.tsx
new file mode 100644
index 0000000..677a046
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Job/index.tsx
@@ -0,0 +1,21 @@
+/**
+ * @desc 求职意向
+ * @author pengdaokuan
+ */
+import React from 'react';
+import '../../../styles/template-one.less';
+import './index.less';
+
+function Job() {
+ return (
+
+
求职意向 Work
+
+ - 职位:前端工程师
+ - 城市:广州|成都|海口
+
+
+ );
+}
+
+export default Job;
diff --git a/app/renderer/container/templates/templateOne/components/Post/index.less b/app/renderer/container/templates/templateOne/components/Post/index.less
new file mode 100644
index 0000000..2191a53
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Post/index.less
@@ -0,0 +1,51 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 0px;
+ border-bottom: 1px solid #eee;
+ }
+ .list {
+ padding-top: 12px;
+ padding-bottom: 32px;
+ font-size: 13px;
+ p {
+ margin-bottom: 0px;
+ line-height: 22px;
+ }
+ .flex {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+ flex-wrap: wrap;
+ }
+ .left {
+ width: 25%;
+ color: rgba(0, 0, 0, 0.8);
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ .right {
+ width: 75%;
+ color: rgba(0, 0, 0, 0.8);
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/components/Post/index.tsx b/app/renderer/container/templates/templateOne/components/Post/index.tsx
new file mode 100644
index 0000000..5dfc666
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Post/index.tsx
@@ -0,0 +1,42 @@
+/**
+ * @desc 在校经历
+ * @author pengdaokuan
+ */
+import './index.less';
+import React from 'react';
+
+function Post() {
+ return (
+
+
在校经历 Post
+
+ -
+
+
2016.09-2017.09
+
文艺部会长
+
+
+
校团委学生会
+
+ 计划、组织、协调各年级学生组织的文艺和文化娱乐活动,承办好学生会部的学生文艺晚会。有效地与社团部开展合作项目
+
+
+
+ -
+
+
2017.09-2019.04
+
前端工程师
+
+
+
湖南瞎说大学网络中心
+
+ 担任TickNet工作室前端工程师,与湖南瞎说大学网络中心合作,围绕微信企业号开发或主导多个应用,任职期间基于微信企业号开发校内闲余市场,采用Vue.js主导开发,并与湖南xxx科技有限公司合作,主导开发该公司官网及后台管理
+
+
+
+
+
+ );
+}
+
+export default Post;
diff --git a/app/renderer/container/templates/templateOne/components/Project/index.less b/app/renderer/container/templates/templateOne/components/Project/index.less
new file mode 100644
index 0000000..e52ec91
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Project/index.less
@@ -0,0 +1,65 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 0px;
+ border-bottom: 1px solid #eee;
+ }
+ .list {
+ padding-top: 12px;
+ padding-bottom: 32px;
+ font-size: 13px;
+ .flex {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+ flex-wrap: wrap;
+
+ .left {
+ width: 25%;
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+
+ .right {
+ width: 75%;
+ color: rgba(0, 0, 0, 0.8);
+ font-size: 13px;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ .text {
+ width: 100%;
+ ul.item-box {
+ line-height: 20px;
+ li.item-content {
+ list-style: outside;
+ margin-left: 18px;
+ color: #345975;
+ span {
+ color: rgba(0, 0, 0, 0.8);
+ }
+ }
+ }
+ }
+ }
+ p {
+ margin-bottom: 0px;
+ }
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/components/Project/index.tsx b/app/renderer/container/templates/templateOne/components/Project/index.tsx
new file mode 100644
index 0000000..6086395
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Project/index.tsx
@@ -0,0 +1,45 @@
+/**
+ * @desc 项目经历
+ * @author pengdaokuan
+ * @createTime 2021-03-22
+ * @lastModify 2021-03-22
+ */
+import React from 'react';
+import './index.less';
+
+function Project() {
+ return (
+
+
项目经历 Project
+
+ -
+
+
+ 2021.03 - 2021.05
+
+
+
+
+ visResumeMook 可视化简历平台 -前端工程师
+
+
+
+
+ -
+ Electron + React Hooks 打造简历平台,只需输入一次信息,套用多份模版
+
+ -
+ 通过 jsonfile 方式实现主题换肤,支持导出 PDF 简历文档
+
+ -
+ 通过可视化拖拽形式,自定义组件模版
+
+
+
+
+
+
+ );
+}
+
+export default Project;
diff --git a/app/renderer/container/templates/templateOne/components/Skill/index.less b/app/renderer/container/templates/templateOne/components/Skill/index.less
new file mode 100644
index 0000000..afc7e4e
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Skill/index.less
@@ -0,0 +1,23 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ border-bottom: 1px solid #eee;
+ margin-bottom: 0px;
+ }
+ .skill {
+ padding: 12px 0 32px;
+ .item {
+ list-style: outside;
+ margin-left: 24px;
+ line-height: 24px;
+ font-size: 13px;
+ color: #345975;
+ font-weight: bold;
+ }
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/components/Skill/index.tsx b/app/renderer/container/templates/templateOne/components/Skill/index.tsx
new file mode 100644
index 0000000..9b9288b
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Skill/index.tsx
@@ -0,0 +1,27 @@
+/**
+ * @desc 技能
+ * @author pengdaokuan
+ */
+import React from 'react';
+import './index.less';
+
+function Skill() {
+ return (
+
+
技能证书 Skill
+
+ - 熟悉 Vue.js,了解数据双向绑定原理、阅读过 NextTick 源码
+ - 熟悉 React,了解并使用 Hooks 特性,阅读过 redux 源码,开发 rc-redux-model 中间件
+ - 阅读过 Antd 部分优秀组件源码,并参考借鉴,开发组内 UI 组件库
+ - 了解 Vscode,开发组内项目辅助工具 vscode-beehive-extension 插件
+ - 了解 Webpack 编译原理,了解 babel 转码原理,编写过 babel 插件
+ - 了解 Electron,了解 Node.js 以及 Git 团队协作开发工具
+ - 解设计模式,对于特定场景,能采用合适的设计模式进行解决
+ - 了解 MYSQL,了解数据库优化常用方法
+ - 了解基于微信公众号应用开发,采用 Taro 开发微信小程序,具备良好的网络基础知识
+
+
+ );
+}
+
+export default Skill;
diff --git a/app/renderer/container/templates/templateOne/components/Synopsis/index.less b/app/renderer/container/templates/templateOne/components/Synopsis/index.less
new file mode 100644
index 0000000..8350d0b
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Synopsis/index.less
@@ -0,0 +1,21 @@
+.content {
+ padding-top: 20px;
+ p.name {
+ margin-bottom: 8px;
+ color: #01426f;
+ font-size: 28px;
+ font-weight: bold;
+ }
+ p.job {
+ margin-bottom: 8px;
+ color: #01426f;
+ font-size: 18px;
+ font-weight: bold;
+ }
+ p.summary {
+ margin-top: 1rem;
+ color: rgba(0, 0, 0, 0.8);
+ line-height: 22px;
+ font-size: 13px;
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/components/Synopsis/index.tsx b/app/renderer/container/templates/templateOne/components/Synopsis/index.tsx
new file mode 100644
index 0000000..bc63192
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Synopsis/index.tsx
@@ -0,0 +1,25 @@
+/**
+ * @desc 简单介绍
+ * @author pengdaokuan
+ */
+import React from 'react';
+import './index.less';
+
+function Synopsis() {
+ return (
+
+
彭道宽
+
前端工程师
+
+ {[
+ '投身开源,rc-redux-model 库作者,SugarTurboS Club 开源组织负责人',
+ '掘金 lv3 博主,掘金文章 10w+ 阅读量,github blog 300+ star',
+ '具备良好语言表达能力和沟通能力,能快速融入团队,适应新环境。',
+ '具有代码洁癖,前后端分离,自我学习能力强,对新技术具有钻研精神',
+ ].join(',')}
+
+
+ );
+}
+
+export default Synopsis;
diff --git a/app/renderer/container/templates/templateOne/components/Work/index.less b/app/renderer/container/templates/templateOne/components/Work/index.less
new file mode 100644
index 0000000..da9dca7
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Work/index.less
@@ -0,0 +1,47 @@
+.content {
+ width: 100%;
+ position: relative;
+ .label {
+ padding-bottom: 8px;
+ color: #01426f;
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 0px;
+ border-bottom: 1px solid #eee;
+ }
+ .list {
+ padding-top: 12px;
+ padding-bottom: 32px;
+ font-size: 13px;
+ p {
+ margin-bottom: 0px;
+ line-height: 20px;
+ }
+ .flex {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+ flex-wrap: wrap;
+ .left {
+ width: 25%;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ .right {
+ width: 75%;
+ p:first-child {
+ margin-bottom: 4px;
+ height: 18px;
+ line-height: 18px;
+ color: #01426f;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/components/Work/index.tsx b/app/renderer/container/templates/templateOne/components/Work/index.tsx
new file mode 100644
index 0000000..6452507
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/components/Work/index.tsx
@@ -0,0 +1,28 @@
+/**
+ * @desc 工作经历
+ * @author pengdaokuan
+ */
+import './index.less';
+import React from 'react';
+
+function Work() {
+ return (
+
+
工作经历 Post
+
+ -
+
+
+
CVTE
+
就职于CVTE,部门人送广州彭于晏,其他的没啥介绍了
+
+
+
+
+ );
+}
+
+export default Work;
diff --git a/app/renderer/container/templates/templateOne/index.less b/app/renderer/container/templates/templateOne/index.less
new file mode 100644
index 0000000..9c31506
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/index.less
@@ -0,0 +1,44 @@
+.flex {
+ display: flex;
+ flex-direction: row;
+}
+.a4-box {
+ width: 820px;
+ height: 1160px;
+ background-color: #fff;
+
+ .container {
+ width: 100%;
+ height: 100%;
+ background-color: #ffffff;
+ .left {
+ width: 27%;
+ .avatar {
+ width: 100%;
+ height: 180px;
+ text-align: center;
+ background-color: #eee;
+ }
+ .fillColor {
+ width: 100%;
+ height: 32px;
+ background-color: rgb(250, 225, 4);
+ }
+ .baseData {
+ width: 100%;
+ height: calc(100% - 180px - 32px);
+ color: #fff;
+ background-color: #01426f;
+ }
+ }
+ /* 内容区域 */
+ .center {
+ width: 72%;
+ padding: 0 20px;
+ box-sizing: border-box;
+ .listData {
+ margin-top: 32px;
+ }
+ }
+ }
+}
diff --git a/app/renderer/container/templates/templateOne/index.tsx b/app/renderer/container/templates/templateOne/index.tsx
new file mode 100644
index 0000000..00abd86
--- /dev/null
+++ b/app/renderer/container/templates/templateOne/index.tsx
@@ -0,0 +1,51 @@
+/**
+ * @desc 模板1
+ * @author pengdaokuan
+ */
+import React from 'react';
+import './index.less';
+import Avatar from './components/Avatar';
+import BaseInfo from './components/BaseInfo';
+import Contact from './components/Contact';
+import Job from './components/Job';
+import Certificate from './components/Certificate';
+import Synopsis from './components/Synopsis';
+import Skill from './components/Skill';
+import Post from './components/Post';
+import Project from './components/Project';
+import Work from './components/Work';
+
+function TemplateOne() {
+ // 必须带有id,以方便导出时获取DOM元素内容
+ return (
+
+
+ {/* 左侧 */}
+
+ {/* 内容 */}
+
+
+
+ );
+}
+
+export default TemplateOne;
diff --git a/app/renderer/global.d.ts b/app/renderer/global.d.ts
new file mode 100644
index 0000000..f802d19
--- /dev/null
+++ b/app/renderer/global.d.ts
@@ -0,0 +1,10 @@
+// global.d.ts
+declare module '*.jpg' {
+ const jpg: string;
+ export default jpg;
+}
+
+declare module '*.png' {
+ const png: string;
+ export default png;
+}
diff --git a/src/index.html b/app/renderer/index.html
similarity index 66%
rename from src/index.html
rename to app/renderer/index.html
index 46d2042..ed50e9e 100644
--- a/src/index.html
+++ b/app/renderer/index.html
@@ -3,9 +3,14 @@
- Document
+ VisResumeMook
+
-