1.从接口获取手写内容,处理成由单个字组成的数组(包括符号)
2.合成所有图的时候,会闪现outputCanvas合成的图,注意隐藏
3.可以进行多个手写内容切换
4.基于uniapp的
<template>
<view class="content">
<!-- 头部 -->
<view class="navBarBox">
<!-- 头部导航 -->
<u-navbar height="120rpx">
<view class="u-nav-slot" slot="left">
<view class="flex alignCenterClass flexBetween" @click="_close">
<view class="leftBox">
<img src="static/imgs/leftIcon.png" alt="" />
</view>
</view>
</view>
<view class="centerBox" slot="center">
<view class="title"> 批注声明 </view>
<u--text
type="error"
text="请您在区域内逐字手写以下文字,全部写完后点击保存!"
size="30rpx"
align="center"
>
</u--text>
</view>
<view class="u-nav-slot flex" slot="right">
<view
class="btn-box signerBox"
v-if="isComplete"
@click="_submitDraw"
>
<text> 完成 </text>
<u-icon name="checkmark" color="#fff" size="26"></u-icon>
</view>
</view>
</u-navbar>
</view>
<view class="content-model">
<view class="model-left">
<view
v-for="(item, index) in docNoteList"
class="note-item"
:key="item.code"
@click="_checkNotes(item, 'click')"
>
<view
class="note-btn btn-box"
:class="curNode.code == item.code ? 'actice-node' : ''"
><text>批注{{ index + 1 }}</text>
<u-icon
v-if="curNode.code == item.code"
name="edit-pen"
color="#fff"
size="28"
></u-icon
></view>
<u-icon
v-if="item.isDone"
name="checkmark-circle"
color="#087e6a"
size="26"
></u-icon>
</view>
</view>
<view class="container">
<view class="notes-list">
<scroll-view
scroll-y="true"
style="height: 600rpx"
class="scroll-view_w"
enable-flex="true"
scroll-with-animation="true"
>
<view class="note-box">
<view
v-for="item in curNode.notesList"
@click="_checkItem(item)"
:key="item.index"
class="notes-item"
:class="activeItem.index == item.index ? 'active' : ''"
>
<view class="note-label">{{ item.label }}</view>
<!-- 展示写好的字 -->
<view class="note-img">
<img v-if="item.imgSrc" :src="item.imgSrc" alt="" />
</view>
</view>
</view>
</scroll-view>
</view>
<view class="main-model">
<view class="show-canvas">
<!-- canvas背景字 -->
<view class="bg-text"
><text>{{ activeItem.label }}</text></view
>
<!-- 当前需要手写的字的canvas -->
<view class="canvas-container">
<canvas
canvas-id="inputCanvas"
class="input-canvas"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
</view>
</view>
</view>
<!-- 将单个字的图片合并章所用的canvas -->
<view class="show-result">
<canvas
:style="{ height: outputHeight, width: outputWidth }"
canvas-id="outputCanvas"
class="output-canvas"
></canvas>
</view>
</view>
</view>
</view>
</template>
<script>
import { pathToBase64 } from "../../utils/image-tools/index.js";
import { addNoteImg, getDocData } from "@/utils/api.js";
export default {
components: {},
data() {
return {
show: true,
isDrawing: false,
startX: 0,
startY: 0,
strokes: [],
charObjects: [],
timer: null,
delay: 500, // 写完后的延迟
fj: "",
outputHeight: "100px",
outputWidth: "100px",
tempFilePath: "", //当前显示的图片
activeItem: {}, //当前的批注文字
document: "",
docNoteList: [], //所有批注列表
curNode: {
notesList: [],
}, //当前批注
isComplete: false, //当前文档所有批注书否全部写完,控制完成按钮的显示
tempPathObj: {},
showImg: "", //批注合成图
};
},
computed: {},
methods: {
// 返回上一页
_close() {
uni.redirectTo({
url: "/pages/index/fileEdit?documentId=" + this.document.documentId,
});
},
_checkItem(val) {
this.activeItem = { ...val };
this.tempFilePath = val.imgSrc || "";
},
handleTouchStart(e) {
e.preventDefault(); // 阻止默认滚动行为
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const touch = e.touches[0];
this.isDrawing = true;
this.startX = touch.x;
this.startY = touch.y;
this.strokes.push({
x: touch.x,
y: touch.y,
});
},
handleTouchMove(e) {
e.preventDefault(); // 阻止默认滚动行为
if (!this.isDrawing) return;
const touch = e.touches[0];
const context = uni.createCanvasContext("inputCanvas", this);
context.setStrokeStyle("#000");
context.setLineWidth(15);
context.setLineJoin('round');
context.setLineCap('round');
context.moveTo(this.startX, this.startY);
context.lineTo(touch.x, touch.y);
context.stroke();
context.draw(true);
this.startX = touch.x;
this.startY = touch.y;
this.strokes.push({
x: touch.x,
y: touch.y,
});
},
handleTouchEnd(e) {
e.preventDefault(); // 阻止默认滚动行为
this.isDrawing = false;
// 写完后延迟,清空Canvas,获取手写图
this.timer = setTimeout(this.addChar, this.delay);
},
addChar() {
const inputContext = uni.createCanvasContext("inputCanvas", this);
uni.canvasToTempFilePath({
canvasId: "inputCanvas",
success: (res) => {
// 清空 inputCanvas 上的内容
inputContext.clearRect(0, 0, 700, 700);
inputContext.draw();
this._pathToBase64(res.tempFilePath, "show");
},
});
},
//批注合成处理
_drawImage(notesList, imgCode) {
this.showImg = "";
let outputContext = "";
outputContext = uni.createCanvasContext("outputCanvas", this);
const charSize = 40; // 调整字符大小
const maxCharsPerRow = 20; // 每行最大字符数
// 动态设置高度
const numRows = Math.ceil(notesList.length / maxCharsPerRow); // 计算行数
this.outputHeight = `${numRows * charSize}px`; // 动态计算输出画布的高度
this.outputWidth = `${maxCharsPerRow * charSize}px`; // 动态计算输出画布的宽度
// 绘制字符
let rowSpacing = "";
let colSpacing = "";
notesList.forEach((item, index) => {
const rowIndex = Math.floor(index / maxCharsPerRow); // 当前字符的行索引
const colIndex = index % maxCharsPerRow; // 当前字符的列索引
rowSpacing = rowIndex * charSize;
colSpacing = colIndex * charSize;
outputContext.drawImage(
item.tempFilePath,
colSpacing,
rowSpacing,
charSize,
charSize
);
});
setTimeout(() => {
outputContext.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "outputCanvas",
success: (result) => {
uni.compressImage({
//压缩图片
src: result.tempFilePath,
success: (res) => {
pathToBase64(res.tempFilePath)
.then((base64) => {
//赋值
const _nodeImg = this.filterBase64(base64);
this.tempPathObj[imgCode] = _nodeImg;
console.log("tempPathObj", this.tempPathObj);
this.showImg = _nodeImg;
// 清空 outputContext 上的内容
outputContext.clearRect(
0,
0,
colSpacing + charSize * charSize,
rowSpacing + numRows * charSize
);
outputContext.draw();
})
.catch((error) => {
console.error(error);
});
},
});
},
});
});
}, 500);
},
_checkNotePass() {
let isComplete = true; //是否全部批注都写完,默认都写完了
const _curNode = this.curNode;
this.docNoteList.map((item) => {
if (item.code == _curNode.code) {
item.isDone = _curNode.notesList.every((item) => {
return item.imgSrc != "";
});
if (item.isDone) {
this._drawImage(_curNode.notesList, _curNode.imgCode);
}
}
if (!item.isDone) {
//如有未完成的
isComplete = false;
}
});
this.isComplete = isComplete;
},
_pathToBase64(val, code) {
uni.compressImage({
//压缩图片
src: val,
success: (res) => {
pathToBase64(res.tempFilePath)
.then((base64) => {
const _notesList = this.curNode.notesList;
const signImg = this.filterBase64(base64);
// 手写处理
_notesList.map((item) => {
if (item.index == this.activeItem.index) {
item.imgSrc = signImg;
item.tempFilePath = res.tempFilePath;
}
});
this.tempFilePath = signImg;
this._checkNotePass();
// 自动轮下一个
if (this.activeItem.index < _notesList.length - 1) {
this._checkItem(_notesList[this.activeItem.index + 1]);
}
})
.catch((error) => {
console.error(error);
});
},
});
},
// 过滤base64太长有换行字符方法
filterBase64(codeImages) {
return codeImages.replace(/[\r\n]/g, "");
},
// 保存更新
async _submitDraw() {
let documentData = {
...this.document.documentData,
};
for (const key in this.tempPathObj) {
documentData[key] = this.tempPathObj[key];
}
const query = {
documentId: this.document.documentId,
documentData: JSON.stringify(documentData),
};
await addNoteImg(query).then(({ data: res }) => {
if (res.code == 0) {
uni.showToast({
icon: "success",
title: "保存成功",
});
uni.redirectTo({
url: "/pages/index/fileEdit?documentId=" + this.document.documentId,
});
}
});
},
// 获取文书详情
async _getDocData(documentId) {
await getDocData({ documentId }).then(({ data: res }) => {
const _documentData = JSON.parse(res.data.documentData);
this.document = {
documentId: res.data.documentId,
documentData: _documentData,
};
this._checkDocData(_documentData);
});
},
// 处理批注,形成列表
_checkDocData(data) {
let docNote = [];
for (const key in data) {
const element = data[key];
//根据实际需求处理这一步
if (key.includes("note_text")) {
let notesList = [];
// 处理批注内容
const _text = element.split("");
for (let i = 0; i < _text.length; i++) {
const ele = _text[i];
notesList.push({
label: ele,
imgSrc: "",
index: i,
tempFilePath: "",
code: key,
});
}
const _key = key.split("note_text")[1] || "";
const _imgCode = "note_img" + _key;
docNote.push({
code: key,
imgCode: _imgCode,
label: element,
nodeImg: "", //最终合成的图片
isDone: false, //当前批注是否已写完
notesList, //所有批注问字
});
this.$set(this.tempPathObj, _imgCode, "");
}
}
console.log(docNote, 142545);
this.docNoteList = docNote;
this.curNode = { ...docNote[0] };
this._checkItem(this.curNode.notesList[0]);
},
// 处理当前批注数据
_checkNotes(value) {
this.curNode = { ...value };
this.showImg = this.tempPathObj[value.imgCode];
this._checkItem(this.curNode.notesList[0]);
},
},
beforeDestroy() {},
onLoad(option) {
this._getDocData(option.documentId);
},
created() {},
};
</script>
<style scoped lang="scss">
.content {
height: 100vh;
}
.content-model {
width: 100%;
margin-top: 80rpx;
display: flex;
justify-content: space-around;
align-items: flex-start;
.model-left {
width: 12%;
.note-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.note-btn {
margin-bottom: 30rpx;
background-color: #ee7c36;
}
.actice-node {
background-color: $mainColor;
}
}
}
.centerBox {
text-align: center;
}
.signerBox {
background-color: $mainColor;
}
.btn-box {
display: flex;
justify-content: space-around;
align-items: center;
color: #fff;
padding: 12rpx 50rpx;
border-radius: 40rpx;
font-size: 40rpx;
}
.navBarBox {
.leftBox {
width: 40rpx;
height: 40rpx;
margin-right: 40rpx;
margin-top: -20rpx;
img {
width: 100%;
height: 100%;
}
}
}
.container {
display: flex;
flex-direction: column;
align-items: center;
width: 87%;
.show-result {
position: fixed;
bottom: -200prx;
width: 40%;
z-index: 1;
background-color: #ee7c36;
}
.notes-list {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
flex-wrap: wrap;
// background-color: rgba(223, 220, 219,0.2);
background-color: #fff;
margin-bottom: 25rpx;
position: relative;
z-index: 999;
.scroll-view_w {
width: 100% !important;
}
.note-box {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
}
.note-label,
.note-img {
width: 120rpx;
height: 120rpx;
line-height: 120rpx;
text-align: center;
margin-right: 5rpx;
margin-bottom: 3rpx;
border: 2rpx solid #999;
background-color: #fff;
font-size: 100rpx;
img {
width: 100%;
height: 100%;
}
}
.active {
.note-label,
.note-img {
border: 2rpx solid rgba(212, 21, 53, 0.9);
}
}
}
}
.main-model {
display: flex;
justify-content: center;
width: 100%;
.show-img,
.show-canvas {
position: relative;
width: 700rpx;
height: 700rpx;
border: 8rpx dashed #dddee1;
}
.show-img img {
z-index: 999;
}
.show-canvas .bg-text {
position: absolute;
top: -30rpx;
left: 0;
width: 100%;
height: 100%;
font-size: 300px;
line-height: 700rpx;
text-align: center;
color: rgba(153, 153, 153, 0.1);
}
.canvas-container {
width: 100%;
height: 100%;
.input-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 10rpx;
touch-action: none;
// background-color: #ee7c36;
/* 禁止默认触摸动作 */
}
}
}
.temp-img {
width: 300rpx;
height: 100rpx;
img {
width: 100%;
height: 100%;
}
border: 2rpx solid #dddee1;
}
</style>
效果图: