refactor event

This commit is contained in:
Timi
2026-08-05 22:50:22 +08:00
parent 22ff13b1fe
commit 1243068fc3
46 changed files with 961 additions and 789 deletions
+354
View File
@@ -0,0 +1,354 @@
<template>
<div v-can="permissionCodes.event.update" class="registration-event-sort">
<div class="tip" v-text="'按住右侧拖动手柄调整事件顺序,保存后生效'" />
<div ref="sortListRef" 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 }"
>
<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-group>
<div v-else class="empty-block" v-text="loading ? '正在加载登记事件' : '暂无登记事件'" />
</div>
<t-button
class="save"
theme="primary"
:loading="saving"
:disabled="!sortDirty || saving || loading"
@click="saveSort"
>
保存排序
</t-button>
</div>
</template>
<script setup lang="ts">
import { Toast, type TagProps } from "tdesign-mobile-vue";
import EventAPI from "@/api/EventAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { EventItem, EventStatus } from "@/types/EventItem";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "EventSort"
});
const navBarStore = useNavBarStore();
const defaultColor = "#2f88ff";
const eventList = ref<EventItem[]>([]);
const sortListRef = ref<HTMLElement>();
const loading = ref(false);
const saving = 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 {
if (status === "DRAFT") {
return "草稿";
}
if (status === "INACTIVE") {
return "不可登记";
}
return "可登记";
}
function getStatusTheme(status?: EventStatus): TagProps["theme"] {
if (status === "DRAFT") {
return "warning";
}
if (status === "INACTIVE") {
return "default";
}
return "primary";
}
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;
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 (saving.value || loading.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 (saving.value || loading.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> {
if (!sortDirty.value || saving.value) {
return;
}
saving.value = true;
try {
const idList = eventList.value
.map(item => item.id)
.filter((id): id is string => Boolean(id));
await EventAPI.sort(idList);
eventList.value.forEach((item, index) => {
item.sort = index;
});
sortDirty.value = false;
showToast("登记事件排序已保存");
} catch {
showToast("登记事件排序保存失败", "error");
} finally {
saving.value = false;
}
}
function showToast(message: string, theme: "success" | "error" = "success"): void {
Toast({
theme,
direction: "row",
message
});
}
async function loadData(): Promise<void> {
loading.value = true;
try {
const result = await EventAPI.list({
index: 0,
size: 100
});
eventList.value = result.list || [];
sortDirty.value = false;
} finally {
loading.value = false;
}
}
onMounted(async () => {
navBarStore.clearRight();
try {
await loadData();
} catch {
showToast("登记事件加载失败", "error");
}
});
onUnmounted(() => {
navBarStore.clearRight();
});
</script>
<style scoped lang="less">
.registration-event-sort {
padding: calc(var(--app-nav-height) + 1rem) 0 6rem;
.tip {
color: #52607a;
padding: 0 1rem .75rem;
font-size: .8125rem;
}
.items {
.cell {
position: relative;
&.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;
}
}
}
.empty-block {
color: #52607a;
padding: 2rem 1rem;
text-align: center;
}
.save {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
}
:global(html.theme-dark) {
.registration-event-sort {
.tip,
.empty-block,
.handle {
color: #94a3b8;
}
.item {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .5rem 1rem rgba(2, 6, 23, .28);
}
}
}
</style>