# 05 · 拖拽 + 落点判定 ## 做什么 实现 Card 拖出、Zone 接收、onDrop 触发游戏 move。验证端到端: - 拖单张牌 - 多选(Shift+click)后拖一组 - 跨 zone 拖动 ## 遇到什么问题 ### 问题 1:Playwright 拖拽总 timeout — "subtree intercepts pointer events" `dragstart` 不会从被遮挡的 SVG 子元素触发。`` / `` 等会"吃掉"事件。 **解决**:把 SVG 包在 `
` 里再 `dangerouslySetInnerHTML`: ```tsx dangerouslySetInnerHTML={{ __html: `
${svg}
`, }} ``` ### 问题 2:React StrictMode + Client 双重挂载 → state 翻倍 + React duplicate key `Client(...)` 内部用了全局 store;StrictMode 双重挂载让 store 注册两次,state 出现两次(桌上有 4 张牌而非 2 张)。 **解决**: - `Client(...)` 用 `useMemo(() => Client({...}), [game])` 包裹,避免每次 render 重新创建 - `main.tsx` 暂时移除 ``(在 boardgame.io 的 React client 升级前) ### 问题 3:多选拖拽只拖了一张 Zone 没把 `selectedIds` 传给 Card,Card 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 ```ts const DRAG_MIME = 'application/x-tts-cards'; e.dataTransfer.setData(DRAG_MIME, JSON.stringify({ cardIds, fromZoneId: props.fromZoneId, })); e.dataTransfer.effectAllowed = 'move'; ``` ### Zone 接收流程 ```ts 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 协调 ```ts 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 修复) ## 关联 - [06-three-games.md](./06-three-games.md) — 用 DragTest 验证拖拽 - [07-bugfixes-round-1.md](./07-bugfixes-round-1.md) — 拖拽 bug 修复 - [lessons-learned.md §7-12](./lessons-learned.md) — 拖拽相关踩坑