兼容 ios

This commit is contained in:
icssoa
2025-09-04 20:18:18 +08:00
parent 083c8b1325
commit f01d1107b9
15 changed files with 316 additions and 146 deletions

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@ohos/svg": "2.2.1"
}
}

View File

@@ -0,0 +1,5 @@
import { BuilderNode } from "@kit.ArkUI";
export class CoolSvg {
load(src: string, color: string) {}
}

View File

@@ -10,6 +10,7 @@
>
<!-- 图片容器 - 可拖拽和缩放的图片区域 -->
<view class="cl-cropper__image">
<!-- @vue-ignore -->
<image
class="cl-cropper__image-inner"
:class="[

View File

@@ -3,28 +3,35 @@
class="cl-draggable"
:class="[
{
'cl-draggable--grid': props.columns > 1
'cl-draggable--columns': props.columns > 1
},
pt.className
]"
>
<!-- @vue-ignore -->
<view
v-for="(item, index) in list"
:key="getItemKey(item, index)"
class="cl-draggable__item"
:class="[
{
'cl-draggable__item--disabled': disabled
},
dragging && dragIndex == index ? `opacity-80 ${pt.ghost?.className}` : ''
'cl-draggable__item--disabled': disabled,
'cl-draggable__item--dragging': dragging && dragIndex == index,
'cl-draggable__item--animating': dragging && dragIndex != index
}
]"
:style="getItemStyle(index)"
@touchstart="
(event: UniTouchEvent) => {
onTouchStart(event, index);
onTouchStart(event, index, 'touch');
}
"
@touchmove.stop.prevent="onTouchMove"
@longpress="
(event: UniTouchEvent) => {
onTouchStart(event, index, 'longpress');
}
"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>
<slot
@@ -44,6 +51,7 @@
import { computed, ref, getCurrentInstance, type PropType, watch } from "vue";
import { isNull, parsePt, uuid } from "@/cool";
import type { PassThroughProps } from "../../types";
import { vibrate } from "@/uni_modules/cool-vibrate";
defineOptions({
name: "cl-draggable"
@@ -89,15 +97,15 @@ const props = defineProps({
type: Boolean,
default: false
},
/** 动画持续时间(毫秒) */
animation: {
type: Number,
default: 150
},
/** 列数1为单列纵向布局>1为多列网格布局 */
columns: {
type: Number,
default: 1
},
// 是否需要长按触发
longPress: {
type: Boolean,
default: true
}
});
@@ -376,12 +384,6 @@ function getItemStyle(index: number) {
return style;
}
// 为非拖拽元素添加过渡动画
if (props.animation > 0 && !isCurrent) {
style["transition-property"] = "transform";
style["transition-duration"] = `${props.animation}ms`;
}
// 拖拽状态下的样式处理
if (dragging.value) {
if (isCurrent) {
@@ -391,9 +393,7 @@ function getItemStyle(index: number) {
} else {
// 其他元素:显示排序预览位移
const translateOffset = getItemTranslateOffset(index);
if (translateOffset.x != 0 || translateOffset.y != 0) {
style["transform"] = `translate(${translateOffset.x}px, ${translateOffset.y}px)`;
}
style["transform"] = `translate(${translateOffset.x}px, ${translateOffset.y}px)`;
}
}
@@ -513,22 +513,31 @@ function checkMovedToOtherElement(): boolean {
* @param event 触摸事件对象
* @param index 触摸的项目索引
*/
async function onTouchStart(event: UniTouchEvent, index: number): Promise<void> {
async function onTouchStart(event: UniTouchEvent, index: number, type: string) {
// 如果是长按触发,但未开启长按功能,则直接返回
if (type == "longpress" && !props.longPress) return;
// 如果是普通触摸触发,但已开启长按功能,则直接返回
if (type == "touch" && props.longPress) return;
// 检查是否禁用或索引无效
if (props.disabled) return;
if (getItemDisabled(index)) return;
if (index < 0 || index >= list.value.length) return;
// 获取触摸点
const touch = event.touches[0];
// 初始化拖拽状态
dragging.value = true;
// 初始化拖拽索引
dragIndex.value = index;
insertIndex.value = index; // 初始插入位置为原位置
startX.value = touch.clientX;
startY.value = touch.clientY;
offsetX.value = 0;
offsetY.value = 0;
// 初始化拖拽数据项
dragItem.value = list.value[index];
// 先获取所有项目的位置信息,为后续计算做准备
@@ -536,6 +545,14 @@ async function onTouchStart(event: UniTouchEvent, index: number): Promise<void>
// 触发开始事件
emit("start", index);
// 震动
vibrate(1);
// 阻止事件冒泡
event.stopPropagation();
// 阻止默认行为
event.preventDefault();
}
/**
@@ -575,7 +592,7 @@ function onTouchMove(event: TouchEvent): void {
}
}
// 阻止默认行为和事件冒泡
// 阻止默认行为
event.preventDefault();
}
@@ -585,7 +602,10 @@ function onTouchMove(event: TouchEvent): void {
function onTouchEnd(): void {
if (!dragging.value) return;
// 旧索引
const oldIndex = dragIndex.value;
// 新索引
const newIndex = insertIndex.value;
// 如果位置发生变化,立即更新数组
@@ -604,15 +624,11 @@ function onTouchEnd(): void {
dropping.value = true;
dragging.value = false;
// 让拖拽元素回到自然位置(偏移归零)
offsetX.value = 0;
offsetY.value = 0;
// 重置所有状态
reset();
// 等待放下动画完成后重置所有状态
setTimeout(() => {
emit("end", newIndex >= 0 ? newIndex : oldIndex);
reset();
}, 10);
emit("end", newIndex >= 0 ? newIndex : oldIndex);
}
/**
@@ -652,21 +668,30 @@ watch(
<style lang="scss" scoped>
.cl-draggable {
@apply flex-col relative overflow-visible;
}
.cl-draggable--grid {
@apply flex-row flex-wrap;
}
&--columns {
@apply flex-row flex-wrap;
}
.cl-draggable__item {
@apply relative;
}
&__item {
@apply relative z-10;
.cl-draggable__item--dragging {
@apply opacity-80;
}
// #ifdef APP-IOS
@apply transition-none opacity-100;
// #endif
.cl-draggable__item--disabled {
@apply opacity-60;
&--dragging {
@apply opacity-80 z-20;
}
&--disabled {
@apply opacity-60;
}
&--animating {
@apply duration-200;
transition-property: transform;
}
}
}
</style>

View File

@@ -22,8 +22,6 @@
:hold-keyboard="false"
:clearable="false"
@change="onChange"
@focus="animateCursor(true)"
@blur="animateCursor(true)"
></cl-input>
</view>
@@ -36,12 +34,13 @@
{
'is-disabled': disabled,
'is-dark': isDark,
'is-active': value.length == index && isFocus
'is-active': value.length >= index && isFocus
},
pt.item?.className
]"
>
<cl-text
:color="value.length >= index && isFocus ? 'primary' : ''"
:pt="{
className: pt.value?.className
}"
@@ -50,9 +49,6 @@
<view
class="cl-input-otp__cursor"
:class="[pt.cursor?.className]"
:style="{
opacity: cursorOpacity
}"
v-if="value.length == index && isFocus && item == ''"
></view>
</view>
@@ -184,51 +180,6 @@ function onChange(val: string) {
emit("done", val);
}
}
/**
* 光标闪烁透明度值
* 范围: 0.3-1.0
*/
const cursorOpacity = ref(0.3);
/**
* 光标闪烁动画帧ID
*/
let cursorAnimationId = 0;
/**
* 控制光标闪烁动画
* @param isIncreasing 透明度是否递增
*/
function animateCursor(isIncreasing: boolean) {
// #ifdef APP
// 未获得焦点时不执行动画
if (!isFocus.value) {
return;
}
// 取消上一次动画
if (cursorAnimationId != 0) {
cancelAnimationFrame(cursorAnimationId);
cursorAnimationId = 0;
}
// 执行动画帧
cursorAnimationId = requestAnimationFrame(() => {
// 根据方向调整透明度值
cursorOpacity.value += isIncreasing ? 0.01 : -0.01;
// 到达边界值时改变方向
if (cursorOpacity.value > 1) {
animateCursor(false);
} else if (cursorOpacity.value <= 0.3) {
animateCursor(true);
} else {
animateCursor(isIncreasing);
}
});
// #endif
}
</script>
<style lang="scss" scoped>
@@ -248,7 +199,7 @@ function animateCursor(isIncreasing: boolean) {
}
&__item {
@apply flex flex-row items-center justify-center;
@apply flex flex-row items-center justify-center duration-100;
@apply border border-solid border-surface-200 rounded-lg bg-white;
height: 80rpx;
width: 80rpx;
@@ -272,10 +223,10 @@ function animateCursor(isIncreasing: boolean) {
}
&__cursor {
@apply absolute;
@apply absolute duration-100;
@apply bg-primary-500;
width: 2rpx;
height: 36rpx;
height: 24rpx;
}
// #ifdef H5 || MP
@@ -285,7 +236,7 @@ function animateCursor(isIncreasing: boolean) {
@keyframes flash {
0% {
opacity: 0.3;
opacity: 0;
}
50% {
@@ -293,7 +244,7 @@ function animateCursor(isIncreasing: boolean) {
}
100% {
opacity: 0.3;
opacity: 0;
}
}
// #endif

View File

@@ -1,33 +1,25 @@
<template>
<view
ref="loadingRef"
class="cl-loading"
:class="[
{
'cl-loading--dark': isDark && color == '',
'cl-loading--spin': loading,
'!border-primary-500': color == 'primary',
'!border-green-500': color == 'success',
'!border-yellow-500': color == 'warn',
'!border-red-500': color == 'error',
'!border-surface-500': color == 'info',
'!border-surface-700': color == 'dark',
'!border-white': color == 'light',
'!border-surface-300': color == 'disabled',
'!border-r-transparent': true
},
pt.className
]"
:style="{
// #ifdef APP
transform: `rotate(${rotate}deg)`,
// #endif
height: getRpx(size!),
width: getRpx(size!),
borderWidth: getRpx(2),
height: getPx(size!),
width: getPx(size!),
// #ifndef APP
borderWidth: '1px',
borderTopColor: color,
borderRightColor: 'transparent',
borderBottomColor: color,
borderLeftColor: color
// #endif
}"
v-if="loading"
>
@@ -35,8 +27,8 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { isDark, parsePt } from "@/cool";
import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
import { ctx, isDark, parsePt } from "@/cool";
import type { ClIconProps } from "../cl-icon/props";
import { useSize } from "../../hooks";
@@ -68,7 +60,7 @@ const props = defineProps({
}
});
const { getRpx } = useSize();
const { getPxValue, getPx } = useSize();
// 透传样式类型定义
type PassThrough = {
@@ -79,25 +71,113 @@ type PassThrough = {
// 解析透传样式
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 旋转角度
const rotate = ref(0);
// 组件引用
const loadingRef = shallowRef<UniElement | null>(null);
const color = computed<string>(() => {
if (props.color == "") {
return isDark.value ? "#ffffff" : (ctx.color["surface-700"] as string);
}
switch (props.color) {
case "primary":
return ctx.color["primary-500"] as string;
case "success":
return "#22c55e";
case "warn":
return "#eab308";
case "error":
return "#ef4444";
case "info":
return "#71717a";
case "dark":
return "#3f3f46";
case "light":
return "#ffffff";
case "disabled":
return "#d4d4d8";
default:
return props.color;
}
});
async function drawLoading() {
// #ifdef APP
await nextTick();
if (loadingRef.value == null) {
return;
}
const drawContext = loadingRef.value!.getDrawableContext();
// 重置画布准备绘制新的loading图形
drawContext!.reset();
drawContext!.beginPath();
// 获取loading图标的尺寸和半径
const size = getPxValue(props.size!);
const radius = size / 2;
const centerX = radius;
const centerY = radius;
// 设置线宽
const lineWidth = 1;
// 缺口角度为60度Math.PI / 3用于形成loading的缺口效果
const gapAngle = Math.PI / 3; // 缺口60度
// 起始角度为顶部(-90度
const startAngle = -Math.PI / 2; // 从顶部开始
// 结束角度为起始角度加上300度360-60形成环形缺口
const endAngle = startAngle + (2 * Math.PI - gapAngle); // 画300度
// 绘制圆弧形成loading环
drawContext!.arc(centerX, centerY, radius - lineWidth, startAngle, endAngle, false);
// 设置描边颜色和线宽
drawContext!.strokeStyle = color.value;
drawContext!.lineWidth = lineWidth;
// 执行描边操作
drawContext!.stroke();
// 更新画布显示
drawContext!.update();
// #endif
}
// 开始旋转动画
function start() {
requestAnimationFrame(() => {
// 增加旋转角度
rotate.value += 1;
async function start() {
// #ifdef APP
await drawLoading();
// 如果仍在加载中则继续旋转
if (props.loading) {
start();
if (loadingRef.value == null) {
return;
}
loadingRef.value!.animate(
[
{
transform: "rotate(0deg)"
},
{
transform: "rotate(360deg)"
}
],
{
duration: 2500,
easing: "linear",
iterations: 999999
}
});
);
// #endif
}
// 组件挂载后监听loading状态
onMounted(() => {
// #ifdef APP-UVUE
// #ifdef APP
watch(
computed(() => props.loading),
(val: boolean) => {
@@ -110,6 +190,13 @@ onMounted(() => {
immediate: true
}
);
watch(
computed(() => [props.color, props.size, isDark.value]),
() => {
drawLoading();
}
);
// #endif
});
</script>
@@ -117,7 +204,10 @@ onMounted(() => {
<style lang="scss" scoped>
.cl-loading {
@apply flex flex-row items-center justify-center rounded-full;
// #ifndef APP
@apply border-surface-700 border-solid;
// #endif
&--dark {
border-color: white !important;

View File

@@ -2,7 +2,7 @@
<view class="cl-loadmore-wrapper">
<view class="cl-loadmore">
<cl-loading
:size="30"
:size="28"
:pt="{
className: `mr-2 ${pt.icon?.className}`
}"
@@ -59,7 +59,7 @@ const props = defineProps({
// 是否显示底部安全区
safeAreaBottom: {
type: Boolean,
default: true
default: false
}
});

View File

@@ -384,7 +384,7 @@ function onTouchEnd() {
angle: currentAngle.value
});
}
vibrate(2); // 震动反馈
vibrate(1); // 震动反馈
}
// 监听模式变化,重新初始化

View File

@@ -1,5 +1,6 @@
import { computed, type ComputedRef } from "vue";
import { config } from "../config";
import { rpx2px } from "@/cool";
/**
* 字号管理类
@@ -73,6 +74,35 @@ class Size {
}
};
/**
* 获取px值
* @param val - 需要转换的值 10、10rpx、10px
* @returns 转换后的px值
*/
getPxValue = (val: number | string) => {
const scale = this.getScale();
if (typeof val == "string") {
const num = parseFloat(val);
const unit = val.replace(`${num}`, "");
if (unit == "px") {
return num * scale;
} else {
return rpx2px(num * scale);
}
} else {
return rpx2px(val * scale);
}
};
/**
* 获取px值
*/
getPx = (val: number | string) => {
return this.getPxValue(val) + "px";
};
/**
* 获取当前字号在预设中的索引
*/