index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  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. var familyId = uni.getStorageSync('familyId')
  193. if (!familyId) {
  194. return
  195. }
  196. this.loadDashboard({ familyId: familyId, year: this.currentYear, daysAhead: 7 })
  197. .then(this.drawCanvas)
  198. },
  199. onCanvasReady: function(res) {
  200. if (!res || !res[0] || !res[0].node) {
  201. return
  202. }
  203. var ctx = res[0].node.getContext('2d')
  204. var dpr = uni.getSystemInfoSync().pixelRatio
  205. this.canvasContext = ctx
  206. this.canvasContext.scale(dpr, dpr)
  207. this.drawCanvas()
  208. },
  209. drawCanvas: function() {
  210. if (!this.canvasContext) {
  211. return
  212. }
  213. var ctx = this.canvasContext
  214. var w = this.canvasWidth
  215. var h = this.canvasHeight
  216. var cx = w / 2
  217. var cy = h / 2
  218. // 清空画布
  219. ctx.clearRect(0, 0, w, h)
  220. ctx.fillStyle = '#FFF8F0'
  221. ctx.fillRect(0, 0, w, h)
  222. // 获取数据
  223. var members = this.members || []
  224. var relationships = this.relationships || []
  225. // R0: 中心五维能量雷达图
  226. this.drawRadar(ctx, cx, cy, w * 0.11, members)
  227. // R1: 内圈成员节点
  228. this.drawMemberNodes(ctx, cx, cy, w * 0.18, members)
  229. // R2-R3: 外圈相关事项
  230. this.drawRelatedItems(ctx, cx, cy, w * 0.32, w * 0.46, members)
  231. // 绘制关系连线
  232. this.drawRelationshipLines(ctx, cx, cy, relationships)
  233. },
  234. drawRadar: function(ctx, cx, cy, r, members) {
  235. var axes = ['木', '火', '土', '金', '水']
  236. var angleStep = (Math.PI * 2) / 5
  237. // 绘制五边形背景
  238. ctx.beginPath()
  239. for (var i = 0; i < 5; i++) {
  240. var angle = i * angleStep - Math.PI / 2
  241. var x = cx + r * Math.cos(angle)
  242. var y = cy + r * Math.sin(angle)
  243. if (i === 0) {
  244. ctx.moveTo(x, y)
  245. } else {
  246. ctx.lineTo(x, y)
  247. }
  248. }
  249. ctx.closePath()
  250. ctx.fillStyle = 'rgba(249, 114, 22, 0.1)'
  251. ctx.fill()
  252. ctx.strokeStyle = '#F97316'
  253. ctx.lineWidth = 1
  254. ctx.stroke()
  255. // 绘制轴线
  256. for (var j = 0; j < 5; j++) {
  257. var angle = j * angleStep - Math.PI / 2
  258. var x = cx + r * Math.cos(angle)
  259. var y = cy + r * Math.sin(angle)
  260. ctx.beginPath()
  261. ctx.moveTo(cx, cy)
  262. ctx.lineTo(x, y)
  263. ctx.strokeStyle = 'rgba(249, 114, 22, 0.3)'
  264. ctx.stroke()
  265. // 绘制轴标签
  266. var labelX = cx + (r + 15) * Math.cos(angle)
  267. var labelY = cy + (r + 15) * Math.sin(angle)
  268. ctx.fillStyle = '#F97316'
  269. ctx.font = '12px sans-serif'
  270. ctx.textAlign = 'center'
  271. ctx.textBaseline = 'middle'
  272. ctx.fillText(axes[j], labelX, labelY)
  273. }
  274. },
  275. drawMemberNodes: function(ctx, cx, cy, r, members) {
  276. var angleStep = (Math.PI * 2) / (members.length || 1)
  277. for (var i = 0; i < members.length; i++) {
  278. var member = members[i]
  279. var angle = i * angleStep - Math.PI / 2
  280. var x = cx + r * Math.cos(angle)
  281. var y = cy + r * Math.sin(angle)
  282. // 绘制圆形节点
  283. ctx.beginPath()
  284. ctx.arc(x, y, 15, 0, Math.PI * 2)
  285. ctx.fillStyle = this.getMemberColor(member.effectiveRole)
  286. ctx.fill()
  287. ctx.strokeStyle = '#FFF'
  288. ctx.lineWidth = 2
  289. ctx.stroke()
  290. // 绘制名字
  291. ctx.fillStyle = '#FFF'
  292. ctx.font = '10px sans-serif'
  293. ctx.textAlign = 'center'
  294. ctx.textBaseline = 'middle'
  295. ctx.fillText(member.nickname || member.name, x, y)
  296. // 存储点击区域
  297. if (!this.memberHitAreas) {
  298. this.memberHitAreas = []
  299. }
  300. this.memberHitAreas.push({
  301. type: 'member',
  302. data: member,
  303. x: x,
  304. y: y,
  305. r: 15
  306. })
  307. }
  308. },
  309. drawRelatedItems: function(ctx, cx, cy, rInner, rOuter, members) {
  310. var items = this.relatedItems && this.relatedItems.tasks ? this.relatedItems.tasks : []
  311. var angleStep = (Math.PI * 2) / (items.length || 1)
  312. var r = (rInner + rOuter) / 2
  313. for (var i = 0; i < items.length; i++) {
  314. var item = items[i]
  315. var angle = i * angleStep - Math.PI / 2
  316. var x = cx + r * Math.cos(angle)
  317. var y = cy + r * Math.sin(angle)
  318. // 绘制 emoji
  319. ctx.font = '16px sans-serif'
  320. ctx.textAlign = 'center'
  321. ctx.textBaseline = 'middle'
  322. ctx.fillText(item.icon || '📋', x, y)
  323. // 存储点击区域
  324. if (!this.itemHitAreas) {
  325. this.itemHitAreas = []
  326. }
  327. this.itemHitAreas.push({
  328. type: 'item',
  329. data: item,
  330. x: x,
  331. y: y,
  332. r: 12
  333. })
  334. }
  335. },
  336. drawRelationshipLines: function(ctx, cx, cy, relationships) {
  337. for (var i = 0; i < relationships.length; i++) {
  338. var rel = relationships[i]
  339. if (!rel.fromMember || !rel.toMember) {
  340. continue
  341. }
  342. // 计算起点和终点
  343. var memberCount = this.members.length || 1
  344. var angleStep = (Math.PI * 2) / memberCount
  345. var r = this.canvasWidth * 0.18
  346. var fromIndex = -1
  347. var toIndex = -1
  348. for (var j = 0; j < this.members.length; j++) {
  349. if (this.members[j].id === rel.fromMember.id) {
  350. fromIndex = j
  351. }
  352. if (this.members[j].id === rel.toMember.id) {
  353. toIndex = j
  354. }
  355. }
  356. if (fromIndex < 0 || toIndex < 0) {
  357. continue
  358. }
  359. var fromAngle = fromIndex * angleStep - Math.PI / 2
  360. var toAngle = toIndex * angleStep - Math.PI / 2
  361. var x1 = cx + r * Math.cos(fromAngle)
  362. var y1 = cy + r * Math.sin(fromAngle)
  363. var x2 = cx + r * Math.cos(toAngle)
  364. var y2 = cy + r * Math.sin(toAngle)
  365. // 贝塞尔曲线
  366. ctx.beginPath()
  367. ctx.moveTo(x1, y1)
  368. var cx1 = (x1 + x2) / 2 + (y2 - y1) * 0.2
  369. var cy1 = (y1 + y2) / 2 - (x2 - x1) * 0.2
  370. ctx.quadraticCurveTo(cx1, cy1, x2, y2)
  371. // 根据分数设置颜色
  372. var score = rel.score || 0
  373. if (score >= 80) {
  374. ctx.strokeStyle = '#10B981'
  375. } else if (score >= 60) {
  376. ctx.strokeStyle = '#F59E0B'
  377. } else {
  378. ctx.strokeStyle = '#EF4444'
  379. }
  380. ctx.lineWidth = 2
  381. ctx.stroke()
  382. }
  383. },
  384. getMemberColor: function(role) {
  385. var colorMap = {
  386. 'parent': '#F97316',
  387. 'child': '#3B82F6',
  388. 'teacher': '#8B5CF6'
  389. }
  390. return colorMap[role] || '#6B7280'
  391. },
  392. onCanvasTouchEnd: function(e) {
  393. if (!e || !e.changedTouches || !e.changedTouches[0]) {
  394. return
  395. }
  396. var touch = e.changedTouches[0]
  397. var x = touch.x
  398. var y = touch.y
  399. // 检测成员点击
  400. if (this.memberHitAreas) {
  401. for (var i = 0; i < this.memberHitAreas.length; i++) {
  402. var area = this.memberHitAreas[i]
  403. var dx = x - area.x
  404. var dy = y - area.y
  405. if (dx * dx + dy * dy <= area.r * area.r) {
  406. this.showMemberDetail(area.data)
  407. return
  408. }
  409. }
  410. }
  411. // 检测事项点击
  412. if (this.itemHitAreas) {
  413. for (var j = 0; j < this.itemHitAreas.length; j++) {
  414. var itemArea = this.itemHitAreas[j]
  415. var dx2 = x - itemArea.x
  416. var dy2 = y - itemArea.y
  417. if (dx2 * dx2 + dy2 * dy2 <= itemArea.r * itemArea.r) {
  418. this.onItemClick(itemArea.data)
  419. return
  420. }
  421. }
  422. }
  423. },
  424. showMemberDetail: function(member) {
  425. this.selectedMember = member
  426. this.showMemberPopup = true
  427. },
  428. closeMemberPopup: function() {
  429. this.showMemberPopup = false
  430. this.selectedMember = {}
  431. },
  432. goToMemberDetail: function() {
  433. if (!this.selectedMember || !this.selectedMember.id) {
  434. return
  435. }
  436. uni.navigateTo({
  437. url: '/pages/tianpan/member-detail?id=' + this.selectedMember.id
  438. })
  439. this.closeMemberPopup()
  440. },
  441. switchTab: function(index) {
  442. this.currentTab = index
  443. },
  444. onItemClick: function(item) {
  445. if (!item || !item.id) {
  446. return
  447. }
  448. // 根据类型跳转
  449. if (item.type === 'task') {
  450. uni.navigateTo({ url: '/pages/tasks/detail?id=' + item.id })
  451. } else if (item.type === 'assessment') {
  452. uni.navigateTo({ url: '/pages/assessment/detail?id=' + item.id })
  453. } else if (item.type === 'alert') {
  454. uni.navigateTo({ url: '/pages/health/alert-detail?id=' + item.id })
  455. } else if (item.type === 'milestone') {
  456. uni.navigateTo({ url: '/pages/family/milestone-detail?id=' + item.id })
  457. }
  458. },
  459. goToDailyFortune: function() {
  460. uni.navigateTo({
  461. url: '/pages/tianpan/daily-fortune'
  462. })
  463. },
  464. loadDailyFortune: function() {
  465. var familyId = uni.getStorageSync('familyId')
  466. if (!familyId) {
  467. return
  468. }
  469. var today = new Date()
  470. var dateStr = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate()
  471. this.loadDailyFortune({ familyId: familyId, date: dateStr })
  472. .then(this.setFortuneText)
  473. },
  474. setFortuneText: function(data) {
  475. if (data && data.advice) {
  476. this.dailyFortuneText = data.advice
  477. }
  478. var today = new Date()
  479. this.currentDate = today.getFullYear() + '年' + (today.getMonth() + 1) + '月' + today.getDate() + '日'
  480. },
  481. loadRelatedItems: function() {
  482. var familyId = uni.getStorageSync('familyId')
  483. if (!familyId) {
  484. return
  485. }
  486. this.loadRelatedItems({ familyId: familyId })
  487. }
  488. },
  489. watch: {
  490. members: function(newVal) {
  491. if (newVal && this.canvasContext) {
  492. this.drawCanvas()
  493. }
  494. },
  495. relationships: function(newVal) {
  496. if (newVal && this.canvasContext) {
  497. this.drawCanvas()
  498. }
  499. }
  500. }
  501. }
  502. </script>
  503. <style>
  504. .tianpan-container {
  505. min-height: 100vh;
  506. background: linear-gradient(180deg, #FFF8F0 0%, #FFFFFF 100%);
  507. padding-bottom: 20px;
  508. }
  509. /* 自定义导航栏 */
  510. .custom-nav {
  511. background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
  512. padding: 44px 16px 12px;
  513. color: #FFFFFF;
  514. }
  515. .nav-header {
  516. display: flex;
  517. justify-content: space-between;
  518. align-items: center;
  519. }
  520. .nav-title {
  521. font-size: 20px;
  522. font-weight: bold;
  523. }
  524. .year-picker {
  525. display: flex;
  526. align-items: center;
  527. background: rgba(255, 255, 255, 0.2);
  528. padding: 4px 12px;
  529. border-radius: 16px;
  530. }
  531. .year-text {
  532. font-size: 14px;
  533. margin-right: 4px;
  534. }
  535. .picker-icon {
  536. font-size: 10px;
  537. }
  538. .family-name {
  539. text-align: center;
  540. margin-top: 8px;
  541. font-size: 14px;
  542. opacity: 0.9;
  543. }
  544. /* 画布区域 */
  545. .canvas-wrapper {
  546. display: flex;
  547. justify-content: center;
  548. padding: 20px 0;
  549. }
  550. .tianpan-canvas {
  551. width: 375px;
  552. height: 375px;
  553. background: #FFF8F0;
  554. border-radius: 16px;
  555. box-shadow: 0 4px 12px rgba(249, 114, 22, 0.1);
  556. }
  557. /* 标签栏 */
  558. .tab-bar {
  559. display: flex;
  560. justify-content: space-around;
  561. background: #FFFFFF;
  562. margin: 0 16px;
  563. padding: 12px 0;
  564. border-radius: 12px;
  565. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  566. }
  567. .tab-item {
  568. padding: 6px 16px;
  569. border-radius: 20px;
  570. font-size: 14px;
  571. color: #6B7280;
  572. }
  573. .tab-item.active {
  574. background: #F97316;
  575. color: #FFFFFF;
  576. }
  577. /* 相关事项列表 */
  578. .related-items-list {
  579. padding: 16px;
  580. }
  581. .item-card {
  582. display: flex;
  583. align-items: center;
  584. background: #FFFFFF;
  585. padding: 16px;
  586. border-radius: 12px;
  587. margin-bottom: 12px;
  588. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  589. }
  590. .item-icon {
  591. font-size: 28px;
  592. margin-right: 12px;
  593. }
  594. .item-content {
  595. flex: 1;
  596. }
  597. .item-title {
  598. font-size: 16px;
  599. font-weight: 500;
  600. color: #1F2937;
  601. display: block;
  602. margin-bottom: 4px;
  603. }
  604. .item-desc {
  605. font-size: 12px;
  606. color: #9CA3AF;
  607. display: block;
  608. }
  609. .item-arrow {
  610. font-size: 20px;
  611. color: #D1D5DB;
  612. }
  613. .empty-state {
  614. text-align: center;
  615. padding: 40px 0;
  616. color: #9CA3AF;
  617. font-size: 14px;
  618. }
  619. /* 每日运势卡片 */
  620. .fortune-card {
  621. margin: 16px;
  622. padding: 20px;
  623. background: linear-gradient(135deg, #FED7AA 0%, #FDBA74 100%);
  624. border-radius: 16px;
  625. box-shadow: 0 4px 12px rgba(249, 114, 22, 0.2);
  626. }
  627. .fortune-header {
  628. display: flex;
  629. justify-content: space-between;
  630. align-items: center;
  631. margin-bottom: 12px;
  632. }
  633. .fortune-title {
  634. font-size: 18px;
  635. font-weight: bold;
  636. color: #7C2D12;
  637. }
  638. .fortune-date {
  639. font-size: 12px;
  640. color: #9A3412;
  641. }
  642. .fortune-content {
  643. margin-bottom: 12px;
  644. }
  645. .fortune-text {
  646. font-size: 14px;
  647. color: #9A3412;
  648. line-height: 1.6;
  649. }
  650. .fortune-arrow {
  651. text-align: right;
  652. font-size: 12px;
  653. color: #7C2D12;
  654. }
  655. /* 成员弹窗 */
  656. .member-popup {
  657. position: fixed;
  658. top: 0;
  659. left: 0;
  660. right: 0;
  661. bottom: 0;
  662. z-index: 1000;
  663. }
  664. .popup-overlay {
  665. position: absolute;
  666. top: 0;
  667. left: 0;
  668. right: 0;
  669. bottom: 0;
  670. background: rgba(0, 0, 0, 0.5);
  671. }
  672. .popup-content {
  673. position: absolute;
  674. bottom: 0;
  675. left: 0;
  676. right: 0;
  677. background: #FFFFFF;
  678. border-radius: 20px 20px 0 0;
  679. padding: 24px;
  680. max-height: 70vh;
  681. overflow-y: auto;
  682. }
  683. .popup-header {
  684. display: flex;
  685. justify-content: space-between;
  686. align-items: center;
  687. margin-bottom: 20px;
  688. }
  689. .popup-title {
  690. font-size: 20px;
  691. font-weight: bold;
  692. color: #1F2937;
  693. }
  694. .popup-close {
  695. font-size: 28px;
  696. color: #9CA3AF;
  697. line-height: 1;
  698. }
  699. .popup-body {
  700. margin-bottom: 20px;
  701. }
  702. .member-info-row {
  703. display: flex;
  704. margin-bottom: 12px;
  705. }
  706. .info-label {
  707. width: 80px;
  708. color: #6B7280;
  709. font-size: 14px;
  710. }
  711. .info-value {
  712. flex: 1;
  713. color: #1F2937;
  714. font-size: 14px;
  715. }
  716. .energy-bars {
  717. margin-top: 20px;
  718. }
  719. .energy-bar-item {
  720. display: flex;
  721. align-items: center;
  722. margin-bottom: 12px;
  723. }
  724. .energy-label {
  725. width: 40px;
  726. color: #6B7280;
  727. font-size: 14px;
  728. }
  729. .energy-bar {
  730. flex: 1;
  731. height: 8px;
  732. background: #E5E7EB;
  733. border-radius: 4px;
  734. margin: 0 12px;
  735. overflow: hidden;
  736. }
  737. .energy-fill {
  738. height: 100%;
  739. background: linear-gradient(90deg, #F97316 0%, #FB923C 100%);
  740. border-radius: 4px;
  741. }
  742. .energy-value {
  743. width: 30px;
  744. text-align: right;
  745. color: #1F2937;
  746. font-size: 14px;
  747. }
  748. .popup-footer {
  749. display: flex;
  750. justify-content: center;
  751. }
  752. .detail-btn {
  753. background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
  754. color: #FFFFFF;
  755. border: none;
  756. padding: 12px 48px;
  757. border-radius: 24px;
  758. font-size: 16px;
  759. }
  760. /* 加载状态 */
  761. .loading-overlay {
  762. position: fixed;
  763. top: 0;
  764. left: 0;
  765. right: 0;
  766. bottom: 0;
  767. background: rgba(255, 255, 255, 0.9);
  768. display: flex;
  769. flex-direction: column;
  770. justify-content: center;
  771. align-items: center;
  772. z-index: 2000;
  773. }
  774. .loading-spinner {
  775. width: 40px;
  776. height: 40px;
  777. border: 4px solid #E5E7EB;
  778. border-top-color: #F97316;
  779. border-radius: 50%;
  780. animation: spin 1s linear infinite;
  781. margin-bottom: 16px;
  782. }
  783. @keyframes spin {
  784. to {
  785. transform: rotate(360deg);
  786. }
  787. }
  788. </style>