index.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
  1. <template>
  2. <view class="tianpan-container">
  3. <!-- 自定义导航栏 -->
  4. <view class="custom-nav">
  5. <view class="nav-header">
  6. <picker mode="selector" :range="yearOptions" :value="yearIndex" @change="onYearChange">
  7. <view class="year-picker">
  8. <text class="year-text">{{ currentYear }}</text>
  9. <text class="picker-icon">▼</text>
  10. </view>
  11. </picker>
  12. </view>
  13. <view class="family-name" v-if="familyName">{{ familyName }}</view>
  14. </view>
  15. <!-- 天盘画布 -->
  16. <view class="canvas-wrapper">
  17. <canvas
  18. type="2d"
  19. id="tianpanCanvas"
  20. class="tianpan-canvas"
  21. @touchend="onCanvasTouchEnd"
  22. ></canvas>
  23. </view>
  24. <!-- 标签栏 -->
  25. <view class="tab-bar">
  26. <view
  27. class="tab-item"
  28. :class="{ active: currentTab === 0 }"
  29. @click="switchTab(0)"
  30. >
  31. <text>任务</text>
  32. </view>
  33. <view
  34. class="tab-item"
  35. :class="{ active: currentTab === 1 }"
  36. @click="switchTab(1)"
  37. >
  38. <text>测评</text>
  39. </view>
  40. <view
  41. class="tab-item"
  42. :class="{ active: currentTab === 2 }"
  43. @click="switchTab(2)"
  44. >
  45. <text>预警</text>
  46. </view>
  47. <view
  48. class="tab-item"
  49. :class="{ active: currentTab === 3 }"
  50. @click="switchTab(3)"
  51. >
  52. <text>里程碑</text>
  53. </view>
  54. </view>
  55. <!-- 相关事项列表 -->
  56. <view class="related-items-list">
  57. <view
  58. class="item-card"
  59. v-for="item in currentItems"
  60. :key="item.id"
  61. @click="onItemClick(item)"
  62. >
  63. <view class="item-icon">{{ item.icon || '📋' }}</view>
  64. <view class="item-content">
  65. <text class="item-title">{{ item.title }}</text>
  66. <text class="item-desc">{{ item.description || '' }}</text>
  67. </view>
  68. <view class="item-arrow">›</view>
  69. </view>
  70. <view class="empty-state" v-if="currentItems.length === 0">
  71. <text>暂无相关事项</text>
  72. </view>
  73. </view>
  74. <!-- 每日运势卡片 -->
  75. <view class="fortune-card" @click="goToDailyFortune">
  76. <view class="fortune-header">
  77. <text class="fortune-title">今日运势</text>
  78. <text class="fortune-date">{{ currentDate }}</text>
  79. </view>
  80. <view class="fortune-content">
  81. <text class="fortune-text">{{ dailyFortuneText || '点击查看今日家庭能量运势' }}</text>
  82. </view>
  83. <view class="fortune-arrow">查看详情 ›</view>
  84. </view>
  85. <!-- 传统命理入口卡片 -->
  86. <view class="fortune-card tradition-card" @click="goToTraditional">
  87. <view class="fortune-header">
  88. <text class="fortune-title">家庭传统命理</text>
  89. </view>
  90. <view class="fortune-content">
  91. <text class="fortune-text">查看家庭成员八字、星座、灵数与五维基础能量分</text>
  92. </view>
  93. <view class="fortune-arrow">进入命理详情 ›</view>
  94. </view>
  95. <!-- 成员详情弹窗 -->
  96. <view class="member-popup" v-if="showMemberPopup" @touchmove.stop>
  97. <view class="popup-overlay" @click="closeMemberPopup"></view>
  98. <view class="popup-content">
  99. <view class="popup-header">
  100. <text class="popup-title">{{ selectedMember.nickname || selectedMember.name }}</text>
  101. <text class="popup-close" @click="closeMemberPopup">×</text>
  102. </view>
  103. <view class="popup-body">
  104. <view class="member-info-row">
  105. <text class="info-label">生肖:</text>
  106. <text class="info-value">{{ selectedMember.zodiacName || selectedMember.zodiac || '-' }}</text>
  107. </view>
  108. <view class="member-info-row">
  109. <text class="info-label">星座:</text>
  110. <text class="info-value">{{ selectedMember.westernSign || '-' }}</text>
  111. </view>
  112. <view class="member-info-row">
  113. <text class="info-label">代际:</text>
  114. <text class="info-value">{{ selectedMember.generation || '-' }}</text>
  115. </view>
  116. <view class="member-info-row" v-if="selectedMember.lifeNumber">
  117. <text class="info-label">灵数:</text>
  118. <text class="info-value">{{ selectedMember.lifeNumber }}</text>
  119. </view>
  120. <view class="member-info-row" v-if="selectedMember.eightCharacters">
  121. <text class="info-label">八字:</text>
  122. <text class="info-value">{{ getEightCharsText(selectedMember.eightCharacters) }}</text>
  123. </view>
  124. <view class="energy-bars">
  125. <view class="energy-bar-item" v-for="energy in selectedMember.energy" :key="energy.type">
  126. <text class="energy-label">{{ energy.label }}</text>
  127. <view class="energy-bar">
  128. <view class="energy-fill" :style="{ width: energy.value + '%' }"></view>
  129. </view>
  130. <text class="energy-value">{{ energy.value }}</text>
  131. </view>
  132. </view>
  133. <view class="energy-bars" v-if="selectedMember.dimensionBaseScores">
  134. <text class="energy-section-title">五维基础分</text>
  135. <view class="energy-bar-item" v-for="(val, key) in selectedMember.dimensionBaseScores" :key="getDimKey(key)">
  136. <text class="energy-label">{{ getDimLabel(key) }}</text>
  137. <view class="energy-bar">
  138. <view class="energy-fill" :style="{ width: val + '%', background: getDimColor(key) }"></view>
  139. </view>
  140. <text class="energy-value">{{ val }}</text>
  141. </view>
  142. </view>
  143. </view>
  144. <view class="popup-footer">
  145. <button class="detail-btn" @click="goToMemberDetail">查看详情</button>
  146. </view>
  147. </view>
  148. </view>
  149. <!-- 加载状态 -->
  150. <view class="loading-overlay" v-if="loading">
  151. <view class="loading-spinner"></view>
  152. <text>加载中...</text>
  153. </view>
  154. </view>
  155. </template>
  156. <script>
  157. import { mapState, mapGetters, mapActions } from 'vuex'
  158. export default {
  159. data: function() {
  160. return {
  161. currentYear: new Date().getFullYear(),
  162. yearOptions: [],
  163. yearIndex: 0,
  164. currentTab: 0,
  165. showMemberPopup: false,
  166. selectedMember: {},
  167. canvasContext: null,
  168. canvasWidth: 375,
  169. canvasHeight: 375,
  170. members: [],
  171. relationships: [],
  172. relatedItems: [],
  173. dailyFortuneText: '',
  174. currentDate: ''
  175. }
  176. },
  177. computed: {
  178. ...mapGetters('tianpan', ['members', 'relationships', 'familyWuxing', 'familyName']),
  179. ...mapState('tianpan', ['loading', 'error']),
  180. currentItems: function() {
  181. var tabMap = ['tasks', 'assessments', 'alerts', 'milestones']
  182. var items = this.relatedItems && this.relatedItems[tabMap[this.currentTab]] ? this.relatedItems[tabMap[this.currentTab]] : []
  183. return items
  184. }
  185. },
  186. onLoad: function() {
  187. this.initYearOptions()
  188. this.loadDashboardData()
  189. this.loadDailyFortune()
  190. this.loadRelatedItems()
  191. },
  192. onReady: function() {
  193. var query = uni.createSelectorQuery()
  194. query.select('#tianpanCanvas')
  195. .fields({ node: true, size: true })
  196. .exec(this.onCanvasReady)
  197. },
  198. methods: {
  199. ...mapActions('tianpan', ['loadDashboard', 'loadDailyFortune', 'loadRelatedItems']),
  200. initYearOptions: function() {
  201. var current = new Date().getFullYear()
  202. for (var i = current - 2; i <= current + 2; i++) {
  203. this.yearOptions.push(i)
  204. if (i === current) {
  205. this.yearIndex = this.yearOptions.length - 1
  206. }
  207. }
  208. this.currentYear = current
  209. },
  210. onYearChange: function(e) {
  211. var index = e.detail.value
  212. var year = this.yearOptions[index]
  213. if (year !== this.currentYear) {
  214. this.currentYear = year
  215. this.loadDashboardData()
  216. }
  217. },
  218. loadDashboardData: function() {
  219. this.loadDashboard({ year: this.currentYear, daysAhead: 7 })
  220. .then(this.drawCanvas)
  221. },
  222. onCanvasReady: function(res) {
  223. if (!res || !res[0] || !res[0].node) {
  224. return
  225. }
  226. var ctx = res[0].node.getContext('2d')
  227. var dpr = uni.getWindowInfo().pixelRatio
  228. this.canvasContext = ctx
  229. this.canvasContext.scale(dpr, dpr)
  230. this.drawCanvas()
  231. },
  232. drawCanvas: function() {
  233. if (!this.canvasContext) {
  234. return
  235. }
  236. var ctx = this.canvasContext
  237. var w = this.canvasWidth
  238. var h = this.canvasHeight
  239. var cx = w / 2
  240. var cy = h / 2
  241. // 清空画布
  242. ctx.clearRect(0, 0, w, h)
  243. ctx.fillStyle = '#FFF8F0'
  244. ctx.fillRect(0, 0, w, h)
  245. // 获取数据
  246. var members = this.members || []
  247. var relationships = this.relationships || []
  248. // R0: 中心五维能量雷达图
  249. this.drawRadar(ctx, cx, cy, w * 0.11, members)
  250. // R1: 内圈成员节点
  251. this.drawMemberNodes(ctx, cx, cy, w * 0.18, members)
  252. // R2-R3: 外圈相关事项
  253. this.drawRelatedItems(ctx, cx, cy, w * 0.32, w * 0.46, members)
  254. // 绘制关系连线
  255. this.drawRelationshipLines(ctx, cx, cy, relationships)
  256. },
  257. drawRadar: function(ctx, cx, cy, r, members) {
  258. var axes = ['木', '火', '土', '金', '水']
  259. var elementKeys = ['wood', 'fire', 'earth', 'metal', 'water']
  260. var elementColors = ['#10B981', '#FF6B9D', '#FF8C42', '#6366F1', '#F59E0B']
  261. var angleStep = (Math.PI * 2) / 5
  262. // 获取五行数据
  263. var wuxing = this.familyWuxing || {}
  264. var percentages = (wuxing.aggregatePercentages) || {}
  265. var strongest = wuxing.strongestElement || ''
  266. var weakest = wuxing.weakestElement || ''
  267. var familyLabel = this.familyName || '家庭五行'
  268. // 绘制背景五边形
  269. ctx.beginPath()
  270. for (var i = 0; i < 5; i++) {
  271. var angle = i * angleStep - Math.PI / 2
  272. var bx = cx + r * Math.cos(angle)
  273. var by = cy + r * Math.sin(angle)
  274. if (i === 0) {
  275. ctx.moveTo(bx, by)
  276. } else {
  277. ctx.lineTo(bx, by)
  278. }
  279. }
  280. ctx.closePath()
  281. ctx.fillStyle = 'rgba(249, 114, 22, 0.08)'
  282. ctx.fill()
  283. ctx.strokeStyle = 'rgba(249, 114, 22, 0.25)'
  284. ctx.lineWidth = 1
  285. ctx.stroke()
  286. // 绘制彩色轴线 + 轴标签
  287. for (var j = 0; j < 5; j++) {
  288. var a2 = j * angleStep - Math.PI / 2
  289. var lx = cx + r * Math.cos(a2)
  290. var ly = cy + r * Math.sin(a2)
  291. ctx.beginPath()
  292. ctx.moveTo(cx, cy)
  293. ctx.lineTo(lx, ly)
  294. ctx.strokeStyle = elementColors[j] + '44'
  295. ctx.lineWidth = 1
  296. ctx.stroke()
  297. // 轴标签(元素名)
  298. var labelX = cx + (r + 16) * Math.cos(a2)
  299. var labelY = cy + (r + 16) * Math.sin(a2)
  300. ctx.fillStyle = elementColors[j]
  301. ctx.font = 'bold 12px sans-serif'
  302. ctx.textAlign = 'center'
  303. ctx.textBaseline = 'middle'
  304. ctx.fillText(axes[j], labelX, labelY)
  305. }
  306. // 绘制数据驱动多边形(按百分比缩放)
  307. var hasData = false
  308. for (var k = 0; k < elementKeys.length; k++) {
  309. if (percentages[elementKeys[k]] && percentages[elementKeys[k]] > 0) {
  310. hasData = true
  311. break
  312. }
  313. }
  314. if (hasData) {
  315. ctx.beginPath()
  316. for (var m = 0; m < 5; m++) {
  317. var ang = m * angleStep - Math.PI / 2
  318. var pct = (percentages[elementKeys[m]] || 0) / 100
  319. var dataR = r * pct
  320. var dx = cx + dataR * Math.cos(ang)
  321. var dy = cy + dataR * Math.sin(ang)
  322. if (m === 0) {
  323. ctx.moveTo(dx, dy)
  324. } else {
  325. ctx.lineTo(dx, dy)
  326. }
  327. }
  328. ctx.closePath()
  329. ctx.fillStyle = 'rgba(249, 114, 22, 0.2)'
  330. ctx.fill()
  331. ctx.strokeStyle = '#F97316'
  332. ctx.lineWidth = 2
  333. ctx.stroke()
  334. // 数据节点圆点 + 百分比标签
  335. for (var n = 0; n < 5; n++) {
  336. var ang2 = n * angleStep - Math.PI / 2
  337. var pct2 = (percentages[elementKeys[n]] || 0) / 100
  338. var dataR2 = r * pct2
  339. var px = cx + dataR2 * Math.cos(ang2)
  340. var py = cy + dataR2 * Math.sin(ang2)
  341. // 圆点
  342. ctx.beginPath()
  343. ctx.arc(px, py, 3, 0, Math.PI * 2)
  344. ctx.fillStyle = elementColors[n]
  345. ctx.fill()
  346. // 百分比标签(在圆点外侧)
  347. var pctLabelR = dataR2 + (pct2 > 0.3 ? -14 : 14)
  348. var pLabelX = cx + pctLabelR * Math.cos(ang2)
  349. var pLabelY = cy + pctLabelR * Math.sin(ang2)
  350. ctx.fillStyle = elementColors[n]
  351. ctx.font = '10px sans-serif'
  352. ctx.textAlign = 'center'
  353. ctx.textBaseline = 'middle'
  354. ctx.fillText((percentages[elementKeys[n]] || 0) + '%', pLabelX, pLabelY)
  355. }
  356. }
  357. // 中心文字:家庭名 + 最强/最弱
  358. ctx.fillStyle = '#1F2937'
  359. ctx.font = 'bold 11px sans-serif'
  360. ctx.textAlign = 'center'
  361. ctx.textBaseline = 'middle'
  362. ctx.fillText(familyLabel, cx, cy - 6)
  363. if (strongest || weakest) {
  364. ctx.fillStyle = '#6B7280'
  365. ctx.font = '9px sans-serif'
  366. var strongestZh = strongest === 'wood' ? '木' : strongest === 'fire' ? '火' : strongest === 'earth' ? '土' : strongest === 'metal' ? '金' : strongest === 'water' ? '水' : strongest
  367. var weakestZh = weakest === 'wood' ? '木' : weakest === 'fire' ? '火' : weakest === 'earth' ? '土' : weakest === 'metal' ? '金' : weakest === 'water' ? '水' : weakest
  368. ctx.fillText('强:' + strongestZh + ' 弱:' + weakestZh, cx, cy + 7)
  369. }
  370. },
  371. drawMemberNodes: function(ctx, cx, cy, r, members) {
  372. var angleStep = (Math.PI * 2) / (members.length || 1)
  373. for (var i = 0; i < members.length; i++) {
  374. var member = members[i]
  375. var angle = i * angleStep - Math.PI / 2
  376. var x = cx + r * Math.cos(angle)
  377. var y = cy + r * Math.sin(angle)
  378. // 绘制圆形节点
  379. ctx.beginPath()
  380. ctx.arc(x, y, 15, 0, Math.PI * 2)
  381. ctx.fillStyle = this.getMemberColor(member.effectiveRole)
  382. ctx.fill()
  383. ctx.strokeStyle = '#FFF'
  384. ctx.lineWidth = 2
  385. ctx.stroke()
  386. // 绘制名字
  387. ctx.fillStyle = '#FFF'
  388. ctx.font = '10px sans-serif'
  389. ctx.textAlign = 'center'
  390. ctx.textBaseline = 'middle'
  391. ctx.fillText(member.nickname || member.name, x, y)
  392. // 绘制生肖+灵数标注
  393. var showLabel = member.zodiacName || member.lifeNumber
  394. if (showLabel) {
  395. var labelParts = []
  396. if (member.zodiacName) labelParts.push(member.zodiacName)
  397. if (member.lifeNumber) labelParts.push('灵数' + member.lifeNumber)
  398. var label = labelParts.join('·')
  399. ctx.font = '9px sans-serif'
  400. ctx.fillStyle = this.getMemberColor(member.effectiveRole)
  401. ctx.textAlign = 'center'
  402. ctx.textBaseline = 'top'
  403. ctx.fillText(label, x, y + 18)
  404. }
  405. // 存储点击区域
  406. if (!this.memberHitAreas) {
  407. this.memberHitAreas = []
  408. }
  409. this.memberHitAreas.push({
  410. type: 'member',
  411. data: member,
  412. x: x,
  413. y: y,
  414. r: 15
  415. })
  416. }
  417. },
  418. drawRelatedItems: function(ctx, cx, cy, rInner, rOuter, members) {
  419. var items = this.relatedItems && this.relatedItems.tasks ? this.relatedItems.tasks : []
  420. var angleStep = (Math.PI * 2) / (items.length || 1)
  421. var r = (rInner + rOuter) / 2
  422. for (var i = 0; i < items.length; i++) {
  423. var item = items[i]
  424. var angle = i * angleStep - Math.PI / 2
  425. var x = cx + r * Math.cos(angle)
  426. var y = cy + r * Math.sin(angle)
  427. // 绘制 emoji
  428. ctx.font = '16px sans-serif'
  429. ctx.textAlign = 'center'
  430. ctx.textBaseline = 'middle'
  431. ctx.fillText(item.icon || '📋', x, y)
  432. // 存储点击区域
  433. if (!this.itemHitAreas) {
  434. this.itemHitAreas = []
  435. }
  436. this.itemHitAreas.push({
  437. type: 'item',
  438. data: item,
  439. x: x,
  440. y: y,
  441. r: 12
  442. })
  443. }
  444. },
  445. drawRelationshipLines: function(ctx, cx, cy, relationships) {
  446. for (var i = 0; i < relationships.length; i++) {
  447. var rel = relationships[i]
  448. if (!rel.fromMember || !rel.toMember) {
  449. continue
  450. }
  451. // 计算起点和终点
  452. var memberCount = this.members.length || 1
  453. var angleStep = (Math.PI * 2) / memberCount
  454. var r = this.canvasWidth * 0.18
  455. var fromIndex = -1
  456. var toIndex = -1
  457. for (var j = 0; j < this.members.length; j++) {
  458. if (this.members[j].id === rel.fromMember.id) {
  459. fromIndex = j
  460. }
  461. if (this.members[j].id === rel.toMember.id) {
  462. toIndex = j
  463. }
  464. }
  465. if (fromIndex < 0 || toIndex < 0) {
  466. continue
  467. }
  468. var fromAngle = fromIndex * angleStep - Math.PI / 2
  469. var toAngle = toIndex * angleStep - Math.PI / 2
  470. var x1 = cx + r * Math.cos(fromAngle)
  471. var y1 = cy + r * Math.sin(fromAngle)
  472. var x2 = cx + r * Math.cos(toAngle)
  473. var y2 = cy + r * Math.sin(toAngle)
  474. // 贝塞尔曲线
  475. ctx.beginPath()
  476. ctx.moveTo(x1, y1)
  477. var cx1 = (x1 + x2) / 2 + (y2 - y1) * 0.2
  478. var cy1 = (y1 + y2) / 2 - (x2 - x1) * 0.2
  479. ctx.quadraticCurveTo(cx1, cy1, x2, y2)
  480. // 根据分数设置颜色
  481. var score = rel.score || 0
  482. if (score >= 80) {
  483. ctx.strokeStyle = '#10B981'
  484. } else if (score >= 60) {
  485. ctx.strokeStyle = '#F59E0B'
  486. } else {
  487. ctx.strokeStyle = '#EF4444'
  488. }
  489. ctx.lineWidth = 2
  490. ctx.stroke()
  491. }
  492. },
  493. getMemberColor: function(role) {
  494. var colorMap = {
  495. 'parent': '#F97316',
  496. 'child': '#3B82F6',
  497. 'teacher': '#8B5CF6'
  498. }
  499. return colorMap[role] || '#6B7280'
  500. },
  501. getEightCharsText: function(eightChars) {
  502. if (!eightChars) return ''
  503. var parts = []
  504. var keys = ['year', 'month', 'day', 'hour']
  505. for (var i = 0; i < keys.length; i++) {
  506. if (eightChars[keys[i]]) parts.push(eightChars[keys[i]])
  507. }
  508. return parts.join(' ')
  509. },
  510. getDimKey: function(key) { return 'dim-' + key },
  511. getDimLabel: function(key) {
  512. var labels = { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
  513. return labels[key] || key
  514. },
  515. getDimColor: function(key) {
  516. var colors = { body: '#FF8C42', mind: '#FF6B9D', wisdom: '#6366F1', action: '#10B981', wealth: '#F59E0B' }
  517. return colors[key] || '#6B7280'
  518. },
  519. onCanvasTouchEnd: function(e) {
  520. if (!e || !e.changedTouches || !e.changedTouches[0]) {
  521. return
  522. }
  523. var touch = e.changedTouches[0]
  524. var x = touch.x
  525. var y = touch.y
  526. // 检测成员点击
  527. if (this.memberHitAreas) {
  528. for (var i = 0; i < this.memberHitAreas.length; i++) {
  529. var area = this.memberHitAreas[i]
  530. var dx = x - area.x
  531. var dy = y - area.y
  532. if (dx * dx + dy * dy <= area.r * area.r) {
  533. this.showMemberDetail(area.data)
  534. return
  535. }
  536. }
  537. }
  538. // 检测事项点击
  539. if (this.itemHitAreas) {
  540. for (var j = 0; j < this.itemHitAreas.length; j++) {
  541. var itemArea = this.itemHitAreas[j]
  542. var dx2 = x - itemArea.x
  543. var dy2 = y - itemArea.y
  544. if (dx2 * dx2 + dy2 * dy2 <= itemArea.r * itemArea.r) {
  545. this.onItemClick(itemArea.data)
  546. return
  547. }
  548. }
  549. }
  550. },
  551. showMemberDetail: function(member) {
  552. this.selectedMember = member
  553. this.showMemberPopup = true
  554. },
  555. closeMemberPopup: function() {
  556. this.showMemberPopup = false
  557. this.selectedMember = {}
  558. },
  559. goToMemberDetail: function() {
  560. if (!this.selectedMember || !this.selectedMember.id) {
  561. return
  562. }
  563. uni.navigateTo({
  564. url: '/pages/tianpan/member-detail?id=' + this.selectedMember.id
  565. })
  566. this.closeMemberPopup()
  567. },
  568. switchTab: function(index) {
  569. this.currentTab = index
  570. },
  571. onItemClick: function(item) {
  572. if (!item || !item.id) {
  573. return
  574. }
  575. // 根据类型跳转
  576. if (item.type === 'task') {
  577. uni.navigateTo({ url: '/pages/tasks/detail?id=' + item.id })
  578. } else if (item.type === 'assessment') {
  579. uni.navigateTo({ url: '/pages/assessment/detail?id=' + item.id })
  580. } else if (item.type === 'alert') {
  581. uni.navigateTo({ url: '/pages/health/alert-detail?id=' + item.id })
  582. } else if (item.type === 'milestone') {
  583. uni.navigateTo({ url: '/pages/family/milestone-detail?id=' + item.id })
  584. }
  585. },
  586. goToDailyFortune: function() {
  587. uni.navigateTo({
  588. url: '/pages/tianpan/daily-fortune'
  589. })
  590. },
  591. goToTraditional: function() {
  592. uni.navigateTo({
  593. url: '/pages/tianpan/traditional'
  594. })
  595. },
  596. loadDailyFortune: function() {
  597. var today = new Date()
  598. var dateStr = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate()
  599. this.loadDailyFortune({ date: dateStr })
  600. .then(this.setFortuneText)
  601. },
  602. setFortuneText: function(data) {
  603. if (data && data.advice) {
  604. this.dailyFortuneText = data.advice
  605. }
  606. var today = new Date()
  607. this.currentDate = today.getFullYear() + '年' + (today.getMonth() + 1) + '月' + today.getDate() + '日'
  608. },
  609. loadRelatedItems: function() {
  610. this.loadRelatedItems({})
  611. }
  612. },
  613. watch: {
  614. members: function(newVal) {
  615. if (newVal && this.canvasContext) {
  616. this.drawCanvas()
  617. }
  618. },
  619. relationships: function(newVal) {
  620. if (newVal && this.canvasContext) {
  621. this.drawCanvas()
  622. }
  623. }
  624. }
  625. }
  626. </script>
  627. <style>
  628. .tianpan-container {
  629. min-height: 100vh;
  630. background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
  631. padding-bottom: 20px;
  632. }
  633. /* 自定义导航栏 */
  634. .custom-nav {
  635. background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
  636. padding: 44px 16px 12px;
  637. color: #FFFFFF;
  638. }
  639. .nav-header {
  640. display: flex;
  641. justify-content: space-between;
  642. align-items: center;
  643. }
  644. .year-picker {
  645. display: flex;
  646. align-items: center;
  647. background: rgba(255, 255, 255, 0.2);
  648. padding: 4px 12px;
  649. border-radius: 16px;
  650. }
  651. .year-text {
  652. font-size: 14px;
  653. margin-right: 4px;
  654. }
  655. .picker-icon {
  656. font-size: 10px;
  657. }
  658. .family-name {
  659. text-align: center;
  660. margin-top: 8px;
  661. font-size: 14px;
  662. opacity: 0.9;
  663. }
  664. /* 画布区域 */
  665. .canvas-wrapper {
  666. display: flex;
  667. justify-content: center;
  668. padding: 20px 0;
  669. }
  670. .tianpan-canvas {
  671. width: 375px;
  672. height: 375px;
  673. background: #FFF8F0;
  674. border-radius: 16px;
  675. box-shadow: 0 4px 12px rgba(249, 114, 22, 0.1);
  676. }
  677. /* 标签栏 */
  678. .tab-bar {
  679. display: flex;
  680. justify-content: space-around;
  681. background: #FFFFFF;
  682. margin: 0 16px;
  683. padding: 12px 0;
  684. border-radius: 12px;
  685. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  686. }
  687. .tab-item {
  688. padding: 6px 16px;
  689. border-radius: 20px;
  690. font-size: 14px;
  691. color: #6B7280;
  692. }
  693. .tab-item.active {
  694. background: #F97316;
  695. color: #FFFFFF;
  696. }
  697. /* 相关事项列表 */
  698. .related-items-list {
  699. padding: 16px;
  700. }
  701. .item-card {
  702. display: flex;
  703. align-items: center;
  704. background: #FFFFFF;
  705. padding: 16px;
  706. border-radius: 12px;
  707. margin-bottom: 12px;
  708. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  709. }
  710. .item-icon {
  711. font-size: 28px;
  712. margin-right: 12px;
  713. }
  714. .item-content {
  715. flex: 1;
  716. }
  717. .item-title {
  718. font-size: 16px;
  719. font-weight: 500;
  720. color: #1F2937;
  721. display: block;
  722. margin-bottom: 4px;
  723. }
  724. .item-desc {
  725. font-size: 12px;
  726. color: #9CA3AF;
  727. display: block;
  728. }
  729. .item-arrow {
  730. font-size: 20px;
  731. color: #D1D5DB;
  732. }
  733. .empty-state {
  734. text-align: center;
  735. padding: 40px 0;
  736. color: #9CA3AF;
  737. font-size: 14px;
  738. }
  739. /* 每日运势卡片 */
  740. .fortune-card {
  741. margin: 16px;
  742. padding: 20px;
  743. background: linear-gradient(135deg, #FED7AA 0%, #FDBA74 100%);
  744. border-radius: 16px;
  745. box-shadow: 0 4px 12px rgba(249, 114, 22, 0.2);
  746. }
  747. .fortune-header {
  748. display: flex;
  749. justify-content: space-between;
  750. align-items: center;
  751. margin-bottom: 12px;
  752. }
  753. .fortune-title {
  754. font-size: 18px;
  755. font-weight: bold;
  756. color: #7C2D12;
  757. }
  758. .fortune-date {
  759. font-size: 12px;
  760. color: #9A3412;
  761. }
  762. .fortune-content {
  763. margin-bottom: 12px;
  764. }
  765. .fortune-text {
  766. font-size: 14px;
  767. color: #9A3412;
  768. line-height: 1.6;
  769. }
  770. .fortune-arrow {
  771. text-align: right;
  772. font-size: 12px;
  773. color: #7C2D12;
  774. }
  775. /* 成员弹窗 */
  776. .member-popup {
  777. position: fixed;
  778. top: 0;
  779. left: 0;
  780. right: 0;
  781. bottom: 0;
  782. z-index: 1000;
  783. }
  784. .popup-overlay {
  785. position: absolute;
  786. top: 0;
  787. left: 0;
  788. right: 0;
  789. bottom: 0;
  790. background: rgba(0, 0, 0, 0.5);
  791. }
  792. .popup-content {
  793. position: absolute;
  794. bottom: 0;
  795. left: 0;
  796. right: 0;
  797. background: #FFFFFF;
  798. border-radius: 20px 20px 0 0;
  799. padding: 24px;
  800. max-height: 70vh;
  801. overflow-y: auto;
  802. }
  803. .popup-header {
  804. display: flex;
  805. justify-content: space-between;
  806. align-items: center;
  807. margin-bottom: 20px;
  808. }
  809. .popup-title {
  810. font-size: 20px;
  811. font-weight: bold;
  812. color: #1F2937;
  813. }
  814. .popup-close {
  815. font-size: 28px;
  816. color: #9CA3AF;
  817. line-height: 1;
  818. }
  819. .popup-body {
  820. margin-bottom: 20px;
  821. }
  822. .member-info-row {
  823. display: flex;
  824. margin-bottom: 12px;
  825. }
  826. .info-label {
  827. width: 80px;
  828. color: #6B7280;
  829. font-size: 14px;
  830. }
  831. .info-value {
  832. flex: 1;
  833. color: #1F2937;
  834. font-size: 14px;
  835. }
  836. .energy-bars {
  837. margin-top: 20px;
  838. }
  839. .energy-section-title {
  840. font-size: 14px;
  841. font-weight: bold;
  842. color: #1F2937;
  843. margin-bottom: 12px;
  844. display: block;
  845. }
  846. .energy-bar-item {
  847. display: flex;
  848. align-items: center;
  849. margin-bottom: 12px;
  850. }
  851. .energy-label {
  852. width: 40px;
  853. color: #6B7280;
  854. font-size: 14px;
  855. }
  856. .energy-bar {
  857. flex: 1;
  858. height: 8px;
  859. background: #E5E7EB;
  860. border-radius: 4px;
  861. margin: 0 12px;
  862. overflow: hidden;
  863. }
  864. .energy-fill {
  865. height: 100%;
  866. background: linear-gradient(90deg, #F97316 0%, #FB923C 100%);
  867. border-radius: 4px;
  868. }
  869. .energy-value {
  870. width: 30px;
  871. text-align: right;
  872. color: #1F2937;
  873. font-size: 14px;
  874. }
  875. .popup-footer {
  876. display: flex;
  877. justify-content: center;
  878. }
  879. .detail-btn {
  880. background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
  881. color: #FFFFFF;
  882. border: none;
  883. padding: 12px 48px;
  884. border-radius: 24px;
  885. font-size: 16px;
  886. }
  887. /* 传统命理入口卡片 */
  888. .tradition-card {
  889. background: linear-gradient(135deg, #F5F0FF 0%, #E0D4FF 100%);
  890. box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
  891. }
  892. .tradition-card .fortune-title {
  893. color: #6D28D9;
  894. }
  895. .tradition-card .fortune-text {
  896. color: #7C3AED;
  897. }
  898. .tradition-card .fortune-arrow {
  899. color: #8B5CF6;
  900. }
  901. /* 加载遮罩 */
  902. .loading-overlay {
  903. position: fixed;
  904. top: 0;
  905. left: 0;
  906. right: 0;
  907. bottom: 0;
  908. background: rgba(255, 255, 255, 0.9);
  909. display: flex;
  910. flex-direction: column;
  911. justify-content: center;
  912. align-items: center;
  913. z-index: 2000;
  914. }
  915. .loading-spinner {
  916. width: 40px;
  917. height: 40px;
  918. border: 4px solid #E5E7EB;
  919. border-top-color: #F97316;
  920. border-radius: 50%;
  921. animation: spin 1s linear infinite;
  922. margin-bottom: 16px;
  923. }
  924. @keyframes spin {
  925. to {
  926. transform: rotate(360deg);
  927. }
  928. }
  929. </style>