| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817 |
- <template>
- <view class="wuxing-sandbox">
- <!-- header slot -->
- <slot name="header">
- <view class="sandbox-header">
- <image class="sandbox-logo" src="/static/logo.png" mode="aspectFit"></image>
- <text class="sandbox-title">浠艾福能量沙盘</text>
- <!-- 右上角:live 模式显示个人/全家切换,preview 模式显示徽章文案 -->
- <view class="sandbox-switch" v-if="mode !== 'preview'" @click="onToggle(currentView === 'family' ? 'personal' : 'family')">
- <text class="sandbox-switch-icon">{{ currentView === 'family' ? '👨👩👧👦' : '👤' }}</text>
- <text class="sandbox-switch-label">{{ currentView === 'family' ? '全家' : (currentMember ? currentMember.name : '个人') }}</text>
- <text class="sandbox-switch-arrow">⇄</text>
- </view>
- <text class="sandbox-badge" v-else>{{ displayBadgeText }}</text>
- </view>
- </slot>
- <view class="sandbox-star" :style="{height: starHeight}">
- <view class="star-glow-bg"></view>
- <!-- Canvas 2D 五行星图 -->
- <canvas id="starCanvas" type="2d" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1"></canvas>
- <!-- Canvas 粒子动画覆盖层 -->
- <canvas id="flowCanvas" type="2d" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1;pointer-events:none"></canvas>
- <!-- Canvas 点击感应层 -->
- <view class="canvas-tap-area" @click="onCanvasTap"></view>
- <!-- 顶点:从 sortedDimensions 顺时针排 [心, 身, 智, 富, 行] -->
- <view class="star-point star-point--top star-point--row" @click="onPointClick(sortedDimensions[0].code)">
- <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[0].code)">
- <text class="point-icon">{{ sortedDimensions[0].icon }}</text>
- </view>
- <text class="point-label">{{ sortedDimensions[0].name }}</text>
- <text class="point-energy">{{ sortedDimensions[0].energy }}</text>
- </view>
- <view class="star-point star-point--right" @click="onPointClick(sortedDimensions[1].code)">
- <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[1].code)">
- <text class="point-icon">{{ sortedDimensions[1].icon }}</text>
- </view>
- <text class="point-label">{{ sortedDimensions[1].name }}</text>
- <text class="point-energy">{{ sortedDimensions[1].energy }}</text>
- </view>
- <view class="star-point star-point--lr star-point--row" @click="onPointClick(sortedDimensions[2].code)">
- <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[2].code)">
- <text class="point-icon">{{ sortedDimensions[2].icon }}</text>
- </view>
- <text class="point-label">{{ sortedDimensions[2].name }}</text>
- <text class="point-energy">{{ sortedDimensions[2].energy }}</text>
- </view>
- <view class="star-point star-point--ll star-point--rev" @click="onPointClick(sortedDimensions[3].code)">
- <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[3].code)">
- <text class="point-icon">{{ sortedDimensions[3].icon }}</text>
- </view>
- <text class="point-label">{{ sortedDimensions[3].name }}</text>
- <text class="point-energy">{{ sortedDimensions[3].energy }}</text>
- </view>
- <view class="star-point star-point--left" @click="onPointClick(sortedDimensions[4].code)">
- <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[4].code)">
- <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>
- <!-- 中心点击区域(太极由 Canvas 绘制) -->
- <view class="star-center" @click="onCenterClick"></view>
- </view>
- <!-- dimension-bar slot -->
- <slot name="dimension-bar" v-if="showDimensionBar && mode !== 'preview'">
- <view class="dimension-bar">
- <view class="dim-item dim-item--earth">🌏身 {{ activeScores[0] }}%</view>
- <view class="dim-item dim-item--fire">🔥心 {{ activeScores[1] }}%</view>
- <view class="dim-item dim-item--metal">⚡智 {{ activeScores[2] }}%</view>
- <view class="dim-item dim-item--wood">🌿行 {{ activeScores[3] }}%</view>
- <view class="dim-item dim-item--water">💧富 {{ activeScores[4] }}%</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'
- import { FlowAnimation } from './wuxing-sandbox-helpers/flow-animation.js'
- var DIM_KEYS = ['body', 'mind', 'wisdom', 'action', 'wealth']
- /** 维度 → 五行映射(API 缺字段时的兜底) */
- var DIM_META = {
- mind: { name: '心', icon: '🔥', element: '火', color: 'fire' },
- action: { name: '行', icon: '🌿', element: '木', color: 'wood' },
- wealth: { name: '富', icon: '💧', element: '水', color: 'water' },
- wisdom: { name: '智', icon: '⚡', element: '金', color: 'metal' },
- body: { name: '身', icon: '🌏', element: '土', color: 'earth' }
- }
- export default {
- props: {
- /** 模式:'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 [] }
- },
- /** "我的"个人数据(参数指定成员的 EnergySandboxDTO,含 bodyScore/mindScore/.../overallScore) */
- personalData: {
- type: Object,
- default: null
- },
- /** 顶部徽章文字,空则自动计算 */
- 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: '540rpx'
- },
- /** 是否启用全家沙盘切换(控制个人/全家开关显隐) */
- familyUnlocked: {
- type: Boolean,
- default: false
- },
- /** 家庭真实成员数(来自 /api/family/member/list,用于全家切换判断;缺省时用 members 长度) */
- memberCount: {
- type: Number,
- default: 0
- },
- /** 当前视角模式(由父组件控制) */
- currentView: {
- type: String,
- default: 'personal',
- validator: function(v) { return v === 'personal' || v === 'family' }
- }
- },
- data: function() {
- return {
- viewMode: 'personal',
- 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,
- _flowAnim: null,
- _flowCanvasReady: false
- }
- },
- 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 p = this.personalData
- if (p) {
- return [
- p.bodyScore || 0,
- p.mindScore || 0,
- p.wisdomScore || 0,
- p.actionScore || 0,
- p.wealthScore || 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 p = this.personalData
- if (p && p.overallScore != null) return p.overallScore
- 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
- },
- activeClassName: function() {
- return this.currentView === 'personal' ? 'toggle-item--active' : ''
- },
- activeFamilyClassName: function() {
- return this.currentView === 'family' ? 'toggle-item--active' : ''
- },
- /** 家庭能量值(用于太极阳面) */
- familyEnergy: function() {
- if (this.isPreview) {
- var s = this.previewScores
- return Math.round((s[0] + s[1] + s[2] + s[3] + s[4]) / 5)
- }
- return this.viewMode === 'family' ? (this.overallScore || 0) : (this.overallScore || 0)
- },
- /** 个人能量值(用于太极阴面) */
- personalEnergy: function() {
- if (this.isPreview) return 0
- if (this.viewMode === 'member') {
- var m = this.currentMember
- return m ? (m.overallScore || 0) : 0
- }
- // 家庭模式下取当前选中成员
- var m = this.currentMember
- return m ? (m.overallScore || 0) : 0
- },
- /** 按五行顺序绕圈排列
- * 固定序:心(火)→身(土)→智(金)→富(水)→行(木),心始终在顶部
- * 其余按五行相生序顺时针依次排:左边→右边→左下→右下
- */
- sortedDimensions: function() {
- var order = ['mind', 'body', 'wisdom', 'wealth', 'action']
- 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 all = []
- for (var j = 0; j < order.length; j++) {
- var code = order[j]
- var d = dimMap[code]
- var meta = DIM_META[code] || {}
- if (d) {
- if (!d.name) d.name = meta.name || ''
- if (!d.icon) d.icon = meta.icon || ''
- if (!d.element) d.element = meta.element || ''
- all.push(d)
- } else {
- all.push({ code: code, name: meta.name || '', icon: meta.icon || '', element: meta.element || '', energy: 0, healthIndex: 0 })
- }
- }
- return all
- // all = [心→顶, 身→右, 智→右下, 富→左下, 行→左]
- }
- },
- watch: {
- _allDimTs: function() {
- if (this.canvasReady) {
- this._startAnimation()
- }
- },
- sortedDimensions: function(newVal) {
- if (this._flowAnim && this._flowCanvasReady) {
- var dimOrder = newVal.map(function(d) { return d.code })
- this._flowAnim.refreshPaths({ dimOrder: dimOrder })
- }
- },
- currentView: function(newVal) {
- if (newVal && newVal !== this.viewMode) {
- this.viewMode = newVal
- }
- },
- hasMultipleMembers: function(newVal) {
- if (!newVal && this.viewMode === 'family') {
- this.viewMode = 'personal'
- }
- }
- },
- mounted: function() {
- var self = this
- // 预览模式:生成随机五维值(缓存以避免每次 mount 重算)
- if (this.isPreview) {
- var cacheKey = '_wuxing_preview_scores'
- var cached = uni.getStorageSync(cacheKey)
- if (cached && Array.isArray(cached) && cached.length === 5) {
- this.previewScores = cached
- } else {
- var scores = []
- for (var i = 0; i < 5; i++) {
- scores.push(Math.floor(Math.random() * 51) + 35)
- }
- this.previewScores = scores
- uni.setStorageSync(cacheKey, scores)
- }
- }
- // 等待 DOM 就绪后初始化 Canvas
- setTimeout(function() {
- self.initCanvas()
- }, 100)
- },
- beforeDestroy: function() {
- if (this._animator) {
- this._animator.cancel()
- this._animator = null
- }
- if (this._flowAnim) {
- this._flowAnim.stop()
- this._flowAnim = null
- }
- },
- methods: {
- /* ===========================
- 视图切换
- =========================== */
- 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
- })
- },
- /** 个人/全家切换(由父组件控制数据源) */
- onToggle: function(view) {
- if (view === 'family') {
- var total = this.memberCount > 0 ? this.memberCount : (this.members ? this.members.length : 0)
- if (total < 2) {
- uni.showModal({
- title: '需要更多家庭成员',
- content: '至少需要 2 位家庭成员才能查看全家能量,快去添加吧!',
- confirmText: '去添加',
- cancelText: '取消',
- success: function(res) {
- if (res.confirm) {
- uni.navigateTo({ url: '/pages/profile-extra/family-members' })
- }
- }
- })
- return
- }
- }
- this.viewMode = view
- if (view === this.currentView) return
- this.$emit('view-change', view)
- },
- /** 维度 code → CSS 颜色类名(不依赖 API 的 element 字段) */
- dimElementColor: function(code) {
- var meta = DIM_META[code]
- return meta ? meta.color : '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.getWindowInfo().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()
- self._initFlowCanvas()
- })
- },
- _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, {
- familyEnergy: this.familyEnergy,
- personalEnergy: this.personalEnergy
- })
- },
- /* ===========================
- 动画控制
- =========================== */
- _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)
- },
- /* ===========================
- 粒子流量动画
- =========================== */
- _initFlowCanvas: function() {
- var self = this
- var dimOrder = this.sortedDimensions.map(function(d) { return d.code })
- uni.createSelectorQuery().in(this).select('#flowCanvas')
- .fields({ node: true, size: 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.getWindowInfo().pixelRatio || 2
- canvas.width = width * pixelRatio
- canvas.height = height * pixelRatio
- var ctx = canvas.getContext('2d')
- ctx.scale(pixelRatio, pixelRatio)
- var outerR = Math.min(width, height) * 0.50
- var vertices = calcStarVertices(width / 2, height / 2, outerR)
- self._flowAnim = new FlowAnimation(ctx, width, height, {
- outer: vertices.outer,
- inner: vertices.inner,
- dimOrder: dimOrder
- })
- self._flowAnim.start()
- self._flowCanvasReady = true
- })
- },
- /* ===========================
- 点击检测
- =========================== */
- 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.46
- var armIdx = detectHitArm(relX, relY, cx, cy, outerR, calcStarVertices)
- if (armIdx >= 0) {
- this.onPointClick(armIndexToDomain(armIdx))
- }
- }
- }
- }
- </script>
- <style scoped>
- /* ============================================================
- 容器
- ============================================================ */
- .wuxing-sandbox {
- margin: 20rpx 30rpx;
- padding: 20rpx 24rpx 24rpx;
- background: linear-gradient(135deg, #1A2A4A, #1E3A5F, #16213E);
- border-radius: 24rpx;
- position: relative;
- overflow: hidden;
- }
- .wuxing-sandbox::before {
- content: '';
- position: absolute;
- top: -50%;
- left: -50%;
- width: 200%;
- height: 200%;
- 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: flex-start;
- align-items: center;
- margin-bottom: 12rpx;
- position: relative;
- z-index: 5;
- }
- .sandbox-title { font-size: 26rpx; font-weight: bold; color: #8FC5E8; margin-left: 8rpx; }
- .sandbox-logo { width: 32rpx; height: 32rpx; flex-shrink: 0; }
- .sandbox-badge {
- font-size: 20rpx;
- color: rgba(143,197,232,0.7);
- background: rgba(143,197,232,0.1);
- padding: 4rpx 14rpx;
- border-radius: 20rpx;
- margin-left: auto;
- }
- .sandbox-switch {
- display: flex;
- align-items: center;
- gap: 6rpx;
- font-size: 22rpx;
- color: #fff;
- background: linear-gradient(135deg, #667eea, #764ba2);
- padding: 10rpx 20rpx;
- border-radius: 28rpx;
- margin-left: auto;
- flex-shrink: 0;
- box-shadow: 0 4rpx 12rpx rgba(102,126,234,0.45);
- border: 1rpx solid rgba(255,255,255,0.25);
- }
- .sandbox-switch:active {
- opacity: 0.85;
- box-shadow: none;
- }
- .sandbox-switch-icon { font-size: 24rpx; line-height: 1; }
- .sandbox-switch-label { font-size: 22rpx; font-weight: 700; max-width: 140rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
- .sandbox-switch-arrow { font-size: 20rpx; opacity: 0.8; }
- /* ---- 五角星区域 ---- */
- .sandbox-star {
- position: relative;
- width: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- /* 背景光晕 */
- .star-glow-bg {
- position: absolute; width: 500rpx; height: 500rpx;
- 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;
- }
- /* Canvas 点击感应层 */
- .canvas-tap-area {
- position: absolute;
- top: 0; left: 0; right: 0; bottom: 0;
- z-index: 2;
- }
- /* ============================================================
- 顶点:图标 + 中文标签
- ============================================================ */
- .star-point {
- position: absolute; transform: translate(-50%, -50%); z-index: 3; cursor: pointer;
- display: flex; flex-direction: column; align-items: center;
- }
- /* 横排 */
- .star-point--row {
- flex-direction: row;
- gap: 6rpx;
- }
- .star-point--rev {
- flex-direction: row-reverse;
- gap: 6rpx;
- }
- .star-point--top { top: 1%; left: 50%; }
- .star-point--left { top: 38%; left: 5%; }
- .star-point--right { top: 38%; left: 95%; }
- .star-point--lr { top: 89%; left: 85%; }
- .star-point--ll { top: 89%; left: 15%; }
- .point-dot {
- 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: 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;
- }
- .star-point--row .point-label,
- .star-point--rev .point-label {
- margin-top: 0;
- }
- .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;
- }
- .star-point--row .point-energy,
- .star-point--rev .point-energy {
- margin-top: 0;
- }
- /* ============================================================
- 中心点击区域
- ============================================================ */
- .star-center {
- position: absolute; top: 50%; left: 50%;
- transform: translate(-50%, -50%); z-index: 4;
- width: 180rpx; height: 180rpx; border-radius: 50%;
- cursor: pointer;
- }
- /* ============================================================
- 维度详情条
- ============================================================ */
- .dimension-bar {
- 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; 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); }
- /* ============================================================
- 个人/全家切换开关
- ============================================================ */
- .sandbox-toggle {
- display: flex;
- justify-content: center;
- align-items: center;
- margin-bottom: 12rpx;
- position: relative;
- z-index: 5;
- gap: 0;
- }
- .toggle-item {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 8rpx 28rpx;
- font-size: 24rpx;
- color: rgba(143,197,232,0.6);
- background: rgba(143,197,232,0.08);
- border: 1rpx solid rgba(143,197,232,0.15);
- border-radius: 30rpx 0 0 30rpx;
- transition: all 0.2s;
- }
- .toggle-item--family {
- border-radius: 0 30rpx 30rpx 0;
- border-left: none;
- }
- .toggle-item--active {
- color: #fff;
- background: linear-gradient(135deg, #5B9BD5, #3A7CC2);
- border-color: #5B9BD5;
- }
- .toggle-icon {
- font-size: 26rpx;
- margin-right: 8rpx;
- }
- .toggle-text {
- font-weight: bold;
- }
- </style>
|