Vue3 在线 PDF 编辑 2.0 图片批注、批注清空与批注记录功能解析

各位开发者朋友们!昨天为大家介绍了 PDF 批注的撤回与反撤回功能,今天咱们接着深入剖析本次更新的另外几个实用特性 ------ 图片批注、批注清空以及批注记录的实现细节。相较于撤回与反撤回功能,今天这些内容相对容易理解。废话不多说,让我们直接开始技术揭秘!

图片批注:轻松添加可视化元素

图片批注的添加方式和文字批注类似,都是依据当前页码,借助 fabric.Image 将图片添加到页面的左上或左下位置,用户可以自由拖动图片进行批注。

typescript 复制代码
const addImage = (event: { page: string | number, canvas: any, canvasRefs: any }, imageUrl: string) => {
    if (!event || !event.canvas || !event.canvasRefs) return
    fabricCanvas = event.canvas
    const pointer = getPointer(event.canvasRefs);
    // 使用 fabric.Image.fromURL 加载图片
    fabric.Image.fromURL(imageUrl, (img: any) => {
        // 设置图片的位置
        img.set({
            left: pointer.x,
            top: pointer.y,
            id: `image_${new Date().getTime()}`,
        });
        // 设置图片的缩放比例
        img.scale(0.5);
        // 将图片添加到画布
        event.canvas.add(img);
        // 渲染画布
        event.canvas.renderAll();
        saveState({ ...event, type: 'add' })
    });
}

这里要提醒一下,我在项目里用的是本地图片,保存后无法回显。如果大家在项目中使用,建议先把图片上传到服务器,或者直接转成 base64 格式(不过我还没测试过这种方式),如果是下载的话就不需要考虑那么多了。

功能预览

批注清空:一键清理画布

批注清空操作非常简单,只需要遍历所有的 canvas,然后调用 clear 方法即可。

scss 复制代码
const clearActiveObjectAll = (canvasObj: any) => {
    for (let key in canvasObj) {
        canvasObj[key].clear()
        canvasObj[key].renderAll();
    }
}
功能预览

批注记录:精准管理批注信息

批注记录功能是在昨天提到的记录批注信息的 saveState 方法基础上实现的。无论 canvas 进行何种操作,都要更新 queueStack 记录。

ini 复制代码
export const useStack = () => {
    const undoStack: any = [];
    const redoStack: any = [];
    const queueStack = ref<any>(new Map()); // 存储当前操作的画布
    let isUndo = false; // 是否撤回
    // 记录当前状态
    const saveState = (event: any) => {
        if (!isUndo) {
            const state = event.canvas.toJSON();
            undoStack.push({
                state: state,
                canvas: event.canvas,
                page: event.page,
                type: event.type,
            });
            // 清空反撤回栈
            redoStack.length = 0;
            storeQueue(event.page, event.canvas); // 存储当前操作的画布
        }
    }

    // 撤回操作
    const undo = () => {
        if (undoStack.length > 0) {
            // 取出上一个状态
            isUndo = true; // 设置为撤回状态
            const previousState = undoStack.pop();
            redoStack.push(previousState); // 保存当前状态到反撤回栈
            if (previousState.type === 'modify' || previousState.type === 'remove') {
                const stateToRestore = undoStack[undoStack.length - 1]; // 获取上一个状态
                stateToRestore.canvas.loadFromJSON(stateToRestore.state, () => {
                    stateToRestore.canvas.renderAll();
                    isUndo = false; // 设置为撤回状态
                });
                storeQueue(previousState.page, previousState.canvas); // 存储当前操作的画布
            } else if (previousState?.state.objects.length === 1) {
                // 如果只有一个对象,直接清空画布
                previousState.canvas.clear();
                storeQueue(previousState.page, previousState.canvas); // 存储当前操作的画布
                isUndo = false; // 设置为撤回状态
            } else {
                const objects = previousState.state.objects.slice(0, previousState.state.objects.length - 1); // 删除最后一个对象
                const stateToRestore = previousState
                previousState && previousState.canvas.loadFromJSON({ ...stateToRestore.state, objects }, () => {
                    previousState.canvas.renderAll();
                    isUndo = false; // 设置为撤回状态
                });
                storeQueue(previousState.page, previousState.canvas); // 存储当前操作的画布
            }

        }
    }

    // 反撤回操作
    const redo = () => {
        if (redoStack.length > 0) {
            // 取出反撤回栈中的状态
            isUndo = true; // 设置为撤回状态
            const stateToRestore = redoStack.pop();
            // 保存当前状态到撤回栈
            undoStack.push(stateToRestore);
            stateToRestore.canvas.loadFromJSON(stateToRestore.state, () => {
                stateToRestore.canvas.renderAll();
                isUndo = false; // 设置为撤回状态
            });
            storeQueue(stateToRestore.page, stateToRestore.canvas); // 存储当前操作的画布
        }
    }
    const storeQueue = (page: number, canvas: any) => {
        const dataArr = canvas.getObjects()
        if (dataArr.length > 0) {
            queueStack.value.set(page + 1, canvas.getObjects()); // 存储当前操作的画布
        } else {
            queueStack.value.delete(page + 1); // 删除当前操作的画布
        }
    }
    return {
        saveState,
        undo,
        redo,
        queueStack: queueStack,
    }
}   

这里我使用了 Map 数据格式来存储批注记录,因为 Map 在进行属性的增删改查操作时,比 Object 的开销要小很多。

storeQueue 方法中,还需要判断当前画布上是否还有批注信息,如果没有,就需要清空该画布的记录。

对于批注记录功能,我还进行了扩展,实现了点击记录即可回滚到当前批注所在页并选中该批注,同时也可以直接在批注记录中删除任何一条批注。前面的文章已经介绍过跳转页码的实现,这里就不再赘述,下面给大家展示一下选中批注和删除批注的实现代码。

ini 复制代码
const setActiveObject = (canvas: any, targetId: string, type = "setActive") => {
    const objects = canvas.getObjects();
    for (let i = 0; i < objects.length; i++) {
        const object = objects[i];
        if (object.id === targetId) {
            // 将具有指定 ID 的元素设置为活动对象
            type === "setActive" ? canvas.setActiveObject(object) : canvas.remove(object);
            // 重新渲染画布
            canvas.renderAll();
            break;
        }
    }
}

这段代码会根据传入的 id 和类型,判断当前 canvas 中的批注信息,从而实现选中或删除操作。

功能预览

总结与展望

本次新增的撤回与反撤回功能,让 PDF 批注操作更加容错和灵活,目前已经形成了 "批注编辑→保存→回显→撤回 / 反撤回" 的完整闭环。此次更新的图片批注、批注清空和批注记录功能,为 PDF 批注操作增添了更多元素。

后续,我们会对现有功能进行优化,比如增加画线选项,像箭头、曲线等。虽然目前缩放功能还没有特别好的实现方案,但请大家放心,我一定会在后续完成这个功能。

对于源码感兴趣的小伙伴,欢迎前往 项目仓库 或者 gitee 仓库 体验完整功能。同时,也期待大家在评论区分享对功能扩展的想法,你的建议很可能会成为下一个版本的核心特性! 🚀

本次新增的图片批注、批注清空、批注记录,让 PDF 批注操作增加了更多的元素,后期的话可能就是对于现有的功能进行一些优化,比如增加画线选项,如箭头、曲线等等,至于缩放,目前还没有特别好的实现方案,不过不用担心,小编后面也一定会做出来。

对于源码感兴趣的小伙伴,欢迎前往 项目仓库 或者 gitee仓库 体验完整功能,也期待你在评论区分享对功能扩展的想法 ------ 你的建议可能成为下一个版本的核心特性!

相关推荐
Eliauk__2 分钟前
深入剖析 Vue 双向数据绑定机制 —— 从响应式原理到 v-model 实现全解析
前端·javascript·面试
代码小学僧2 分钟前
Cursor 的系统级提示词被大佬逆向出来了!一起来看看优秀 prompt是怎么写的
前端·ai编程·cursor
MrsBaek6 分钟前
前端笔记-Axios
前端·笔记
洋流9 分钟前
什么?还没弄懂关键字this?一篇文章带你速通
前端·javascript
晴殇i10 分钟前
for...in 循环的坑,别再用它遍历 JavaScript 数组了!
前端·javascript
littleplayer12 分钟前
iOS 单元测试详细讲解-DeepSeek
前端
littleplayer14 分钟前
iOS 单元测试与 UI 测试详解-DeepSeek
前端·单元测试·测试
运营猫小海豚14 分钟前
DooTask功能与企业适配性分析
开源·github
夜熵16 分钟前
Vue中nextTick()用法
前端·面试
小桥风满袖16 分钟前
Three.js-硬要自学系列15 (圆弧顶点、几何体方法、曲线简介、圆、椭圆、样条曲线、贝塞尔曲线)
前端·css·three.js