• 基于NoCode构建简历编辑器


    基于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数据的配置。

    Copy
    // 数据定义 // 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的配置成功渲染,最终呈现的效果为空而已。

    Copy
    // 组件定义 // 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也都是可以的,这只是我觉得实现的比较简单的方案。

    Copy
    // 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有所变化而导致元素超出原定范围的话,还需要处理一下。

    Copy
    // 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>
    折叠

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

    Copy
    // src/views/main-panel/components/reference-line/index.tsx
    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}

    组件独立编辑#

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

    Copy
    // 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数据中,最后在视图中就会应用其修改了。

    Copy
    // 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的编写的。

    Copy
    // 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的结构中。

    Copy
    // 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

    Copy
    // 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实现背景效果。

    Copy
    // 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, }, };

    每日一题#

    Copy
    https://github.com/WindrunnerMax/EveryDay

    参考#

    Copy
    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
  • 相关阅读:
    java项目-第139期ssm博客系统-ssm毕业设计_计算机毕业设计
    【Python】Pandas(学习笔记)
    网络协议:透彻解析HTTPS协议
    面试官:设计模式中的适配器模式是什么?
    conan 基本配置
    数字化时代的财务管理:挑战与机遇
    2023秋招面试准备
    jvm工具-jps、jstat、jmap、jstack
    Kubernetes kustomize 技术从入门到企业实战(下)
    Oracle超全SQL,细节狂魔
  • 原文地址:https://www.cnblogs.com/WindrunnerMax/p/16513995.html