优化拖动逻辑
This commit is contained in:
+143
-89
@@ -10,6 +10,15 @@ const draggingIndex = ref<number | null>(null);
|
|||||||
const overIndex = ref<number | null>(null);
|
const overIndex = ref<number | null>(null);
|
||||||
const dragPreview = ref<HTMLElement | null>(null);
|
const dragPreview = ref<HTMLElement | null>(null);
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
|
// 指针拖拽新增状态
|
||||||
|
const insertSide = ref<'before' | 'after' | null>(null);
|
||||||
|
const pointerId = ref<number | null>(null);
|
||||||
|
const startX = ref(0);
|
||||||
|
const startY = ref(0);
|
||||||
|
const lastX = ref(0);
|
||||||
|
const lastY = ref(0);
|
||||||
|
const dragStarted = ref(false);
|
||||||
|
const DRAG_THRESHOLD = 3; // 像素阈值,避免误触
|
||||||
const editingIndex = ref<number | null>(null);
|
const editingIndex = ref<number | null>(null);
|
||||||
const editingValue = ref('');
|
const editingValue = ref('');
|
||||||
const addingMapIndex = ref<number | null>(null);
|
const addingMapIndex = ref<number | null>(null);
|
||||||
@@ -138,20 +147,78 @@ function getTokenWrapperInfo(token: string) {
|
|||||||
return store.getTokenWrapperInfo(token);
|
return store.getTokenWrapperInfo(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragStart(index: number, e: DragEvent) {
|
// 指针事件版拖拽:更高性能且可自定义插入指示
|
||||||
|
function onPointerDown(index: number, e: PointerEvent) {
|
||||||
|
if (editingIndex.value === index) return;
|
||||||
draggingIndex.value = index;
|
draggingIndex.value = index;
|
||||||
|
pointerId.value = e.pointerId;
|
||||||
|
startX.value = e.clientX;
|
||||||
|
startY.value = e.clientY;
|
||||||
|
lastX.value = e.clientX;
|
||||||
|
lastY.value = e.clientY;
|
||||||
|
dragStarted.value = false;
|
||||||
|
isDragging.value = false;
|
||||||
|
insertSide.value = null;
|
||||||
|
|
||||||
|
// 监听全局移动与抬起
|
||||||
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
|
window.addEventListener('pointerup', handlePointerUp, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(e: PointerEvent) {
|
||||||
|
lastX.value = e.clientX;
|
||||||
|
lastY.value = e.clientY;
|
||||||
|
const dx = e.clientX - startX.value;
|
||||||
|
const dy = e.clientY - startY.value;
|
||||||
|
if (!dragStarted.value && Math.hypot(dx, dy) > DRAG_THRESHOLD) {
|
||||||
|
dragStarted.value = true;
|
||||||
isDragging.value = true;
|
isDragging.value = true;
|
||||||
|
if (draggingIndex.value != null) createPointerPreview(draggingIndex.value);
|
||||||
|
}
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
positionPreview(e.clientX, e.clientY);
|
||||||
|
updateOverIndexAndSide(e.clientX, e.clientY);
|
||||||
|
}
|
||||||
|
|
||||||
if (e.dataTransfer) {
|
function handlePointerUp(e: PointerEvent) {
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
e.dataTransfer.setData('text/plain', tokens.value[index] || '');
|
if (!dragStarted.value || draggingIndex.value == null) {
|
||||||
|
cleanupDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const from = draggingIndex.value!;
|
||||||
|
const j = overIndex.value;
|
||||||
|
const side = insertSide.value;
|
||||||
|
if (j != null && side) {
|
||||||
|
let to = j;
|
||||||
|
if (side === 'before') {
|
||||||
|
to = j - (from < j ? 1 : 0);
|
||||||
|
} else {
|
||||||
|
to = j + (from > j ? 1 : 0);
|
||||||
|
}
|
||||||
|
if (to < 0) to = 0;
|
||||||
|
if (to >= tokens.value.length) to = tokens.value.length - 1;
|
||||||
|
store.reorderTokens(from, to);
|
||||||
|
showNotification('已重新排序', 'success');
|
||||||
|
}
|
||||||
|
cleanupDrag();
|
||||||
|
}
|
||||||
|
|
||||||
// 创建自定义拖拽预览
|
function cleanupDrag() {
|
||||||
const dragElement = e.target as HTMLElement;
|
draggingIndex.value = null;
|
||||||
|
overIndex.value = null;
|
||||||
|
isDragging.value = false;
|
||||||
|
insertSide.value = null;
|
||||||
|
pointerId.value = null;
|
||||||
|
if (dragPreview.value) {
|
||||||
|
document.body.removeChild(dragPreview.value);
|
||||||
|
dragPreview.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPointerPreview(index: number) {
|
||||||
const token = tokens.value[index] || '';
|
const token = tokens.value[index] || '';
|
||||||
const translation = displayTrans(token);
|
const translation = displayTrans(token);
|
||||||
|
|
||||||
// 创建预览元素
|
|
||||||
const preview = document.createElement('div');
|
const preview = document.createElement('div');
|
||||||
preview.className = 'drag-preview';
|
preview.className = 'drag-preview';
|
||||||
preview.innerHTML = `
|
preview.innerHTML = `
|
||||||
@@ -161,83 +228,38 @@ function onDragStart(index: number, e: DragEvent) {
|
|||||||
<span class="drag-preview-trans">${translation}</span>
|
<span class="drag-preview-trans">${translation}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 设置预览样式(减少布局与重绘)
|
|
||||||
preview.style.position = 'fixed';
|
preview.style.position = 'fixed';
|
||||||
preview.style.top = '0';
|
preview.style.top = '0';
|
||||||
preview.style.left = '0';
|
preview.style.left = '0';
|
||||||
preview.style.zIndex = '1000';
|
preview.style.zIndex = '1000';
|
||||||
preview.style.pointerEvents = 'none';
|
preview.style.pointerEvents = 'none';
|
||||||
preview.style.visibility = 'hidden';
|
|
||||||
// 降低绘制成本
|
|
||||||
;(preview.style as any).contain = 'layout style paint';
|
;(preview.style as any).contain = 'layout style paint';
|
||||||
preview.style.willChange = 'transform, opacity';
|
preview.style.willChange = 'transform, opacity';
|
||||||
|
|
||||||
document.body.appendChild(preview);
|
document.body.appendChild(preview);
|
||||||
dragPreview.value = preview;
|
dragPreview.value = preview;
|
||||||
|
|
||||||
// 设置拖拽图像
|
|
||||||
e.dataTransfer.setDragImage(preview, 0, 0);
|
|
||||||
|
|
||||||
// 预览节点在 dragend 中统一清理,避免频繁移除导致卡顿
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragOver(index: number, e: DragEvent) {
|
function positionPreview(x: number, y: number) {
|
||||||
e.preventDefault();
|
if (!dragPreview.value) return;
|
||||||
if (draggingIndex.value === null) return;
|
dragPreview.value.style.transform = `translate(${x + 12}px, ${y + 12}px)`;
|
||||||
|
|
||||||
e.dataTransfer!.dropEffect = 'move';
|
|
||||||
|
|
||||||
// 只有当拖拽到不同位置时才更新
|
|
||||||
if (overIndex.value !== index) {
|
|
||||||
overIndex.value = index;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragEnter(index: number, e: DragEvent) {
|
function updateOverIndexAndSide(x: number, y: number) {
|
||||||
e.preventDefault();
|
insertSide.value = null;
|
||||||
if (draggingIndex.value !== null && draggingIndex.value !== index) {
|
|
||||||
overIndex.value = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDragLeave(e: DragEvent) {
|
|
||||||
// 只有当离开整个拖拽区域时才清除
|
|
||||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
||||||
const x = e.clientX;
|
|
||||||
const y = e.clientY;
|
|
||||||
|
|
||||||
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
|
|
||||||
overIndex.value = null;
|
overIndex.value = null;
|
||||||
}
|
const el = document.elementFromPoint(x, y) as HTMLElement | null;
|
||||||
}
|
if (!el) return;
|
||||||
|
const tokenEl = el.closest('.pe-token-compact, .pe-token-detail') as HTMLElement | null;
|
||||||
function onDrop(index: number, e: DragEvent) {
|
if (!tokenEl) return;
|
||||||
e.preventDefault();
|
const idxAttr = tokenEl.getAttribute('data-index');
|
||||||
if (draggingIndex.value == null) return;
|
if (idxAttr == null) return;
|
||||||
|
const idx = parseInt(idxAttr, 10);
|
||||||
// 执行重排序
|
if (Number.isNaN(idx)) return;
|
||||||
store.reorderTokens(draggingIndex.value, index);
|
if (idx === draggingIndex.value) { overIndex.value = null; insertSide.value = null; return; }
|
||||||
|
const rect = tokenEl.getBoundingClientRect();
|
||||||
// 重置状态
|
const midX = rect.left + rect.width / 2;
|
||||||
draggingIndex.value = null;
|
overIndex.value = idx;
|
||||||
overIndex.value = null;
|
insertSide.value = x < midX ? 'before' : 'after';
|
||||||
isDragging.value = false;
|
|
||||||
|
|
||||||
showNotification('已重新排序', 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDragEnd() {
|
|
||||||
// 清理拖拽状态
|
|
||||||
draggingIndex.value = null;
|
|
||||||
overIndex.value = null;
|
|
||||||
isDragging.value = false;
|
|
||||||
|
|
||||||
if (dragPreview.value) {
|
|
||||||
document.body.removeChild(dragPreview.value);
|
|
||||||
dragPreview.value = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginEdit(i: number) {
|
function beginEdit(i: number) {
|
||||||
@@ -456,20 +478,15 @@ function displayTrans(key: string): string {
|
|||||||
<div
|
<div
|
||||||
v-for="(k,i) in tokens"
|
v-for="(k,i) in tokens"
|
||||||
:key="k + '_' + i"
|
:key="k + '_' + i"
|
||||||
:draggable="editingIndex !== i"
|
:data-index="i"
|
||||||
:class="{
|
:class="{
|
||||||
'dragging': draggingIndex === i,
|
'dragging': draggingIndex === i,
|
||||||
'drag-over': overIndex === i && draggingIndex !== i,
|
'insert-before': overIndex === i && insertSide === 'before' && draggingIndex !== i,
|
||||||
'drag-placeholder': overIndex === i && draggingIndex !== null && draggingIndex !== i,
|
'insert-after': overIndex === i && insertSide === 'after' && draggingIndex !== i,
|
||||||
'editing': editingIndex === i
|
'editing': editingIndex === i
|
||||||
}"
|
}"
|
||||||
class="pe-token-compact"
|
class="pe-token-compact"
|
||||||
@dragstart="onDragStart(i, $event)"
|
@pointerdown="onPointerDown(i, $event)"
|
||||||
@dragover="onDragOver(i, $event)"
|
|
||||||
@dragenter="onDragEnter(i, $event)"
|
|
||||||
@dragleave="onDragLeave"
|
|
||||||
@drop="onDrop(i, $event)"
|
|
||||||
@dragend="onDragEnd"
|
|
||||||
@dblclick="beginEdit(i)"
|
@dblclick="beginEdit(i)"
|
||||||
:title="`${k} → ${displayTrans(k)}`"
|
:title="`${k} → ${displayTrans(k)}`"
|
||||||
>
|
>
|
||||||
@@ -537,20 +554,15 @@ function displayTrans(key: string): string {
|
|||||||
<div
|
<div
|
||||||
v-for="(k,i) in tokens"
|
v-for="(k,i) in tokens"
|
||||||
:key="k + '_' + i"
|
:key="k + '_' + i"
|
||||||
:draggable="true"
|
:data-index="i"
|
||||||
:class="{
|
:class="{
|
||||||
'dragging': draggingIndex === i,
|
'dragging': draggingIndex === i,
|
||||||
'drag-over': overIndex === i && draggingIndex !== i,
|
'insert-before': overIndex === i && insertSide === 'before' && draggingIndex !== i,
|
||||||
'drag-placeholder': overIndex === i && draggingIndex !== null && draggingIndex !== i,
|
'insert-after': overIndex === i && insertSide === 'after' && draggingIndex !== i,
|
||||||
'editing': editingIndex === i || addingMapIndex === i
|
'editing': editingIndex === i || addingMapIndex === i
|
||||||
}"
|
}"
|
||||||
class="pe-token-detail"
|
class="pe-token-detail"
|
||||||
@dragstart="onDragStart(i, $event)"
|
@pointerdown="onPointerDown(i, $event)"
|
||||||
@dragover="onDragOver(i, $event)"
|
|
||||||
@dragenter="onDragEnter(i, $event)"
|
|
||||||
@dragleave="onDragLeave"
|
|
||||||
@drop="onDrop(i, $event)"
|
|
||||||
@dragend="onDragEnd"
|
|
||||||
>
|
>
|
||||||
<div class="pe-token-header">
|
<div class="pe-token-header">
|
||||||
<span class="pe-handle-detail">⋮⋮</span>
|
<span class="pe-handle-detail">⋮⋮</span>
|
||||||
@@ -1286,6 +1298,7 @@ function displayTrans(key: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pe-token-detail {
|
.pe-token-detail {
|
||||||
|
position: relative;
|
||||||
background-color: var(--color-bg-secondary);
|
background-color: var(--color-bg-secondary);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
@@ -1718,6 +1731,47 @@ function displayTrans(key: string): string {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 插入方向指示:目标项向前/后移动并显示清晰插入方向 */
|
||||||
|
.pe-token-compact.insert-before,
|
||||||
|
.pe-token-detail.insert-before {
|
||||||
|
transform: translateX(10px);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pe-token-compact.insert-after,
|
||||||
|
.pe-token-detail.insert-after {
|
||||||
|
transform: translateX(-10px);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pe-token-compact.insert-before::before,
|
||||||
|
.pe-token-detail.insert-before::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 60%;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pe-token-compact.insert-after::after,
|
||||||
|
.pe-token-detail.insert-after::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: -6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 3px;
|
||||||
|
height: 60%;
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
/* 拖拽容器样式 */
|
/* 拖拽容器样式 */
|
||||||
.pe-drag-container {
|
.pe-drag-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
Reference in New Issue
Block a user