implement store sort

This commit is contained in:
Timi
2026-08-14 16:08:05 +08:00
parent 30f9d0e84e
commit 4d4abe5de3
+46 -150
View File
@@ -1,18 +1,33 @@
<template> <template>
<div v-can="permissionCodes.store.update" class="store-sort"> <div v-can="permissionCodes.store.update" class="store-sort">
<div class="tip" v-text="'按住右侧拖动手柄调整门店顺序,保存后生效'" /> <div class="tip" v-text="'按住右侧拖动手柄调整门店顺序,保存后生效'" />
<div ref="sortListRef" class="items"> <div class="items">
<t-cell-group v-if="storeList.length" theme="card"> <t-cell-group v-if="storeList.length" theme="card">
<vue-draggable-next
v-model="storeList"
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="(item, index) in storeList" v-for="element in storeList"
:key="item.id" :key="element.id"
class="cell" class="cell"
:title="item.name || '未命名门店'" :title="element.name || '未命名门店'"
:description="`编码:${item.code || '-'}`" :description="`编码:${element.code || '-'}`"
:class="{ dragging: draggingIndex === index }"
> >
<template #note> <template #note>
<t-tag :theme="getStatusTheme(item.status)" variant="outline" :content="getStatusText(item.status)" /> <t-tag
:theme="getStatusTheme(element.status)"
variant="outline"
:content="getStatusText(element.status)"
/>
</template> </template>
<template #rightIcon> <template #rightIcon>
<t-icon <t-icon
@@ -20,18 +35,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>
@@ -49,6 +56,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 StoreAPI from "@/api/StoreAPI"; import StoreAPI from "@/api/StoreAPI";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { Store, StoreStatus } from "@/types/Store"; import type { Store, StoreStatus } from "@/types/Store";
@@ -59,24 +67,15 @@ defineOptions({
}); });
const navBarStore = useNavBarStore(); const navBarStore = useNavBarStore();
const router = useRouter();
// ---------- 列表数据 ---------- // ---------- 列表数据 ----------
const storeList = ref<Store[]>([]); const storeList = ref<Store[]>([]);
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?: StoreStatus): string { function getStatusText(status?: StoreStatus): string {
@@ -89,124 +88,10 @@ function getStatusTheme(status?: StoreStatus): TagProps["theme"] {
// ---------- 拖拽排序 ---------- // ---------- 拖拽排序 ----------
function resolveDropIndex(clientY: number): number { function handleSortChange(): void {
const elements = sortListRef.value?.querySelectorAll<HTMLElement>(".cell");
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] = storeList.value.splice(draggingIndex.value, 1);
storeList.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> {
@@ -225,6 +110,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 {
@@ -277,22 +163,25 @@ onUnmounted(() => {
} }
.items { .items {
.draggable {
.cell { .cell {
position: relative; position: relative;
&.dragging {
z-index: 1;
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16);
}
.handle { .handle {
color: #52607a; color: #52607a;
cursor: grab; cursor: grab;
touch-action: none;
}
} }
&.dragging, .ghost {
&.dragging * { opacity: .4;
touch-action: none; }
.chosen,
.dragging {
z-index: 1;
box-shadow: 0 .5rem 1rem rgba(15, 23, 42, .16);
} }
} }
} }
@@ -319,9 +208,16 @@ onUnmounted(() => {
color: #94a3b8; color: #94a3b8;
} }
.cell.dragging { .draggable {
.cell {
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>