项目概述
DevEco 电子签名应用是一款专为移动设备设计的高效电子签名解决方案,基于 OpenHarmony 平台开发。该应用旨在满足日益增长的电子签名需求,为用户提供便捷、安全且具有法律效力的电子签名功能。在数字化转型的时代背景下,我们的应用致力于简化签名流程,提高工作效率,减少纸质文档的使用,为绿色环保贡献力量。
通过直观的用户界面和流畅的操作体验,DevEco 电子签名应用让用户能够随时随地完成签名任务,无需打印文件或亲自到场。无论是商务合同、法律文件还是日常文档,我们的应用都能提供专业的签名解决方案。
功能特点
核心功能
- 精准手写签名:采用先进的触控算法,确保签名流畅自然,完美还原用户的笔迹特点
- 多样化绘制选项:
- 四种标准颜色选择(黑色、红色、蓝色、绿色)
- 可调节的线条粗细(1-10 级调节)
- 圆润的线条连接,提供专业签名效果
- 操作管理功能:
- 一键清除画布,快速重新开始
- 撤销功能,方便纠正误操作
- 即时保存签名,防止工作丢失
用户体验优势
- 简洁直观的界面设计:遵循现代 UI 设计原则,界面清晰易懂
- 响应式布局:自适应不同屏幕尺寸,提供一致的用户体验
- 操作反馈:通过 Toast 提示等方式,提供及时的操作反馈
- 轻量级应用:占用资源少,运行流畅,适合各种设备
技术架构
开发框架
- 基于 OpenHarmony 的 ArkTS/ETS 语言开发
- 采用声明式 UI 编程范式,提高开发效率和代码可维护性
核心技术
- Canvas 绘图技术:利用 CanvasRenderingContext2D 实现高精度绘图
- 触控事件处理:精确捕捉并处理多点触控事件
- 状态管理:使用@State 装饰器进行组件状态管理
- 离屏渲染:通过 OffscreenCanvas 技术实现高效图像处理和导出
应用场景
- 合同签署:远程签署商业合同、协议和备忘录
- 报价确认:快速确认商业报价和订单
- 会议纪要:为会议纪要和决议添加签名确认
- 法律文件:签署法律文件和声明
- 公证材料:为需要公证的材料添加电子签名
- 授权确认:提供授权确认的电子签名
- 快递签收:替代传统的纸质签收单
- 考勤签到:用于员工考勤或活动签到
- 个人认证:在各类需要身份确认的场景中使用
完整源码
import promptAction from '@ohos.promptAction';
import image from '@ohos.multimedia.image';
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
import router from '@ohos.router';
// 定义点的接口
interface Point {
x: number;
y: number;
}
// 定义路径的接口
interface PathData {
path: Point[];
color: string;
width: number;
}
@Entry
@Component
struct SignaturePage {
// Canvas相关状态
@State canvasWidth: number = 0;
@State canvasHeight: number = 0;
@State isDrawing: boolean = false;
@State lineColor: string = '#000000';
@State lineWidth: number = 3;
@State lastX: number = 0;
@State lastY: number = 0;
// 用于保存绘制历史,支持撤销功能
@State drawHistory: PathData[] = [];
@State currentPath: Point[] = [];
// 画布上下文
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
// 获取应用上下文,用于保存图片
private context16: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private appContext: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
aboutToAppear() {
// 页面加载时的初始化操作
}
// 开始绘制
startDrawing(event: TouchEvent) {
this.isDrawing = true;
this.lastX = event.touches[0].x;
this.lastY = event.touches[0].y;
// 开始新路径
this.context.beginPath();
this.context.moveTo(this.lastX, this.lastY);
// 记录当前路径的起点
this.currentPath = [{ x: this.lastX, y: this.lastY }];
}
// 绘制中
drawing(event: TouchEvent) {
if (!this.isDrawing) {
return;
}
const currentX = event.touches[0].x;
const currentY = event.touches[0].y;
// 绘制线条
this.context.lineWidth = this.lineWidth;
this.context.strokeStyle = this.lineColor;
this.context.lineJoin = 'round';
this.context.lineCap = 'round';
this.context.lineTo(currentX, currentY);
this.context.stroke();
// 更新最后位置
this.lastX = currentX;
this.lastY = currentY;
// 记录当前路径点
this.currentPath.push({ x: currentX, y: currentY });
}
// 结束绘制
endDrawing() {
if (!this.isDrawing) {
return;
}
this.isDrawing = false;
// 保存当前路径到历史记录
if (this.currentPath.length > 1) {
this.drawHistory.push({
path: [...this.currentPath],
color: this.lineColor,
width: this.lineWidth
});
}
this.currentPath = [];
}
// 清除画布
clearCanvas() {
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.drawHistory = [];
this.currentPath = [];
}
// 撤销上一步
undo() {
if (this.drawHistory.length === 0) {
promptAction.showToast({
message: '没有可撤销的操作',
duration: 2000
});
return;
}
// 移除最后一个路径
this.drawHistory.pop();
// 重新绘制所有路径
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
for (let item of this.drawHistory) {
this.context.beginPath();
this.context.lineWidth = item.width;
this.context.strokeStyle = item.color;
this.context.lineJoin = 'round';
this.context.lineCap = 'round';
if (item.path.length > 0) {
this.context.moveTo(item.path[0].x, item.path[0].y);
for (let i = 1; i < item.path.length; i++) {
this.context.lineTo(item.path[i].x, item.path[i].y);
}
this.context.stroke();
}
}
}
// 保存签名
async saveSignature() {
try {
// 创建离屏Canvas
let offscreenCanvas = new OffscreenCanvas(this.canvasWidth, this.canvasHeight);
let offContext = offscreenCanvas.getContext("2d");
// 将当前画布内容绘制到离屏Canvas
offContext.fillStyle = '#FFFFFF';
offContext.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
// 重新绘制所有路径到离屏Canvas
for (let item of this.drawHistory) {
offContext.beginPath();
offContext.lineWidth = item.width;
offContext.strokeStyle = item.color;
offContext.lineJoin = 'round';
offContext.lineCap = 'round';
if (item.path.length > 0) {
offContext.moveTo(item.path[0].x, item.path[0].y);
for (let i = 1; i < item.path.length; i++) {
offContext.lineTo(item.path[i].x, item.path[i].y);
}
offContext.stroke();
}
}
// 转换为图片 - 使用transferToImageBitmap替代toDataURL
let imageBitmap = offscreenCanvas.transferToImageBitmap();
// 显示成功消息
promptAction.showToast({
message: '签名已保存',
duration: 2000
});
return imageBitmap;
} catch (error) {
console.error('保存签名失败:', error);
promptAction.showToast({
message: '保存签名失败',
duration: 2000
});
return null;
}
}
build() {
Column() {
// 标题栏
Row() {
Text('电子签名')
.fontSize(20)
.fontWeight(FontWeight.Medium)
.margin({ left: 16 })
Blank()
Button('保存')
.fontSize(16)
.height(36)
.backgroundColor('#007DFF')
.borderRadius(18)
.onClick(() => {
this.saveSignature();
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')
// 签名区域
Column() {
Text('请在下方区域签名')
.fontSize(16)
.fontColor('#666666')
.margin({ top: 20, bottom: 10 })
// 签名画布
Canvas(this.context)
.width('100%')
.layoutWeight(4) // 增加Canvas的权重,给予更多空间
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ bottom: 16 }) // 添加底部间距
.onAreaChange((oldArea: Area, newArea: Area) => {
// 使用新区域的尺寸
this.canvasWidth = newArea.width as number;
this.canvasHeight = newArea.height as number;
})
.onReady(() => {
// Canvas准备就绪
})
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.startDrawing(event);
} else if (event.type === TouchType.Move) {
this.drawing(event);
} else if (event.type === TouchType.Up) {
this.endDrawing();
}
})
// 工具栏容器
Column() {
// 按钮和颜色选择器行
Row() {
Button('清除')
.fontSize(16)
.height(40)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.borderRadius(20)
.onClick(() => {
this.clearCanvas();
})
Button('撤销')
.fontSize(16)
.height(40)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.borderRadius(20)
.margin({ left: 16 })
.onClick(() => {
this.undo();
})
Blank()
// 颜色选择器
Row() {
ForEach(['#000000', '#FF0000', '#0000FF', '#008000'], (color: string) => {
Column() {
Circle({ width: 24, height: 24 })
.fill(color)
.stroke(this.lineColor === color ? '#007DFF' : 'transparent')
.strokeWidth(2)
.margin(4)
.onClick(() => {
this.lineColor = color;
})
}
})
}
}
.width('100%')
.height(48)
.margin({ top: 16 })
// 线宽选择器行
Row() {
Text('线条粗细:')
.fontSize(16)
.fontColor('#666666')
Slider({
value: this.lineWidth,
min: 1,
max: 10,
step: 1,
style: SliderStyle.OutSet
})
.width('60%')
.onChange((value: number) => {
this.lineWidth = value;
})
Text(this.lineWidth.toFixed(0))
.fontSize(16)
.fontColor('#666666')
.width(24)
.textAlign(TextAlign.Center)
}
.width('100%')
.height(48)
.margin({ top: 8 })
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.layoutWeight(1) // 使用layoutWeight而不是固定高度
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
原文链接:https://code.ifrontend.net/archives/649,转载请注明出处。
评论0