2026-06-08-wuxing-sandbox-refactor-design.md 6.9 KB

能量沙盘组件全面重构设计

日期: 2026-06-08 状态: 待实施 范围: cfc-frontend/components/wuxing-sandbox.vue 及 3 个消费页面

1. 背景与目标

当前 wuxing-sandbox.vue 是一个 812 行的单文件组件,混合了 5 种职责(Canvas 渲染、填充动画、点击检测、视图模式切换、UI 编排),并包含 5 处死代码。sandbox 对象 prop 耦合后端 DTO 结构,导致消费页面需要 safeSandbox hack 规避 WXML 编译问题。

目标

  1. 将 812 行单文件拆分为 1 个薄编排组件 + 3 个纯 JS 工具模块
  2. 重新设计 props/events/slots 接口,扁平化、显式化
  3. 清理死代码(约 50 行)
  4. 保持 3 个消费页面的导入路径不变,仅适配接口

2. 架构方案:单组件 + 提取工具模块

2.1 文件结构

components/
├── wuxing-sandbox.vue              # 主组件(orchestrator,~250行)
└── wuxing-sandbox-helpers/
    ├── render.js                   # Canvas 渲染函数(~180行)
    ├── animation.js                # 动画控制器类(~80行)
    └── hitdetect.js                # 点击检测工具(~50行)

2.2 为什么不用子组件拆分?

uni-app 微信小程序中,<canvas type="2d"> 与 HTML 元素(star-point 标签)在同一组件层级才能正确叠放。拆成子组件会引入:

  • canvas createSelectorQuery().in(this) 作用域问题
  • 跨组件 canvas 坐标对齐复杂性
  • HTML 覆盖层 z-index 在子组件间不可靠

提取为纯 JS 工具模块是 uni-app Vue 2 下最安全的方式,无 mixin 命名冲突风险,函数可独立测试。

3. 接口设计

3.1 Props(扁平化替代 sandbox 对象)

Prop Type Default 说明
mode String 'preview' 'preview' 随机数据 / 'live' 真实数据
scores Object {body:0,mind:0,wisdom:0,action:0,wealth:0} 五维分值(0-100)
overallScore Number 0 综合分值
members Array [] 家庭成员数组(MemberEnergyDTO 格式)
badgeText String '' 顶部徽章文字,空则自动计算
showDimensionBar Boolean true 是否显示底部维度条
starHeight String '480rpx' 星图区域高度

关键变更:删除 sandbox 对象 prop,用 scores + overallScore + members 替代。组件内部处理 null/undefined,消费页面无需 safeSandbox hack。

3.2 Events

Event Payload 说明
@point-click {domain, viewMode, member} 点击星臂维度(保留原有)
@center-click {viewMode, selectedMemberIndex} 点击中心太极区域
@mode-change {viewMode, selectedMemberIndex} 家庭↔个人模式切换后触发

3.3 Slots

Slot 说明
header 替换默认标题+徽章区域
dimension-bar 替换默认维度详情条

3.4 内部状态(非 prop)

  • viewMode: 'family' | 'member' — 当前视图
  • selectedMemberIndex: Number — 当前选中成员下标
  • previewScores: Array — 预览模式随机值
  • canvasReady, canvasWidth, canvasHeight, ctx, dpr — Canvas 状态
  • animBody/Mind/Wisdom/Action/Wealth — 动画插值

4. 工具模块详细设计

4.1 render.js — Canvas 渲染(纯函数,无状态)

// 导出
export function calcStarVertices(cx, cy, outerR) → { outer: [{x,y}], inner: [{x,y}] }
export function renderStar(ctx, w, h, ts, options) → void
export function drawTaiji(ctx, cx, cy, r) → void

// ts = [body, mind, wisdom, action, wealth] 归一化值 (0~1)
// options = { colors, outerR, showTaiji, taijiLabel }

renderStar 内部调用 calcStarVerticesdrawTaiji,一次调用完成整个星图渲染。

4.2 animation.js — 动画控制器

export class FillAnimator {
  constructor(getCurrentTs, onFrame, onComplete)
  start(targetTs, duration = 600) → void   // ease-out cubic
  cancel() → void                            // 清理 timer
}
  • getCurrentTs() 返回当前 5 维插值数组
  • onFrame(interpolatedTs) 每帧回调
  • onComplete() 动画完成回调
  • 使用 setTimeout(fn, 16) 模拟 requestAnimationFrame(uni-app 兼容)

4.3 hitdetect.js — 点击检测

export function pointInPolygon(x, y, polygon) → boolean
export function detectHitArm(relX, relY, cx, cy, outerR) → number  // 0~4 或 -1

detectHitArm 封装了星臂多边形计算 + 遍历检测,返回命中星臂索引。

5. 主组件 wuxing-sandbox.vue 结构(~250 行)

<template> (~55行)
  - header slot
  - star area: canvas + tap area + 5 point labels + center click
  - dimension-bar slot (含默认内容)

<script> (~120行)
  - import from helpers
  - props 定义
  - data() — 内部状态
  - computed — isPreview, currentMember, normalizedScores, activeTs, displayBadgeText 等
  - watch — activeTs 变化触发动画
  - mounted — 预览数据生成 + canvas 初始化
  - beforeDestroy — animator.cancel()
  - methods — toggleEnergyMode, onPointClick, onCanvasTap, initCanvas

<style scoped> (~80行)
  - 保留现有样式,微调

6. 死代码清理

方法 行数 原因
_calcFillPolygon ~12行 填充算法改为全臂渐变后不再调用
pointInCircle ~5行 未被任何代码引用
dimVal ~8行 未被模板或方法调用
curVal ~3行 dimVal 的包装,同样未使用
curMax ~3行 固定返回 100,未使用

7. 消费页面适配

7.1 parent-index.vue

Before:

<wuxing-sandbox mode="parent" :sandbox="safeSandbox" @point-click="goToDomain" />

After:

<wuxing-sandbox mode="live"
  :scores="energyScores"
  :overall-score="energyOverall"
  :members="energyMembers"
  @point-click="goToDomain" />

删除 safeSandbox computed,新增 3 个简单 computed:

energyScores() {
  var d = this.energyData
  return d ? { body: d.bodyScore||0, mind: d.mindScore||0, wisdom: d.wisdomScore||0, action: d.actionScore||0, wealth: d.wealthScore||0 } : { body:0,mind:0,wisdom:0,action:0,wealth:0 }
},
energyOverall() { return (this.energyData && this.energyData.overallScore) || 0 },
energyMembers() { return (this.energyData && this.energyData.members) || [] }

7.2 index.vue / discover/index.vue

仅 mode 值不变('preview'),去掉 sandbox 相关,无需其他改动:

<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />

8. 不变项

  • 导入路径:3 个页面 import WuxingSandbox from '../../components/wuxing-sandbox.vue' 不变
  • 视觉效果:五角星渲染、太极图、填充动画、星臂颜色完全不变
  • Canvas type="2d" 绘制逻辑:仅从组件 methods 迁移到 render.js,算法不变
  • uni-app 兼容性:禁止可选链 ?.、禁止 CSS Grid,继续遵守