|
|
@@ -1,116 +1,419 @@
|
|
|
<template>
|
|
|
- <view class="wuxing-sandbox">
|
|
|
+<view class="wuxing-sandbox">
|
|
|
+ <!-- header slot -->
|
|
|
+ <slot name="header">
|
|
|
<view class="sandbox-header">
|
|
|
- <text class="sandbox-title">🌟 五行能量沙盘</text>
|
|
|
- <text class="sandbox-badge">{{ badgeText }}</text>
|
|
|
+ <text class="sandbox-title">🌟 家庭能量沙盘</text>
|
|
|
+ <text class="sandbox-badge">{{ displayBadgeText }}</text>
|
|
|
</view>
|
|
|
+ </slot>
|
|
|
|
|
|
- <view class="sandbox-star">
|
|
|
- <!-- ===== 背景光晕 ===== -->
|
|
|
- <view class="star-glow-bg"></view>
|
|
|
+ <view class="sandbox-star" :style="{height: starHeight}">
|
|
|
+ <view class="star-glow-bg"></view>
|
|
|
|
|
|
- <!-- ===== 五角星轮廓(clip-path) ===== -->
|
|
|
- <view class="star-shape">
|
|
|
- <view class="star-shape__fill"></view>
|
|
|
- <view class="star-shape__outline"></view>
|
|
|
- </view>
|
|
|
-
|
|
|
- <!-- ===== 五角星连线(内五边形) ===== -->
|
|
|
- <view class="pentagram-lines">
|
|
|
- <view class="p-line p-line--1"></view>
|
|
|
- <view class="p-line p-line--2"></view>
|
|
|
- <view class="p-line p-line--3"></view>
|
|
|
- <view class="p-line p-line--4"></view>
|
|
|
- <view class="p-line p-line--5"></view>
|
|
|
- </view>
|
|
|
+ <!-- Canvas 2D 五行星图 -->
|
|
|
+ <canvas id="starCanvas" type="2d" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1"></canvas>
|
|
|
|
|
|
- <!-- ===== 顶点 1:心·火(上) ===== -->
|
|
|
- <view class="star-point star-point--top" @click="onPointClick('mind')" hover-class="point-hover">
|
|
|
- <view class="point-glow point-glow--fire"></view>
|
|
|
- <view class="point-dot point-dot--fire">
|
|
|
- <text class="point-icon">🔥</text>
|
|
|
- </view>
|
|
|
- <text class="point-label">心·火</text>
|
|
|
- <text class="point-status" v-if="mode === 'preview'">点此登录</text>
|
|
|
- </view>
|
|
|
+ <!-- Canvas 点击感应层 -->
|
|
|
+ <view class="canvas-tap-area" @click="onCanvasTap"></view>
|
|
|
|
|
|
- <!-- ===== 顶点 2:富·水(右上) ===== -->
|
|
|
- <view class="star-point star-point--ur" @click="onPointClick('wealth')" hover-class="point-hover">
|
|
|
- <view class="point-glow point-glow--water"></view>
|
|
|
- <view class="point-dot point-dot--water">
|
|
|
- <text class="point-icon">💧</text>
|
|
|
- </view>
|
|
|
- <text class="point-label">富·水</text>
|
|
|
- <text class="point-status" v-if="mode === 'preview'">点此登录</text>
|
|
|
+ <!-- 顶点:从 sortedDimensions 数据驱动 (心/行/富/智/身) -->
|
|
|
+ <view class="star-point star-point--top" @click="onPointClick(sortedDimensions[0].code)">
|
|
|
+ <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[0].element)">
|
|
|
+ <text class="point-icon">{{ sortedDimensions[0].icon }}</text>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- ===== 顶点 3:身·土(右下) ===== -->
|
|
|
- <view class="star-point star-point--lr" @click="onPointClick('body')" hover-class="point-hover">
|
|
|
- <view class="point-glow point-glow--earth"></view>
|
|
|
- <view class="point-dot point-dot--earth">
|
|
|
- <text class="point-icon">🌏</text>
|
|
|
- </view>
|
|
|
- <text class="point-label">身·土</text>
|
|
|
- <text class="point-status" v-if="mode === 'preview'">点此登录</text>
|
|
|
+ <text class="point-label">{{ sortedDimensions[0].name }}</text>
|
|
|
+ <text class="point-energy">{{ sortedDimensions[0].energy }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="star-point star-point--left" @click="onPointClick(sortedDimensions[1].code)">
|
|
|
+ <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[1].element)">
|
|
|
+ <text class="point-icon">{{ sortedDimensions[1].icon }}</text>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- ===== 顶点 4:智·金(左下) ===== -->
|
|
|
- <view class="star-point star-point--ll" @click="onPointClick('wisdom')" hover-class="point-hover">
|
|
|
- <view class="point-glow point-glow--metal"></view>
|
|
|
- <view class="point-dot point-dot--metal">
|
|
|
- <text class="point-icon">⚔️</text>
|
|
|
- </view>
|
|
|
- <text class="point-label">智·金</text>
|
|
|
- <text class="point-status" v-if="mode === 'preview'">点此登录</text>
|
|
|
+ <text class="point-label">{{ sortedDimensions[1].name }}</text>
|
|
|
+ <text class="point-energy">{{ sortedDimensions[1].energy }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="star-point star-point--right" @click="onPointClick(sortedDimensions[2].code)">
|
|
|
+ <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[2].element)">
|
|
|
+ <text class="point-icon">{{ sortedDimensions[2].icon }}</text>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- ===== 顶点 5:行·木(左上) ===== -->
|
|
|
- <view class="star-point star-point--ul" @click="onPointClick('action')" hover-class="point-hover">
|
|
|
- <view class="point-glow point-glow--wood"></view>
|
|
|
- <view class="point-dot point-dot--wood">
|
|
|
- <text class="point-icon">🌿</text>
|
|
|
- </view>
|
|
|
- <text class="point-label">行·木</text>
|
|
|
- <text class="point-status" v-if="mode === 'preview'">点此登录</text>
|
|
|
+ <text class="point-label">{{ sortedDimensions[2].name }}</text>
|
|
|
+ <text class="point-energy">{{ sortedDimensions[2].energy }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="star-point star-point--ll" @click="onPointClick(sortedDimensions[3].code)">
|
|
|
+ <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[3].element)">
|
|
|
+ <text class="point-icon">{{ sortedDimensions[3].icon }}</text>
|
|
|
</view>
|
|
|
-
|
|
|
- <!-- ===== 中心 ===== -->
|
|
|
- <view class="star-center">
|
|
|
- <view class="center-icon-wrap">
|
|
|
- <text class="center-icon">☯</text>
|
|
|
- </view>
|
|
|
- <text class="center-text">五行平衡</text>
|
|
|
- <text class="center-sub">共同成长</text>
|
|
|
+ <text class="point-label">{{ sortedDimensions[3].name }}</text>
|
|
|
+ <text class="point-energy">{{ sortedDimensions[3].energy }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="star-point star-point--lr" @click="onPointClick(sortedDimensions[4].code)">
|
|
|
+ <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[4].element)">
|
|
|
+ <text class="point-icon">{{ sortedDimensions[4].icon }}</text>
|
|
|
</view>
|
|
|
+ <text class="point-label">{{ sortedDimensions[4].name }}</text>
|
|
|
+ <text class="point-energy">{{ sortedDimensions[4].energy }}</text>
|
|
|
</view>
|
|
|
|
|
|
- <view class="shenke-hint" v-if="mode === 'preview'">
|
|
|
- <text class="hint-text">🌱 完成测评,点亮「身·土」</text>
|
|
|
+ <!-- 中心点击区域(太极由 Canvas 绘制) -->
|
|
|
+ <view class="star-center" @click="onCenterClick">
|
|
|
+ <text class="energy-hint">{{ viewMode === 'family' ? '家庭能量' : (currentMember ? currentMember.name : '个人') }}</text>
|
|
|
</view>
|
|
|
+ </view>
|
|
|
|
|
|
- <view class="dimension-bar" v-if="mode !== 'preview'">
|
|
|
- <view class="dim-item dim-item--wood">🌿行</view>
|
|
|
- <text class="dim-arrow">→</text>
|
|
|
- <view class="dim-item dim-item--fire">🔥心</view>
|
|
|
- <text class="dim-arrow">→</text>
|
|
|
- <view class="dim-item dim-item--earth">🌏身</view>
|
|
|
- <text class="dim-arrow">→</text>
|
|
|
- <view class="dim-item dim-item--metal">⚔️智</view>
|
|
|
- <text class="dim-arrow">→</text>
|
|
|
- <view class="dim-item dim-item--water">💧富</view>
|
|
|
+ <!-- dimension-bar slot -->
|
|
|
+ <slot name="dimension-bar" v-if="showDimensionBar && mode !== 'preview'">
|
|
|
+ <view class="dimension-bar">
|
|
|
+ <view class="dim-item dim-item--wood">🌿身 {{ activeScores[0] }}%</view>
|
|
|
+ <view class="dim-item dim-item--fire">🔥心 {{ activeScores[1] }}%</view>
|
|
|
+ <view class="dim-item dim-item--earth">🌏智 {{ activeScores[2] }}%</view>
|
|
|
+ <view class="dim-item dim-item--metal">⚡行 {{ activeScores[3] }}%</view>
|
|
|
+ <view class="dim-item dim-item--water">💧富 {{ activeScores[4] }}%</view>
|
|
|
</view>
|
|
|
- </view>
|
|
|
+ </slot>
|
|
|
+</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+import { renderStar, calcStarVertices } from './wuxing-sandbox-helpers/render.js'
|
|
|
+import { FillAnimator } from './wuxing-sandbox-helpers/animation.js'
|
|
|
+import { detectHitArm, armIndexToDomain } from './wuxing-sandbox-helpers/hitdetect.js'
|
|
|
+
|
|
|
+var DIM_KEYS = ['body', 'mind', 'wisdom', 'action', 'wealth']
|
|
|
+
|
|
|
export default {
|
|
|
props: {
|
|
|
- mode: { type: String, default: 'preview' },
|
|
|
- badgeText: { type: String, default: '综合成长力 0%' }
|
|
|
+ /** 模式:'preview' 随机数据 / 'live' 真实数据 */
|
|
|
+ mode: {
|
|
|
+ type: String,
|
|
|
+ default: 'preview',
|
|
|
+ validator: function(v) { return v === 'preview' || v === 'live' }
|
|
|
+ },
|
|
|
+ /** 五维分值对象 {body,mind,wisdom,action,wealth},0-100 */
|
|
|
+ scores: {
|
|
|
+ type: Object,
|
|
|
+ default: function() { return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 } }
|
|
|
+ },
|
|
|
+ /** 综合分值 0-100 */
|
|
|
+ overallScore: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ /** 家庭成员数组(MemberEnergyDTO 格式) */
|
|
|
+ members: {
|
|
|
+ type: Array,
|
|
|
+ default: function() { return [] }
|
|
|
+ },
|
|
|
+ /** 顶部徽章文字,空则自动计算 */
|
|
|
+ badgeText: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ /** 是否显示底部维度条 */
|
|
|
+ showDimensionBar: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ /** 五维能量数据(账本模式) */
|
|
|
+ dimensions: {
|
|
|
+ type: Array,
|
|
|
+ default: function() { return [] }
|
|
|
+ },
|
|
|
+ totalEnergy: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ totalHealthIndex: {
|
|
|
+ type: Number,
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ /** 星图区域高度 */
|
|
|
+ starHeight: {
|
|
|
+ type: String,
|
|
|
+ default: '480rpx'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data: function() {
|
|
|
+ return {
|
|
|
+ viewMode: 'family',
|
|
|
+ selectedMemberIndex: 0,
|
|
|
+ previewScores: [0, 0, 0, 0, 0],
|
|
|
+ canvasReady: false,
|
|
|
+ canvasWidth: 0,
|
|
|
+ canvasHeight: 0,
|
|
|
+ ctx: null,
|
|
|
+ dpr: 1,
|
|
|
+ _canvasLeft: 0,
|
|
|
+ _canvasTop: 0,
|
|
|
+ animBody: 0,
|
|
|
+ animMind: 0,
|
|
|
+ animWisdom: 0,
|
|
|
+ animAction: 0,
|
|
|
+ animWealth: 0,
|
|
|
+ _animator: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ isPreview: function() {
|
|
|
+ return this.mode === 'preview'
|
|
|
+ },
|
|
|
+ currentMember: function() {
|
|
|
+ if (this.isPreview) return null
|
|
|
+ var m = this.members
|
|
|
+ if (!m || m.length === 0) return null
|
|
|
+ if (this.selectedMemberIndex >= m.length) return m[0]
|
|
|
+ return m[this.selectedMemberIndex]
|
|
|
+ },
|
|
|
+ /** 当前活跃的五维值数组 [body, mind, wisdom, action, wealth] */
|
|
|
+ activeScores: function() {
|
|
|
+ if (this.isPreview) return this.previewScores
|
|
|
+ if (this.viewMode === 'family') {
|
|
|
+ var s = this.scores
|
|
|
+ return [
|
|
|
+ s.body || 0,
|
|
|
+ s.mind || 0,
|
|
|
+ s.wisdom || 0,
|
|
|
+ s.action || 0,
|
|
|
+ s.wealth || 0
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ var m = this.currentMember
|
|
|
+ if (!m) return [0, 0, 0, 0, 0]
|
|
|
+ return [
|
|
|
+ m.bodyScore || 0,
|
|
|
+ m.mindScore || 0,
|
|
|
+ m.wisdomScore || 0,
|
|
|
+ m.actionScore || 0,
|
|
|
+ m.wealthScore || 0
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ activeOverall: function() {
|
|
|
+ if (this.isPreview) {
|
|
|
+ var s = this.previewScores
|
|
|
+ return Math.round((s[0] + s[1] + s[2] + s[3] + s[4]) / 5)
|
|
|
+ }
|
|
|
+ if (this.viewMode === 'family') return this.overallScore || 0
|
|
|
+ var m = this.currentMember
|
|
|
+ return m ? (m.overallScore || 0) : 0
|
|
|
+ },
|
|
|
+ activeTs: function() {
|
|
|
+ return [
|
|
|
+ this.activeScores[0] / 100,
|
|
|
+ this.activeScores[1] / 100,
|
|
|
+ this.activeScores[2] / 100,
|
|
|
+ this.activeScores[3] / 100,
|
|
|
+ this.activeScores[4] / 100
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ displayBadgeText: function() {
|
|
|
+ if (this.badgeText) return this.badgeText
|
|
|
+ var label = this.viewMode === 'family' ? '家庭能量' : (this.currentMember ? this.currentMember.name : '个人能量')
|
|
|
+ return label + ' ' + (this.activeOverall || 0) + '%'
|
|
|
+ },
|
|
|
+ _allDimTs: function() {
|
|
|
+ return this.activeTs
|
|
|
+ },
|
|
|
+ hasMultipleMembers: function() {
|
|
|
+ return this.members && this.members.length > 1
|
|
|
+ },
|
|
|
+ /** 按五行布局排序的维度数据(心·火(顶)/行·木(左)/富·水(右)/智·金(左下)/身·土(右下)) */
|
|
|
+ sortedDimensions: function() {
|
|
|
+ var order = ['mind', 'action', 'wealth', 'wisdom', 'body']
|
|
|
+ var dimMap = {}
|
|
|
+ for (var i = 0; i < this.dimensions.length; i++) {
|
|
|
+ var d = this.dimensions[i]
|
|
|
+ if (d && d.code) dimMap[d.code] = d
|
|
|
+ }
|
|
|
+ var result = []
|
|
|
+ for (var j = 0; j < order.length; j++) {
|
|
|
+ var code = order[j]
|
|
|
+ result.push(dimMap[code] || { code: code, name: '', icon: '', element: '', energy: 0, healthIndex: 0 })
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ _allDimTs: function() {
|
|
|
+ if (this.canvasReady) {
|
|
|
+ this._startAnimation()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted: function() {
|
|
|
+ var self = this
|
|
|
+
|
|
|
+ // 预览模式:生成随机五维值
|
|
|
+ if (this.isPreview) {
|
|
|
+ var scores = []
|
|
|
+ for (var i = 0; i < 5; i++) {
|
|
|
+ scores.push(Math.floor(Math.random() * 51) + 35)
|
|
|
+ }
|
|
|
+ this.previewScores = scores
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待 DOM 就绪后初始化 Canvas
|
|
|
+ setTimeout(function() {
|
|
|
+ self.initCanvas()
|
|
|
+ }, 100)
|
|
|
+ },
|
|
|
+ beforeDestroy: function() {
|
|
|
+ if (this._animator) {
|
|
|
+ this._animator.cancel()
|
|
|
+ this._animator = null
|
|
|
+ }
|
|
|
},
|
|
|
methods: {
|
|
|
- onPointClick(domain) {
|
|
|
- this.$emit('point-click', domain)
|
|
|
+ /* ===========================
|
|
|
+ 视图切换
|
|
|
+ =========================== */
|
|
|
+
|
|
|
+ onCenterClick: function() {
|
|
|
+ // 预览模式不切换
|
|
|
+ if (this.isPreview) {
|
|
|
+ this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var members = this.members
|
|
|
+ if (!members || members.length === 0) {
|
|
|
+ this.viewMode = 'family'
|
|
|
+ this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.viewMode === 'family') {
|
|
|
+ this.viewMode = 'member'
|
|
|
+ } else {
|
|
|
+ if (members.length === 1) {
|
|
|
+ this.viewMode = 'family'
|
|
|
+ this.selectedMemberIndex = 0
|
|
|
+ } else {
|
|
|
+ var nextIdx = this.selectedMemberIndex + 1
|
|
|
+ if (nextIdx >= members.length) {
|
|
|
+ this.viewMode = 'family'
|
|
|
+ this.selectedMemberIndex = 0
|
|
|
+ } else {
|
|
|
+ this.selectedMemberIndex = nextIdx
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
|
|
|
+ this.$emit('mode-change', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
|
|
|
+ },
|
|
|
+
|
|
|
+ onPointClick: function(code) {
|
|
|
+ this.$emit('point-click', {
|
|
|
+ code: code,
|
|
|
+ viewMode: this.viewMode,
|
|
|
+ member: this.currentMember
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 五行元素 → CSS 颜色类名 */
|
|
|
+ dimElementColor: function(element) {
|
|
|
+ var map = { '火': 'fire', '水': 'water', '土': 'earth', '金': 'metal', '木': 'wood' }
|
|
|
+ return map[element] || 'fire'
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ===========================
|
|
|
+ Canvas 初始化 & 渲染
|
|
|
+ =========================== */
|
|
|
+
|
|
|
+ initCanvas: function() {
|
|
|
+ var self = this
|
|
|
+ uni.createSelectorQuery().in(this).select('#starCanvas')
|
|
|
+ .fields({ node: true, size: true, rect: true })
|
|
|
+ .exec(function(res) {
|
|
|
+ if (!res || !res[0] || !res[0].node) return
|
|
|
+
|
|
|
+ var canvas = res[0].node
|
|
|
+ var width = res[0].width
|
|
|
+ var height = res[0].height
|
|
|
+ if (!width || !height) return
|
|
|
+
|
|
|
+ var pixelRatio = uni.getSystemInfoSync().pixelRatio || 2
|
|
|
+
|
|
|
+ canvas.width = width * pixelRatio
|
|
|
+ canvas.height = height * pixelRatio
|
|
|
+
|
|
|
+ var ctx = canvas.getContext('2d')
|
|
|
+ ctx.scale(pixelRatio, pixelRatio)
|
|
|
+
|
|
|
+ self.ctx = ctx
|
|
|
+ self.canvasWidth = width
|
|
|
+ self.canvasHeight = height
|
|
|
+ self.dpr = pixelRatio
|
|
|
+ self._canvasLeft = res[0].left || 0
|
|
|
+ self._canvasTop = res[0].top || 0
|
|
|
+ self.canvasReady = true
|
|
|
+
|
|
|
+ self._startAnimation()
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ _doRender: function() {
|
|
|
+ if (!this.ctx) return
|
|
|
+ var ts = [this.animBody, this.animMind, this.animWisdom, this.animAction, this.animWealth]
|
|
|
+ renderStar(this.ctx, this.canvasWidth, this.canvasHeight, ts)
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ===========================
|
|
|
+ 动画控制
|
|
|
+ =========================== */
|
|
|
+
|
|
|
+ _startAnimation: function() {
|
|
|
+ var self = this
|
|
|
+ if (!this._animator) {
|
|
|
+ this._animator = new FillAnimator(
|
|
|
+ function() {
|
|
|
+ return [self.animBody, self.animMind, self.animWisdom, self.animAction, self.animWealth]
|
|
|
+ },
|
|
|
+ function(interpolated) {
|
|
|
+ self.animBody = interpolated[0]
|
|
|
+ self.animMind = interpolated[1]
|
|
|
+ self.animWisdom = interpolated[2]
|
|
|
+ self.animAction = interpolated[3]
|
|
|
+ self.animWealth = interpolated[4]
|
|
|
+ self._doRender()
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+ this._animator.start(this.activeTs, 600)
|
|
|
+ },
|
|
|
+
|
|
|
+ /* ===========================
|
|
|
+ 点击检测
|
|
|
+ =========================== */
|
|
|
+
|
|
|
+ onCanvasTap: function(e) {
|
|
|
+ if (!this.canvasReady) return
|
|
|
+
|
|
|
+ var pageX = e.detail.x
|
|
|
+ var pageY = e.detail.y
|
|
|
+ if (typeof pageX !== 'number') {
|
|
|
+ var touch = e.touches ? e.touches[0] : null
|
|
|
+ if (touch) {
|
|
|
+ pageX = touch.x || touch.clientX || 0
|
|
|
+ pageY = touch.y || touch.clientY || 0
|
|
|
+ } else {
|
|
|
+ var changedTouch = e.changedTouches ? e.changedTouches[0] : null
|
|
|
+ if (changedTouch) {
|
|
|
+ pageX = changedTouch.x || changedTouch.clientX || 0
|
|
|
+ pageY = changedTouch.y || changedTouch.clientY || 0
|
|
|
+ } else {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var relX = pageX - (this._canvasLeft || 0)
|
|
|
+ var relY = pageY - (this._canvasTop || 0)
|
|
|
+ if (relX < 0 || relY < 0 || relX > this.canvasWidth || relY > this.canvasHeight) return
|
|
|
+
|
|
|
+ var cx = this.canvasWidth / 2
|
|
|
+ var cy = this.canvasHeight / 2
|
|
|
+ var outerR = Math.min(this.canvasWidth, this.canvasHeight) * 0.42
|
|
|
+ var armIdx = detectHitArm(relX, relY, cx, cy, outerR, calcStarVertices)
|
|
|
+ if (armIdx >= 0) {
|
|
|
+ this.onPointClick(armIndexToDomain(armIdx))
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -118,13 +421,13 @@ export default {
|
|
|
|
|
|
<style scoped>
|
|
|
/* ============================================================
|
|
|
- 容器 — 暗色渐变背景
|
|
|
- ============================================================ */
|
|
|
+容器
|
|
|
+============================================================ */
|
|
|
.wuxing-sandbox {
|
|
|
margin: 20rpx 30rpx;
|
|
|
- padding: 24rpx 24rpx 20rpx;
|
|
|
- background: linear-gradient(135deg, #0f0f23, #1a1a3e, #16213e);
|
|
|
- border-radius: 28rpx;
|
|
|
+ padding: 20rpx 24rpx 24rpx;
|
|
|
+ background: linear-gradient(135deg, #1A2A4A, #1E3A5F, #16213E);
|
|
|
+ border-radius: 24rpx;
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
@@ -135,301 +438,122 @@ export default {
|
|
|
left: -50%;
|
|
|
width: 200%;
|
|
|
height: 200%;
|
|
|
- background: radial-gradient(ellipse at 30% 20%, rgba(255, 215, 0, 0.03), transparent 60%),
|
|
|
- radial-gradient(ellipse at 70% 80%, rgba(66, 165, 245, 0.03), transparent 60%);
|
|
|
+ background: radial-gradient(ellipse at 30% 20%, rgba(91, 155, 213, 0.04), transparent 60%),
|
|
|
+ radial-gradient(ellipse at 70% 80%, rgba(66, 165, 245, 0.03), transparent 60%);
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
|
|
|
-/* ============================================================
|
|
|
- 标题栏
|
|
|
- ============================================================ */
|
|
|
+/* ---- 标题 ---- */
|
|
|
.sandbox-header {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 20rpx;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
position: relative;
|
|
|
- z-index: 2;
|
|
|
-}
|
|
|
-.sandbox-title {
|
|
|
- font-size: 28rpx;
|
|
|
- font-weight: bold;
|
|
|
- color: #FFD700;
|
|
|
+ z-index: 5;
|
|
|
}
|
|
|
+.sandbox-title { font-size: 26rpx; font-weight: bold; color: #8FC5E8; }
|
|
|
.sandbox-badge {
|
|
|
font-size: 20rpx;
|
|
|
- color: rgba(255, 215, 0, 0.7);
|
|
|
- background: rgba(255, 215, 0, 0.1);
|
|
|
+ color: rgba(143,197,232,0.7);
|
|
|
+ background: rgba(143,197,232,0.1);
|
|
|
padding: 4rpx 14rpx;
|
|
|
border-radius: 20rpx;
|
|
|
}
|
|
|
|
|
|
-/* ============================================================
|
|
|
- 五角星区域
|
|
|
- ============================================================ */
|
|
|
+/* ---- 五角星区域 ---- */
|
|
|
.sandbox-star {
|
|
|
position: relative;
|
|
|
- height: 460rpx;
|
|
|
+ width: 100%;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
}
|
|
|
|
|
|
-/* ---- 背景光晕 ---- */
|
|
|
+/* 背景光晕 */
|
|
|
.star-glow-bg {
|
|
|
- position: absolute;
|
|
|
- width: 360rpx;
|
|
|
- height: 360rpx;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- background: radial-gradient(circle, rgba(255, 215, 0, 0.08), transparent 70%);
|
|
|
- border-radius: 50%;
|
|
|
- pointer-events: none;
|
|
|
+ position: absolute; width: 440rpx; height: 440rpx;
|
|
|
+ top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
|
+ background: radial-gradient(circle, rgba(91,155,213,0.10), transparent 70%);
|
|
|
+ border-radius: 50%; pointer-events: none;
|
|
|
}
|
|
|
|
|
|
-/* ---- 五角星形状(clip-path) ---- */
|
|
|
-.star-shape {
|
|
|
+/* Canvas 点击感应层 */
|
|
|
+.canvas-tap-area {
|
|
|
position: absolute;
|
|
|
- width: 320rpx;
|
|
|
- height: 304rpx;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- pointer-events: none;
|
|
|
-}
|
|
|
-.star-shape__fill {
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
- clip-path: polygon(
|
|
|
- 50% 0%, 61% 35%, 98% 35%, 68% 57%,
|
|
|
- 79% 91%, 50% 70%, 21% 91%, 32% 57%,
|
|
|
- 2% 35%, 39% 35%
|
|
|
- );
|
|
|
- background: linear-gradient(135deg, rgba(255, 215, 0, 0.12), rgba(255, 180, 50, 0.04));
|
|
|
- animation: starPulse 4s ease-in-out infinite;
|
|
|
-}
|
|
|
-.star-shape__outline {
|
|
|
- position: absolute;
|
|
|
- inset: -2rpx;
|
|
|
- clip-path: polygon(
|
|
|
- 50% 0%, 61% 35%, 98% 35%, 68% 57%,
|
|
|
- 79% 91%, 50% 70%, 21% 91%, 32% 57%,
|
|
|
- 2% 35%, 39% 35%
|
|
|
- );
|
|
|
- background: transparent;
|
|
|
- border: 1rpx solid rgba(255, 215, 0, 0.15);
|
|
|
- /* fake border via slightly larger duplicate */
|
|
|
-}
|
|
|
-/* Real outline using pseudo element trick */
|
|
|
-.star-shape__outline::before {
|
|
|
- content: '';
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
- clip-path: polygon(
|
|
|
- 50% 0%, 61% 35%, 98% 35%, 68% 57%,
|
|
|
- 79% 91%, 50% 70%, 21% 91%, 32% 57%,
|
|
|
- 2% 35%, 39% 35%
|
|
|
- );
|
|
|
- background: transparent;
|
|
|
- box-shadow: 0 0 20rpx rgba(255, 215, 0, 0.06);
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes starPulse {
|
|
|
- 0%, 100% { opacity: 1; }
|
|
|
- 50% { opacity: 0.6; }
|
|
|
-}
|
|
|
-
|
|
|
-/* ---- 五角星连线(五条边) ---- */
|
|
|
-.pentagram-lines {
|
|
|
- position: absolute;
|
|
|
- inset: 0;
|
|
|
- pointer-events: none;
|
|
|
-}
|
|
|
-.p-line {
|
|
|
- position: absolute;
|
|
|
- height: 2rpx;
|
|
|
- background: linear-gradient(90deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.05));
|
|
|
- transform-origin: left center;
|
|
|
- top: 0;
|
|
|
- left: 0;
|
|
|
+ top: 0; left: 0; right: 0; bottom: 0;
|
|
|
+ z-index: 2;
|
|
|
}
|
|
|
|
|
|
-/* Line 1: Fire → Earth (top → lower-right) */
|
|
|
-.p-line--1 { top: 46rpx; left: 250rpx; width: 290rpx; transform: rotate(68deg); }
|
|
|
-/* Line 2: Earth → Wood (lower-right → upper-left) */
|
|
|
-.p-line--2 { top: 340rpx; left: 340rpx; width: 290rpx; transform: rotate(-106deg); }
|
|
|
-/* Line 3: Wood → Water (upper-left → upper-right) */
|
|
|
-.p-line--3 { top: 156rpx; left: 72rpx; width: 364rpx; transform: rotate(0deg); }
|
|
|
-/* Line 4: Water → Metal (upper-right → lower-left) */
|
|
|
-.p-line--4 { top: 156rpx; left: 400rpx; width: 290rpx; transform: rotate(106deg); }
|
|
|
-/* Line 5: Metal → Fire (lower-left → top) */
|
|
|
-.p-line--5 { top: 340rpx; left: 100rpx; width: 290rpx; transform: rotate(-68deg); }
|
|
|
-
|
|
|
/* ============================================================
|
|
|
- 顶点
|
|
|
- ============================================================ */
|
|
|
+顶点:图标 + 中文标签
|
|
|
+============================================================ */
|
|
|
.star-point {
|
|
|
- position: absolute;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- z-index: 3;
|
|
|
- cursor: pointer;
|
|
|
-}
|
|
|
-.point-hover {
|
|
|
- opacity: 0.8;
|
|
|
-}
|
|
|
-
|
|
|
-/* 顶点定位 */
|
|
|
-.star-point--top { top: 8%; left: 50%; }
|
|
|
-.star-point--ur { top: 32%; left: 84%; }
|
|
|
-.star-point--lr { top: 76%; left: 72%; }
|
|
|
-.star-point--ll { top: 76%; left: 28%; }
|
|
|
-.star-point--ul { top: 32%; left: 16%; }
|
|
|
-
|
|
|
-/* ---- 顶点光晕 ---- */
|
|
|
-.point-glow {
|
|
|
- position: absolute;
|
|
|
- width: 90rpx;
|
|
|
- height: 90rpx;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- border-radius: 50%;
|
|
|
- opacity: 0.25;
|
|
|
- pointer-events: none;
|
|
|
-}
|
|
|
-.point-glow--fire { background: radial-gradient(circle, #FF6B35, transparent 70%); }
|
|
|
-.point-glow--water { background: radial-gradient(circle, #42A5F5, transparent 70%); }
|
|
|
-.point-glow--earth { background: radial-gradient(circle, #8D6E63, transparent 70%); }
|
|
|
-.point-glow--metal { background: radial-gradient(circle, #FFD700, transparent 70%); }
|
|
|
-.point-glow--wood { background: radial-gradient(circle, #4CAF50, transparent 70%); }
|
|
|
+ position: absolute; transform: translate(-50%, -50%); z-index: 3; cursor: pointer;
|
|
|
+ display: flex; flex-direction: column; align-items: center;
|
|
|
+}
|
|
|
+.star-point--top { top: 3%; left: 50%; }
|
|
|
+.star-point--left { top: 38%; left: 5%; }
|
|
|
+.star-point--right { top: 38%; left: 95%; }
|
|
|
+.star-point--lr { top: 73%; left: 75%; }
|
|
|
+.star-point--ll { top: 73%; left: 25%; }
|
|
|
|
|
|
-/* ---- 顶点圆点 ---- */
|
|
|
.point-dot {
|
|
|
- width: 56rpx;
|
|
|
- height: 56rpx;
|
|
|
- border-radius: 50%;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- position: relative;
|
|
|
- z-index: 1;
|
|
|
-}
|
|
|
-.point-dot--fire { background: linear-gradient(135deg, #FF6B35, #E65100); box-shadow: 0 0 16rpx rgba(255, 107, 53, 0.4); }
|
|
|
-.point-dot--water { background: linear-gradient(135deg, #42A5F5, #1565C0); box-shadow: 0 0 16rpx rgba(66, 165, 245, 0.4); }
|
|
|
-.point-dot--earth { background: linear-gradient(135deg, #8D6E63, #5D4037); box-shadow: 0 0 16rpx rgba(141, 110, 99, 0.4); }
|
|
|
-.point-dot--metal { background: linear-gradient(135deg, #FFD700, #FF8F00); box-shadow: 0 0 16rpx rgba(255, 215, 0, 0.4); }
|
|
|
-.point-dot--wood { background: linear-gradient(135deg, #4CAF50, #2E7D32); box-shadow: 0 0 16rpx rgba(76, 175, 80, 0.4); }
|
|
|
-
|
|
|
-.point-icon {
|
|
|
- font-size: 28rpx;
|
|
|
- line-height: 1;
|
|
|
-}
|
|
|
+ width: 52rpx; height: 52rpx; border-radius: 50%;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ box-shadow: 0 0 24rpx rgba(255,255,255,0.25);
|
|
|
+}
|
|
|
+.point-dot--fire { background: linear-gradient(135deg, #FF6B35, #E65100); }
|
|
|
+.point-dot--water { background: linear-gradient(135deg, #42A5F5, #1565C0); }
|
|
|
+.point-dot--earth { background: linear-gradient(135deg, #8D6E63, #5D4037); }
|
|
|
+.point-dot--metal { background: linear-gradient(135deg, #FFD700, #FF8F00); }
|
|
|
+.point-dot--wood { background: linear-gradient(135deg, #4CAF50, #2E7D32); }
|
|
|
+
|
|
|
+.point-icon { font-size: 26rpx; line-height: 1; }
|
|
|
|
|
|
-/* ---- 顶点标签 ---- */
|
|
|
.point-label {
|
|
|
- font-size: 20rpx;
|
|
|
- color: rgba(255, 255, 255, 0.9);
|
|
|
- font-weight: bold;
|
|
|
- margin-top: 8rpx;
|
|
|
- white-space: nowrap;
|
|
|
-}
|
|
|
-.point-status {
|
|
|
- font-size: 16rpx;
|
|
|
- color: #F97316;
|
|
|
+ font-size: 22rpx; font-weight: bold; color: rgba(255,255,255,0.9);
|
|
|
+ margin-top: 6rpx; text-shadow: 0 1rpx 4rpx rgba(0,0,0,0.5);
|
|
|
+ letter-spacing: 2rpx;
|
|
|
+}
|
|
|
+.point-energy {
|
|
|
+ font-size: 18rpx; color: rgba(143,197,232,0.8);
|
|
|
+ background: rgba(143,197,232,0.12);
|
|
|
+ padding: 1rpx 10rpx; border-radius: 12rpx;
|
|
|
margin-top: 2rpx;
|
|
|
- white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
- 中心圆
|
|
|
- ============================================================ */
|
|
|
+中心点击区域
|
|
|
+============================================================ */
|
|
|
.star-center {
|
|
|
- position: absolute;
|
|
|
- top: 50%;
|
|
|
- left: 50%;
|
|
|
- transform: translate(-50%, -50%);
|
|
|
- width: 110rpx;
|
|
|
- height: 110rpx;
|
|
|
- border-radius: 50%;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- z-index: 4;
|
|
|
- background: radial-gradient(circle at 40% 35%, rgba(255, 215, 0, 0.15), rgba(255, 215, 0, 0.03));
|
|
|
- border: 1rpx solid rgba(255, 215, 0, 0.2);
|
|
|
- box-shadow: 0 0 30rpx rgba(255, 215, 0, 0.08), inset 0 0 20rpx rgba(255, 215, 0, 0.04);
|
|
|
- animation: centerPulse 3s ease-in-out infinite;
|
|
|
-}
|
|
|
-
|
|
|
-@keyframes centerPulse {
|
|
|
- 0%, 100% { box-shadow: 0 0 30rpx rgba(255, 215, 0, 0.08), inset 0 0 20rpx rgba(255, 215, 0, 0.04); }
|
|
|
- 50% { box-shadow: 0 0 40rpx rgba(255, 215, 0, 0.15), inset 0 0 30rpx rgba(255, 215, 0, 0.08); }
|
|
|
+ position: absolute; top: 50%; left: 50%;
|
|
|
+ transform: translate(-50%, -50%); z-index: 4;
|
|
|
+ width: 130rpx; height: 130rpx; border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
|
|
|
|
-.center-icon-wrap {
|
|
|
- width: 36rpx;
|
|
|
- height: 36rpx;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-bottom: 2rpx;
|
|
|
-}
|
|
|
-.center-icon {
|
|
|
- font-size: 28rpx;
|
|
|
- color: #FFD700;
|
|
|
-}
|
|
|
-.center-text {
|
|
|
- font-size: 20rpx;
|
|
|
- color: #FFD700;
|
|
|
- font-weight: bold;
|
|
|
- line-height: 1.3;
|
|
|
-}
|
|
|
-.center-sub {
|
|
|
- font-size: 16rpx;
|
|
|
- color: rgba(255, 215, 0, 0.5);
|
|
|
- line-height: 1.2;
|
|
|
+.energy-hint {
|
|
|
+ position: absolute; bottom: -30rpx; left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ font-size: 18rpx; color: rgba(143,197,232,0.4);
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
- 提示 & 维度条
|
|
|
- ============================================================ */
|
|
|
-.shenke-hint {
|
|
|
- text-align: center;
|
|
|
- margin-top: 8rpx;
|
|
|
- position: relative;
|
|
|
- z-index: 2;
|
|
|
-}
|
|
|
-.hint-text {
|
|
|
- font-size: 22rpx;
|
|
|
- color: rgba(255, 215, 0, 0.7);
|
|
|
-}
|
|
|
-
|
|
|
+维度详情条
|
|
|
+============================================================ */
|
|
|
.dimension-bar {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- margin-top: 16rpx;
|
|
|
- gap: 6rpx;
|
|
|
- position: relative;
|
|
|
- z-index: 2;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ margin-top: 16rpx; gap: 8rpx;
|
|
|
+ position: relative; z-index: 5; flex-wrap: wrap;
|
|
|
}
|
|
|
.dim-item {
|
|
|
- font-size: 20rpx;
|
|
|
- padding: 4rpx 12rpx;
|
|
|
- border-radius: 10rpx;
|
|
|
-}
|
|
|
-.dim-item--wood { color: #4CAF50; background: rgba(76, 175, 80, 0.1); }
|
|
|
-.dim-item--fire { color: #FF6B35; background: rgba(255, 107, 53, 0.1); }
|
|
|
-.dim-item--earth { color: #8D6E63; background: rgba(141, 110, 99, 0.1); }
|
|
|
-.dim-item--metal { color: #FFD700; background: rgba(255, 215, 0, 0.1); }
|
|
|
-.dim-item--water { color: #42A5F5; background: rgba(66, 165, 245, 0.1); }
|
|
|
-.dim-arrow {
|
|
|
- font-size: 18rpx;
|
|
|
- color: rgba(255, 255, 255, 0.2);
|
|
|
+ font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 10rpx; white-space: nowrap;
|
|
|
}
|
|
|
+.dim-item--wood { color: #4CAF50; background: rgba(76,175,80,0.12); }
|
|
|
+.dim-item--fire { color: #FF6B35; background: rgba(255,107,53,0.12); }
|
|
|
+.dim-item--earth { color: #8D6E63; background: rgba(141,110,99,0.12); }
|
|
|
+.dim-item--metal { color: #FFD700; background: rgba(255,215,0,0.12); }
|
|
|
+.dim-item--water { color: #42A5F5; background: rgba(66,165,245,0.12); }
|
|
|
</style>
|