Files
huajishe-tts/docs/dev-log/04-ui-primitive-impl.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

2.7 KiB
Raw Permalink Blame History

04 · UI 原语实现

做什么

实现 <Board> <Zone> <Card> <Token> <ActionButton> 五个组件,让它们按 game.ui schema 驱动渲染。

遇到什么问题

问题 1Board 怎么知道 zone 持有什么实体?

zone 定义是 UI 层的(位置、类型),但哪些 card/tokens 在哪个 zone 是数据层的。

解决:引入 collection 字段约定:

{ id: 'p0-deck', collection: 'p0Deck', ... }

Board 调 resolveZoneEntities(zone, G) → 读 G[collection]ID 数组)+ G[entityField ?? 'drawPile']ID → 实体映射)。

问题 2手牌区怎么扇形展示

spacingwidth 谁先决定?人手少时扇形,人多时网格?

解决fan type 内部用三角函数算 offset

const fanAngle = layout?.fan ?? 30;
const spacing = layout?.spacing ?? 30;
const half = (entities.length - 1) / 2;
const offset = (i - half) * spacing;
const rot = (i - half) * (fanAngle / Math.max(half, 1));

问题 3pile / discard 渲染策略不同

  • pile(牌堆):只显示顶牌 + 数量徽章
  • discard(弃牌堆):同 pile但可能面朝上
  • area(区域):所有牌铺开
  • hand(手牌):扇形展开

解决:在 Zone 组件按 type 分支渲染。

问题 4SVG 在 React 里直接渲染

dangerouslySetInnerHTML 注入 SVG 字符串(来自 template.front(card))。

风险v1 接受,因为游戏定义是可信方写的;如果 v2 接入第三方游戏,需要 SVG 沙箱。

怎么解决

组件分工

<Board>
├─ 遍历 game.ui.zones
│   └─ <Zone def={...} entities={resolveZoneEntities(def, G)}>
│        ├─ <Card> / <Token> 渲染
│        └─ <ZoneDebugLabel>debug 模式下)
├─ children自定义 UI 覆盖层)
└─ 点击空白区域(清空选择)

完整文件

  • packages/protocol/src/index.ts — 共享类型 + resolveZoneEntities helper
  • packages/ui/src/Board.tsx — 顶层容器
  • packages/ui/src/Zone.tsx — 区域容器6 种 type
  • packages/ui/src/Card.tsx — 单卡SVG 渲染)
  • packages/ui/src/Token.tsx — 单 token
  • packages/ui/src/ActionButton.tsx — 触发 move

验证

  • pnpm -r ts6 个包全部通过
  • pnpm --filter @tts-like/web build成功283 KB / 90 KB gzip
  • pnpm --filter @tts-like/web devHTTP 200

成果

  • Commit b2d6236 feat(ui): render zones/cards/actions from game.ui schema
  • War 游戏的 4 个 pile zone + 1 个翻牌按钮都能在浏览器里看到
  • 切到拖拽前的最后状态

关联