index.vue 24 KB

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