日期: 2026-06-08 状态: 待实施 范围: cfc-frontend/components/wuxing-sandbox.vue 及 3 个消费页面
当前 wuxing-sandbox.vue 是一个 812 行的单文件组件,混合了 5 种职责(Canvas 渲染、填充动画、点击检测、视图模式切换、UI 编排),并包含 5 处死代码。sandbox 对象 prop 耦合后端 DTO 结构,导致消费页面需要 safeSandbox hack 规避 WXML 编译问题。
目标:
components/
├── wuxing-sandbox.vue # 主组件(orchestrator,~250行)
└── wuxing-sandbox-helpers/
├── render.js # Canvas 渲染函数(~180行)
├── animation.js # 动画控制器类(~80行)
└── hitdetect.js # 点击检测工具(~50行)
uni-app 微信小程序中,<canvas type="2d"> 与 HTML 元素(star-point 标签)在同一组件层级才能正确叠放。拆成子组件会引入:
createSelectorQuery().in(this) 作用域问题提取为纯 JS 工具模块是 uni-app Vue 2 下最安全的方式,无 mixin 命名冲突风险,函数可独立测试。
| 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。
| Event | Payload | 说明 |
|---|---|---|
@point-click |
{domain, viewMode, member} |
点击星臂维度(保留原有) |
@center-click |
{viewMode, selectedMemberIndex} |
点击中心太极区域 |
@mode-change |
{viewMode, selectedMemberIndex} |
家庭↔个人模式切换后触发 |
| Slot | 说明 |
|---|---|
header |
替换默认标题+徽章区域 |
dimension-bar |
替换默认维度详情条 |
viewMode: 'family' | 'member' — 当前视图selectedMemberIndex: Number — 当前选中成员下标previewScores: Array — 预览模式随机值canvasReady, canvasWidth, canvasHeight, ctx, dpr — Canvas 状态animBody/Mind/Wisdom/Action/Wealth — 动画插值// 导出
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 内部调用 calcStarVertices、drawTaiji,一次调用完成整个星图渲染。
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 兼容)export function pointInPolygon(x, y, polygon) → boolean
export function detectHitArm(relX, relY, cx, cy, outerR) → number // 0~4 或 -1
detectHitArm 封装了星臂多边形计算 + 遍历检测,返回命中星臂索引。
<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行)
- 保留现有样式,微调
| 方法 | 行数 | 原因 |
|---|---|---|
_calcFillPolygon |
~12行 | 填充算法改为全臂渐变后不再调用 |
pointInCircle |
~5行 | 未被任何代码引用 |
dimVal |
~8行 | 未被模板或方法调用 |
curVal |
~3行 | dimVal 的包装,同样未使用 |
curMax |
~3行 | 固定返回 100,未使用 |
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) || [] }
仅 mode 值不变('preview'),去掉 sandbox 相关,无需其他改动:
<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
import WuxingSandbox from '../../components/wuxing-sandbox.vue' 不变?.、禁止 CSS Grid,继续遵守