添加cl-draggable组件
This commit is contained in:
1
App.uvue
1
App.uvue
@@ -3,6 +3,7 @@ import { useStore } from "@/cool";
|
|||||||
|
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
import TouchEmulator from "hammer-touchemulator";
|
import TouchEmulator from "hammer-touchemulator";
|
||||||
|
// 模拟移动端调试的触摸事件
|
||||||
TouchEmulator();
|
TouchEmulator();
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ const list = computed<Item[]>(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 隐藏原生 tabBar
|
// 隐藏原生 tabBar
|
||||||
|
// #ifndef MP
|
||||||
uni.hideTabBar();
|
uni.hideTabBar();
|
||||||
|
// #endif
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { computed, ref } from "vue";
|
|||||||
import uniTheme from "@/theme.json";
|
import uniTheme from "@/theme.json";
|
||||||
import { router } from "../router";
|
import { router } from "../router";
|
||||||
import { ctx } from "../ctx";
|
import { ctx } from "../ctx";
|
||||||
|
import { isNull } from "../utils";
|
||||||
|
|
||||||
// 主题类型定义,仅支持 light 和 dark
|
// 主题类型定义,仅支持 light 和 dark
|
||||||
type Theme = "light" | "dark";
|
type Theme = "light" | "dark";
|
||||||
@@ -46,7 +47,7 @@ export function getStyle(key: string): string | null {
|
|||||||
* @returns 颜色值
|
* @returns 颜色值
|
||||||
*/
|
*/
|
||||||
export const getColor = (name: string) => {
|
export const getColor = (name: string) => {
|
||||||
if (ctx.color == null) {
|
if (isNull(ctx.color)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cool-unix",
|
"name": "cool-unix",
|
||||||
"version": "8.0.2",
|
"version": "8.0.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
|
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
|
||||||
|
|||||||
@@ -285,6 +285,12 @@
|
|||||||
"navigationBarTitleText": "Avatar 头像"
|
"navigationBarTitleText": "Avatar 头像"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "data/draggable",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "Draggable 拖拽"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "status/badge",
|
"path": "status/badge",
|
||||||
"style": {
|
"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("时间轴"),
|
label: t("时间轴"),
|
||||||
icon: "timeline-view",
|
icon: "timeline-view",
|
||||||
path: "/pages/demo/data/timeline"
|
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 { ClColProps, ClColPassThrough } from "./components/cl-col/props";
|
||||||
import type { ClCollapseProps, ClCollapsePassThrough } from "./components/cl-collapse/props";
|
import type { ClCollapseProps, ClCollapsePassThrough } from "./components/cl-collapse/props";
|
||||||
import type { ClCountdownProps, ClCountdownPassThrough } from "./components/cl-countdown/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 { ClFloatViewProps } from "./components/cl-float-view/props";
|
||||||
import type { ClFooterProps, ClFooterPassThrough } from "./components/cl-footer/props";
|
import type { ClFooterProps, ClFooterPassThrough } from "./components/cl-footer/props";
|
||||||
import type { ClIconProps, ClIconPassThrough } from "./components/cl-icon/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-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-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-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-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-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>;
|
"cl-icon": (typeof import('./components/cl-icon/cl-icon.uvue')['default']) & import('vue').DefineComponent<ClIconProps>;
|
||||||
|
|||||||
Reference in New Issue
Block a user