icssoa 8 месяцев назад
Родитель
Сommit
28a6627224

BIN
.cool/icons/remixicon.zip


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.cool/remixicon/RemixIcon_Collection_2507261544.remixicon


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
.cool/remixicon/RemixIcon_Collection_2507282304.remixicon


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
icons/remixicon/index.scss


+ 2 - 2
icons/remixicon/index.ts

@@ -263,7 +263,6 @@ export const remixicon = {
 	"list-check": "eeba",
 	"list-ordered": "eebb",
 	"list-radio": "f39b",
-	"translate-2": "f226",
 	"sort-asc": "f15f",
 	"sort-desc": "f160",
 	"send-backward": "f0d6",
@@ -695,5 +694,6 @@ export const remixicon = {
 	"shield-user-line": "f10c",
 	"shield-user-fill": "f10b",
 	"circle-line": "f3c2",
-	"circle-fill": "f3c1"
+	"circle-fill": "f3c1",
+	"sketching": "f35f"
 };

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 	"name": "cool-unix",
-	"version": "8.0.3",
+	"version": "8.0.4",
 	"license": "MIT",
 	"scripts": {
 		"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",

+ 6 - 0
pages.json

@@ -370,6 +370,12 @@
 					}
 				},
 				{
+					"path": "other/sign",
+					"style": {
+						"navigationBarTitleText": "Sign 签名"
+					}
+				},
+				{
 					"path": "other/day-uts",
 					"style": {
 						"navigationBarTitleText": "DayUts 日期"

+ 48 - 0
pages/demo/other/sign.uvue

@@ -0,0 +1,48 @@
+<template>
+	<cl-page>
+		<cl-sign
+			ref="signRef"
+			:width="windowWidth"
+			:fullscreen="isFullscreen"
+			:enable-brush="isBrush"
+		></cl-sign>
+
+		<view class="p-3">
+			<cl-list>
+				<cl-list-item label="操作">
+					<cl-button type="info" @click="clear">清空</cl-button>
+					<cl-button @click="preview">预览</cl-button>
+				</cl-list-item>
+
+				<cl-list-item label="全屏">
+					<cl-switch v-model="isFullscreen"></cl-switch>
+				</cl-list-item>
+
+				<cl-list-item label="毛笔效果">
+					<cl-switch v-model="isBrush"></cl-switch>
+				</cl-list-item>
+			</cl-list>
+		</view>
+	</cl-page>
+</template>
+
+<script setup lang="ts">
+import { ref } from "vue";
+import DemoItem from "../components/item.uvue";
+
+const { windowWidth } = uni.getWindowInfo();
+
+const isFullscreen = ref(false);
+const isBrush = ref(true);
+const signRef = ref<ClSignComponentPublicInstance | null>(null);
+
+function clear() {
+	signRef.value?.clear();
+}
+
+function preview() {
+	signRef.value?.toPng().then((res) => {
+		console.log(res);
+	});
+}
+</script>

+ 5 - 0
pages/index/home.uvue

@@ -358,6 +358,11 @@ const data = computed<Item[]>(() => {
 					path: "/pages/demo/other/qrcode"
 				},
 				{
+					label: "签名",
+					icon: "sketching",
+					path: "/pages/demo/other/sign"
+				},
+				{
 					label: "DayUts",
 					icon: "timer-2-line",
 					path: "/pages/demo/other/day-uts"

+ 1 - 1
uni_modules/cool-ui/components/cl-page/back-top.uvue

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

+ 1 - 1
uni_modules/cool-ui/components/cl-page/theme.uvue

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

+ 8 - 1
uni_modules/cool-ui/components/cl-select-date/props.ts

@@ -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 - 0
uni_modules/cool-ui/components/cl-sign/cl-sign.uvue

@@ -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 - 0
uni_modules/cool-ui/components/cl-sign/props.ts

@@ -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;
+};

+ 3 - 1
uni_modules/cool-ui/index.d.ts

@@ -1,4 +1,4 @@
-import type { ClActionSheetItem, ClActionSheetOptions, PassThroughProps, Type, ClButtonType, Size, ClListViewItem, ClInputType, ClListItem, Justify, ClConfirmAction, ClConfirmOptions, ClToastOptions, ClSelectOption, ClPopupDirection, ClQrcodeMode, ClTabsItem, ClTextType, ClUploadItem } from "./types";
+import type { ClActionSheetItem, ClActionSheetOptions, PassThroughProps, Type, ClButtonType, Size, ClListViewItem, ClInputType, ClListItem, Justify, ClConfirmAction, ClConfirmOptions, ClToastOptions, ClSelectOption, ClPopupDirection, ClQrcodeMode, ClSelectDateShortcut, ClTabsItem, ClTextType, ClUploadItem } from "./types";
 import { type UiInstance } from "./hooks";
 import { type QrcodeOptions } from "./draw";
 
@@ -48,6 +48,7 @@ import type { ClSelectProps, ClSelectPassThrough } from "./components/cl-select/
 import type { ClSelectDateProps, ClSelectDatePassThrough } from "./components/cl-select-date/props";
 import type { ClSelectTimeProps, ClSelectTimePassThrough } from "./components/cl-select-time/props";
 import type { ClSelectTriggerProps, ClSelectTriggerPassThrough } from "./components/cl-select-trigger/props";
+import type { ClSignProps, ClSignPassThrough } from "./components/cl-sign/props";
 import type { ClSkeletonProps, ClSkeletonPassThrough } from "./components/cl-skeleton/props";
 import type { ClSliderProps, ClSliderPassThrough } from "./components/cl-slider/props";
 import type { ClStickyProps } from "./components/cl-sticky/props";
@@ -114,6 +115,7 @@ declare module "vue" {
 		"cl-select-date": (typeof import('./components/cl-select-date/cl-select-date.uvue')['default']) & import('vue').DefineComponent<ClSelectDateProps>;
 		"cl-select-time": (typeof import('./components/cl-select-time/cl-select-time.uvue')['default']) & import('vue').DefineComponent<ClSelectTimeProps>;
 		"cl-select-trigger": (typeof import('./components/cl-select-trigger/cl-select-trigger.uvue')['default']) & import('vue').DefineComponent<ClSelectTriggerProps>;
+		"cl-sign": (typeof import('./components/cl-sign/cl-sign.uvue')['default']) & import('vue').DefineComponent<ClSignProps>;
 		"cl-skeleton": (typeof import('./components/cl-skeleton/cl-skeleton.uvue')['default']) & import('vue').DefineComponent<ClSkeletonProps>;
 		"cl-slider": (typeof import('./components/cl-slider/cl-slider.uvue')['default']) & import('vue').DefineComponent<ClSliderProps>;
 		"cl-sticky": (typeof import('./components/cl-sticky/cl-sticky.uvue')['default']) & import('vue').DefineComponent<ClStickyProps>;

+ 5 - 0
uni_modules/cool-ui/types/component.d.ts

@@ -147,3 +147,8 @@ declare type ClQrcodeComponentPublicInstance = {
 declare type ClProgressCircleComponentPublicInstance = {
 	animate: (value: number) => void;
 };
+
+declare type ClSignComponentPublicInstance = {
+	clear: () => void;
+	toPng: () => Promise<string>;
+};

Некоторые файлы не были показаны из-за большого количества измененных файлов