family-dashboard.vue 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046
  1. <template>
  2. <!-- 家庭天盘详情页 -->
  3. <view class="family-dashboard">
  4. <!-- PDF 导出用隐藏 canvas -->
  5. <canvas canvas-id="weeklyReportCanvas" id="weeklyReportCanvas" style="width: 1px; height: 1px; position: absolute; top: -1000rpx;"></canvas>
  6. <!-- 头部导航 -->
  7. <view class="dashboard-header">
  8. <view class="dashboard-icon" @click="goBack">
  9. <text class="dashboard-icon-text">&#x2190;</text>
  10. </view>
  11. <text class="dashboard-title">家庭天盘</text>
  12. <view class="dashboard-menu" @click="showMenu = !showMenu">
  13. <text class="dashboard-menu-text">&#x2630;</text>
  14. <view class="dashboard-menu-dropdown" v-if="showMenu">
  15. <view class="menu-item" @click="onMenuItemClick('week')">本周运势</view>
  16. <view class="menu-item" @click="onMenuItemClick('month')">本月趋势</view>
  17. <view class="menu-item" @click="onMenuItemClick('history')">历史数据</view>
  18. <view class="menu-item" @click="onMenuItemClick('export')">导出周报</view>
  19. </view>
  20. </view>
  21. </view>
  22. <!-- 数据概览卡片 -->
  23. <view class="dashboard-card" :style="cardStyle">
  24. <view class="card-header">
  25. <text class="card-title">{{ currentDateLabel }}</text>
  26. <text class="card-subtitle">五行能量动态</text>
  27. </view>
  28. <view class="card-body">
  29. <!-- 运势数据展示 -->
  30. <view class="fortune-data">
  31. <!-- 主导五行 -->
  32. <view class="data-item">
  33. <view class="data-icon" :style="{ backgroundColor: elementColors[familyFortune.dominantElement] }">
  34. <text class="data-icon-text">🌱</text>
  35. </view>
  36. <view class="data-info">
  37. <text class="data-label">主导五行</text>
  38. <text class="data-value">{{ getElementName(familyFortune.dominantElement) }}</text>
  39. </view>
  40. </view>
  41. <!-- 吉位 -->
  42. <view class="data-item">
  43. <view class="data-icon" :style="{ backgroundColor: '#FFA500' }">
  44. <text class="data-icon-text">🧭</text>
  45. </view>
  46. <view class="data-info">
  47. <text class="data-label">今日吉位</text>
  48. <text class="data-value">{{ familyFortune.luckyDirection }}方</text>
  49. </view>
  50. </view>
  51. <!-- 运势 -->
  52. <view class="data-item">
  53. <view class="data-icon" :style="{ backgroundColor: getFortuneColor(familyFortune.fortuneLevel) }">
  54. <text class="data-icon-text">⭐</text>
  55. </view>
  56. <view class="data-info">
  57. <text class="data-label">今日运势</text>
  58. <text class="data-value">{{ familyFortune.fortuneLevelText }}</text>
  59. </view>
  60. </view>
  61. </view>
  62. <!-- 心情进度条 -->
  63. <view class="mood-section">
  64. <text class="mood-emoji">{{ familyFortune.moodEmoji }}</text>
  65. <text class="mood-text">{{ familyFortune.moodText }}</text>
  66. <view class="mood-bar">
  67. <view class="mood-bar-fill" :style="{ width: familyFortune.moodScore + '%' }"></view>
  68. </view>
  69. </view>
  70. <!-- 五行罗盘 -->
  71. <view class="compass-section">
  72. <canvas id="dashboardCompass" type="2d" class="dashboard-compass"></canvas>
  73. </view>
  74. </view>
  75. <view class="card-footer">
  76. <text class="footer-tip">✨ {{ familyFortune.dailyTip }}</text>
  77. </view>
  78. </view>
  79. <!-- 周数据趋势图 -->
  80. <view class="dashboard-section" v-if="viewMode === 'week'">
  81. <view class="section-header">
  82. <text class="section-title">本周能量趋势</text>
  83. </view>
  84. <view class="section-body">
  85. <view class="trend-chart">
  86. <canvas id="weekTrendChart" type="2d" class="trend-canvas"></canvas>
  87. </view>
  88. </view>
  89. </view>
  90. <!-- 月数据趋势图 -->
  91. <view class="dashboard-section" v-if="viewMode === 'month'">
  92. <view class="section-header">
  93. <text class="section-title">本月运势走势</text>
  94. </view>
  95. <view class="section-body">
  96. <view class="month-trend">
  97. <view class="trend-calendar">
  98. <view class="calendar-header">
  99. <text class="calendar-month">{{ currentMonthLabel }}</text>
  100. </view>
  101. <view class="calendar-grid">
  102. <view class="calendar-day" v-for="(day, index) in monthDays" :key="index" :class="day.dayClass">
  103. <text class="day-number">{{ day.day }}</text>
  104. <view class="day-fortune" v-if="day.fortune">
  105. <view class="fortune-bubble" :class="'fortune-lv-' + getFortuneLevel(day.fortune)"></view>
  106. </view>
  107. </view>
  108. </view>
  109. </view>
  110. </view>
  111. </view>
  112. </view>
  113. <!-- 历史周报列表 -->
  114. <view class="dashboard-section" v-if="viewMode === 'history'">
  115. <view class="section-header">
  116. <text class="section-title">历史周报</text>
  117. </view>
  118. <view class="section-body">
  119. <view class="report-list">
  120. <view class="report-item" v-for="(report, index) in weeklyReports" :key="index" @click="viewWeeklyReport(report)">
  121. <view class="report-icon">📄</view>
  122. <view class="report-info">
  123. <text class="report-title">第 {{ report.weekNumber }} 周报告</text>
  124. <text class="report-date">{{ report.startDate }} ~ {{ report.endDate }}</text>
  125. </view>
  126. <view class="report-action">
  127. <text class="report-download" @click.stop="downloadReport(report)">下载</text>
  128. </view>
  129. </view>
  130. </view>
  131. </view>
  132. </view>
  133. <!-- 周报详情模态框 -->
  134. <view class="modal-overlay" v-if="showWeeklyReport" @click="showWeeklyReport = false">
  135. <view class="weekly-report-modal" @click.stop>
  136. <view class="modal-header">
  137. <text class="modal-title">第 {{ currentWeeklyReport.weekNumber }} 周报告</text>
  138. <view class="modal-close" @click="showWeeklyReport = false">✕</view>
  139. </view>
  140. <view class="modal-body">
  141. <scroll-view scroll-y="true" class="modal-content">
  142. <mp-html :content="currentWeeklyReport.content" />
  143. </scroll-view>
  144. </view>
  145. <view class="modal-footer">
  146. <button class="modal-btn" @click="downloadReport(currentWeeklyReport)">下载PDF</button>
  147. </view>
  148. </view>
  149. </view>
  150. </view>
  151. </template>
  152. <script>
  153. import MpHtml from '@/components/mp-html/mp-html.vue'
  154. import { parseDate } from '../../utils/format.js'
  155. import { getFamilyFortune, getWeekFortune, getMonthFortune, getFortuneReports, reportAppEnter
  156. } from '../../utils/api.js'
  157. /**
  158. * FamilyDashboard — 家庭天盘详情页
  159. * 包含:运势数据、能量趋势、历史周报、月度日历等
  160. */
  161. export default {
  162. name: 'FamilyDashboard',
  163. components: { MpHtml },
  164. data() {
  165. return {
  166. showMenu: false,
  167. viewMode: 'week', // week, month, history
  168. familyFortune: {
  169. dominantElement: '木',
  170. luckyDirection: '东',
  171. fortuneLevel: '平',
  172. fortuneLevelText: '平',
  173. fortuneDescription: '',
  174. moodScore: 65,
  175. moodEmoji: '😊',
  176. moodText: '平静',
  177. dailyTip: '',
  178. weeklyTip: ''
  179. },
  180. currentDate: new Date(),
  181. weekFortunes: [],
  182. monthFortunes: [],
  183. weeklyReports: [],
  184. elementColors: {
  185. '木': '#10B981',
  186. '火': '#FF6B9D',
  187. '土': '#FF8C42',
  188. '金': '#6366F1',
  189. '水': '#3B82F6'
  190. },
  191. // 详情模态框
  192. showWeeklyReport: false,
  193. currentWeeklyReport: null,
  194. // Canvas 对象
  195. compassCtx: null,
  196. weekChartCtx: null
  197. }
  198. },
  199. computed: {
  200. cardStyle() {
  201. return {
  202. backgroundColor: this.elementColors[this.familyFortune.dominantElement] + '10'
  203. }
  204. },
  205. currentDateLabel() {
  206. const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
  207. return `${this.familyFortune.luckyDirection}方 · ${week[this.currentDate.getDay()]} ${this.formatDate(this.currentDate)}`
  208. },
  209. currentMonthLabel() {
  210. return `${this.currentDate.getFullYear()}年${this.currentDate.getMonth() + 1}月`
  211. },
  212. monthDays() {
  213. const year = this.currentDate.getFullYear()
  214. const month = this.currentDate.getMonth()
  215. const days = []
  216. // 获取当月第一天是星期几
  217. const firstDay = new Date(year, month, 1).getDay()
  218. // 获取当月总天数
  219. const lastDate = new Date(year, month + 1, 0).getDate()
  220. // 填充上月尾部
  221. for (let i = 0; i < firstDay; i++) {
  222. const emptyDay = { day: '', date: null }
  223. emptyDay.dayClass = this.getDayClass(emptyDay).join(' ')
  224. days.push(emptyDay)
  225. }
  226. // 填充当月
  227. for (let i = 1; i <= lastDate; i++) {
  228. const date = new Date(year, month, i)
  229. const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
  230. const dayData = {
  231. day: i,
  232. date: date,
  233. fortune: null
  234. }
  235. // 查找对应的运势数据
  236. const fortune = this.monthFortunes.find(f =>
  237. f.fortuneDate === dateStr
  238. )
  239. if (fortune) {
  240. dayData.fortune = fortune
  241. }
  242. // 预计算 class 字符串(小程序 :class 不支持方法调用)
  243. dayData.dayClass = this.getDayClass(dayData).join(' ')
  244. days.push(dayData)
  245. }
  246. return days
  247. }
  248. },
  249. onLoad() {
  250. reportAppEnter('tiandi');
  251. // 初始化页面
  252. this.loadTodayFortune()
  253. this.loadWeekFortuneData()
  254. this.loadMonthlyFortuneData()
  255. this.loadWeeklyReports()
  256. // 初始化 Canvas
  257. this.$nextTick(() => {
  258. this.initCompassCanvas()
  259. this.initWeekChartCanvas()
  260. })
  261. },
  262. methods: {
  263. // 加载今日运势数据
  264. loadTodayFortune() {
  265. var self = this
  266. getFamilyFortune().then(function(res) {
  267. if (res.code === 200 && res.data) {
  268. self.familyFortune = {
  269. dominantElement: res.data.dominantElement || '木',
  270. luckyDirection: res.data.luckyDirection || '东',
  271. fortuneLevel: res.data.fortuneLevel || '平',
  272. fortuneLevelText: res.data.fortuneLevelText || res.data.fortuneLevel || '平',
  273. fortuneDescription: res.data.fortuneDescription || '',
  274. moodScore: res.data.moodScore || 65,
  275. moodEmoji: res.data.moodEmoji || '😊',
  276. moodText: res.data.moodText || '平静',
  277. dailyTip: res.data.dailyTip || '',
  278. weeklyTip: res.data.weeklyTip || ''
  279. }
  280. }
  281. }).catch(function(e) {
  282. console.error('加载今日运势失败', e)
  283. })
  284. },
  285. // 加载本周运势数据
  286. loadWeekFortuneData() {
  287. var self = this
  288. getWeekFortune().then(function(res) {
  289. if (res.code === 200 && res.data) {
  290. self.weekFortunes = res.data
  291. }
  292. }).catch(function(e) {
  293. console.error('加载本周运势失败', e)
  294. })
  295. },
  296. // 加载本月运势数据
  297. loadMonthlyFortuneData() {
  298. var self = this
  299. getMonthFortune().then(function(res) {
  300. if (res.code === 200 && res.data) {
  301. self.monthFortunes = res.data
  302. }
  303. }).catch(function(e) {
  304. console.error('加载本月运势失败', e)
  305. })
  306. },
  307. // 加载历史周报
  308. loadWeeklyReports() {
  309. var self = this
  310. getFortuneReports().then(function(res) {
  311. if (res.code === 200 && res.data) {
  312. self.weeklyReports = res.data
  313. }
  314. }).catch(function(e) {
  315. console.error('加载历史周报失败', e)
  316. })
  317. },
  318. // 初始化罗盘 Canvas
  319. initCompassCanvas() {
  320. const query = uni.createSelectorQuery().in(this)
  321. query.select('#dashboardCompass')
  322. .fields({ node: true, size: true })
  323. .exec((res) => {
  324. if (!res || !res[0] || !res[0].node) return
  325. const canvas = res[0].node
  326. const dpr = uni.getWindowInfo().pixelRatio || 2
  327. const w = res[0].width
  328. const h = res[0].height
  329. canvas.width = w * dpr
  330. canvas.height = h * dpr
  331. const ctx = canvas.getContext('2d')
  332. ctx.scale(dpr, dpr)
  333. this.compassCtx = ctx
  334. this.drawCompass(ctx, w, h)
  335. })
  336. },
  337. // 绘制罗盘
  338. drawCompass(ctx, w, h) {
  339. const cx = w / 2
  340. const cy = h / 2
  341. const outerR = Math.min(cx, cy) - 4
  342. // 清空
  343. ctx.clearRect(0, 0, w, h)
  344. // 五行方位
  345. const directions = [
  346. { label: '北', element: '水', angle: -Math.PI / 2 },
  347. { label: '东', element: '木', angle: 0 },
  348. { label: '南', element: '火', angle: Math.PI / 2 },
  349. { label: '西', element: '金', angle: Math.PI },
  350. { label: '中', element: '土', angle: null }
  351. ]
  352. // 吉位方向
  353. const luckyDir = this.familyFortune.luckyDirection
  354. const luckyColor = this.elementColors[this.familyFortune.dominantElement]
  355. // 外圈背景
  356. ctx.beginPath()
  357. ctx.arc(cx, cy, outerR, 0, Math.PI * 2)
  358. ctx.fillStyle = '#FFF5F7'
  359. ctx.fill()
  360. ctx.strokeStyle = luckyColor + '40'
  361. ctx.lineWidth = 1.5
  362. ctx.stroke()
  363. // 绘制四个方位
  364. for (let i = 0; i < 4; i++) {
  365. const d = directions[i]
  366. const color = this.elementColors[d.element]
  367. // 扇区线
  368. ctx.beginPath()
  369. ctx.moveTo(cx, cy)
  370. const ex = cx + outerR * Math.cos(d.angle)
  371. const ey = cy + outerR * Math.sin(d.angle)
  372. ctx.lineTo(ex, ey)
  373. ctx.strokeStyle = color + '30'
  374. ctx.lineWidth = 1
  375. ctx.stroke()
  376. // 方位标签
  377. const labelR = outerR * 0.7
  378. const lx = cx + labelR * Math.cos(d.angle)
  379. const ly = cy + labelR * Math.sin(d.angle)
  380. const isLucky = d.label === luckyDir
  381. if (isLucky) {
  382. // 吉位高亮圆
  383. ctx.beginPath()
  384. ctx.arc(lx, ly, 12, 0, Math.PI * 2)
  385. ctx.fillStyle = luckyColor + '20'
  386. ctx.fill()
  387. ctx.strokeStyle = luckyColor
  388. ctx.lineWidth = 1.5
  389. ctx.stroke()
  390. }
  391. ctx.fillStyle = isLucky ? luckyColor : '#9CA3AF'
  392. ctx.font = (isLucky ? 'bold ' : '') + '12px sans-serif'
  393. ctx.textAlign = 'center'
  394. ctx.textBaseline = 'middle'
  395. ctx.fillText(d.label, lx, ly)
  396. }
  397. // 中心点
  398. ctx.beginPath()
  399. ctx.arc(cx, cy, 5, 0, Math.PI * 2)
  400. ctx.fillStyle = luckyColor
  401. ctx.fill()
  402. ctx.strokeStyle = '#FFFFFF'
  403. ctx.lineWidth = 2
  404. ctx.stroke()
  405. },
  406. // 初始化周趋势图 Canvas
  407. initWeekChartCanvas() {
  408. const query = uni.createSelectorQuery().in(this)
  409. query.select('#weekTrendChart')
  410. .fields({ node: true, size: true })
  411. .exec((res) => {
  412. if (!res || !res[0] || !res[0].node) return
  413. const canvas = res[0].node
  414. const dpr = uni.getWindowInfo().pixelRatio || 2
  415. const w = res[0].width
  416. const h = res[0].height
  417. canvas.width = w * dpr
  418. canvas.height = h * dpr
  419. const ctx = canvas.getContext('2d')
  420. ctx.scale(dpr, dpr)
  421. this.weekChartCtx = ctx
  422. this.drawWeekTrendChart(ctx, w, h)
  423. })
  424. },
  425. // 绘制周趋势图
  426. drawWeekTrendChart(ctx, w, h) {
  427. const padding = 40
  428. const chartW = w - padding * 2
  429. const chartH = h - padding * 2
  430. // 清空
  431. ctx.clearRect(0, 0, w, h)
  432. // 背景
  433. ctx.fillStyle = '#FFFFFF'
  434. ctx.fillRect(padding, padding, chartW, chartH)
  435. ctx.strokeStyle = '#F3F4F6'
  436. ctx.lineWidth = 1
  437. ctx.strokeRect(padding, padding, chartW, chartH)
  438. if (this.weekFortunes.length === 0) return
  439. // 坐标系
  440. const maxScore = 100
  441. const minScore = 0
  442. const step = 20
  443. const scoreSteps = Math.ceil((maxScore - minScore) / step)
  444. // Y 轴刻度
  445. ctx.fillStyle = '#6B7280'
  446. ctx.font = '10px sans-serif'
  447. ctx.textAlign = 'right'
  448. ctx.textBaseline = 'middle'
  449. for (let i = 0; i <= scoreSteps; i++) {
  450. const y = padding + chartH - (i * chartH / scoreSteps)
  451. const score = minScore + i * step
  452. ctx.fillText(score, padding - 10, y)
  453. ctx.beginPath()
  454. ctx.moveTo(padding - 5, y)
  455. ctx.lineTo(padding, y)
  456. ctx.strokeStyle = '#E5E7EB'
  457. ctx.stroke()
  458. }
  459. // X 轴刻度
  460. const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
  461. const labels = this.weekFortunes.map((f, i) => weekDays[i] || '')
  462. ctx.fillStyle = '#6B7280'
  463. ctx.font = '10px sans-serif'
  464. ctx.textAlign = 'center'
  465. ctx.textBaseline = 'top'
  466. for (let i = 0; i < labels.length; i++) {
  467. const x = padding + i * (chartW / (labels.length - 1))
  468. ctx.fillText(labels[i], x, padding + chartH + 10)
  469. ctx.beginPath()
  470. ctx.moveTo(x, padding + chartH)
  471. ctx.lineTo(x, padding + chartH + 5)
  472. ctx.strokeStyle = '#E5E7EB'
  473. ctx.stroke()
  474. }
  475. // 绘制折线
  476. ctx.beginPath()
  477. for (let i = 0; i < this.weekFortunes.length; i++) {
  478. const f = this.weekFortunes[i]
  479. const x = padding + i * (chartW / (this.weekFortunes.length - 1))
  480. const y = padding + chartH - (f.fortuneScore - minScore) * chartH / (maxScore - minScore)
  481. if (i === 0) {
  482. ctx.moveTo(x, y)
  483. } else {
  484. ctx.lineTo(x, y)
  485. }
  486. // 数据点
  487. ctx.beginPath()
  488. ctx.arc(x, y, 4, 0, Math.PI * 2)
  489. ctx.fillStyle = this.getFortuneColor(f.fortuneLevel)
  490. ctx.fill()
  491. ctx.strokeStyle = '#FFFFFF'
  492. ctx.lineWidth = 2
  493. ctx.stroke()
  494. // 恢复路径
  495. ctx.beginPath()
  496. ctx.moveTo(x, y)
  497. if (i < this.weekFortunes.length - 1) {
  498. const nextF = this.weekFortunes[i + 1]
  499. const nextX = padding + (i + 1) * (chartW / (this.weekFortunes.length - 1))
  500. const nextY = padding + chartH - (nextF.fortuneScore - minScore) * chartH / (maxScore - minScore)
  501. ctx.lineTo(nextX, nextY)
  502. }
  503. }
  504. ctx.strokeStyle = this.elementColors[this.familyFortune.dominantElement]
  505. ctx.lineWidth = 2
  506. ctx.stroke()
  507. },
  508. // 格式化日期
  509. formatDate(date) {
  510. if (!date) return ''
  511. const d = parseDate(date)
  512. if (!d) return ''
  513. return `${d.getMonth() + 1}月${d.getDate()}日`
  514. },
  515. // 菜单点击处理
  516. onMenuItemClick(mode) {
  517. this.showMenu = false
  518. if (mode === 'export') {
  519. uni.showToast({ title: '周报生成中...', icon: 'loading' })
  520. this.exportWeeklyReport()
  521. return
  522. }
  523. this.viewMode = mode
  524. },
  525. // 返回上一页
  526. goBack() {
  527. uni.navigateBack()
  528. },
  529. // 获取五行名称
  530. getElementName(element) {
  531. const names = { 木: '木', 火: '火', 土: '土', 金: '金', 水: '水' }
  532. return names[element] || element
  533. },
  534. // 获取运势颜色
  535. getFortuneColor(level) {
  536. const colors = {
  537. '大吉': '#EF4444',
  538. '吉': '#F97316',
  539. '平': '#FBBF24',
  540. '凶': '#6366F1',
  541. '大凶': '#8B5CF6'
  542. }
  543. return colors[level] || '#666666'
  544. },
  545. // 获取运势等级数值(用于CSS class)
  546. getFortuneLevel(dayFortune) {
  547. if (!dayFortune) return 2
  548. var levelMap = { '大吉': 0, '吉': 1, '平': 2, '凶': 3, '大凶': 4 }
  549. var level = dayFortune.fortuneLevel || dayFortune.fortuneLevelText || ''
  550. return levelMap[level] !== undefined ? levelMap[level] : 2
  551. },
  552. // 获取日历日样式
  553. getDayClass(day) {
  554. const classes = []
  555. if (!day.date) {
  556. classes.push('empty-day')
  557. return classes
  558. }
  559. if (day.day === new Date().getDate() &&
  560. day.date.getMonth() === new Date().getMonth() &&
  561. day.date.getFullYear() === new Date().getFullYear()) {
  562. classes.push('current-day')
  563. }
  564. if (day.fortune) {
  565. classes.push('has-data')
  566. classes.push('fortune-lv-' + this.getFortuneLevel(day.fortune))
  567. }
  568. return classes
  569. },
  570. // 查看周报
  571. viewWeeklyReport(report) {
  572. this.currentWeeklyReport = report
  573. this.showWeeklyReport = true
  574. },
  575. // 下载周报
  576. downloadReport(report) {
  577. uni.showToast({ title: '周报生成中...', icon: 'loading' })
  578. // TODO: 对接后端生成PDF接口
  579. setTimeout(() => {
  580. uni.showToast({ title: '周报已生成,请在文件管理中查看', icon: 'none' })
  581. }, 2000)
  582. },
  583. // 导出本周周报
  584. exportWeeklyReport() {
  585. uni.showToast({ title: '正在生成本周周报', icon: 'loading' })
  586. // TODO: 将画布数据发送到后端生成PDF
  587. setTimeout(() => {
  588. uni.showToast({ title: '周报已生成,请在历史周报中下载', icon: 'none' })
  589. }, 3000)
  590. }
  591. }
  592. }
  593. </script>
  594. <style scoped>
  595. .family-dashboard {
  596. background-color: #F9FAFB;
  597. min-height: 100vh;
  598. padding-bottom: 40rpx;
  599. }
  600. /* ===== 头部导航 ===== */
  601. .dashboard-header {
  602. position: sticky;
  603. top: 0;
  604. z-index: 100;
  605. background-color: #FFFFFF;
  606. display: flex;
  607. flex-direction: row;
  608. align-items: center;
  609. justify-content: space-between;
  610. padding: 30rpx 30rpx 16rpx;
  611. border-bottom: 1rpx solid #F3F4F6;
  612. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  613. }
  614. .dashboard-icon {
  615. width: 64rpx;
  616. height: 64rpx;
  617. border-radius: 32rpx;
  618. background-color: #FFF0F3;
  619. display: flex;
  620. align-items: center;
  621. justify-content: center;
  622. }
  623. .dashboard-icon-text {
  624. font-size: 32rpx;
  625. color: #FF6B9D;
  626. }
  627. .dashboard-title {
  628. font-size: 32rpx;
  629. font-weight: 700;
  630. color: #1E293B;
  631. }
  632. .dashboard-menu {
  633. position: relative;
  634. }
  635. .dashboard-menu-text {
  636. font-size: 36rpx;
  637. color: #9CA3AF;
  638. }
  639. .dashboard-menu-dropdown {
  640. position: absolute;
  641. right: 0;
  642. top: 60rpx;
  643. background: #FFFFFF;
  644. border-radius: 12rpx;
  645. box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
  646. padding: 12rpx 0;
  647. min-width: 200rpx;
  648. z-index: 10;
  649. }
  650. .menu-item {
  651. padding: 20rpx 24rpx;
  652. font-size: 28rpx;
  653. color: #374151;
  654. }
  655. .menu-item:active {
  656. background-color: #F9FAFB;
  657. }
  658. /* ===== 数据卡片 ===== */
  659. .dashboard-card {
  660. margin: 20rpx 30rpx;
  661. border-radius: 24rpx;
  662. overflow: hidden;
  663. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
  664. }
  665. .card-header {
  666. padding: 24rpx 30rpx 16rpx;
  667. border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
  668. }
  669. .card-title {
  670. font-size: 28rpx;
  671. font-weight: 600;
  672. color: #1E293B;
  673. }
  674. .card-subtitle {
  675. font-size: 22rpx;
  676. color: #64748B;
  677. margin-top: 4rpx;
  678. }
  679. .card-body {
  680. padding: 24rpx;
  681. }
  682. .card-footer {
  683. background-color: rgba(255, 255, 255, 0.8);
  684. padding: 16rpx 30rpx;
  685. border-top: 1rpx solid rgba(0, 0, 0, 0.05);
  686. }
  687. .footer-tip {
  688. font-size: 24rpx;
  689. color: #6B7280;
  690. }
  691. /* 运势数据项 */
  692. .fortune-data {
  693. display: flex;
  694. flex-direction: row;
  695. flex-wrap: wrap;
  696. margin-bottom: 24rpx;
  697. }
  698. .data-item {
  699. width: 50%;
  700. display: flex;
  701. flex-direction: row;
  702. align-items: center;
  703. margin-bottom: 16rpx;
  704. }
  705. .data-icon {
  706. width: 56rpx;
  707. height: 56rpx;
  708. border-radius: 16rpx;
  709. display: flex;
  710. align-items: center;
  711. justify-content: center;
  712. margin-right: 16rpx;
  713. }
  714. .data-icon-text {
  715. font-size: 28rpx;
  716. }
  717. .data-info {
  718. display: flex;
  719. flex-direction: column;
  720. }
  721. .data-label {
  722. font-size: 22rpx;
  723. color: #64748B;
  724. margin-bottom: 4rpx;
  725. }
  726. .data-value {
  727. font-size: 26rpx;
  728. font-weight: 600;
  729. color: #1E293B;
  730. }
  731. /* 心情部分 */
  732. .mood-section {
  733. margin: 24rpx 0;
  734. }
  735. .mood-emoji {
  736. font-size: 52rpx;
  737. margin-right: 16rpx;
  738. }
  739. .mood-text {
  740. font-size: 28rpx;
  741. font-weight: 600;
  742. color: #1E293B;
  743. vertical-align: bottom;
  744. }
  745. .mood-bar {
  746. margin-top: 16rpx;
  747. height: 12rpx;
  748. background-color: #E5E7EB;
  749. border-radius: 6rpx;
  750. overflow: hidden;
  751. }
  752. .mood-bar-fill {
  753. height: 100%;
  754. background: linear-gradient(90deg, #FF6B9D, #FF8FB1);
  755. border-radius: 6rpx;
  756. transition: width 0.5s;
  757. }
  758. /* 五行罗盘 */
  759. .compass-section {
  760. text-align: center;
  761. margin: 24rpx 0;
  762. }
  763. .dashboard-compass {
  764. width: 300rpx;
  765. height: 300rpx;
  766. margin: 0 auto;
  767. }
  768. /* ===== 数据趋势区块 ===== */
  769. .dashboard-section {
  770. margin: 20rpx 30rpx;
  771. background-color: #FFFFFF;
  772. border-radius: 24rpx;
  773. overflow: hidden;
  774. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
  775. }
  776. .section-header {
  777. padding: 20rpx 30rpx;
  778. border-bottom: 1rpx solid #F3F4F6;
  779. }
  780. .section-title {
  781. font-size: 28rpx;
  782. font-weight: 600;
  783. color: #1E293B;
  784. }
  785. .section-body {
  786. padding: 20rpx 30rpx 30rpx;
  787. }
  788. /* 周趋势图 */
  789. .trend-chart {
  790. height: 400rpx;
  791. }
  792. .trend-canvas {
  793. width: 100%;
  794. height: 400rpx;
  795. }
  796. /* 月趋势 - 日历 */
  797. .trend-calendar {
  798. font-size: 24rpx;
  799. }
  800. .calendar-header {
  801. text-align: center;
  802. margin-bottom: 20rpx;
  803. }
  804. .calendar-month {
  805. font-size: 28rpx;
  806. font-weight: 600;
  807. color: #1E293B;
  808. }
  809. .calendar-grid {
  810. display: grid;
  811. grid-template-columns: repeat(7, 1fr);
  812. gap: 8rpx;
  813. }
  814. .calendar-day {
  815. aspect-ratio: 1;
  816. display: flex;
  817. flex-direction: column;
  818. align-items: center;
  819. justify-content: center;
  820. border-radius: 8rpx;
  821. position: relative;
  822. }
  823. .calendar-day.empty-day {
  824. background-color: transparent;
  825. cursor: default;
  826. }
  827. .calendar-day.has-data {
  828. background-color: #F9FAFB;
  829. }
  830. .calendar-day.current-day {
  831. background-color: #FFF0F3;
  832. border: 1rpx solid #FF6B9D;
  833. }
  834. .day-number {
  835. font-size: 24rpx;
  836. }
  837. .day-fortune {
  838. width: 100%;
  839. height: 36rpx;
  840. display: flex;
  841. align-items: center;
  842. justify-content: center;
  843. }
  844. .fortune-bubble {
  845. width: 16rpx;
  846. height: 16rpx;
  847. border-radius: 50%;
  848. }
  849. .fortune-lv-0, .fortune-lv-1 { background-color: #EF4444; }
  850. .fortune-lv-2 { background-color: #FBBF24; }
  851. .fortune-lv-3, .fortune-lv-4 { background-color: #8B5CF6; }
  852. /* ===== 历史周报 ===== */
  853. .report-list {
  854. display: flex;
  855. flex-direction: column;
  856. gap: 20rpx;
  857. }
  858. .report-item {
  859. display: flex;
  860. flex-direction: row;
  861. align-items: center;
  862. background-color: #F9FAFB;
  863. padding: 20rpx;
  864. border-radius: 16rpx;
  865. }
  866. .report-item:active {
  867. opacity: 0.8;
  868. }
  869. .report-icon {
  870. width: 64rpx;
  871. height: 64rpx;
  872. background-color: #FFF0F3;
  873. border-radius: 16rpx;
  874. margin-right: 16rpx;
  875. display: flex;
  876. align-items: center;
  877. justify-content: center;
  878. font-size: 32rpx;
  879. color: #FF6B9D;
  880. }
  881. .report-info {
  882. flex: 1;
  883. display: flex;
  884. flex-direction: column;
  885. }
  886. .report-title {
  887. font-size: 28rpx;
  888. font-weight: 600;
  889. color: #1E293B;
  890. }
  891. .report-date {
  892. font-size: 22rpx;
  893. color: #64748B;
  894. margin-top: 4rpx;
  895. }
  896. .report-action {
  897. padding: 8rpx 16rpx;
  898. }
  899. .report-download {
  900. font-size: 24rpx;
  901. color: #FF6B9D;
  902. font-weight: 500;
  903. }
  904. /* ===== 周报模态框 ===== */
  905. .modal-overlay {
  906. position: fixed;
  907. top: 0;
  908. left: 0;
  909. right: 0;
  910. bottom: 0;
  911. background-color: rgba(0, 0, 0, 0.5);
  912. z-index: 1000;
  913. display: flex;
  914. align-items: center;
  915. justify-content: center;
  916. }
  917. .weekly-report-modal {
  918. width: 90%;
  919. max-width: 600rpx;
  920. max-height: 80vh;
  921. background: #FFFFFF;
  922. border-radius: 24rpx;
  923. overflow: hidden;
  924. }
  925. .modal-header {
  926. padding: 24rpx 30rpx;
  927. border-bottom: 1rpx solid #F3F4F6;
  928. display: flex;
  929. flex-direction: row;
  930. align-items: center;
  931. justify-content: space-between;
  932. }
  933. .modal-title {
  934. font-size: 28rpx;
  935. font-weight: 600;
  936. color: #1E293B;
  937. }
  938. .modal-close {
  939. font-size: 32rpx;
  940. color: #9CA3AF;
  941. }
  942. .modal-close:active {
  943. opacity: 0.7;
  944. }
  945. .modal-body {
  946. padding: 0;
  947. }
  948. .modal-content {
  949. max-height: 60vh;
  950. padding: 24rpx 30rpx;
  951. }
  952. .modal-footer {
  953. padding: 20rpx 30rpx;
  954. border-top: 1rpx solid #F3F4F6;
  955. text-align: center;
  956. }
  957. .modal-btn {
  958. background-color: #FF6B9D;
  959. color: #FFFFFF;
  960. border: none;
  961. border-radius: 8rpx;
  962. padding: 12rpx 24rpx;
  963. font-size: 26rpx;
  964. margin: 0;
  965. }
  966. </style>