添加cl-draggable组件
This commit is contained in:
1
App.uvue
1
App.uvue
@@ -3,6 +3,7 @@ import { useStore } from "@/cool";
|
||||
|
||||
// #ifdef H5
|
||||
import TouchEmulator from "hammer-touchemulator";
|
||||
// 模拟移动端调试的触摸事件
|
||||
TouchEmulator();
|
||||
// #endif
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ const list = computed<Item[]>(() => {
|
||||
});
|
||||
|
||||
// 隐藏原生 tabBar
|
||||
// #ifndef MP
|
||||
uni.hideTabBar();
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { computed, ref } from "vue";
|
||||
import uniTheme from "@/theme.json";
|
||||
import { router } from "../router";
|
||||
import { ctx } from "../ctx";
|
||||
import { isNull } from "../utils";
|
||||
|
||||
// 主题类型定义,仅支持 light 和 dark
|
||||
type Theme = "light" | "dark";
|
||||
@@ -46,7 +47,7 @@ export function getStyle(key: string): string | null {
|
||||
* @returns 颜色值
|
||||
*/
|
||||
export const getColor = (name: string) => {
|
||||
if (ctx.color == null) {
|
||||
if (isNull(ctx.color)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cool-unix",
|
||||
"version": "8.0.2",
|
||||
"version": "8.0.3",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
|
||||
|
||||
@@ -285,6 +285,12 @@
|
||||
"navigationBarTitleText": "Avatar 头像"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "data/draggable",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Draggable 拖拽"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "status/badge",
|
||||
"style": {
|
||||
|
||||
178
pages/demo/data/draggable.uvue
Normal file
178
pages/demo/data/draggable.uvue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<view class="p-3 overflow-visible">
|
||||
<demo-item label="纵向排列">
|
||||
<cl-draggable v-model="list">
|
||||
<template #item="{ item, index }">
|
||||
<view
|
||||
class="flex flex-row items-center p-3 bg-surface-100 rounded-lg mb-2"
|
||||
:class="{
|
||||
'!bg-surface-300': (item as UTSJSONObject).disabled
|
||||
}"
|
||||
>
|
||||
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
|
||||
</view>
|
||||
</template>
|
||||
</cl-draggable>
|
||||
</demo-item>
|
||||
|
||||
<demo-item label="结合列表使用">
|
||||
<cl-list border>
|
||||
<cl-draggable v-model="list2">
|
||||
<template #item="{ item, index, dragging, dragIndex }">
|
||||
<cl-list-item
|
||||
icon="chat-thread-line"
|
||||
:label="(item as UTSJSONObject).label"
|
||||
arrow
|
||||
:pt="{
|
||||
inner: {
|
||||
className:
|
||||
dragging && dragIndex == index ? '!bg-surface-100' : ''
|
||||
}
|
||||
}"
|
||||
></cl-list-item>
|
||||
</template>
|
||||
</cl-draggable>
|
||||
</cl-list>
|
||||
</demo-item>
|
||||
|
||||
<demo-item label="横向排列">
|
||||
<cl-draggable v-model="list3" :columns="4">
|
||||
<template #item="{ item, index }">
|
||||
<view
|
||||
class="flex flex-row items-center justify-center p-3 bg-surface-100 rounded-lg m-1"
|
||||
:class="{
|
||||
'!bg-surface-300': (item as UTSJSONObject).disabled
|
||||
}"
|
||||
>
|
||||
<cl-text>{{ (item as UTSJSONObject).label }}</cl-text>
|
||||
</view>
|
||||
</template>
|
||||
</cl-draggable>
|
||||
</demo-item>
|
||||
|
||||
<demo-item label="结合图片使用">
|
||||
<cl-draggable v-model="list4" :columns="4">
|
||||
<template #item="{ item, index }">
|
||||
<view class="p-[2px]">
|
||||
<cl-image
|
||||
:src="(item as UTSJSONObject).url"
|
||||
mode="widthFix"
|
||||
:pt="{
|
||||
className: '!w-full'
|
||||
}"
|
||||
preview
|
||||
></cl-image>
|
||||
</view>
|
||||
</template>
|
||||
</cl-draggable>
|
||||
</demo-item>
|
||||
</view>
|
||||
</cl-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import DemoItem from "../components/item.uvue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const list = ref<UTSJSONObject[]>([
|
||||
{
|
||||
label: "明月几时有,把酒问青天"
|
||||
},
|
||||
{
|
||||
label: "不知天上宫阙,今夕是何年",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: "我欲乘风归去,又恐琼楼玉宇"
|
||||
},
|
||||
{
|
||||
label: "高处不胜寒,起舞弄清影"
|
||||
},
|
||||
{
|
||||
label: "何似在人间"
|
||||
}
|
||||
]);
|
||||
|
||||
const list2 = ref<UTSJSONObject[]>([
|
||||
{
|
||||
label: "明月几时有,把酒问青天"
|
||||
},
|
||||
{
|
||||
label: "不知天上宫阙,今夕是何年"
|
||||
},
|
||||
{
|
||||
label: "我欲乘风归去,又恐琼楼玉宇"
|
||||
},
|
||||
{
|
||||
label: "高处不胜寒,起舞弄清影"
|
||||
},
|
||||
{
|
||||
label: "何似在人间"
|
||||
}
|
||||
]);
|
||||
|
||||
const list3 = ref<UTSJSONObject[]>([
|
||||
{
|
||||
label: "项目1"
|
||||
},
|
||||
{
|
||||
label: "项目2"
|
||||
},
|
||||
{
|
||||
label: "项目3"
|
||||
},
|
||||
{
|
||||
label: "项目4"
|
||||
},
|
||||
{
|
||||
label: "项目5"
|
||||
},
|
||||
{
|
||||
label: "项目6"
|
||||
},
|
||||
{
|
||||
label: "项目7"
|
||||
},
|
||||
{
|
||||
label: "项目8",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: "项目9"
|
||||
},
|
||||
{
|
||||
label: "项目10"
|
||||
},
|
||||
{
|
||||
label: "项目11"
|
||||
},
|
||||
{
|
||||
label: "项目12"
|
||||
}
|
||||
]);
|
||||
|
||||
const list4 = ref<UTSJSONObject[]>([
|
||||
{
|
||||
url: "https://unix.cool-js.com/images/demo/1.jpg"
|
||||
},
|
||||
{
|
||||
url: "https://unix.cool-js.com/images/demo/2.jpg"
|
||||
},
|
||||
{
|
||||
url: "https://unix.cool-js.com/images/demo/3.jpg"
|
||||
},
|
||||
{
|
||||
url: "https://unix.cool-js.com/images/demo/4.jpg"
|
||||
},
|
||||
{
|
||||
url: "https://unix.cool-js.com/images/demo/5.jpg"
|
||||
},
|
||||
{
|
||||
url: "https://unix.cool-js.com/images/demo/6.jpg"
|
||||
},
|
||||
{
|
||||
url: "https://unix.cool-js.com/images/demo/7.jpg"
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
@@ -271,6 +271,11 @@ const data = computed<Item[]>(() => {
|
||||
label: t("时间轴"),
|
||||
icon: "timeline-view",
|
||||
path: "/pages/demo/data/timeline"
|
||||
},
|
||||
{
|
||||
label: t("拖拽"),
|
||||
icon: "drag-move-line",
|
||||
path: "/pages/demo/data/draggable"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
673
uni_modules/cool-ui/components/cl-draggable/cl-draggable.uvue
Normal file
673
uni_modules/cool-ui/components/cl-draggable/cl-draggable.uvue
Normal file
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<view
|
||||
class="cl-draggable"
|
||||
:class="[
|
||||
{
|
||||
'cl-draggable--grid': props.columns > 1
|
||||
},
|
||||
pt.className
|
||||
]"
|
||||
>
|
||||
<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}` : ''
|
||||
]"
|
||||
:style="getItemStyle(index)"
|
||||
@touchstart="
|
||||
(event: UniTouchEvent) => {
|
||||
onTouchStart(event, index);
|
||||
}
|
||||
"
|
||||
@touchmove.stop.prevent="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<slot
|
||||
name="item"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:dragging="dragging"
|
||||
:dragIndex="dragIndex"
|
||||
:insertIndex="insertIndex"
|
||||
>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, getCurrentInstance, type PropType, watch } from "vue";
|
||||
import { isNull, parsePt, uuid } from "@/cool";
|
||||
import type { PassThroughProps } from "../../types";
|
||||
|
||||
defineOptions({
|
||||
name: "cl-draggable"
|
||||
});
|
||||
|
||||
defineSlots<{
|
||||
item(props: {
|
||||
item: UTSJSONObject;
|
||||
index: number;
|
||||
dragging: boolean;
|
||||
dragIndex: number;
|
||||
insertIndex: number;
|
||||
}): any;
|
||||
}>();
|
||||
|
||||
// 项目位置信息类型定义
|
||||
type ItemPosition = {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
// 位移偏移量类型定义
|
||||
type TranslateOffset = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
/** PassThrough 样式配置 */
|
||||
pt: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
/** 数据数组,支持双向绑定 */
|
||||
modelValue: {
|
||||
type: Array as PropType<UTSJSONObject[]>,
|
||||
default: () => []
|
||||
},
|
||||
/** 是否禁用拖拽功能 */
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 动画持续时间(毫秒) */
|
||||
animation: {
|
||||
type: Number,
|
||||
default: 150
|
||||
},
|
||||
/** 列数:1为单列纵向布局,>1为多列网格布局 */
|
||||
columns: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "change", "start", "end"]);
|
||||
|
||||
const { proxy } = getCurrentInstance()!;
|
||||
|
||||
// 透传样式类型定义
|
||||
type PassThrough = {
|
||||
className?: string;
|
||||
ghost?: PassThroughProps;
|
||||
};
|
||||
|
||||
/** PassThrough 样式解析 */
|
||||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||
|
||||
/** 数据列表 */
|
||||
const list = ref<UTSJSONObject[]>([]);
|
||||
|
||||
/** 是否正在拖拽 */
|
||||
const dragging = ref(false);
|
||||
/** 当前拖拽元素的原始索引 */
|
||||
const dragIndex = ref(-1);
|
||||
/** 预期插入的目标索引 */
|
||||
const insertIndex = ref(-1);
|
||||
/** 触摸开始时的Y坐标 */
|
||||
const startY = ref(0);
|
||||
/** 触摸开始时的X坐标 */
|
||||
const startX = ref(0);
|
||||
/** Y轴偏移量 */
|
||||
const offsetY = ref(0);
|
||||
/** X轴偏移量 */
|
||||
const offsetX = ref(0);
|
||||
/** 当前拖拽的数据项 */
|
||||
const dragItem = ref<UTSJSONObject>({});
|
||||
/** 所有项目的位置信息缓存 */
|
||||
const itemPositions = ref<ItemPosition[]>([]);
|
||||
/** 是否处于放下动画状态 */
|
||||
const dropping = ref(false);
|
||||
/** 动态计算的项目高度 */
|
||||
const itemHeight = ref(0);
|
||||
/** 动态计算的项目宽度 */
|
||||
const itemWidth = ref(0);
|
||||
/** 是否已开始排序模拟(防止误触) */
|
||||
const sortingStarted = ref(false);
|
||||
|
||||
/**
|
||||
* 重置所有拖拽相关的状态
|
||||
* 在拖拽结束后调用,确保组件回到初始状态
|
||||
*/
|
||||
function reset() {
|
||||
dragging.value = false; // 拖拽状态
|
||||
dropping.value = false; // 放下动画状态
|
||||
dragIndex.value = -1; // 拖拽元素索引
|
||||
insertIndex.value = -1; // 插入位置索引
|
||||
offsetX.value = 0; // X轴偏移
|
||||
offsetY.value = 0; // Y轴偏移
|
||||
dragItem.value = {}; // 拖拽的数据项
|
||||
itemPositions.value = []; // 位置信息缓存
|
||||
itemHeight.value = 0; // 动态计算的高度
|
||||
itemWidth.value = 0; // 动态计算的宽度
|
||||
sortingStarted.value = false; // 排序模拟状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算网格布局中元素的位移偏移
|
||||
* @param index 当前元素索引
|
||||
* @param dragIdx 拖拽元素索引
|
||||
* @param insertIdx 插入位置索引
|
||||
* @returns 包含 x 和 y 坐标偏移的对象
|
||||
*/
|
||||
function calculateGridOffset(index: number, dragIdx: number, insertIdx: number): TranslateOffset {
|
||||
const cols = props.columns;
|
||||
|
||||
// 计算当前元素在网格中的行列位置
|
||||
const currentRow = Math.floor(index / cols);
|
||||
const currentCol = index % cols;
|
||||
|
||||
// 计算元素在拖拽后的新位置索引
|
||||
let newIndex = index;
|
||||
|
||||
if (dragIdx < insertIdx) {
|
||||
// 向后拖拽:dragIdx+1 到 insertIdx 之间的元素需要向前移动一位
|
||||
if (index > dragIdx && index <= insertIdx) {
|
||||
newIndex = index - 1;
|
||||
}
|
||||
} else if (dragIdx > insertIdx) {
|
||||
// 向前拖拽:insertIdx 到 dragIdx-1 之间的元素需要向后移动一位
|
||||
if (index >= insertIdx && index < dragIdx) {
|
||||
newIndex = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算新位置的行列坐标
|
||||
const newRow = Math.floor(newIndex / cols);
|
||||
const newCol = newIndex % cols;
|
||||
|
||||
// 使用动态计算的网格尺寸
|
||||
const cellWidth = itemWidth.value;
|
||||
const cellHeight = itemHeight.value;
|
||||
|
||||
// 计算实际的像素位移
|
||||
const offsetX = (newCol - currentCol) * cellWidth;
|
||||
const offsetY = (newRow - currentRow) * cellHeight;
|
||||
|
||||
return { x: offsetX, y: offsetY };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算网格布局的插入位置
|
||||
* @param dragCenterX 拖拽元素中心点X坐标
|
||||
* @param dragCenterY 拖拽元素中心点Y坐标
|
||||
* @returns 最佳插入位置索引
|
||||
*/
|
||||
function calculateGridInsertIndex(dragCenterX: number, dragCenterY: number): number {
|
||||
if (itemPositions.value.length == 0) {
|
||||
return dragIndex.value;
|
||||
}
|
||||
|
||||
let closestIndex = dragIndex.value;
|
||||
let minDistance = Infinity;
|
||||
|
||||
// 使用欧几里得距离找到最近的网格位置(包括原位置)
|
||||
for (let i = 0; i < itemPositions.value.length; i++) {
|
||||
const position = itemPositions.value[i];
|
||||
|
||||
// 计算到元素中心点的距离
|
||||
const centerX = position.left + position.width / 2;
|
||||
const centerY = position.top + position.height / 2;
|
||||
|
||||
// 使用欧几里得距离公式
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(dragCenterX - centerX, 2) + Math.pow(dragCenterY - centerY, 2)
|
||||
);
|
||||
|
||||
// 更新最近的位置
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单列布局的插入位置
|
||||
* @param clientY Y坐标
|
||||
* @returns 最佳插入位置索引
|
||||
*/
|
||||
function calculateSingleColumnInsertIndex(clientY: number): number {
|
||||
let closestIndex = dragIndex.value;
|
||||
let minDistance = Infinity;
|
||||
|
||||
// 遍历所有元素,找到距离最近的元素中心
|
||||
for (let i = 0; i < itemPositions.value.length; i++) {
|
||||
const position = itemPositions.value[i];
|
||||
|
||||
// 计算到元素中心点的距离
|
||||
const itemCenter = position.top + position.height / 2;
|
||||
const distance = Math.abs(clientY - itemCenter);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算拖拽元素的最佳插入位置
|
||||
* @param clientPosition 在主轴上的坐标(仅用于单列布局的Y轴坐标)
|
||||
* @returns 最佳插入位置的索引
|
||||
*/
|
||||
function calculateInsertIndex(clientPosition: number): number {
|
||||
// 如果没有位置信息,保持原位置
|
||||
if (itemPositions.value.length == 0) {
|
||||
return dragIndex.value;
|
||||
}
|
||||
|
||||
// 根据布局类型选择计算方式
|
||||
if (props.columns > 1) {
|
||||
// 多列网格布局:计算拖拽元素的中心点坐标,使用2D坐标计算最近位置
|
||||
const dragPos = itemPositions.value[dragIndex.value];
|
||||
const dragCenterX = dragPos.left + dragPos.width / 2 + offsetX.value;
|
||||
const dragCenterY = dragPos.top + dragPos.height / 2 + offsetY.value;
|
||||
return calculateGridInsertIndex(dragCenterX, dragCenterY);
|
||||
} else {
|
||||
// 单列布局:基于Y轴距离计算最近的元素中心
|
||||
return calculateSingleColumnInsertIndex(clientPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单列布局的位移偏移
|
||||
* @param index 元素索引
|
||||
* @param dragIdx 拖拽元素索引
|
||||
* @param insertIdx 插入位置索引
|
||||
* @returns 位移偏移对象
|
||||
*/
|
||||
function calculateSingleColumnOffset(
|
||||
index: number,
|
||||
dragIdx: number,
|
||||
insertIdx: number
|
||||
): TranslateOffset {
|
||||
if (dragIdx < insertIdx) {
|
||||
// 向下拖拽:dragIdx+1 到 insertIdx 之间的元素向上移动
|
||||
if (index > dragIdx && index <= insertIdx) {
|
||||
return { x: 0, y: -itemHeight.value };
|
||||
}
|
||||
} else if (dragIdx > insertIdx) {
|
||||
// 向上拖拽:insertIdx 到 dragIdx-1 之间的元素向下移动
|
||||
if (index >= insertIdx && index < dragIdx) {
|
||||
return { x: 0, y: itemHeight.value };
|
||||
}
|
||||
}
|
||||
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算非拖拽元素的位移偏移量
|
||||
* @param index 元素索引
|
||||
* @returns 包含 x 和 y 坐标偏移的对象
|
||||
*/
|
||||
function getItemTranslateOffset(index: number): TranslateOffset {
|
||||
// 只在满足所有条件时才计算位移:拖拽中、非放下状态、已开始排序
|
||||
if (!dragging.value || dropping.value || !sortingStarted.value) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
const dragIdx = dragIndex.value;
|
||||
const insertIdx = insertIndex.value;
|
||||
|
||||
// 跳过正在拖拽的元素(拖拽元素由位置控制)
|
||||
if (index == dragIdx) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
// 没有位置变化时不需要位移(拖回原位置)
|
||||
if (dragIdx == insertIdx) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
// 根据布局类型计算位移
|
||||
if (props.columns > 1) {
|
||||
// 多列网格布局:使用2D位移计算
|
||||
return calculateGridOffset(index, dragIdx, insertIdx);
|
||||
} else {
|
||||
// 单列布局:使用简单的纵向位移
|
||||
return calculateSingleColumnOffset(index, dragIdx, insertIdx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算项目的完整样式对象
|
||||
* @param index 项目索引
|
||||
* @returns 样式对象
|
||||
*/
|
||||
function getItemStyle(index: number) {
|
||||
const style = {};
|
||||
const isCurrent = dragIndex.value == index;
|
||||
|
||||
// 多列布局时设置等宽分布
|
||||
if (props.columns > 1) {
|
||||
const widthPercent = 100 / props.columns;
|
||||
style["flex-basis"] = `${widthPercent}%`;
|
||||
style["width"] = `${widthPercent}%`;
|
||||
style["box-sizing"] = "border-box";
|
||||
}
|
||||
|
||||
// 放下动画期间,只保留基础样式
|
||||
if (dropping.value) {
|
||||
return style;
|
||||
}
|
||||
|
||||
// 为非拖拽元素添加过渡动画
|
||||
if (props.animation > 0 && !isCurrent) {
|
||||
style["transition-property"] = "transform";
|
||||
style["transition-duration"] = `${props.animation}ms`;
|
||||
style["transition-timing-function"] = "ease";
|
||||
}
|
||||
|
||||
// 拖拽状态下的样式处理
|
||||
if (dragging.value) {
|
||||
if (isCurrent) {
|
||||
// 拖拽元素:跟随移动
|
||||
style["transform"] = `translate(${offsetX.value}px, ${offsetY.value}px)`;
|
||||
style["z-index"] = "100";
|
||||
} else {
|
||||
// 其他元素:显示排序预览位移
|
||||
const translateOffset = getItemTranslateOffset(index);
|
||||
if (translateOffset.x != 0 || translateOffset.y != 0) {
|
||||
style["transform"] = `translate(${translateOffset.x}px, ${translateOffset.y}px)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有项目的位置信息
|
||||
*/
|
||||
async function getItemPosition(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.select(".cl-draggable")
|
||||
.boundingClientRect()
|
||||
.exec((res) => {
|
||||
const box = res[0] as NodeInfo;
|
||||
|
||||
itemWidth.value = (box.width ?? 0) / props.columns;
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.in(proxy)
|
||||
.selectAll(".cl-draggable__item")
|
||||
.boundingClientRect()
|
||||
.exec((res) => {
|
||||
const rects = res[0] as NodeInfo[];
|
||||
const positions: ItemPosition[] = [];
|
||||
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
const rect = rects[i];
|
||||
|
||||
if (i == 0) {
|
||||
itemHeight.value = rect.height ?? 0;
|
||||
}
|
||||
|
||||
positions.push({
|
||||
top: rect.top ?? 0,
|
||||
left: rect.left ?? 0,
|
||||
width: itemWidth.value,
|
||||
height: itemHeight.value
|
||||
});
|
||||
}
|
||||
|
||||
itemPositions.value = positions;
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目是否禁用
|
||||
* @param index 项目索引
|
||||
* @returns 是否禁用
|
||||
*/
|
||||
function getItemDisabled(index: number): boolean {
|
||||
return !isNull(list.value[index]["disabled"]) && (list.value[index]["disabled"] as boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查拖拽元素的中心点是否移动到其他元素区域
|
||||
*/
|
||||
function checkMovedToOtherElement(): boolean {
|
||||
// 如果没有位置信息,默认未移出
|
||||
if (itemPositions.value.length == 0) return false;
|
||||
|
||||
const dragIdx = dragIndex.value;
|
||||
const dragPosition = itemPositions.value[dragIdx];
|
||||
|
||||
// 计算拖拽元素当前的中心点位置(考虑拖拽偏移)
|
||||
const dragCenterX = dragPosition.left + dragPosition.width / 2 + offsetX.value;
|
||||
const dragCenterY = dragPosition.top + dragPosition.height / 2 + offsetY.value;
|
||||
|
||||
// 根据布局类型采用不同的判断策略
|
||||
if (props.columns > 1) {
|
||||
// 多列网格布局:检查中心点是否与其他元素区域重叠
|
||||
for (let i = 0; i < itemPositions.value.length; i++) {
|
||||
if (i == dragIdx) continue;
|
||||
|
||||
const otherPosition = itemPositions.value[i];
|
||||
const isOverlapping =
|
||||
dragCenterX >= otherPosition.left &&
|
||||
dragCenterX <= otherPosition.left + otherPosition.width &&
|
||||
dragCenterY >= otherPosition.top &&
|
||||
dragCenterY <= otherPosition.top + otherPosition.height;
|
||||
|
||||
if (isOverlapping) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 检查是否向上移动超过上一个元素的中线
|
||||
if (dragIdx > 0) {
|
||||
const prevPosition = itemPositions.value[dragIdx - 1];
|
||||
const prevCenterY = prevPosition.top + prevPosition.height / 2;
|
||||
if (dragCenterY <= prevCenterY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否向下移动超过下一个元素的中线
|
||||
if (dragIdx < itemPositions.value.length - 1) {
|
||||
const nextPosition = itemPositions.value[dragIdx + 1];
|
||||
const nextCenterY = nextPosition.top + nextPosition.height / 2;
|
||||
if (dragCenterY >= nextCenterY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸开始事件处理
|
||||
* @param event 触摸事件对象
|
||||
* @param index 触摸的项目索引
|
||||
*/
|
||||
async function onTouchStart(event: UniTouchEvent, index: number): Promise<void> {
|
||||
// 检查是否禁用或索引无效
|
||||
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];
|
||||
|
||||
// 先获取所有项目的位置信息,为后续计算做准备
|
||||
await getItemPosition();
|
||||
|
||||
// 触发开始事件
|
||||
emit("start", index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸移动事件处理
|
||||
* @param event 触摸事件对象
|
||||
*/
|
||||
function onTouchMove(event: TouchEvent): void {
|
||||
if (!dragging.value) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
|
||||
// 更新拖拽偏移量
|
||||
offsetX.value = touch.clientX - startX.value;
|
||||
offsetY.value = touch.clientY - startY.value;
|
||||
|
||||
// 智能启动排序模拟:只有移出原元素区域才开始
|
||||
if (!sortingStarted.value) {
|
||||
if (checkMovedToOtherElement()) {
|
||||
sortingStarted.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 只有开始排序模拟后才计算插入位置
|
||||
if (sortingStarted.value) {
|
||||
// 计算拖拽元素当前的中心点坐标
|
||||
const dragPos = itemPositions.value[dragIndex.value];
|
||||
const dragCenterX = dragPos.left + dragPos.width / 2 + offsetX.value;
|
||||
const dragCenterY = dragPos.top + dragPos.height / 2 + offsetY.value;
|
||||
|
||||
// 根据布局类型选择坐标轴:网格布局使用X坐标,单列布局使用Y坐标
|
||||
const dragCenter = props.columns > 1 ? dragCenterX : dragCenterY;
|
||||
|
||||
// 计算最佳插入位置
|
||||
const newIndex = calculateInsertIndex(dragCenter);
|
||||
if (newIndex != insertIndex.value) {
|
||||
insertIndex.value = newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
// 阻止默认行为和事件冒泡
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* 触摸结束事件处理
|
||||
*/
|
||||
function onTouchEnd(): void {
|
||||
if (!dragging.value) return;
|
||||
|
||||
const oldIndex = dragIndex.value;
|
||||
const newIndex = insertIndex.value;
|
||||
|
||||
// 如果位置发生变化,立即更新数组
|
||||
if (oldIndex != newIndex && newIndex >= 0) {
|
||||
const newList = [...list.value];
|
||||
const item = newList.splice(oldIndex, 1)[0];
|
||||
newList.splice(newIndex, 0, item);
|
||||
list.value = newList;
|
||||
|
||||
// 触发变化事件
|
||||
emit("update:modelValue", list.value);
|
||||
emit("change", list.value);
|
||||
}
|
||||
|
||||
// 开始放下动画
|
||||
dropping.value = true;
|
||||
dragging.value = false;
|
||||
|
||||
// 让拖拽元素回到自然位置(偏移归零)
|
||||
offsetX.value = 0;
|
||||
offsetY.value = 0;
|
||||
|
||||
// 等待放下动画完成后重置所有状态
|
||||
setTimeout(() => {
|
||||
emit("end", newIndex >= 0 ? newIndex : oldIndex);
|
||||
reset();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据平台选择合适的key
|
||||
* @param item 数据项
|
||||
* @param index 索引
|
||||
* @returns 合适的key
|
||||
*/
|
||||
function getItemKey(item: UTSJSONObject, index: number): string {
|
||||
// #ifdef MP
|
||||
// 小程序环境使用 index 作为 key,避免数据错乱
|
||||
return `${index}`;
|
||||
// #endif
|
||||
|
||||
// #ifndef MP
|
||||
// 其他平台使用 uid,提供更好的性能
|
||||
return item["uid"] as string;
|
||||
// #endif
|
||||
}
|
||||
|
||||
watch(
|
||||
computed(() => props.modelValue),
|
||||
(val: UTSJSONObject[]) => {
|
||||
list.value = val.map((e) => {
|
||||
return {
|
||||
uid: e["uid"] ?? uuid(),
|
||||
...e
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-draggable {
|
||||
@apply flex-col relative overflow-visible;
|
||||
}
|
||||
|
||||
.cl-draggable--grid {
|
||||
@apply flex-row flex-wrap;
|
||||
}
|
||||
|
||||
.cl-draggable__item {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.cl-draggable__item--dragging {
|
||||
@apply opacity-80;
|
||||
}
|
||||
|
||||
.cl-draggable__item--disabled {
|
||||
@apply opacity-60;
|
||||
}
|
||||
</style>
|
||||
15
uni_modules/cool-ui/components/cl-draggable/props.ts
Normal file
15
uni_modules/cool-ui/components/cl-draggable/props.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { PassThroughProps } from "../../types";
|
||||
|
||||
export type ClDraggablePassThrough = {
|
||||
className?: string;
|
||||
ghost?: PassThroughProps;
|
||||
};
|
||||
|
||||
export type ClDraggableProps = {
|
||||
className?: string;
|
||||
pt?: ClDraggablePassThrough;
|
||||
modelValue?: UTSJSONObject[];
|
||||
disabled?: boolean;
|
||||
animation?: number;
|
||||
columns?: number;
|
||||
};
|
||||
2
uni_modules/cool-ui/index.d.ts
vendored
2
uni_modules/cool-ui/index.d.ts
vendored
@@ -12,6 +12,7 @@ import type { ClCheckboxProps, ClCheckboxPassThrough } from "./components/cl-che
|
||||
import type { ClColProps, ClColPassThrough } from "./components/cl-col/props";
|
||||
import type { ClCollapseProps, ClCollapsePassThrough } from "./components/cl-collapse/props";
|
||||
import type { ClCountdownProps, ClCountdownPassThrough } from "./components/cl-countdown/props";
|
||||
import type { ClDraggableProps, ClDraggablePassThrough } from "./components/cl-draggable/props";
|
||||
import type { ClFloatViewProps } from "./components/cl-float-view/props";
|
||||
import type { ClFooterProps, ClFooterPassThrough } from "./components/cl-footer/props";
|
||||
import type { ClIconProps, ClIconPassThrough } from "./components/cl-icon/props";
|
||||
@@ -77,6 +78,7 @@ declare module "vue" {
|
||||
"cl-col": (typeof import('./components/cl-col/cl-col.uvue')['default']) & import('vue').DefineComponent<ClColProps>;
|
||||
"cl-collapse": (typeof import('./components/cl-collapse/cl-collapse.uvue')['default']) & import('vue').DefineComponent<ClCollapseProps>;
|
||||
"cl-countdown": (typeof import('./components/cl-countdown/cl-countdown.uvue')['default']) & import('vue').DefineComponent<ClCountdownProps>;
|
||||
"cl-draggable": (typeof import('./components/cl-draggable/cl-draggable.uvue')['default']) & import('vue').DefineComponent<ClDraggableProps>;
|
||||
"cl-float-view": (typeof import('./components/cl-float-view/cl-float-view.uvue')['default']) & import('vue').DefineComponent<ClFloatViewProps>;
|
||||
"cl-footer": (typeof import('./components/cl-footer/cl-footer.uvue')['default']) & import('vue').DefineComponent<ClFooterProps>;
|
||||
"cl-icon": (typeof import('./components/cl-icon/cl-icon.uvue')['default']) & import('vue').DefineComponent<ClIconProps>;
|
||||
|
||||
Reference in New Issue
Block a user