From 0e451d07e6564e532703777a64d229b87451759c Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 28 Aug 2026 21:37:27 +0800 Subject: [PATCH] fix(admin): drag-to-reorder goods inside country groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 国家模式左树商品节点支持拖拽排序:行内 before/after 指示线,落点即保存。 优先级采用保序重排(该国家现有优先级集合不变、按新顺序重新分配), 尽量少扰动全局/品类维度的既有排序;跨国家拖动不支持(商品归属固定)。 --- apps/admin/src/views/goods/GoodsView.vue | 89 +++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/apps/admin/src/views/goods/GoodsView.vue b/apps/admin/src/views/goods/GoodsView.vue index 7cb62db..de1f200 100644 --- a/apps/admin/src/views/goods/GoodsView.vue +++ b/apps/admin/src/views/goods/GoodsView.vue @@ -675,6 +675,78 @@ async function onLeftTreeDragEnd(dragNode: any, dropNode: any, position: string) } catch { ElMessage.error('调整失败'); await reloadCategories() } } +// ─── 国家模式:商品在国家分组内拖拽排序(保序重排优先级) ─── +const treeDragGoodId = ref(null) +const treeDragOver = ref<{ id: string; pos: 'before' | 'after' } | null>(null) + +function onTreeGoodDragStart(event: DragEvent, data: any) { + if (mode.value !== 'country') return + treeDragGoodId.value = data.goodId + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/plain', String(data.goodId)) + } +} + +function onTreeGoodDragOver(event: DragEvent, data: any) { + if (mode.value !== 'country' || !treeDragGoodId.value || treeDragGoodId.value === data.goodId) return + event.preventDefault() + event.stopPropagation() + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect() + treeDragOver.value = { + id: data.goodId, + pos: event.clientY < rect.top + rect.height / 2 ? 'before' : 'after', + } +} + +function onTreeGoodDragLeave(_event: DragEvent, data: any) { + if (treeDragOver.value?.id === data.goodId) treeDragOver.value = null +} + +async function onTreeGoodDrop(event: DragEvent, data: any) { + event.preventDefault() + event.stopPropagation() + const draggedId = treeDragGoodId.value + const target = treeDragOver.value + treeDragGoodId.value = null + treeDragOver.value = null + if (mode.value !== 'country' || !draggedId || !target) return + if (target.id !== data.goodId || draggedId === data.goodId) return + + // 目标所在国家分组的商品(新顺序) + const group = leftTreeData.value.find((c: any) => + (c.children ?? []).some((g: any) => g.goodId === target.id), + ) + if (!group) return + const nodes = [...group.children] + const fromIdx = nodes.findIndex(n => n.goodId === draggedId) + if (fromIdx === -1) return // 跨国拖动不支持(商品归属固定国家) + const [moved] = nodes.splice(fromIdx, 1) + let insertIdx = nodes.findIndex(n => n.goodId === target.id) + if (target.pos === 'after') insertIdx++ + nodes.splice(insertIdx, 0, moved) + + // 保序重排:该国家现有优先级集合不变,按新顺序重新分配(尽量少扰动全局/品类排序) + const priorities = nodes.map(n => Number(n.priority || 0)).sort((a, b) => b - a) + moveTopLoading.value = 'drag' + try { + await goodsApi.batchUpdatePriority({ + items: nodes.map((n, i) => ({ id: Number(n.goodId), priority: priorities[i] ?? 0 })), + }) + ElMessage.success('排序已保存') + await refreshLeftTree() + } catch { + ElMessage.error('排序失败') + } finally { + moveTopLoading.value = null + } +} + +function onTreeGoodDragEnd() { + treeDragGoodId.value = null + treeDragOver.value = null +} + // ─── Country CRUD ─── const countryEditVisible = ref(false) const countryEditMode = ref<'create' | 'edit'>('create') @@ -978,8 +1050,20 @@ onMounted(() => loadAll())
@@ -1403,6 +1487,9 @@ onMounted(() => loadAll()) transition: background 0.12s; } .good-node:hover { background: #f0f2f5; } +.good-node.is-dragging { opacity: 0.5; } +.good-node.is-drop-before { box-shadow: inset 0 2px 0 var(--el-color-primary); } +.good-node.is-drop-after { box-shadow: inset 0 -2px 0 var(--el-color-primary); } .good-thumb { width: 36px; height: 36px; border-radius: 6px; flex-shrink: 0; object-fit: cover;