wuxing-sandbox.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. <template>
  2. <view class="wuxing-sandbox">
  3. <!-- header slot -->
  4. <slot name="header">
  5. <view class="sandbox-header">
  6. <image class="sandbox-logo" src="/static/logo.png" mode="aspectFit"></image>
  7. <text class="sandbox-title">浠艾福能量沙盘</text>
  8. <text class="sandbox-badge">{{ displayBadgeText }}</text>
  9. </view>
  10. </slot>
  11. <view class="sandbox-star" :style="{height: starHeight}">
  12. <view class="star-glow-bg"></view>
  13. <!-- Canvas 2D 五行星图 -->
  14. <canvas id="starCanvas" type="2d" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1"></canvas>
  15. <!-- Canvas 粒子动画覆盖层 -->
  16. <canvas id="flowCanvas" type="2d" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1;pointer-events:none"></canvas>
  17. <!-- Canvas 点击感应层 -->
  18. <view class="canvas-tap-area" @click="onCanvasTap"></view>
  19. <!-- 顶点:从 sortedDimensions 顺时针排 [顶, 右, 右下, 左下, 左] -->
  20. <view class="star-point star-point--top star-point--row" @click="onPointClick(sortedDimensions[0].code)">
  21. <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[0].code)">
  22. <text class="point-icon">{{ sortedDimensions[0].icon }}</text>
  23. </view>
  24. <text class="point-label">{{ sortedDimensions[0].name }}</text>
  25. <text class="point-energy">{{ sortedDimensions[0].energy }}</text>
  26. </view>
  27. <view class="star-point star-point--right" @click="onPointClick(sortedDimensions[1].code)">
  28. <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[1].code)">
  29. <text class="point-icon">{{ sortedDimensions[1].icon }}</text>
  30. </view>
  31. <text class="point-label">{{ sortedDimensions[1].name }}</text>
  32. <text class="point-energy">{{ sortedDimensions[1].energy }}</text>
  33. </view>
  34. <view class="star-point star-point--lr star-point--row" @click="onPointClick(sortedDimensions[2].code)">
  35. <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[2].code)">
  36. <text class="point-icon">{{ sortedDimensions[2].icon }}</text>
  37. </view>
  38. <text class="point-label">{{ sortedDimensions[2].name }}</text>
  39. <text class="point-energy">{{ sortedDimensions[2].energy }}</text>
  40. </view>
  41. <view class="star-point star-point--ll star-point--rev" @click="onPointClick(sortedDimensions[3].code)">
  42. <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[3].code)">
  43. <text class="point-icon">{{ sortedDimensions[3].icon }}</text>
  44. </view>
  45. <text class="point-label">{{ sortedDimensions[3].name }}</text>
  46. <text class="point-energy">{{ sortedDimensions[3].energy }}</text>
  47. </view>
  48. <view class="star-point star-point--left" @click="onPointClick(sortedDimensions[4].code)">
  49. <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[4].code)">
  50. <text class="point-icon">{{ sortedDimensions[4].icon }}</text>
  51. </view>
  52. <text class="point-label">{{ sortedDimensions[4].name }}</text>
  53. <text class="point-energy">{{ sortedDimensions[4].energy }}</text>
  54. </view>
  55. <!-- 中心点击区域(太极由 Canvas 绘制) -->
  56. <view class="star-center" @click="onCenterClick"></view>
  57. </view>
  58. <!-- dimension-bar slot -->
  59. <slot name="dimension-bar" v-if="showDimensionBar && mode !== 'preview'">
  60. <view class="dimension-bar">
  61. <view class="dim-item dim-item--earth">🌏身 {{ activeScores[0] }}%</view>
  62. <view class="dim-item dim-item--fire">🔥心 {{ activeScores[1] }}%</view>
  63. <view class="dim-item dim-item--metal">⚡智 {{ activeScores[2] }}%</view>
  64. <view class="dim-item dim-item--wood">🌿行 {{ activeScores[3] }}%</view>
  65. <view class="dim-item dim-item--water">💧富 {{ activeScores[4] }}%</view>
  66. </view>
  67. </slot>
  68. </view>
  69. </template>
  70. <script>
  71. import { renderStar, calcStarVertices } from './wuxing-sandbox-helpers/render.js'
  72. import { FillAnimator } from './wuxing-sandbox-helpers/animation.js'
  73. import { detectHitArm, armIndexToDomain } from './wuxing-sandbox-helpers/hitdetect.js'
  74. import { FlowAnimation } from './wuxing-sandbox-helpers/flow-animation.js'
  75. var DIM_KEYS = ['body', 'mind', 'wisdom', 'action', 'wealth']
  76. /** 维度 → 五行映射(API 缺字段时的兜底) */
  77. var DIM_META = {
  78. mind: { name: '心', icon: '🔥', element: '火', color: 'fire' },
  79. action: { name: '行', icon: '🌿', element: '木', color: 'wood' },
  80. wealth: { name: '富', icon: '💧', element: '水', color: 'water' },
  81. wisdom: { name: '智', icon: '⚡', element: '金', color: 'metal' },
  82. body: { name: '身', icon: '🌏', element: '土', color: 'earth' }
  83. }
  84. export default {
  85. props: {
  86. /** 模式:'preview' 随机数据 / 'live' 真实数据 */
  87. mode: {
  88. type: String,
  89. default: 'preview',
  90. validator: function(v) { return v === 'preview' || v === 'live' }
  91. },
  92. /** 五维分值对象 {body,mind,wisdom,action,wealth},0-100 */
  93. scores: {
  94. type: Object,
  95. default: function() { return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 } }
  96. },
  97. /** 综合分值 0-100 */
  98. overallScore: {
  99. type: Number,
  100. default: 0
  101. },
  102. /** 家庭成员数组(MemberEnergyDTO 格式) */
  103. members: {
  104. type: Array,
  105. default: function() { return [] }
  106. },
  107. /** 顶部徽章文字,空则自动计算 */
  108. badgeText: {
  109. type: String,
  110. default: ''
  111. },
  112. /** 是否显示底部维度条 */
  113. showDimensionBar: {
  114. type: Boolean,
  115. default: true
  116. },
  117. /** 五维能量数据(账本模式) */
  118. dimensions: {
  119. type: Array,
  120. default: function() { return [] }
  121. },
  122. totalEnergy: {
  123. type: Number,
  124. default: 0
  125. },
  126. totalHealthIndex: {
  127. type: Number,
  128. default: 0
  129. },
  130. /** 星图区域高度 */
  131. starHeight: {
  132. type: String,
  133. default: '540rpx'
  134. }
  135. },
  136. data: function() {
  137. return {
  138. viewMode: 'family',
  139. selectedMemberIndex: 0,
  140. previewScores: [0, 0, 0, 0, 0],
  141. canvasReady: false,
  142. canvasWidth: 0,
  143. canvasHeight: 0,
  144. ctx: null,
  145. dpr: 1,
  146. _canvasLeft: 0,
  147. _canvasTop: 0,
  148. animBody: 0,
  149. animMind: 0,
  150. animWisdom: 0,
  151. animAction: 0,
  152. animWealth: 0,
  153. _animator: null,
  154. _flowAnim: null,
  155. _flowCanvasReady: false
  156. }
  157. },
  158. computed: {
  159. isPreview: function() {
  160. return this.mode === 'preview'
  161. },
  162. currentMember: function() {
  163. if (this.isPreview) return null
  164. var m = this.members
  165. if (!m || m.length === 0) return null
  166. if (this.selectedMemberIndex >= m.length) return m[0]
  167. return m[this.selectedMemberIndex]
  168. },
  169. /** 当前活跃的五维值数组 [body, mind, wisdom, action, wealth] */
  170. activeScores: function() {
  171. if (this.isPreview) return this.previewScores
  172. if (this.viewMode === 'family') {
  173. var s = this.scores
  174. return [
  175. s.body || 0,
  176. s.mind || 0,
  177. s.wisdom || 0,
  178. s.action || 0,
  179. s.wealth || 0
  180. ]
  181. }
  182. var m = this.currentMember
  183. if (!m) return [0, 0, 0, 0, 0]
  184. return [
  185. m.bodyScore || 0,
  186. m.mindScore || 0,
  187. m.wisdomScore || 0,
  188. m.actionScore || 0,
  189. m.wealthScore || 0
  190. ]
  191. },
  192. activeOverall: function() {
  193. if (this.isPreview) {
  194. var s = this.previewScores
  195. return Math.round((s[0] + s[1] + s[2] + s[3] + s[4]) / 5)
  196. }
  197. if (this.viewMode === 'family') return this.overallScore || 0
  198. var m = this.currentMember
  199. return m ? (m.overallScore || 0) : 0
  200. },
  201. activeTs: function() {
  202. return [
  203. this.activeScores[0] / 100,
  204. this.activeScores[1] / 100,
  205. this.activeScores[2] / 100,
  206. this.activeScores[3] / 100,
  207. this.activeScores[4] / 100
  208. ]
  209. },
  210. displayBadgeText: function() {
  211. if (this.badgeText) return this.badgeText
  212. var label = this.viewMode === 'family' ? '家庭能量' : (this.currentMember ? this.currentMember.name : '个人能量')
  213. return label + ' ' + (this.activeOverall || 0) + '%'
  214. },
  215. _allDimTs: function() {
  216. return this.activeTs
  217. },
  218. hasMultipleMembers: function() {
  219. return this.members && this.members.length > 1
  220. },
  221. /** 家庭能量值(用于太极阳面) */
  222. familyEnergy: function() {
  223. if (this.isPreview) {
  224. var s = this.previewScores
  225. return Math.round((s[0] + s[1] + s[2] + s[3] + s[4]) / 5)
  226. }
  227. return this.viewMode === 'family' ? (this.overallScore || 0) : (this.overallScore || 0)
  228. },
  229. /** 个人能量值(用于太极阴面) */
  230. personalEnergy: function() {
  231. if (this.isPreview) return 0
  232. if (this.viewMode === 'member') {
  233. var m = this.currentMember
  234. return m ? (m.overallScore || 0) : 0
  235. }
  236. // 家庭模式下取当前选中成员
  237. var m = this.currentMember
  238. return m ? (m.overallScore || 0) : 0
  239. },
  240. /** 按五行顺序绕圈排列
  241. * 五行固定序:身(土)→智(金)→富(水)→行(木)→心(火)
  242. * 最小值放最上面,其余按五行序顺时针依次排:左边→右边→左下→右下
  243. * 预览/无数据时以身为起点
  244. */
  245. sortedDimensions: function() {
  246. var order = ['body', 'wisdom', 'wealth', 'action', 'mind']
  247. var dimMap = {}
  248. for (var i = 0; i < this.dimensions.length; i++) {
  249. var d = this.dimensions[i]
  250. if (d && d.code) dimMap[d.code] = d
  251. }
  252. // 构建完整数据(含兜底)
  253. var all = []
  254. for (var j = 0; j < order.length; j++) {
  255. var code = order[j]
  256. var d = dimMap[code]
  257. var meta = DIM_META[code] || {}
  258. if (d) {
  259. if (!d.name) d.name = meta.name || ''
  260. if (!d.icon) d.icon = meta.icon || ''
  261. if (!d.element) d.element = meta.element || ''
  262. all.push(d)
  263. } else {
  264. all.push({ code: code, name: meta.name || '', icon: meta.icon || '', element: meta.element || '', energy: 0, healthIndex: 0 })
  265. }
  266. }
  267. // all 此时为 [身, 智, 富, 行, 心](五行固定序)
  268. // 找最小值所在的索引
  269. var startIdx = 0 // 默认以身为起点
  270. if (this.mode === 'live' && all.some(function(d) { return d.energy > 0 })) {
  271. var minEnergy = all[0].energy
  272. for (var i = 1; i < all.length; i++) {
  273. if (all[i].energy < minEnergy) {
  274. minEnergy = all[i].energy
  275. startIdx = i
  276. }
  277. }
  278. }
  279. // 从最小值开始,按五行序顺时针绕圈填入 [顶, 右, 右下, 左下, 左]
  280. var result = []
  281. for (var k = 0; k < 5; k++) {
  282. result.push(all[(startIdx + k) % 5])
  283. }
  284. return result
  285. // result = [最小值→顶, 下一位→右, 再→右下, 再→左下, 再→左]
  286. }
  287. },
  288. watch: {
  289. _allDimTs: function() {
  290. if (this.canvasReady) {
  291. this._startAnimation()
  292. }
  293. },
  294. sortedDimensions: function(newVal) {
  295. if (this._flowAnim && this._flowCanvasReady) {
  296. var dimOrder = newVal.map(function(d) { return d.code })
  297. this._flowAnim.refreshPaths({ dimOrder: dimOrder })
  298. }
  299. }
  300. },
  301. mounted: function() {
  302. var self = this
  303. // 预览模式:生成随机五维值(缓存以避免每次 mount 重算)
  304. if (this.isPreview) {
  305. var cacheKey = '_wuxing_preview_scores'
  306. var cached = uni.getStorageSync(cacheKey)
  307. if (cached && Array.isArray(cached) && cached.length === 5) {
  308. this.previewScores = cached
  309. } else {
  310. var scores = []
  311. for (var i = 0; i < 5; i++) {
  312. scores.push(Math.floor(Math.random() * 51) + 35)
  313. }
  314. this.previewScores = scores
  315. uni.setStorageSync(cacheKey, scores)
  316. }
  317. }
  318. // 等待 DOM 就绪后初始化 Canvas
  319. setTimeout(function() {
  320. self.initCanvas()
  321. }, 100)
  322. },
  323. beforeDestroy: function() {
  324. if (this._animator) {
  325. this._animator.cancel()
  326. this._animator = null
  327. }
  328. if (this._flowAnim) {
  329. this._flowAnim.stop()
  330. this._flowAnim = null
  331. }
  332. },
  333. methods: {
  334. /* ===========================
  335. 视图切换
  336. =========================== */
  337. onCenterClick: function() {
  338. // 预览模式不切换
  339. if (this.isPreview) {
  340. this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
  341. return
  342. }
  343. var members = this.members
  344. if (!members || members.length === 0) {
  345. this.viewMode = 'family'
  346. this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
  347. return
  348. }
  349. if (this.viewMode === 'family') {
  350. this.viewMode = 'member'
  351. } else {
  352. if (members.length === 1) {
  353. this.viewMode = 'family'
  354. this.selectedMemberIndex = 0
  355. } else {
  356. var nextIdx = this.selectedMemberIndex + 1
  357. if (nextIdx >= members.length) {
  358. this.viewMode = 'family'
  359. this.selectedMemberIndex = 0
  360. } else {
  361. this.selectedMemberIndex = nextIdx
  362. }
  363. }
  364. }
  365. this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
  366. this.$emit('mode-change', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
  367. },
  368. onPointClick: function(code) {
  369. this.$emit('point-click', {
  370. code: code,
  371. viewMode: this.viewMode,
  372. member: this.currentMember
  373. })
  374. },
  375. /** 维度 code → CSS 颜色类名(不依赖 API 的 element 字段) */
  376. dimElementColor: function(code) {
  377. var meta = DIM_META[code]
  378. return meta ? meta.color : 'fire'
  379. },
  380. /* ===========================
  381. Canvas 初始化 & 渲染
  382. =========================== */
  383. initCanvas: function() {
  384. var self = this
  385. uni.createSelectorQuery().in(this).select('#starCanvas')
  386. .fields({ node: true, size: true, rect: true })
  387. .exec(function(res) {
  388. if (!res || !res[0] || !res[0].node) return
  389. var canvas = res[0].node
  390. var width = res[0].width
  391. var height = res[0].height
  392. if (!width || !height) return
  393. var pixelRatio = uni.getSystemInfoSync().pixelRatio || 2
  394. canvas.width = width * pixelRatio
  395. canvas.height = height * pixelRatio
  396. var ctx = canvas.getContext('2d')
  397. ctx.scale(pixelRatio, pixelRatio)
  398. self.ctx = ctx
  399. self.canvasWidth = width
  400. self.canvasHeight = height
  401. self.dpr = pixelRatio
  402. self._canvasLeft = res[0].left || 0
  403. self._canvasTop = res[0].top || 0
  404. self.canvasReady = true
  405. self._startAnimation()
  406. self._initFlowCanvas()
  407. })
  408. },
  409. _doRender: function() {
  410. if (!this.ctx) return
  411. var ts = [this.animBody, this.animMind, this.animWisdom, this.animAction, this.animWealth]
  412. renderStar(this.ctx, this.canvasWidth, this.canvasHeight, ts, {
  413. familyEnergy: this.familyEnergy,
  414. personalEnergy: this.personalEnergy
  415. })
  416. },
  417. /* ===========================
  418. 动画控制
  419. =========================== */
  420. _startAnimation: function() {
  421. var self = this
  422. if (!this._animator) {
  423. this._animator = new FillAnimator(
  424. function() {
  425. return [self.animBody, self.animMind, self.animWisdom, self.animAction, self.animWealth]
  426. },
  427. function(interpolated) {
  428. self.animBody = interpolated[0]
  429. self.animMind = interpolated[1]
  430. self.animWisdom = interpolated[2]
  431. self.animAction = interpolated[3]
  432. self.animWealth = interpolated[4]
  433. self._doRender()
  434. }
  435. )
  436. }
  437. this._animator.start(this.activeTs, 600)
  438. },
  439. /* ===========================
  440. 粒子流量动画
  441. =========================== */
  442. _initFlowCanvas: function() {
  443. var self = this
  444. var dimOrder = this.sortedDimensions.map(function(d) { return d.code })
  445. uni.createSelectorQuery().in(this).select('#flowCanvas')
  446. .fields({ node: true, size: true })
  447. .exec(function(res) {
  448. if (!res || !res[0] || !res[0].node) return
  449. var canvas = res[0].node
  450. var width = res[0].width
  451. var height = res[0].height
  452. if (!width || !height) return
  453. var pixelRatio = uni.getSystemInfoSync().pixelRatio || 2
  454. canvas.width = width * pixelRatio
  455. canvas.height = height * pixelRatio
  456. var ctx = canvas.getContext('2d')
  457. ctx.scale(pixelRatio, pixelRatio)
  458. var outerR = Math.min(width, height) * 0.50
  459. var vertices = calcStarVertices(width / 2, height / 2, outerR)
  460. self._flowAnim = new FlowAnimation(ctx, width, height, {
  461. outer: vertices.outer,
  462. inner: vertices.inner,
  463. dimOrder: dimOrder
  464. })
  465. self._flowAnim.start()
  466. self._flowCanvasReady = true
  467. })
  468. },
  469. /* ===========================
  470. 点击检测
  471. =========================== */
  472. onCanvasTap: function(e) {
  473. if (!this.canvasReady) return
  474. var pageX = e.detail.x
  475. var pageY = e.detail.y
  476. if (typeof pageX !== 'number') {
  477. var touch = e.touches ? e.touches[0] : null
  478. if (touch) {
  479. pageX = touch.x || touch.clientX || 0
  480. pageY = touch.y || touch.clientY || 0
  481. } else {
  482. var changedTouch = e.changedTouches ? e.changedTouches[0] : null
  483. if (changedTouch) {
  484. pageX = changedTouch.x || changedTouch.clientX || 0
  485. pageY = changedTouch.y || changedTouch.clientY || 0
  486. } else {
  487. return
  488. }
  489. }
  490. }
  491. var relX = pageX - (this._canvasLeft || 0)
  492. var relY = pageY - (this._canvasTop || 0)
  493. if (relX < 0 || relY < 0 || relX > this.canvasWidth || relY > this.canvasHeight) return
  494. var cx = this.canvasWidth / 2
  495. var cy = this.canvasHeight / 2
  496. var outerR = Math.min(this.canvasWidth, this.canvasHeight) * 0.46
  497. var armIdx = detectHitArm(relX, relY, cx, cy, outerR, calcStarVertices)
  498. if (armIdx >= 0) {
  499. this.onPointClick(armIndexToDomain(armIdx))
  500. }
  501. }
  502. }
  503. }
  504. </script>
  505. <style scoped>
  506. /* ============================================================
  507. 容器
  508. ============================================================ */
  509. .wuxing-sandbox {
  510. margin: 20rpx 30rpx;
  511. padding: 20rpx 24rpx 24rpx;
  512. background: linear-gradient(135deg, #1A2A4A, #1E3A5F, #16213E);
  513. border-radius: 24rpx;
  514. position: relative;
  515. overflow: hidden;
  516. }
  517. .wuxing-sandbox::before {
  518. content: '';
  519. position: absolute;
  520. top: -50%;
  521. left: -50%;
  522. width: 200%;
  523. height: 200%;
  524. background: radial-gradient(ellipse at 30% 20%, rgba(91, 155, 213, 0.04), transparent 60%),
  525. radial-gradient(ellipse at 70% 80%, rgba(66, 165, 245, 0.03), transparent 60%);
  526. pointer-events: none;
  527. }
  528. /* ---- 标题 ---- */
  529. .sandbox-header {
  530. display: flex;
  531. justify-content: flex-start;
  532. align-items: center;
  533. margin-bottom: 12rpx;
  534. position: relative;
  535. z-index: 5;
  536. }
  537. .sandbox-title { font-size: 26rpx; font-weight: bold; color: #8FC5E8; margin-left: 8rpx; }
  538. .sandbox-logo { width: 32rpx; height: 32rpx; flex-shrink: 0; }
  539. .sandbox-badge {
  540. font-size: 20rpx;
  541. color: rgba(143,197,232,0.7);
  542. background: rgba(143,197,232,0.1);
  543. padding: 4rpx 14rpx;
  544. border-radius: 20rpx;
  545. margin-left: auto;
  546. }
  547. /* ---- 五角星区域 ---- */
  548. .sandbox-star {
  549. position: relative;
  550. width: 100%;
  551. display: flex;
  552. justify-content: center;
  553. align-items: center;
  554. }
  555. /* 背景光晕 */
  556. .star-glow-bg {
  557. position: absolute; width: 500rpx; height: 500rpx;
  558. top: 50%; left: 50%; transform: translate(-50%, -50%);
  559. background: radial-gradient(circle, rgba(91,155,213,0.10), transparent 70%);
  560. border-radius: 50%; pointer-events: none;
  561. }
  562. /* Canvas 点击感应层 */
  563. .canvas-tap-area {
  564. position: absolute;
  565. top: 0; left: 0; right: 0; bottom: 0;
  566. z-index: 2;
  567. }
  568. /* ============================================================
  569. 顶点:图标 + 中文标签
  570. ============================================================ */
  571. .star-point {
  572. position: absolute; transform: translate(-50%, -50%); z-index: 3; cursor: pointer;
  573. display: flex; flex-direction: column; align-items: center;
  574. }
  575. /* 横排 */
  576. .star-point--row {
  577. flex-direction: row;
  578. gap: 6rpx;
  579. }
  580. .star-point--rev {
  581. flex-direction: row-reverse;
  582. gap: 6rpx;
  583. }
  584. .star-point--top { top: 1%; left: 50%; }
  585. .star-point--left { top: 38%; left: 5%; }
  586. .star-point--right { top: 38%; left: 95%; }
  587. .star-point--lr { top: 89%; left: 85%; }
  588. .star-point--ll { top: 89%; left: 15%; }
  589. .point-dot {
  590. width: 52rpx; height: 52rpx; border-radius: 50%;
  591. display: flex; align-items: center; justify-content: center;
  592. box-shadow: 0 0 24rpx rgba(255,255,255,0.25);
  593. }
  594. .point-dot--fire { background: linear-gradient(135deg, #FF6B35, #E65100); }
  595. .point-dot--water { background: linear-gradient(135deg, #42A5F5, #1565C0); }
  596. .point-dot--earth { background: linear-gradient(135deg, #8D6E63, #5D4037); }
  597. .point-dot--metal { background: linear-gradient(135deg, #FFD700, #FF8F00); }
  598. .point-dot--wood { background: linear-gradient(135deg, #4CAF50, #2E7D32); }
  599. .point-icon { font-size: 26rpx; line-height: 1; }
  600. .point-label {
  601. font-size: 22rpx; font-weight: bold; color: rgba(255,255,255,0.9);
  602. margin-top: 6rpx; text-shadow: 0 1rpx 4rpx rgba(0,0,0,0.5);
  603. letter-spacing: 2rpx;
  604. }
  605. .star-point--row .point-label,
  606. .star-point--rev .point-label {
  607. margin-top: 0;
  608. }
  609. .point-energy {
  610. font-size: 18rpx; color: rgba(143,197,232,0.8);
  611. background: rgba(143,197,232,0.12);
  612. padding: 1rpx 10rpx; border-radius: 12rpx;
  613. margin-top: 2rpx;
  614. }
  615. .star-point--row .point-energy,
  616. .star-point--rev .point-energy {
  617. margin-top: 0;
  618. }
  619. /* ============================================================
  620. 中心点击区域
  621. ============================================================ */
  622. .star-center {
  623. position: absolute; top: 50%; left: 50%;
  624. transform: translate(-50%, -50%); z-index: 4;
  625. width: 180rpx; height: 180rpx; border-radius: 50%;
  626. cursor: pointer;
  627. }
  628. /* ============================================================
  629. 维度详情条
  630. ============================================================ */
  631. .dimension-bar {
  632. display: flex; align-items: center; justify-content: center;
  633. margin-top: 16rpx; gap: 8rpx;
  634. position: relative; z-index: 5; flex-wrap: wrap;
  635. }
  636. .dim-item {
  637. font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 10rpx; white-space: nowrap;
  638. }
  639. .dim-item--wood { color: #4CAF50; background: rgba(76,175,80,0.12); }
  640. .dim-item--fire { color: #FF6B35; background: rgba(255,107,53,0.12); }
  641. .dim-item--earth { color: #8D6E63; background: rgba(141,110,99,0.12); }
  642. .dim-item--metal { color: #FFD700; background: rgba(255,215,0,0.12); }
  643. .dim-item--water { color: #42A5F5; background: rgba(66,165,245,0.12); }
  644. </style>