implement event sort

This commit is contained in:
Timi
2026-08-14 15:18:56 +08:00
parent 25b6e0dacc
commit 8f49cc3f82
3 changed files with 101 additions and 191 deletions
+81 -191
View File
@@ -1,43 +1,46 @@
<template>
<div v-can="permissionCodes.event.update" class="registration-event-sort">
<div class="tip" v-text="'按住右侧拖动手柄调整事件顺序,保存后生效'" />
<div ref="sortListRef" class="items">
<div class="items">
<t-cell-group v-if="eventList.length" theme="card">
<t-cell
class="cell"
:title="item.name || '未命名事件'"
v-for="(item, index) in eventList"
:key="item.id"
:class="{ dragging: draggingIndex === index }"
<vue-draggable-next
v-model="eventList"
class="draggable"
item-key="id"
handle=".handle"
ghost-class="ghost"
chosen-class="chosen"
drag-class="dragging"
:animation="180"
:disabled="isLoading || isSaving"
@change="handleSortChange"
>
<template #leftIcon>
<div class="color" :style="{ background: item.color || defaultColor }" />
</template>
<template #note>
<t-tag
:theme="getStatusTheme(item.status)"
variant="outline"
:content="getStatusText(item.status)"
/>
</template>
<template #rightIcon>
<t-icon
class="handle"
role="button"
name="move"
aria-label="拖动排序"
@pointerdown.stop="startDrag($event, index)"
@pointermove.stop="moveDrag"
@pointerup.stop="finishDrag"
@pointercancel.stop="finishDrag"
@lostpointercapture.stop="finishDrag"
@touchstart.stop="startTouchDrag($event, index)"
@touchmove.stop="moveTouchDrag"
@touchend.stop="finishTouchDrag"
@touchcancel.stop="finishTouchDrag"
/>
</template>
</t-cell>
<t-cell
v-for="element in eventList"
:key="element.id"
class="cell"
:title="element.name || '未命名事件'"
>
<template #leftIcon>
<div class="color" :style="{ background: element.color || defaultColor }" />
</template>
<template #note>
<t-tag
:theme="getStatusTheme(element.status)"
variant="outline"
:content="getStatusText(element.status)"
/>
</template>
<template #rightIcon>
<t-icon
class="handle"
role="button"
name="move"
aria-label="拖动排序"
/>
</template>
</t-cell>
</vue-draggable-next>
</t-cell-group>
<div v-else class="empty-block" v-text="isLoading ? '正在加载登记事件' : '暂无登记事件'" />
</div>
@@ -55,6 +58,7 @@
<script setup lang="ts">
import { Toast, type TagProps } from "tdesign-mobile-vue";
import { VueDraggableNext } from "vue-draggable-next";
import EventAPI from "@/api/EventAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem, EventStatus } from "@/types/EventItem";
@@ -65,25 +69,16 @@ defineOptions({
});
const navBarStore = useNavBarStore();
const router = useRouter();
// ---------- 列表数据 ----------
const defaultColor = "#2f88ff";
const eventList = ref<EventItem[]>([]);
const sortListRef = ref<HTMLElement>();
const isLoading = ref(false);
const isSaving = ref(false);
const sortDirty = ref(false);
// ---------- 拖拽状态 ----------
const draggingIndex = ref(-1);
let pointerId: number | undefined;
let pointerStartY = 0;
let pointerMoved = false;
let pointerTarget: HTMLElement | undefined;
let dragInputType: "pointer" | "touch" | undefined;
// ---------- 展示文案 ----------
function getStatusText(status?: EventStatus): string {
@@ -108,124 +103,10 @@ function getStatusTheme(status?: EventStatus): TagProps["theme"] {
// ---------- 拖拽排序 ----------
function resolveDropIndex(clientY: number): number {
const elements = sortListRef.value?.querySelectorAll<HTMLElement>(".item");
if (!elements?.length) {
return -1;
}
for (let index = 0; index < elements.length; index++) {
const rect = elements[index].getBoundingClientRect();
if (clientY < rect.top + rect.height / 2) {
return index;
}
}
return elements.length - 1;
}
function beginDrag(index: number, clientY: number, target: EventTarget | null): void {
pointerStartY = clientY;
pointerMoved = false;
draggingIndex.value = index;
pointerTarget = target instanceof HTMLElement ? target : undefined;
}
function moveDragAt(clientY: number): void {
if (0 > draggingIndex.value) {
return;
}
if (!pointerMoved && 8 > Math.abs(clientY - pointerStartY)) {
return;
}
pointerMoved = true;
const targetIndex = resolveDropIndex(clientY);
if (0 > targetIndex || targetIndex === draggingIndex.value) {
return;
}
const [item] = eventList.value.splice(draggingIndex.value, 1);
eventList.value.splice(targetIndex, 0, item);
draggingIndex.value = targetIndex;
function handleSortChange(): void {
sortDirty.value = true;
}
function finishDragState(): void {
if (pointerMoved) {
sortDirty.value = true;
}
pointerId = undefined;
pointerTarget = undefined;
dragInputType = undefined;
draggingIndex.value = -1;
}
function startDrag(event: PointerEvent, index: number): void {
if (isSaving.value || isLoading.value || dragInputType || !event.isPrimary || event.button !== 0) {
return;
}
event.preventDefault();
pointerId = event.pointerId;
dragInputType = "pointer";
beginDrag(index, event.clientY, event.currentTarget);
pointerTarget?.setPointerCapture(event.pointerId);
}
function moveDrag(event: PointerEvent): void {
if (dragInputType !== "pointer" || pointerId !== event.pointerId || 0 > draggingIndex.value) {
return;
}
event.preventDefault();
moveDragAt(event.clientY);
}
function finishDrag(event: PointerEvent): void {
if (dragInputType !== "pointer" || pointerId !== event.pointerId) {
return;
}
if (pointerTarget?.hasPointerCapture(event.pointerId)) {
pointerTarget.releasePointerCapture(event.pointerId);
}
finishDragState();
}
function startTouchDrag(event: TouchEvent, index: number): void {
if (isSaving.value || isLoading.value || dragInputType || event.touches.length !== 1) {
return;
}
event.preventDefault();
const touch = event.touches[0];
pointerId = touch.identifier;
dragInputType = "touch";
beginDrag(index, touch.clientY, event.currentTarget);
}
function moveTouchDrag(event: TouchEvent): void {
if (dragInputType !== "touch" || pointerId === undefined || 0 > draggingIndex.value || event.touches.length !== 1) {
return;
}
event.preventDefault();
moveDragAt(event.touches[0].clientY);
}
function finishTouchDrag(event: TouchEvent): void {
if (dragInputType !== "touch" || pointerId === undefined) {
return;
}
const touch = event.changedTouches[0];
if (touch && touch.identifier !== pointerId) {
return;
}
finishDragState();
}
// ---------- 保存 ----------
async function saveSort(): Promise<void> {
@@ -244,6 +125,7 @@ async function saveSort(): Promise<void> {
});
sortDirty.value = false;
Toast.success("登记事件排序已保存");
await router.back();
} catch {
Toast.error("登记事件排序保存失败");
} finally {
@@ -292,39 +174,41 @@ onUnmounted(() => {
}
.items {
.draggable {
.cell {
position: relative;
.cell {
position: relative;
.color {
flex: 0 0 auto;
width: 1.5rem;
height: 1.5rem;
border-radius: .4rem;
}
&.dragging {
.name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.handle {
color: #52607a;
cursor: grab;
touch-action: none;
}
}
.ghost {
opacity: .4;
}
.chosen,
.dragging {
z-index: 1;
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16);
}
.color {
flex: 0 0 auto;
width: 1.5rem;
height: 1.5rem;
border-radius: .4rem;
}
.name {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.handle {
color: #52607a;
cursor: grab;
}
.cell.dragging,
.cell.dragging * {
touch-action: none;
}
}
}
@@ -350,9 +234,15 @@ onUnmounted(() => {
color: #94a3b8;
}
.item {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
.draggable {
.cell {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
}
.ghost {
background: rgba(15, 23, 42, .6);
}
}
}
}