1. 核心交互演进与设计定位 (Interaction Evolution & Design Positioning)

在树状/列表卡片调序设计中,常规的 "Drag & Put"(拿起来形成脱离列表的幽灵卡片,并在目标位置插入外挂占位槽)往往存在以下体验问题:

  1. 割裂感强:拖拽对象脱离了原本的视觉流,变成漂浮半透明盒子,用户丢失了“在同一堆卡片中翻移”的物理手感;
  2. 动效过重:额外插入独立的占位槽(如虚线高亮胶囊、流光框)导致周围卡片剧烈跳动,在紧凑文件侧边栏中显得冗余嘈杂。

原位捏持滑动(In-Place Slide Reordering)与开火车队列顺移(Train Queue Shift) 模式将交互还原为最直观的物理推移:

  • 不拿起来,原位滑动:按住卡片左侧握柄后,卡片自身被提升(z-index: 60、微发光与投影),但始终保留在列表流内,随着手指或光标沿着 Y 轴瞬时滑移;
  • 开火车队列顺移:当被捏住的卡片滑过邻近卡片的中线时,受影响的卡片如同铁轨上的列车车厢(Train Carriages)一般,沿着 Y 轴以微动效(translateY 0.18s)整体顺次往后或往前避让顺移;
  • 文件夹吸附:当滑行经过文件夹时,卡片顺移动效复位,文件夹触发磁吸附高亮(Magnetic Snap),释放时将文件归入该目录。

2. 核心架构与数学模型 (Core Architecture & Mathematical Model)

2.1 步长计算 (Stride Calculation)

在单向垂直排列的列表中,任意相邻卡片从中心到中心的位移为标准步长 stride(即卡片高度 + 间距 gap):

const sourceRect = sourceRow.getBoundingClientRect();
let stride = sourceRect.height + 4;
if (siblingRows.length > 1) {
  const other = siblingRows[sourceIndex === 0 ? 1 : 0];
  const diff = Math.abs(other.getBoundingClientRect().top - sourceRect.top);
  if (diff > 0) stride = diff;
}

2.2 50% 几何交叉与目标槽位映射 (Halfway Crossing & Slot Mapping)

在 pointermove 中,根据当前指针相对于初始按压点的位移量 deltaY,通过 Math.round(deltaY / stride) 形成天然的 50% 交叉触发点:

const deltaY = moveEvent.clientY - startY;
const slotShift = Math.round(deltaY / stride);
const targetIndex = Math.max(0, Math.min(siblingRows.length - 1, sourceIndex + slotShift));
  • 当 deltaY 未过半时,slotShift 为 0,不触发任何扰动;
  • 当向下滑动跨过邻近卡片 50% 高度时,slotShift 瞬时跃迁为 1,目标位置更新。

2.3 队列顺移算法 (Train Queue Shift Algorithm)

无需任何 DOM 节点的插入或删除,仅依靠硬件加速的 CSS transform 驱动兄弟卡片避让:

siblingRows.forEach((r, idx) => {
  if (idx === sourceIndex) return;
  if (targetIndex > sourceIndex) {
    // 向下滑动:处于 (sourceIndex, targetIndex] 之间的卡片整体向上顺移 1 步
    if (idx > sourceIndex && idx <= targetIndex) {
      r.style.transform = `translateY(-${stride}px)`;
    } else {
      r.style.transform = '';
    }
  } else if (targetIndex < sourceIndex) {
    // 向上滑动:处于 [targetIndex, sourceIndex) 之间的卡片整体向下顺移 1 步
    if (idx < sourceIndex && idx >= targetIndex) {
      r.style.transform = `translateY(${stride}px)`;
    } else {
      r.style.transform = '';
    }
  } else {
    r.style.transform = '';
  }
});

3. 极简轻量 CSS 设计 (Minimalist CSS Design)

动效克制,仅依靠贝塞尔平移曲线营造顺滑手感:

/* 被激活原位滑动的卡片 */
.tree-row.is-sliding-active {
  z-index: 60 !important;
  position: relative !important;
  background: var(--paper-card) !important;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14) !important;
  border: 1px solid var(--seal) !important;
  border-radius: 7px;
  cursor: grabbing !important;
  user-select: none;
}

/* 兄弟卡片排队顺滑动效 (开火车顺移) */
.tree-file-row {
  color: var(--ink);
  transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1), background 0.15s ease;
  will-change: transform;
}

/* 文件夹磁吸附 */
.tree-folder-row.drop-folder-magnetic-snap {
  background: var(--paper-hover) !important;
  box-shadow: 0 0 0 2px var(--seal), 0 4px 16px rgba(184, 51, 42, 0.22) !important;
  border-radius: 6px;
  transform: scale(1.025);
  transition: transform 0.12s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.12s ease;
}

4. 释放落槽与数据最终一致性 (Settle Animation & Order Finalization)

在 pointerup 阶段:

  1. 落槽平滑归位动画:计算最终位移偏移量 finalOffset = (finalTargetIndex - sourceIndex) * stride,给 sourceRow 赋予 150ms 贝塞尔曲线过渡,让其平滑滑入避让形成的物理空位中;
  2. 数据一致性更新:在落槽动画结束(setTimeout 150ms)后,更新真实数据数组 customOrder 并提交持久化(IndexedDB / localStorage),清除所有内联 transform,重新渲染树视图,消除 DOM 重排闪烁。

本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Frontend