icssoa преди 8 месеца
родител
ревизия
3b0e006ca0

+ 17 - 0
cool/utils/comm.ts

@@ -579,6 +579,23 @@ export function random(min: number, max: number): number {
 }
 
 /**
+ * 将base64转换为blob
+ * @param data base64数据
+ * @returns blob数据
+ */
+export function base64ToBlob(data: string, type: string = "image/jpeg"): Blob {
+	// #ifdef H5
+	let bytes = window.atob(data.split(",")[1]);
+	let ab = new ArrayBuffer(bytes.length);
+	let ia = new Uint8Array(ab);
+	for (let i = 0; i < bytes.length; i++) {
+		ia[i] = bytes.charCodeAt(i);
+	}
+	return new Blob([ab], { type });
+	// #endif
+}
+
+/**
  * 检查是否为小程序环境
  * @returns 是否为小程序环境
  */

+ 81 - 0
cool/utils/file.ts

@@ -0,0 +1,81 @@
+import { base64ToBlob } from "./comm";
+
+export type CanvasToPngOptions = {
+	canvasId: string;
+	proxy?: ComponentPublicInstance;
+	canvasRef: UniElement;
+};
+
+/**
+ * 将canvas转换为png图片
+ * @param options 转换参数
+ * @returns 图片路径
+ */
+export function canvasToPng(options: CanvasToPngOptions): Promise<string> {
+	return new Promise((resolve) => {
+		// #ifdef APP
+		options.canvasRef.parentElement!.takeSnapshot({
+			success(res) {
+				resolve(res.tempFilePath);
+			},
+			fail(err) {
+				console.error(err);
+				resolve("");
+			}
+		});
+		// #endif
+
+		// #ifdef H5
+		const url = URL.createObjectURL(
+			base64ToBlob(
+				(options.canvasRef as unknown as HTMLCanvasElement)
+					.querySelector("canvas")
+					?.toDataURL("image/png", 1) ?? ""
+			)
+		);
+
+		resolve(url);
+		// #endif
+
+		// #ifdef MP
+		uni.createCanvasContextAsync({
+			id: options.canvasId,
+			component: options.proxy,
+			success(context) {
+				// 获取2D绘图上下文
+				const ctx = context.getContext("2d")!;
+				const canvas = ctx.canvas;
+
+				// 将canvas转换为base64格式的PNG图片数据
+				const data = canvas.toDataURL("image/png", 1);
+				// 获取base64数据部分(去掉data:image/png;base64,前缀)
+				const bdataBase64 = data.split(",")[1];
+
+				// 获取文件系统管理器
+				const fileMg = uni.getFileSystemManager();
+				// 生成临时文件路径
+				// @ts-ignore
+				const filepath = `${wx.env.USER_DATA_PATH}/${uuid()}.png`;
+				// 将base64数据写入文件
+				fileMg.writeFile({
+					filePath: filepath,
+					data: bdataBase64,
+					encoding: "base64",
+					success() {
+						// 写入成功返回文件路径
+						resolve(filepath);
+					},
+					fail() {
+						// 写入失败返回空字符串
+						resolve("");
+					}
+				});
+			},
+			fail(err) {
+				console.error(err);
+				resolve("");
+			}
+		});
+		// #endif
+	});
+}

+ 1 - 0
cool/utils/index.ts

@@ -3,3 +3,4 @@ export * from "./storage";
 export * from "./path";
 export * from "./day";
 export * from "./parse";
+export * from "./file";

+ 7 - 5
pages/demo/other/sign.uvue

@@ -2,8 +2,8 @@
 	<cl-page>
 		<cl-sign
 			ref="signRef"
+			:height="isFullscreen ? windowHeight - 200 : 200"
 			:width="windowWidth"
-			:fullscreen="isFullscreen"
 			:enable-brush="isBrush"
 		></cl-sign>
 
@@ -30,19 +30,21 @@
 import { ref } from "vue";
 import DemoItem from "../components/item.uvue";
 
-const { windowWidth } = uni.getWindowInfo();
+const { windowWidth, windowHeight } = uni.getWindowInfo();
 
 const isFullscreen = ref(false);
 const isBrush = ref(true);
 const signRef = ref<ClSignComponentPublicInstance | null>(null);
 
 function clear() {
-	signRef.value?.clear();
+	signRef.value!.clear();
 }
 
 function preview() {
-	signRef.value?.toPng().then((res) => {
-		console.log(res);
+	signRef.value!.toPng().then((url) => {
+		uni.previewImage({
+			urls: [url]
+		});
 	});
 }
 </script>

+ 9 - 0
types/uni-app.d.ts

@@ -377,6 +377,15 @@ declare const onUnhandledRejection: (
 declare const onUnload: (hook: () => any, target?: ComponentInternalInstance | null) => void;
 
 declare interface UniElement {
+	firstChild: UniElement;
+	lastChild: UniElement;
+	previousSibling: UniElement;
+	parentElement: UniElement;
+	children: UniElement[];
+	attributes: Map<string, any>;
+	dataset: Map<string, any>;
+	style: CSSStyleDeclaration;
+	classList: string[];
 	takeSnapshot(options: {
 		success: (res: { tempFilePath: string }) => void;
 		fail: (err: { errCode: number; errMsg: string }) => void;

+ 9 - 76
uni_modules/cool-ui/components/cl-qrcode/cl-qrcode.uvue

@@ -1,8 +1,5 @@
 <template>
-	<view
-		ref="qrcodeRef"
-		:style="{ width: getPx(props.width) + 'px', height: getPx(props.height) + 'px' }"
-	>
+	<view :style="{ width: getPx(props.width) + 'px', height: getPx(props.height) + 'px' }">
 		<canvas
 			ref="canvasRef"
 			:canvas-id="qrcodeId"
@@ -22,13 +19,13 @@ import {
 	nextTick,
 	computed,
 	type PropType,
-	onUnmounted
+	onUnmounted,
+	shallowRef
 } from "vue";
 
 import { drawQrcode, type QrcodeOptions } from "./draw";
-import { getPx, isHarmony, uuid } from "@/cool";
+import { canvasToPng, getPx, isHarmony, uuid } from "@/cool";
 import type { ClQrcodeMode } from "../../types";
-import { base64ToBlob } from "./utils";
 
 defineOptions({
 	name: "cl-qrcode"
@@ -97,11 +94,8 @@ const { proxy } = getCurrentInstance()!;
 // 二维码组件id
 const qrcodeId = ref<string>("cl-qrcode-" + uuid());
 
-// 二维码组件元素
-const qrcodeRef = ref<UniElement | null>(null);
-
 // 二维码组件画布
-const canvasRef = ref(null);
+const canvasRef = shallowRef<UniElement | null>(null);
 
 /**
  * 主绘制方法,根据当前 props 生成二维码并绘制到 canvas。
@@ -148,71 +142,10 @@ function drawer() {
  * @param call 回调函数,返回图片路径,失败返回空字符串
  */
 function toPng(): Promise<string> {
-	return new Promise((resolve) => {
-		// #ifdef APP
-		qrcodeRef.value!.takeSnapshot({
-			success(res) {
-				resolve(res.tempFilePath);
-			},
-			fail(err) {
-				console.error(err);
-				resolve("");
-			}
-		});
-		// #endif
-
-		// #ifdef H5
-		const url = URL.createObjectURL(
-			base64ToBlob(
-				(canvasRef.value as unknown as HTMLCanvasElement)
-					.querySelector("canvas")
-					?.toDataURL("image/png", 1) ?? ""
-			)
-		);
-
-		resolve(url);
-		// #endif
-
-		// #ifdef MP-WEIXIN
-		uni.createCanvasContextAsync({
-			id: qrcodeId.value,
-			component: proxy,
-			success(context) {
-				// 获取2D绘图上下文
-				const ctx = context.getContext("2d")!;
-				const canvas = ctx.canvas;
-
-				// 将canvas转换为base64格式的PNG图片数据
-				const data = canvas.toDataURL("image/png", 1);
-				// 获取base64数据部分(去掉data:image/png;base64,前缀)
-				const bdataBase64 = data.split(",")[1];
-
-				// 获取文件系统管理器
-				const fileMg = uni.getFileSystemManager();
-				// 生成临时文件路径
-				// @ts-ignore
-				const filepath = `${wx.env.USER_DATA_PATH}/${uuid()}.png`;
-				// 将base64数据写入文件
-				fileMg.writeFile({
-					filePath: filepath,
-					data: bdataBase64,
-					encoding: "base64",
-					success() {
-						// 写入成功返回文件路径
-						resolve(filepath);
-					},
-					fail() {
-						// 写入失败返回空字符串
-						resolve("");
-					}
-				});
-			},
-			fail(err) {
-				console.error(err);
-				resolve("");
-			}
-		});
-		// #endif
+	return canvasToPng({
+		canvasId: qrcodeId.value,
+		proxy,
+		canvasRef: canvasRef.value!
 	});
 }
 

+ 0 - 16
uni_modules/cool-ui/components/cl-qrcode/utils.ts

@@ -1,16 +0,0 @@
-/**
- * 将base64转换为blob
- * @param data base64数据
- * @returns blob数据
- */
-export function base64ToBlob(data: string, type: string = "image/jpeg"): Blob {
-	// #ifdef H5
-	let bytes = window.atob(data.split(",")[1]);
-	let ab = new ArrayBuffer(bytes.length);
-	let ia = new Uint8Array(ab);
-	for (let i = 0; i < bytes.length; i++) {
-		ia[i] = bytes.charCodeAt(i);
-	}
-	return new Blob([ab], { type });
-	// #endif
-}

+ 93 - 115
uni_modules/cool-ui/components/cl-sign/cl-sign.uvue

@@ -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