基于NoCode构建简历编辑器

基于NoCode构建简历编辑器,要参加秋招了,因为各种模版用起来细节上并不是很满意,所以尝试做个简单的拖拽简历编辑器。

描述

GithubResume DEMO

对于无代码NoCode和低代码LowCode还是比较容易混淆的,在我的理解上,NoCode强调自己编程给自己用,给用户的感觉是一个更强大的实用软件,是一个上层的应用,也就是说NoCode需要面向非常固定的领域才能做到好用;而对于LowCode而言,除了要考虑能用界面化的方式搭建流程,还要考虑在需要扩展的时候,把底层也暴露出来,拥有更强的可定制化功能,也就是说相比NoCode可以不把使用场景限定得那么固定。

对于简历编辑器而言,这就算是非常固定的领域了,而且在使用方面不需要去实现过多代码的编写,开箱即用即可,是作为一个上层应用而实现的。对于我个人而言就是单纯的因为要秋招了,网站上各种模版用起来细节上并不是很满意,在晚上睡觉前洗澡的时候突然有个想法要做这个,然后一个周末也就是两天的时间肝出来了一个简单的基于NoCode的简历编辑器。

说回正题,对于实现简历编辑器而言,需要有这几个方面的考虑,当然因为我是两天做出来的,也只是比较简单的实现了部分功能:

  • 需要支持拖动的页面网格布局或自由布局。
  • 对各组件有独立编辑的能力。
  • 生成PDF与预览页面的功能。
  • 生成JSON格式的配置数据。
  • 支持远程物料简历模版的加载。
  • 基础组件图片、文本等的实现。

实现

数据存储

对于数据而言,在这里是维护了一个JSON数据,对于整个简历编辑器而言都有着比较严格的TS定义,所以预先声明组件类型定义是很有必要的,在这里声明了LocalComponentConfig作为组件的类型定义,而对于整个生成的JSON而言,也就完成了作为LocalComponentConfig[]的嵌套。

在项目中显示的简历是完全采用JSON配置的形式来实现的,数据与视图的渲染是完全分离的,那么由此我们就可以通过编写多个JSON配置的形式,来实现不同简历主题模版。如果打开上边提到的Resume DEMO的话,可以看到预先加载了一个简历,这个简历的内容就是完全由JSON配置而得到的,具体而言可以参考src/components/debug/example.ts。如果数据以local storage字符串的形式存储在本地,键值为cld-storage,如果本地local storage没有这个键的话,就会加载示例的初始简历,数据存储形式为{origin: ${data}, expire: number | number},通过JSON.parse可以解析取出数据。有了这个JSON数据的配置。

// 数据定义
// src/types/components-types.ts
export type LocalComponentConfig = {
id: string; // uuid
name: string;
props: Record<string, unknown>;
style: React.CSSProperties;
config: Record<string, unknown>;
children: LocalComponentConfig[];
[key: string]: unknown;
};

在这里实际上我们有两套数据结构的定义,因为目的是实现数据与组件的分离,但是组件也是需要有位置进行定义的,此外由于希望整个编辑器是可拆卸的,具体而言就是每个基础组件是独立注册的,如果将其注册部分移除,对于整个项目是不会产生任何影响的,只是视图无法根据JSON的配置成功渲染,最终呈现的效果为空而已。

// 组件定义
// src/types/components-types.ts
interface ComponentsBase {
name: string;
props?: Record<string, unknown>; // 传递给组件的默认`props`
style?: React.CSSProperties; // 样式配置信息
config?: Record<string, unknown>; // 配置信息
}
export interface LocalComponent extends ComponentsBase {
module: Panel;
} // 组件定义
export const xxx: LocalComponent = {
// ...
} // 组件注册
// src/index.tsx
register(image, richText, blank);

数据通信

因为要维护的JSON数据结构还是比较复杂的,在这里我们使用Context + useImmerReducer来实现的状态管理,当然使用reducer或者Mobx也都是可以的,这只是我觉得实现的比较简单的方案。

// src/store/context.tsx
export const AppProvider: React.FC<{ mode?: ContextProps["mode"] }> = props => {
const { mode = EDITOR_MODE.EDITOR, children } = props;
const [state, dispatch] = useImmerReducer(reducer, defaultContext.state);
return <AppContext.Provider value={{ state, mode, dispatch }}>{children}</AppContext.Provider>;
};

页面网格布局

网格布局的实现比较简单,而且不需要再实现参考线去做对齐的功能,直接在拖拽时显示网格就好。另外如果以后会拓展多种宽度的PDF生成的话,也不会导致之前画布布局太过于混乱,因为本身就是栅格的实现,可以根据宽度自动的处理,当然要是适配移动端的话还是需要再做一套Layout数据的。

这个网格的页面布局实际上就是作为整个页面布局的画布来实现,React的生态有很多这方面的库,我使用了react-grid-layout这个库来实现拖拽,具体使用的话可以在本文的参考部分找到其Github链接,这个库的实现也是蛮不错的,基本可以做到开箱即用,但是细节方面还是很多东西需要处理的。对于layout配置项,因为我们本身是存储了一个JSON的数据结构,所以我们需要通过我们自己定义的数据结构来生成layout,在生成的过程中如果cols或者rowHeight有所变化而导致元素超出原定范围的话,还需要处理一下。

// src/views/main-panel/index.tsx
<ReferenceLine
display={!isRender && dragging}
rows={rowHeight}
cols={cols}
>
<ResponsiveGridLayout
className="pedestal-responsive-grid-layout"
style={{ minHeight }}
layout={layouts}
autoSize
draggableHandle=".pedestal-drag-dot"
margin={[0, 0]}
onLayoutChange={layoutChange}
cols={cols}
rowHeight={rowHeight}
measureBeforeMount
onDragStart={dragStart}
onDragStop={dragStop}
onResizeStart={resizeStart}
onResizeStop={resizeStop}
allowOverlap={allowOverlap}
compactType={null} // 关闭垂直压实
preventCollision // 关闭重新排列
useCSSTransforms={false} // 在`ObserveResize`时会出现动画
>
</ResponsiveGridLayout>
</ReferenceLine>

对于<ReferenceLine/>组件,在这里通过CSS绘制了网格布局的网格点,从而实现参考线的作用。

// src/views/main-panel/components/reference-line/index.tsx
<div
className={classes(
"pedestal-main-reference-line",
props.className,
props.display && "enable"
)}
style={{
backgroundSize: `${cellWidth}px ${props.rows}px`,
backgroundPositionX: cellWidth / 2,
backgroundPositionY: -props.rows / 2,
...props.style,
// background-image: radial-gradient(circle, #999 0.8px, transparent 0);
}}
ref={referenceLineRef}
>
{props.children}
</div>

组件独立编辑

有了基础的画布组件,我们就需要实现各个基础组件,那么基础组件就需要实现独立的编辑功能,而独立的编辑功能又需要三部分的实现:首先是数据的变更,因为编辑最终还是需要体现到数据上,也就是我们要维护的那个JSON数据,因为我们有了数据通信的方案,所以这里只需要定义reducer将其写到对应的组件配置的props或者其他字段中即可。

// src/store/reducer.ts
witch (action.type) {
// ...
case actions.UPDATE_ONE: {
const { id: uuid, key, data, merge = true } = action.payload;
updateOneInNodeTree(state.cld.children, uuid, key, data, merge);
break;
}
// ...
} // src/utils/node-tree-utils.ts
/**
* @param tree LocalComponentConfig.children
* @param uuid string
* @param key string
* @param data unknown
* @returns boolean
*/
export const updateOneInNodeTree = (
tree: LocalComponentConfig["children"],
uuid: string,
key: string,
data: unknown,
merge: boolean
): boolean => {
const node = findOneInNodeTree(tree, uuid);
if (!node) return false;
let preKeyData: unknown = node;
const deepKey = key.split(".");
const lastKey = deepKey[deepKey.length - 1];
for (let i = 0, n = deepKey.length - 1; i < n; ++i) {
if (isObject(preKeyData)) preKeyData = preKeyData[deepKey[i]];
else return false;
}
if (isObject(preKeyData)) {
const target = preKeyData[lastKey];
if (isObject(target) && isObject(data)) {
if (merge) preKeyData[lastKey] = { ...target, ...data };
else preKeyData[lastKey] = { ...data };
} else {
preKeyData[lastKey] = data;
}
return true;
}
return false;
};

接下来是工具栏的实现,对于工具栏而言,我们需要针对选中的元素的name进行一个判别,加载工具栏之后,对于用户的操作,只需要根据当前选中的id通过数据通信应用到JSON数据中,最后在视图中就会应用其修改了。

// src/views/main-panel/components/tool-bar/index.tsx
const deleteBaseSection = () => {
// ...
}; const copySection = () => {
// ...
}; // ... <Trigger
popupVisible={selectedId === config.id}
popup={() => Menu}
position="top"
trigger="contextMenu"
>
{props.children}
</Trigger>

对于编辑面板而言,与工具栏类似,通过加载表单,在表单的数据变动之后通过reducer应用到JSON数据即可,在这里因为实现的编辑器确实比较简单,于是还加载了一个CSS编辑器,通过配合CSS可以实现更多的样式效果,当然通过拓展各个组件编辑面板部分是能够尽量去减少自定义CSS的编写的。

// src/views/editor-panel/index.tsx
const renderEditor = () => {
const [selectNodeName] = state.selectedNode.name.split(".");
if (!selectNodeName) return null;
const componentInstance = getComponentInstanceSync(selectNodeName);
if (!componentInstance || !componentInstance.main) return null;
const Component = componentInstance.editor;
return (
<>
<Component state={state} dispatch={dispatch}></Component>
<CustomCSS state={state} dispatch={dispatch}></CustomCSS>
</>
);
}; // eslint-disable-next-line react-hooks/exhaustive-deps
const EditorPanel = useMemo(() => renderEditor(), [state.selectedNode.id]);

导出PDF

导出PDF功能是借助了浏览器的能力,通过打印即Ctrl + P来实现导出PDF的效果,导出时需要注意:

  • 简历是按照A4纸的大小固定的宽高,如果扩大编辑区域可能会造成简历多于一页。
  • 导出PDF需要设置纸张尺寸为 A4、边距为无、选中背景图形选项 才可以完整导出一页简历。

基础组件

图片组件

图片组件,用以上传图片展示,因为本身没有后端,所以图片只能以base64存储在JSON的结构中。

// src/components/image/index.ts
export const image: LocalComponent = {
name: "image" as const,
props: {
src: "./favicon.ico",
},
config: {
layout: {
x: 0,
y: 0,
w: 20,
h: 20,
isDraggable: true,
isResizable: true,
minW: 2,
minH: 2,
},
},
module: {
control: ImageControl,
main: ImageMain,
editor: ImageEditor,
},
};

富文本组件

富文本组件,用以编辑文字,在这里正好我有一个富文本编辑器的组件实现,可以参考 GithubEditor DEMO

// src/components/text/index.ts
export const richText: LocalComponent = {
name: "rich-text" as const,
props: {},
config: {
layout: {
x: 0,
y: 0,
w: 20,
h: 10,
isDraggable: true,
isResizable: true,
minW: 4,
minH: 2,
},
observeResize: true,
},
module: {
control: RichTextControl,
main: RichText,
editor: RichTextEditor,
},
};

空白组件

空白组件,可以用以作为占位空白符,也可以通过配合CSS实现背景效果。

// src/components/blank/index.ts
export const blank: LocalComponent = {
name: "blank" as const,
props: {},
config: {
layout: {
x: 0,
y: 0,
w: 10,
h: 3,
isDraggable: true,
isResizable: true,
minW: 1,
minH: 1,
},
},
module: {
control: BlankControl,
main: BlankMain,
editor: BlankEditor,
},
};

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

http://javakk.com/2127.html
http://blog.wuweiwang.cn/?p=27961
https://github.com/ctrlplusb/react-sizeme
https://juejin.cn/post/6961309077162950692
https://github.com/WindrunnerMax/DocEditor
https://github.com/react-grid-layout/react-grid-layout

基于NoCode构建简历编辑器的更多相关文章

  1. 基于slate构建文档编辑器

    基于slate构建文档编辑器 slate.js是一个完全可定制的框架,用于构建富文本编辑器,在这里我们使用slate.js构建专注于文档编辑的富文本编辑器. 描述 Github | Editor DE ...

  2. Tridiv:基于 Web 的 CSS 编辑器,创建炫丽 3D 图形

    Tridiv 是一个基于 Web 的编辑器,使用 CSS 创建 3D 形状.它提供了一个传统的四个面板的操作界面,给出了从每个平面的视图,以及一个预览窗格中示出的最终的效果.使用 Tridiv 可以创 ...

  3. Summernote – 基于 Bootstrap 的文本编辑器

    Summernote 是一个简单,灵活,所见即所得(WYSIWYG)的编辑器,基于 jQuery 和 Bootstrap 构建.Summernote 所有主要的操作都支持快捷键,有一个功能强大的 AP ...

  4. 基于 WebSocket 构建跨浏览器的实时应用

    Socket.IO – 基于 WebSocket 构建跨浏览器的实时应用 Socket.IO 是一个功能非常强大的框架,能够帮助你构建基于 WebSocket 的跨浏览器的实时应用.支持主流浏览器,多 ...

  5. 基于Kubernetes 构建.NET Core技术中台

    今天下午在腾讯云+社区社区分享了<基于Kubernetes 构建.NET Core技术中台>,下面是演讲内容的文字实录. 我们为什么需要中台 我们现在处于企业信息化的新时代.为什么这样说呢 ...

  6. 一个基于mysql构建的队列表

    通常大家都会使用redis作为应用的任务队列表,redis的List结构,在一段进行任务的插入,在另一端进行任务的提取. 任务的插入 $redis->lPush("key:task:l ...

  7. 基于Azure构建PredictionIO和Spark的推荐引擎服务

    基于Azure构建PredictionIO和Spark的推荐引擎服务 1. 在Azure构建Ubuntu 16.04虚拟机 假设前提条件您已有 Azure 帐号,登陆 Azure https://po ...

  8. 基于LoadRunner构建接口测试框架

    基于LoadRunner构建接口测试框架 http://www.docin.com/p-775544153.html

  9. 基于soapUI构建WebService测试框架

    基于soapUI构建WebService测试框架 http://www.docin.com/p-775523285.html

随机推荐

  1. .NET混合开发解决方案7 WinForm程序中通过NuGet管理器引用集成WebView2控件

    系列目录     [已更新最新开发文章,点击查看详细] WebView2组件支持在WinForm.WPF.WinUI3.Win32应用程序中集成加载Web网页功能应用.本篇主要介绍如何在WinForm ...

  2. opencv学习之基础

    前段时间一直在钻研深度学习中的卷积神经网络,其中的预处理环节可以说非常关键,主要就是对图片和视频进行处理.而图像处理就涉及到图形学和底层技术细节,这是一个比较精深和专业的领域,假设我们要从头开始做起, ...

  3. 3┃音视频直播系统之浏览器中通过 WebRTC 直播视频实时录制回放下载

    一.录制分类 在音视频会议.在线教育等系统中,录制是一个特别重要的功能 录制一般分为服务端录制和客户端录制 服务端录制:优点是不用担心客户因自身电脑问题造成录制失败(如磁盘空间不足),也不会因录制时抢 ...

  4. 软件项目管理 ——1.2.PMBOK与软件项目管理知识体系

    软件项目管理 --1.2.PMBOK与软件项目管理知识体系 归档于软件项目管理初级学习路线 第一章 软件项目管理基本概念 <初级学习路线合集 > @ 目录 软件项目管理 --1.2.PMB ...

  5. 关于Linux添加字体

    安装字体命令 yum -y install fontconfig 查看已经安装的字体 fc-list # 查看已经已经安装的中文字体 fc-list :lang=zh 在字体目录下创建新的目录或者使用 ...

  6. 一文看懂 ZooKeeper ,面试再也不用背八股(文末送PDF)

    ZooKeeper知识点总结 一.ZooKeeper 的工作机制 二.ZooKeeper 中的 ZAB 协议 三.数据模型与监听器 四.ZooKeeper 的选举机制和流程 本文将以如下内容为主线讲解 ...

  7. nginx 源码安装配置详解(./configure)

    在"./configure"配置中,"--with"表示启用模块,也就是说这些模块在编译时不会自动构建,"--without"表示禁用模块, ...

  8. 初识 Redis 以及其基本使用方法

     1.什么是Redis  redis 是一个高性能的key-value数据库,它支持的类型更多 包括 string(字符串).list(链表).set(集合).zset(sorted set --有序 ...

  9. 记一次生产事故的排查与优化——Java服务假死

    一.现象 在服务器上通过curl命令调用一个Java服务的查询接口,半天没有任何响应.关于该服务的基本功能如下: 1.该服务是一个后台刷新指示器的服务,即该服务会将用户需要的指示器数据提前计算好,放入 ...

  10. 学学dotnet core中的身份验证和授权-1-概念

    前言 身份验证: Authentication 授权: Authorization net core 中的身份验证和授权这两个部分,是相辅相成的.当初我在学在部分的时候,是看的 net core 官网 ...