添加 cl-sign 签名组件
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
'is-show': visible
|
||||
}"
|
||||
>
|
||||
<cl-icon name="skip-up-line" color="white" :size="50"></cl-icon>
|
||||
<cl-icon name="skip-up-line" color="white" size="25px"></cl-icon>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="theme-set" @tap="toggleTheme()">
|
||||
<view class="theme-set__inner" :class="{ 'is-dark': isDark }">
|
||||
<view class="theme-set__icon" v-for="item in list" :key="item">
|
||||
<cl-icon :name="item" color="white" :size="36"></cl-icon>
|
||||
<cl-icon :name="item" color="white" size="18px"></cl-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClSelectOption } from "../../types";
|
||||
import type { ClSelectDateShortcut, ClSelectOption } from "../../types";
|
||||
import type { ClSelectTriggerPassThrough } from "../cl-select-trigger/props";
|
||||
import type { ClPopupPassThrough } from "../cl-popup/props";
|
||||
|
||||
@@ -11,6 +11,7 @@ export type ClSelectDateProps = {
|
||||
className?: string;
|
||||
pt?: ClSelectDatePassThrough;
|
||||
modelValue?: string;
|
||||
values?: string[];
|
||||
headers?: string[];
|
||||
title?: string;
|
||||
placeholder?: string;
|
||||
@@ -25,4 +26,10 @@ export type ClSelectDateProps = {
|
||||
start?: string;
|
||||
end?: string;
|
||||
type?: "year" | "month" | "date" | "hour" | "minute" | "second";
|
||||
rangeable?: boolean;
|
||||
startPlaceholder?: string;
|
||||
endPlaceholder?: string;
|
||||
rangeSeparator?: string;
|
||||
showShortcuts?: boolean;
|
||||
shortcuts?: ClSelectDateShortcut[];
|
||||
};
|
||||
|
||||
367
uni_modules/cool-ui/components/cl-sign/cl-sign.uvue
Normal file
367
uni_modules/cool-ui/components/cl-sign/cl-sign.uvue
Normal file
@@ -0,0 +1,367 @@
|
||||
<template>
|
||||
<view class="cl-sign" :class="[pt.className]">
|
||||
<canvas
|
||||
class="cl-sign__canvas"
|
||||
:id="canvasId"
|
||||
:style="{
|
||||
height: `${size.height}px`,
|
||||
width: `${size.width}px`
|
||||
}"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
></canvas>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { parsePt, uuid } from "@/cool";
|
||||
import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
defineOptions({
|
||||
name: "cl-sign"
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
// 画布宽度
|
||||
width: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
// 画布高度
|
||||
height: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
// 线条颜色
|
||||
strokeColor: {
|
||||
type: String,
|
||||
default: "#000000"
|
||||
},
|
||||
// 线条宽度
|
||||
strokeWidth: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
// 背景颜色
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: "#ffffff"
|
||||
},
|
||||
// 是否启用毛笔效果
|
||||
enableBrush: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 最小线条宽度
|
||||
minStrokeWidth: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
// 最大线条宽度
|
||||
maxStrokeWidth: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
// 速度敏感度
|
||||
velocitySensitivity: {
|
||||
type: Number,
|
||||
default: 0.7
|
||||
},
|
||||
// 是否支持横屏自适应
|
||||
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 PassThrough = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// 解析透传样式配置
|
||||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||
|
||||
// canvas组件上下文
|
||||
let canvasCtx: CanvasContext | null = null;
|
||||
|
||||
// 绘图上下文
|
||||
let drawCtx: CanvasRenderingContext2D | null = null;
|
||||
|
||||
// 生成唯一的canvas ID
|
||||
const canvasId = `cl-sign__${uuid()}`;
|
||||
|
||||
// 触摸状态
|
||||
const isDrawing = ref(false);
|
||||
|
||||
// 上一个触摸点
|
||||
let lastPoint: Point | null = null;
|
||||
|
||||
// 当前线条宽度
|
||||
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中的坐标
|
||||
function getTouchPos(e: TouchEvent): Point {
|
||||
const touch = e.touches[0];
|
||||
const rect = (e.target as any).getBoundingClientRect();
|
||||
return {
|
||||
x: touch.clientX - rect.left,
|
||||
y: touch.clientY - rect.top,
|
||||
time: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// 计算速度并返回动态线条宽度
|
||||
function calculateStrokeWidth(currentPoint: Point): number {
|
||||
if (!lastPoint || !props.enableBrush) {
|
||||
return props.strokeWidth;
|
||||
}
|
||||
|
||||
// 计算距离和时间差
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(currentPoint.x - lastPoint.x, 2) + Math.pow(currentPoint.y - lastPoint.y, 2)
|
||||
);
|
||||
const timeDelta = currentPoint.time - lastPoint.time;
|
||||
|
||||
if (timeDelta <= 0) return currentStrokeWidth.value;
|
||||
|
||||
// 计算速度 (像素/毫秒)
|
||||
const velocity = distance / timeDelta;
|
||||
|
||||
// 添加到速度缓冲区(用于平滑)
|
||||
velocityBuffer.push(velocity);
|
||||
if (velocityBuffer.length > 5) {
|
||||
velocityBuffer.shift();
|
||||
}
|
||||
|
||||
// 计算平均速度
|
||||
const avgVelocity = velocityBuffer.reduce((sum, v) => sum + v, 0) / velocityBuffer.length;
|
||||
|
||||
// 根据速度计算线条宽度(速度越快越细)
|
||||
const normalizedVelocity = Math.min(avgVelocity * props.velocitySensitivity, 1);
|
||||
const widthRange = props.maxStrokeWidth - props.minStrokeWidth;
|
||||
const targetWidth = props.maxStrokeWidth - normalizedVelocity * widthRange;
|
||||
|
||||
// 平滑过渡到目标宽度
|
||||
const smoothFactor = 0.3;
|
||||
return currentStrokeWidth.value + (targetWidth - currentStrokeWidth.value) * smoothFactor;
|
||||
}
|
||||
|
||||
// 触摸开始
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
isDrawing.value = true;
|
||||
lastPoint = getTouchPos(e);
|
||||
|
||||
// 初始化线条宽度和清空速度缓冲
|
||||
currentStrokeWidth.value = props.enableBrush ? props.maxStrokeWidth : props.strokeWidth;
|
||||
velocityBuffer.length = 0;
|
||||
}
|
||||
|
||||
// 触摸移动
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
if (!isDrawing.value || !lastPoint || !drawCtx) return;
|
||||
|
||||
const currentPoint = getTouchPos(e);
|
||||
|
||||
// 计算动态线条宽度
|
||||
const strokeWidth = calculateStrokeWidth(currentPoint);
|
||||
currentStrokeWidth.value = strokeWidth;
|
||||
|
||||
// 绘制线条
|
||||
drawCtx!.beginPath();
|
||||
drawCtx!.moveTo(lastPoint.x, lastPoint.y);
|
||||
drawCtx!.lineTo(currentPoint.x, currentPoint.y);
|
||||
drawCtx!.strokeStyle = props.strokeColor;
|
||||
drawCtx!.lineWidth = strokeWidth;
|
||||
drawCtx!.lineCap = "round";
|
||||
drawCtx!.lineJoin = "round";
|
||||
drawCtx!.stroke();
|
||||
|
||||
lastPoint = currentPoint;
|
||||
emit("change");
|
||||
}
|
||||
|
||||
// 触摸结束
|
||||
function onTouchEnd(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
isDrawing.value = false;
|
||||
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;
|
||||
|
||||
// #ifdef APP
|
||||
drawCtx!.reset();
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
drawCtx!.clearRect(0, 0, width, height);
|
||||
// #endif
|
||||
|
||||
// 填充背景色
|
||||
drawCtx!.fillStyle = props.backgroundColor;
|
||||
drawCtx!.fillRect(0, 0, width, 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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化画布
|
||||
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!.textBaseline = "middle";
|
||||
drawCtx!.textAlign = "center";
|
||||
drawCtx!.miterLimit = 10;
|
||||
|
||||
// 初始化背景
|
||||
clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 判断横屏竖屏
|
||||
getOrientation();
|
||||
|
||||
// 初始化画布
|
||||
initCanvas();
|
||||
|
||||
// 监听屏幕方向变化
|
||||
if (props.autoRotate) {
|
||||
uni.onWindowResize(onOrientationChange);
|
||||
}
|
||||
|
||||
// 监听全屏状态变化
|
||||
watch(
|
||||
() => props.fullscreen,
|
||||
() => {
|
||||
initCanvas();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除屏幕方向监听
|
||||
if (props.autoRotate) {
|
||||
uni.offWindowResize(onOrientationChange);
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
clear,
|
||||
toPng
|
||||
});
|
||||
</script>
|
||||
21
uni_modules/cool-ui/components/cl-sign/props.ts
Normal file
21
uni_modules/cool-ui/components/cl-sign/props.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type ClSignPassThrough = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type ClSignProps = {
|
||||
className?: string;
|
||||
pt?: ClSignPassThrough;
|
||||
width?: number;
|
||||
height?: number;
|
||||
strokeColor?: string;
|
||||
strokeWidth?: number;
|
||||
backgroundColor?: string;
|
||||
enableBrush?: boolean;
|
||||
minStrokeWidth?: number;
|
||||
maxStrokeWidth?: number;
|
||||
velocitySensitivity?: number;
|
||||
autoRotate?: boolean;
|
||||
landscapeWidthRatio?: number;
|
||||
landscapeHeightRatio?: number;
|
||||
fullscreen?: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user