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

86 lines
2.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 04 · UI 原语实现
## 做什么
实现 `<Board> <Zone> <Card> <Token> <ActionButton>` 五个组件,让它们按 `game.ui` schema 驱动渲染。
## 遇到什么问题
### 问题 1Board 怎么知道 zone 持有什么实体?
zone 定义是 UI 层的(位置、类型),但哪些 card/tokens 在哪个 zone 是数据层的。
**解决**:引入 `collection` 字段约定:
```ts
{ id: 'p0-deck', collection: 'p0Deck', ... }
```
Board 调 `resolveZoneEntities(zone, G)` → 读 `G[collection]`ID 数组)+ `G[entityField ?? 'drawPile']`ID → 实体映射)。
### 问题 2手牌区怎么扇形展示
`spacing``width` 谁先决定?人手少时扇形,人多时网格?
**解决**fan type 内部用三角函数算 offset
```ts
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 ts`6 个包全部通过
- `pnpm --filter @tts-like/web build`成功283 KB / 90 KB gzip
- `pnpm --filter @tts-like/web dev`HTTP 200
## 成果
- Commit `b2d6236 feat(ui): render zones/cards/actions from game.ui schema`
- War 游戏的 4 个 pile zone + 1 个翻牌按钮都能在浏览器里看到
- 切到拖拽前的最后状态
## 关联
- [05-drag-and-drop.md](./05-drag-and-drop.md) — 接下来加拖拽
- [06-three-games.md](./06-three-games.md) — 三个游戏靠这些原语 + 拖拽