Files
huajishe-tts/docs/dev-log/05-drag-and-drop.md
Claude 24822d0d6a docs(dev-log): add chronological development log
Per user request: split the development process into per-topic docs under
docs/dev-log/. Each entry follows the same structure:
  What was asked → Problems → How solved

Files:
- README.md            index
- 01-initial-scope-and-design.md
- 02-ui-primitives-api-design.md
- 03-monorepo-scaffold.md
- 04-ui-primitive-impl.md
- 05-drag-and-drop.md
- 06-three-games.md
- 07-bugfixes-round-1.md
- lessons-learned.md   all gotchas consolidated
- decisions.md         locked decisions with dates

Total: 1071 lines across 10 files.
2026-08-17 16:49:09 +08:00

3.3 KiB
Raw Permalink Blame History

05 · 拖拽 + 落点判定

做什么

实现 Card 拖出、Zone 接收、onDrop 触发游戏 move。验证端到端

  • 拖单张牌
  • 多选Shift+click后拖一组
  • 跨 zone 拖动

遇到什么问题

问题 1Playwright 拖拽总 timeout — "subtree intercepts pointer events"

dragstart 不会从被遮挡的 SVG 子元素触发。<rect> / <text> 等会"吃掉"事件。

解决:把 SVG 包在 <div style="pointer-events:none"> 里再 dangerouslySetInnerHTML

dangerouslySetInnerHTML={{
  __html: `<div style="pointer-events:none;width:100%;height:100%;">${svg}</div>`,
}}

问题 2React StrictMode + Client 双重挂载 → state 翻倍 + React duplicate key

Client(...) 内部用了全局 storeStrictMode 双重挂载让 store 注册两次state 出现两次(桌上有 4 张牌而非 2 张)。

解决

  • Client(...)useMemo(() => Client({...}), [game]) 包裹,避免每次 render 重新创建
  • main.tsx 暂时移除 <StrictMode>(在 boardgame.io 的 React client 升级前)

问题 3多选拖拽只拖了一张

Zone 没把 selectedIds 传给 CardCard dragstart 退化为只带自己的 id。

解决Zone 渲染 Card 时传 cardIds={selected ? Array.from(selectedIds) : undefined}Card 内部 cardIds ?? [card.id] 兜底。

问题 4拖拽的多张牌在桌面上重叠

DragTest 手牌 fan 默认 spacing=30 < 卡宽 63点击被上层卡遮挡。

解决DragTest 显式设 layout: { fan: 0, spacing: 70 }

问题 5拖拽到非 drop target zone 时被误判

例如拖到 hand zone没配 dropMove会被当作落到自身。

解决Zone 拖拽前先 if (!isDropTarget) return; 检查。

怎么解决

HTML5 拖拽 + 自定义 MIME

const DRAG_MIME = 'application/x-tts-cards';

e.dataTransfer.setData(DRAG_MIME, JSON.stringify({
  cardIds,
  fromZoneId: props.fromZoneId,
}));
e.dataTransfer.effectAllowed = 'move';

Zone 接收流程

const handleDragOver = (e) => {
  if (!isDropTarget) return;
  if (!e.dataTransfer.types.includes(DRAG_MIME)) return;
  e.preventDefault();  // 关键:允许 drop
  e.dataTransfer.dropEffect = 'move';
};

const handleDrop = (e) => {
  e.preventDefault();
  const raw = e.dataTransfer.getData(DRAG_MIME);
  const payload = JSON.parse(raw);
  onDrop?.(payload.cardIds, payload.fromZoneId, def.id);
};

Board 协调

const handleDrop = (cardIds, fromZoneId, toZoneId) => {
  const target = ui.zones.find(z => z.id === toZoneId);
  if (!target?.dropMove || !onMove) return;
  const args = target.dropArgs
    ? target.dropArgs(cardIds, fromZoneId, toZoneId)
    : [cardIds, fromZoneId, toZoneId];
  onMove(target.dropMove, args);
  setSelectedIds(new Set());  // 拖完清选择
};

验证

  • 拖 6♠ → 桌hand=4, table=1
  • Shift+click 选 3 张2♠/4♠/6♠+ 拖 2♠ → 全部移到桌hand=3♠/5♠, table=2♠/4♠/6♠

成果

  • Commit 2b492d2 feat(ui): HTML5 drag-and-drop + multi-select
  • Commit 8953554 fix(ui): drag pointer-events, StrictMode compat, multi-drag(后续 bug 修复)

关联