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
+1
View File
@@ -24,6 +24,7 @@
"timi-web": "link:E:/WebProject/timi-web", "timi-web": "link:E:/WebProject/timi-web",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"vue": "^3.5.16", "vue": "^3.5.16",
"vue-draggable-next": "^2.3.0",
"vue-echarts": "^8.0.1", "vue-echarts": "^8.0.1",
"vue-router": "4.5.1", "vue-router": "4.5.1",
"vue-virtual-scroller": "2.0.0-beta.8" "vue-virtual-scroller": "2.0.0-beta.8"
+19
View File
@@ -44,6 +44,9 @@ importers:
vue: vue:
specifier: ^3.5.16 specifier: ^3.5.16
version: 3.5.40(typescript@5.8.3) version: 3.5.40(typescript@5.8.3)
vue-draggable-next:
specifier: ^2.3.0
version: 2.3.0(sortablejs@1.15.7)(vue@3.5.40(typescript@5.8.3))
vue-echarts: vue-echarts:
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1(echarts@6.1.0)(vue@3.5.40(typescript@5.8.3)) version: 8.0.1(echarts@6.1.0)(vue@3.5.40(typescript@5.8.3))
@@ -1718,6 +1721,9 @@ packages:
resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
sortablejs@1.15.7:
resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1962,6 +1968,12 @@ packages:
'@vue/composition-api': '@vue/composition-api':
optional: true optional: true
vue-draggable-next@2.3.0:
resolution: {integrity: sha512-ymbY0UIwfSdg0iDN/iyNNwUrTqZ/6KbPryzsvTNXBLuDCuOBdNijSK8yynNtmiSj6RapTPQfjLGQdJrZkzBd2w==}
peerDependencies:
sortablejs: ^1.14.0
vue: ^3.5.17
vue-echarts@8.0.1: vue-echarts@8.0.1:
resolution: {integrity: sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==} resolution: {integrity: sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==}
peerDependencies: peerDependencies:
@@ -3690,6 +3702,8 @@ snapshots:
side-channel-map: 1.0.1 side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2 side-channel-weakmap: 1.0.2
sortablejs@1.15.7: {}
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map-support@0.5.21: source-map-support@0.5.21:
@@ -3920,6 +3934,11 @@ snapshots:
dependencies: dependencies:
vue: 3.5.40(typescript@5.8.3) vue: 3.5.40(typescript@5.8.3)
vue-draggable-next@2.3.0(sortablejs@1.15.7)(vue@3.5.40(typescript@5.8.3)):
dependencies:
sortablejs: 1.15.7
vue: 3.5.40(typescript@5.8.3)
vue-echarts@8.0.1(echarts@6.1.0)(vue@3.5.40(typescript@5.8.3)): vue-echarts@8.0.1(echarts@6.1.0)(vue@3.5.40(typescript@5.8.3)):
dependencies: dependencies:
echarts: 6.1.0 echarts: 6.1.0
+81 -191
View File
@@ -1,43 +1,46 @@
<template> <template>
<div v-can="permissionCodes.event.update" class="registration-event-sort"> <div v-can="permissionCodes.event.update" class="registration-event-sort">
<div class="tip" v-text="'按住右侧拖动手柄调整事件顺序,保存后生效'" /> <div class="tip" v-text="'按住右侧拖动手柄调整事件顺序,保存后生效'" />
<div ref="sortListRef" class="items"> <div class="items">
<t-cell-group v-if="eventList.length" theme="card"> <t-cell-group v-if="eventList.length" theme="card">
<t-cell <vue-draggable-next
class="cell" v-model="eventList"
:title="item.name || '未命名事件'" class="draggable"
v-for="(item, index) in eventList" item-key="id"
:key="item.id" handle=".handle"
:class="{ dragging: draggingIndex === index }" ghost-class="ghost"
chosen-class="chosen"
drag-class="dragging"
:animation="180"
:disabled="isLoading || isSaving"
@change="handleSortChange"
> >
<template #leftIcon> <t-cell
<div class="color" :style="{ background: item.color || defaultColor }" /> v-for="element in eventList"
</template> :key="element.id"
<template #note> class="cell"
<t-tag :title="element.name || '未命名事件'"
:theme="getStatusTheme(item.status)" >
variant="outline" <template #leftIcon>
:content="getStatusText(item.status)" <div class="color" :style="{ background: element.color || defaultColor }" />
/> </template>
</template> <template #note>
<template #rightIcon> <t-tag
<t-icon :theme="getStatusTheme(element.status)"
class="handle" variant="outline"
role="button" :content="getStatusText(element.status)"
name="move" />
aria-label="拖动排序" </template>
@pointerdown.stop="startDrag($event, index)" <template #rightIcon>
@pointermove.stop="moveDrag" <t-icon
@pointerup.stop="finishDrag" class="handle"
@pointercancel.stop="finishDrag" role="button"
@lostpointercapture.stop="finishDrag" name="move"
@touchstart.stop="startTouchDrag($event, index)" aria-label="拖动排序"
@touchmove.stop="moveTouchDrag" />
@touchend.stop="finishTouchDrag" </template>
@touchcancel.stop="finishTouchDrag" </t-cell>
/> </vue-draggable-next>
</template>
</t-cell>
</t-cell-group> </t-cell-group>
<div v-else class="empty-block" v-text="isLoading ? '正在加载登记事件' : '暂无登记事件'" /> <div v-else class="empty-block" v-text="isLoading ? '正在加载登记事件' : '暂无登记事件'" />
</div> </div>
@@ -55,6 +58,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Toast, type TagProps } from "tdesign-mobile-vue"; import { Toast, type TagProps } from "tdesign-mobile-vue";
import { VueDraggableNext } from "vue-draggable-next";
import EventAPI from "@/api/EventAPI"; import EventAPI from "@/api/EventAPI";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem, EventStatus } from "@/types/EventItem"; import type { EventItem, EventStatus } from "@/types/EventItem";
@@ -65,25 +69,16 @@ defineOptions({
}); });
const navBarStore = useNavBarStore(); const navBarStore = useNavBarStore();
const router = useRouter();
// ---------- 列表数据 ---------- // ---------- 列表数据 ----------
const defaultColor = "#2f88ff"; const defaultColor = "#2f88ff";
const eventList = ref<EventItem[]>([]); const eventList = ref<EventItem[]>([]);
const sortListRef = ref<HTMLElement>();
const isLoading = ref(false); const isLoading = ref(false);
const isSaving = ref(false); const isSaving = ref(false);
const sortDirty = 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 { function getStatusText(status?: EventStatus): string {
@@ -108,124 +103,10 @@ function getStatusTheme(status?: EventStatus): TagProps["theme"] {
// ---------- 拖拽排序 ---------- // ---------- 拖拽排序 ----------
function resolveDropIndex(clientY: number): number { function handleSortChange(): void {
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;
sortDirty.value = true; 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> { async function saveSort(): Promise<void> {
@@ -244,6 +125,7 @@ async function saveSort(): Promise<void> {
}); });
sortDirty.value = false; sortDirty.value = false;
Toast.success("登记事件排序已保存"); Toast.success("登记事件排序已保存");
await router.back();
} catch { } catch {
Toast.error("登记事件排序保存失败"); Toast.error("登记事件排序保存失败");
} finally { } finally {
@@ -292,39 +174,41 @@ onUnmounted(() => {
} }
.items { .items {
.draggable {
.cell {
position: relative;
.cell { .color {
position: relative; 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; z-index: 1;
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16); 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; color: #94a3b8;
} }
.item { .draggable {
background: rgba(15, 23, 42, .88); .cell {
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28); background: rgba(15, 23, 42, .88);
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
}
.ghost {
background: rgba(15, 23, 42, .6);
}
} }
} }
} }