|
|
@@ -2,21 +2,22 @@
|
|
|
<view class="cl-sign" :class="[pt.className]">
|
|
|
<canvas
|
|
|
class="cl-sign__canvas"
|
|
|
+ ref="canvasRef"
|
|
|
:id="canvasId"
|
|
|
:style="{
|
|
|
- height: `${size.height}px`,
|
|
|
- width: `${size.width}px`
|
|
|
+ height: `${height}px`,
|
|
|
+ width: `${width}px`
|
|
|
}"
|
|
|
@touchstart="onTouchStart"
|
|
|
- @touchmove="onTouchMove"
|
|
|
+ @touchmove.stop.prevent="onTouchMove"
|
|
|
@touchend="onTouchEnd"
|
|
|
></canvas>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
-import { parsePt, uuid } from "@/cool";
|
|
|
-import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from "vue";
|
|
|
+import { canvasToPng, parsePt, uuid } from "@/cool";
|
|
|
+import { computed, getCurrentInstance, onMounted, ref, shallowRef, watch } from "vue";
|
|
|
|
|
|
defineOptions({
|
|
|
name: "cl-sign"
|
|
|
@@ -76,22 +77,19 @@ const props = defineProps({
|
|
|
autoRotate: {
|
|
|
type: Boolean,
|
|
|
default: true
|
|
|
- },
|
|
|
- // 是否全屏
|
|
|
- fullscreen: {
|
|
|
- type: Boolean,
|
|
|
- default: false
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(["change"]);
|
|
|
|
|
|
const { proxy } = getCurrentInstance()!;
|
|
|
-const { windowWidth, windowHeight } = uni.getWindowInfo();
|
|
|
|
|
|
// 触摸点类型
|
|
|
type Point = { x: number; y: number; time: number };
|
|
|
|
|
|
+// 矩形类型
|
|
|
+type Rect = { left: number; top: number };
|
|
|
+
|
|
|
// 透传样式类型定义
|
|
|
type PassThrough = {
|
|
|
className?: string;
|
|
|
@@ -100,8 +98,8 @@ type PassThrough = {
|
|
|
// 解析透传样式配置
|
|
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|
|
|
|
|
-// canvas组件上下文
|
|
|
-let canvasCtx: CanvasContext | null = null;
|
|
|
+// 签名组件画布
|
|
|
+const canvasRef = shallowRef<UniElement | null>(null);
|
|
|
|
|
|
// 绘图上下文
|
|
|
let drawCtx: CanvasRenderingContext2D | null = null;
|
|
|
@@ -121,57 +119,73 @@ let currentStrokeWidth = ref(3);
|
|
|
// 速度缓冲数组(用于平滑速度变化)
|
|
|
const velocityBuffer: number[] = [];
|
|
|
|
|
|
-// 当前是否为横屏
|
|
|
-const isLandscape = ref(false);
|
|
|
-
|
|
|
-// 动态计算的画布尺寸
|
|
|
-const size = computed(() => {
|
|
|
- if (props.fullscreen) {
|
|
|
- const { windowWidth, windowHeight } = uni.getWindowInfo();
|
|
|
-
|
|
|
- return {
|
|
|
- width: windowWidth,
|
|
|
- height: windowHeight
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- if (isLandscape.value) {
|
|
|
- const { windowWidth } = uni.getWindowInfo();
|
|
|
-
|
|
|
- return {
|
|
|
- width: windowWidth,
|
|
|
- height: props.height
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- width: props.width,
|
|
|
- height: props.height
|
|
|
- };
|
|
|
-});
|
|
|
+// canvas位置信息缓存
|
|
|
+let canvasRect: Rect | null = null;
|
|
|
+
|
|
|
+// 获取canvas位置信息
|
|
|
+function getCanvasRect(): Promise<Rect> {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ // #ifdef MP
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .in(proxy)
|
|
|
+ .select(`#${canvasId}`)
|
|
|
+ .boundingClientRect((rect: any) => {
|
|
|
+ if (rect) {
|
|
|
+ canvasRect = { left: rect.left, top: rect.top };
|
|
|
+ resolve(canvasRect!);
|
|
|
+ } else {
|
|
|
+ resolve({ left: 0, top: 0 });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .exec();
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifndef MP
|
|
|
+ // 非小程序平台,在需要时通过DOM获取位置信息
|
|
|
+ canvasRect = { left: 0, top: 0 };
|
|
|
+ resolve(canvasRect!);
|
|
|
+ // #endif
|
|
|
+ });
|
|
|
+}
|
|
|
|
|
|
// 获取触摸点在canvas中的坐标
|
|
|
function getTouchPos(e: TouchEvent): Point {
|
|
|
const touch = e.touches[0];
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
const rect = (e.target as any).getBoundingClientRect();
|
|
|
+
|
|
|
return {
|
|
|
x: touch.clientX - rect.left,
|
|
|
y: touch.clientY - rect.top,
|
|
|
time: Date.now()
|
|
|
};
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifndef H5
|
|
|
+ // 小程序中使用缓存的位置信息或直接使用触摸坐标
|
|
|
+ const left = canvasRect?.left ?? 0;
|
|
|
+ const top = canvasRect?.top ?? 0;
|
|
|
+
|
|
|
+ return {
|
|
|
+ x: touch.clientX - left,
|
|
|
+ y: touch.clientY - top,
|
|
|
+ time: Date.now()
|
|
|
+ };
|
|
|
+ // #endif
|
|
|
}
|
|
|
|
|
|
// 计算速度并返回动态线条宽度
|
|
|
function calculateStrokeWidth(currentPoint: Point): number {
|
|
|
- if (!lastPoint || !props.enableBrush) {
|
|
|
+ if (lastPoint == null || !props.enableBrush) {
|
|
|
return props.strokeWidth;
|
|
|
}
|
|
|
|
|
|
// 计算距离和时间差
|
|
|
const distance = Math.sqrt(
|
|
|
- Math.pow(currentPoint.x - lastPoint.x, 2) + Math.pow(currentPoint.y - lastPoint.y, 2)
|
|
|
+ Math.pow(currentPoint.x - lastPoint!.x, 2) + Math.pow(currentPoint.y - lastPoint!.y, 2)
|
|
|
);
|
|
|
- const timeDelta = currentPoint.time - lastPoint.time;
|
|
|
+ const timeDelta = currentPoint.time - lastPoint!.time;
|
|
|
|
|
|
if (timeDelta <= 0) return currentStrokeWidth.value;
|
|
|
|
|
|
@@ -198,9 +212,17 @@ function calculateStrokeWidth(currentPoint: Point): number {
|
|
|
}
|
|
|
|
|
|
// 触摸开始
|
|
|
-function onTouchStart(e: TouchEvent) {
|
|
|
+async function onTouchStart(e: TouchEvent) {
|
|
|
e.preventDefault();
|
|
|
isDrawing.value = true;
|
|
|
+
|
|
|
+ // #ifdef MP
|
|
|
+ // 小程序中,如果没有缓存位置信息,先获取
|
|
|
+ if (canvasRect == null) {
|
|
|
+ await getCanvasRect();
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
lastPoint = getTouchPos(e);
|
|
|
|
|
|
// 初始化线条宽度和清空速度缓冲
|
|
|
@@ -211,7 +233,7 @@ function onTouchStart(e: TouchEvent) {
|
|
|
// 触摸移动
|
|
|
function onTouchMove(e: TouchEvent) {
|
|
|
e.preventDefault();
|
|
|
- if (!isDrawing.value || !lastPoint || !drawCtx) return;
|
|
|
+ if (!isDrawing.value || lastPoint == null || drawCtx == null) return;
|
|
|
|
|
|
const currentPoint = getTouchPos(e);
|
|
|
|
|
|
@@ -221,7 +243,7 @@ function onTouchMove(e: TouchEvent) {
|
|
|
|
|
|
// 绘制线条
|
|
|
drawCtx!.beginPath();
|
|
|
- drawCtx!.moveTo(lastPoint.x, lastPoint.y);
|
|
|
+ drawCtx!.moveTo(lastPoint!.x, lastPoint!.y);
|
|
|
drawCtx!.lineTo(currentPoint.x, currentPoint.y);
|
|
|
drawCtx!.strokeStyle = props.strokeColor;
|
|
|
drawCtx!.lineWidth = strokeWidth;
|
|
|
@@ -240,86 +262,54 @@ function onTouchEnd(e: TouchEvent) {
|
|
|
lastPoint = null;
|
|
|
}
|
|
|
|
|
|
-// 判断横竖屏
|
|
|
-function getOrientation() {
|
|
|
- const { windowHeight, windowWidth } = uni.getWindowInfo();
|
|
|
-
|
|
|
- // 判断是否为横屏(宽度大于高度)
|
|
|
- isLandscape.value = windowWidth > windowHeight;
|
|
|
-}
|
|
|
-
|
|
|
-// 屏幕方向变化监听
|
|
|
-function onOrientationChange() {
|
|
|
- setTimeout(() => {
|
|
|
- getOrientation();
|
|
|
-
|
|
|
- // 重新初始化画布
|
|
|
- if (props.autoRotate) {
|
|
|
- initCanvas();
|
|
|
- }
|
|
|
- }, 300); // 延迟确保屏幕方向变化完成
|
|
|
-}
|
|
|
-
|
|
|
// 清除画布
|
|
|
function clear() {
|
|
|
- if (!drawCtx) return;
|
|
|
-
|
|
|
- const { width, height } = size.value;
|
|
|
+ if (drawCtx == null) return;
|
|
|
|
|
|
// #ifdef APP
|
|
|
drawCtx!.reset();
|
|
|
// #endif
|
|
|
+
|
|
|
// #ifndef APP
|
|
|
- drawCtx!.clearRect(0, 0, width, height);
|
|
|
+ drawCtx!.clearRect(0, 0, props.width, props.height);
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // 获取设备像素比
|
|
|
+ const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
|
|
|
+
|
|
|
+ // #ifndef H5
|
|
|
+ // 设置缩放比例
|
|
|
+ drawCtx!.scale(dpr, dpr);
|
|
|
// #endif
|
|
|
|
|
|
// 填充背景色
|
|
|
drawCtx!.fillStyle = props.backgroundColor;
|
|
|
- drawCtx!.fillRect(0, 0, width, height);
|
|
|
+ drawCtx!.fillRect(0, 0, props.width, props.height);
|
|
|
|
|
|
emit("change");
|
|
|
}
|
|
|
|
|
|
// 获取签名图片
|
|
|
function toPng(): Promise<string> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- if (!canvasCtx) {
|
|
|
- reject(new Error("Canvas context not initialized"));
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- uni.canvasToTempFilePath(
|
|
|
- {
|
|
|
- canvasId: canvasId,
|
|
|
- success: (res) => {
|
|
|
- resolve(res.tempFilePath);
|
|
|
- },
|
|
|
- fail: (err) => {
|
|
|
- reject(err);
|
|
|
- }
|
|
|
- },
|
|
|
- proxy
|
|
|
- );
|
|
|
+ return canvasToPng({
|
|
|
+ proxy,
|
|
|
+ canvasId,
|
|
|
+ canvasRef: canvasRef.value!
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 初始化画布
|
|
|
function initCanvas() {
|
|
|
- const { width, height } = size.value;
|
|
|
-
|
|
|
uni.createCanvasContextAsync({
|
|
|
id: canvasId,
|
|
|
component: proxy,
|
|
|
success: (context: CanvasContext) => {
|
|
|
- // 设置canvas上下文
|
|
|
- canvasCtx = context;
|
|
|
-
|
|
|
// 获取绘图上下文
|
|
|
drawCtx = context.getContext("2d")!;
|
|
|
|
|
|
// 设置宽高
|
|
|
- drawCtx!.canvas.width = width;
|
|
|
- drawCtx!.canvas.height = height;
|
|
|
+ drawCtx!.canvas.width = props.width;
|
|
|
+ drawCtx!.canvas.height = props.height;
|
|
|
|
|
|
// 优化渲染质量
|
|
|
drawCtx!.textBaseline = "middle";
|
|
|
@@ -328,38 +318,26 @@ function initCanvas() {
|
|
|
|
|
|
// 初始化背景
|
|
|
clear();
|
|
|
+
|
|
|
+ // #ifdef MP
|
|
|
+ // 小程序中初始化时获取canvas位置信息
|
|
|
+ getCanvasRect();
|
|
|
+ // #endif
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
- // 判断横屏竖屏
|
|
|
- getOrientation();
|
|
|
-
|
|
|
- // 初始化画布
|
|
|
initCanvas();
|
|
|
|
|
|
- // 监听屏幕方向变化
|
|
|
- if (props.autoRotate) {
|
|
|
- uni.onWindowResize(onOrientationChange);
|
|
|
- }
|
|
|
-
|
|
|
- // 监听全屏状态变化
|
|
|
watch(
|
|
|
- () => props.fullscreen,
|
|
|
+ computed(() => [props.width, props.height]),
|
|
|
() => {
|
|
|
initCanvas();
|
|
|
}
|
|
|
);
|
|
|
});
|
|
|
|
|
|
-onUnmounted(() => {
|
|
|
- // 移除屏幕方向监听
|
|
|
- if (props.autoRotate) {
|
|
|
- uni.offWindowResize(onOrientationChange);
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
defineExpose({
|
|
|
clear,
|
|
|
toPng
|