implement event sort
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Generated
+19
@@ -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
|
||||||
|
|||||||
+42
-152
@@ -1,23 +1,34 @@
|
|||||||
<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">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
<t-cell
|
<t-cell
|
||||||
|
v-for="element in eventList"
|
||||||
|
:key="element.id"
|
||||||
class="cell"
|
class="cell"
|
||||||
:title="item.name || '未命名事件'"
|
:title="element.name || '未命名事件'"
|
||||||
v-for="(item, index) in eventList"
|
|
||||||
:key="item.id"
|
|
||||||
:class="{ dragging: draggingIndex === index }"
|
|
||||||
>
|
>
|
||||||
<template #leftIcon>
|
<template #leftIcon>
|
||||||
<div class="color" :style="{ background: item.color || defaultColor }" />
|
<div class="color" :style="{ background: element.color || defaultColor }" />
|
||||||
</template>
|
</template>
|
||||||
<template #note>
|
<template #note>
|
||||||
<t-tag
|
<t-tag
|
||||||
:theme="getStatusTheme(item.status)"
|
:theme="getStatusTheme(element.status)"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:content="getStatusText(item.status)"
|
:content="getStatusText(element.status)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #rightIcon>
|
<template #rightIcon>
|
||||||
@@ -26,18 +37,10 @@
|
|||||||
role="button"
|
role="button"
|
||||||
name="move"
|
name="move"
|
||||||
aria-label="拖动排序"
|
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>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
|
</vue-draggable-next>
|
||||||
</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,15 +174,10 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
|
.draggable {
|
||||||
.cell {
|
.cell {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.dragging {
|
|
||||||
z-index: 1;
|
|
||||||
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color {
|
.color {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
@@ -319,11 +196,18 @@ onUnmounted(() => {
|
|||||||
.handle {
|
.handle {
|
||||||
color: #52607a;
|
color: #52607a;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell.dragging,
|
.ghost {
|
||||||
.cell.dragging * {
|
opacity: .4;
|
||||||
touch-action: none;
|
}
|
||||||
|
|
||||||
|
.chosen,
|
||||||
|
.dragging {
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,10 +234,16 @@ onUnmounted(() => {
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.draggable {
|
||||||
|
.cell {
|
||||||
background: rgba(15, 23, 42, .88);
|
background: rgba(15, 23, 42, .88);
|
||||||
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
|
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
background: rgba(15, 23, 42, .6);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user