family-dashboard.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  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. /**
  156. * FamilyDashboard — 家庭天盘详情页
  157. * 包含:运势数据、能量趋势、历史周报、月度日历等
  158. */
  159. export default {
  160. name: 'FamilyDashboard',
  161. components: { MpHtml },
  162. data() {
  163. return {
  164. showMenu: false,
  165. viewMode: 'week', // week, month, history
  166. familyFortune: {
  167. dominantElement: '木',
  168. luckyDirection: '东',
  169. fortuneLevel: '平',
  170. fortuneLevelText: '平',
  171. fortuneDescription: '今日宜静心养性,保持平和心态',
  172. moodScore: 65,
  173. moodEmoji: '😊',
  174. moodText: '平静',
  175. dailyTip: '保持心情平静,避免过度思虑',
  176. weeklyTip: '本周建议:聚焦个人成长,适当社交'
  177. },
  178. currentDate: new Date(),
  179. weekFortunes: [],
  180. monthFortunes: [],
  181. weeklyReports: [],
  182. elementColors: {
  183. '木': '#10B981',
  184. '火': '#FF6B9D',
  185. '土': '#FF8C42',
  186. '金': '#6366F1',
  187. '水': '#3B82F6'
  188. },
  189. // 详情模态框
  190. showWeeklyReport: false,
  191. currentWeeklyReport: null,
  192. // Canvas 对象
  193. compassCtx: null,
  194. weekChartCtx: null
  195. }
  196. },
  197. computed: {
  198. cardStyle() {
  199. return {
  200. backgroundColor: this.elementColors[this.familyFortune.dominantElement] + '10'
  201. }
  202. },
  203. currentDateLabel() {
  204. const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
  205. return `${this.familyFortune.luckyDirection}方 · ${week[this.currentDate.getDay()]} ${this.formatDate(this.currentDate)}`
  206. },
  207. currentMonthLabel() {
  208. return `${this.currentDate.getFullYear()}年${this.currentDate.getMonth() + 1}月`
  209. },
  210. monthDays() {
  211. const year = this.currentDate.getFullYear()
  212. const month = this.currentDate.getMonth()
  213. const days = []
  214. // 获取当月第一天是星期几
  215. const firstDay = new Date(year, month, 1).getDay()
  216. // 获取当月总天数
  217. const lastDate = new Date(year, month + 1, 0).getDate()
  218. // 填充上月尾部
  219. for (let i = 0; i < firstDay; i++) {
  220. const emptyDay = { day: '', date: null }
  221. emptyDay.dayClass = this.getDayClass(emptyDay).join(' ')
  222. days.push(emptyDay)
  223. }
  224. // 填充当月
  225. for (let i = 1; i <= lastDate; i++) {
  226. const date = new Date(year, month, i)
  227. const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
  228. const dayData = {
  229. day: i,
  230. date: date,
  231. fortune: null
  232. }
  233. // 查找对应的运势数据
  234. const fortune = this.monthFortunes.find(f =>
  235. f.fortuneDate === dateStr
  236. )
  237. if (fortune) {
  238. dayData.fortune = fortune
  239. }
  240. // 预计算 class 字符串(小程序 :class 不支持方法调用)
  241. dayData.dayClass = this.getDayClass(dayData).join(' ')
  242. days.push(dayData)
  243. }
  244. return days
  245. }
  246. },
  247. onLoad() {
  248. // 初始化页面
  249. this.loadTodayFortune()
  250. this.loadWeekFortuneData()
  251. this.loadMonthlyFortuneData()
  252. this.loadWeeklyReports()
  253. // 初始化 Canvas
  254. this.$nextTick(() => {
  255. this.initCompassCanvas()
  256. this.initWeekChartCanvas()
  257. })
  258. },
  259. methods: {
  260. // 加载今日运势数据
  261. loadTodayFortune() {
  262. // TODO: 对接后端 /api/mind/fortune 接口
  263. // 这里使用默认占位数据
  264. this.familyFortune = {
  265. dominantElement: '火',
  266. luckyDirection: '南',
  267. fortuneLevel: '大吉',
  268. fortuneLevelText: '大吉',
  269. fortuneDescription: '今日热情洋溢,宜沟通表达',
  270. moodScore: 85,
  271. moodEmoji: '😄',
  272. moodText: '喜悦',
  273. dailyTip: '把握今日良机,多与家人互动',
  274. weeklyTip: '本周建议:多陪伴家人,适度运动'
  275. }
  276. },
  277. // 加载本周运势数据
  278. loadWeekFortuneData() {
  279. // TODO: 对接后端 /api/mind/fortune/week 接口
  280. const mockData = []
  281. const today = new Date()
  282. for (let i = 0; i < 7; i++) {
  283. const date = new Date(today)
  284. date.setDate(date.getDate() - (today.getDay() - i))
  285. const levels = ['大吉', '吉', '平', '凶', '大凶']
  286. const levelIndex = Math.floor(Math.random() * 5)
  287. const elements = ['木', '火', '土', '金', '水']
  288. mockData.push({
  289. date: this.formatDate(date),
  290. fortuneLevel: levels[levelIndex],
  291. fortuneLevelText: levels[levelIndex],
  292. fortuneScore: 50 + (levelIndex < 2 ? (3 - levelIndex) * 15 : (levelIndex > 2 ? (levelIndex - 2) * -20 : 0)),
  293. dominantElement: elements[Math.floor(Math.random() * 5)]
  294. })
  295. }
  296. this.weekFortunes = mockData
  297. },
  298. // 加载本月运势数据
  299. loadMonthlyFortuneData() {
  300. // TODO: 对接后端 /api/mind/fortune/month 接口
  301. const mockData = []
  302. const today = new Date()
  303. const daysInMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate()
  304. for (let i = 1; i <= daysInMonth; i++) {
  305. const date = new Date(today.getFullYear(), today.getMonth(), i)
  306. const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`
  307. const levels = ['大吉', '吉', '平', '凶', '大凶']
  308. const levelIndex = Math.floor(Math.random() * 5)
  309. mockData.push({
  310. fortuneDate: dateStr,
  311. fortuneLevel: levels[levelIndex],
  312. fortuneLevelText: levels[levelIndex],
  313. luckyDirection: ['东', '南', '西', '北', '中'][Math.floor(Math.random() * 5)] + '方',
  314. dominantElement: ['木', '火', '土', '金', '水'][Math.floor(Math.random() * 5)]
  315. })
  316. }
  317. this.monthFortunes = mockData
  318. },
  319. // 加载历史周报
  320. loadWeeklyReports() {
  321. // TODO: 对接后端 /api/mind/fortune/reports 接口
  322. const mockReports = []
  323. for (let i = 1; i <= 8; i++) {
  324. mockReports.push({
  325. weekNumber: i,
  326. startDate: `2026-06-${String(28 - i * 7).padStart(2, '0')}`,
  327. endDate: `2026-07-${String(4 - i).padStart(2, '0')}`,
  328. content: this.generateMockReportContent(i)
  329. })
  330. }
  331. this.weeklyReports = mockReports
  332. },
  333. // 生成模拟周报内容
  334. generateMockReportContent(weekNumber) {
  335. const elements = ['木', '火', '土', '金', '水']
  336. const element = elements[weekNumber % 5]
  337. return `<div style="font-family: sans-serif; line-height: 1.6; color: #333;">
  338. <h2 style="color: #FF6B9D; text-align: center;">家庭运势周报 - 第${weekNumber}周</h2>
  339. <p style="text-indent: 2em;">本周主导五行:<span style="color: ${this.elementColors[element]};">${element}</span>,
  340. 意味着家庭整体运势偏向${{ 木: '成长与和谐', 火: '热情与活力', 土: '稳定与健康', 金: '收获与进步', 水: '智慧与灵活' }[element]}。</p>
  341. <h3 style="color: #666;">运势概况</h3>
  342. <p style="text-indent: 2em;">本周吉位平均位于 <strong>${['东', '南', '西', '北'][weekNumber % 4]}方</strong>,建议多在此方位进行重要活动。
  343. 运势整体呈现先抑后扬之势,周二、周四较为低迷,周五后逐步好转。</p>
  344. <h3 style="color: #666;">心理状态</h3>
  345. <p style="text-indent: 2em;">家庭成员情绪指数平均${70 + weekNumber}分,整体状态${weekNumber > 4 ? '良好' : '一般'}。
  346. ${weekNumber > 4 ? '建议保持现有和谐氛围' : '需注意情绪调节,避免矛盾升级'}。</p>
  347. <h3 style="color: #666;">宜忌提示</h3>
  348. <p style="text-indent: 2em;"><strong>宜:</strong>${{ 木: '植树种花、交友聚会', 火: '聚餐联谊、旅行观光', 土: '健康管理、家居整理', 金: '投资理财、学习进修', 水: '阅读思考、社交活动' }[element]}</p>
  349. <p style="text-indent: 2em;margin-top: -10px;"><strong>忌:</strong>${{ 木: '冲动举动、过度开支', 火: '过度消耗、睡眠不足', 土: '情绪压抑、忽视健康', 金: '过于拘谨、忽视情感', 水: '沉迷游戏、忽略家人' }[element]}</p>
  350. </div>`
  351. },
  352. // 初始化罗盘 Canvas
  353. initCompassCanvas() {
  354. const query = uni.createSelectorQuery().in(this)
  355. query.select('#dashboardCompass')
  356. .fields({ node: true, size: true })
  357. .exec((res) => {
  358. if (!res || !res[0] || !res[0].node) return
  359. const canvas = res[0].node
  360. const dpr = uni.getWindowInfo().pixelRatio || 2
  361. const w = res[0].width
  362. const h = res[0].height
  363. canvas.width = w * dpr
  364. canvas.height = h * dpr
  365. const ctx = canvas.getContext('2d')
  366. ctx.scale(dpr, dpr)
  367. this.compassCtx = ctx
  368. this.drawCompass(ctx, w, h)
  369. })
  370. },
  371. // 绘制罗盘
  372. drawCompass(ctx, w, h) {
  373. const cx = w / 2
  374. const cy = h / 2
  375. const outerR = Math.min(cx, cy) - 4
  376. // 清空
  377. ctx.clearRect(0, 0, w, h)
  378. // 五行方位
  379. const directions = [
  380. { label: '北', element: '水', angle: -Math.PI / 2 },
  381. { label: '东', element: '木', angle: 0 },
  382. { label: '南', element: '火', angle: Math.PI / 2 },
  383. { label: '西', element: '金', angle: Math.PI },
  384. { label: '中', element: '土', angle: null }
  385. ]
  386. // 吉位方向
  387. const luckyDir = this.familyFortune.luckyDirection
  388. const luckyColor = this.elementColors[this.familyFortune.dominantElement]
  389. // 外圈背景
  390. ctx.beginPath()
  391. ctx.arc(cx, cy, outerR, 0, Math.PI * 2)
  392. ctx.fillStyle = '#FFF5F7'
  393. ctx.fill()
  394. ctx.strokeStyle = luckyColor + '40'
  395. ctx.lineWidth = 1.5
  396. ctx.stroke()
  397. // 绘制四个方位
  398. for (let i = 0; i < 4; i++) {
  399. const d = directions[i]
  400. const color = this.elementColors[d.element]
  401. // 扇区线
  402. ctx.beginPath()
  403. ctx.moveTo(cx, cy)
  404. const ex = cx + outerR * Math.cos(d.angle)
  405. const ey = cy + outerR * Math.sin(d.angle)
  406. ctx.lineTo(ex, ey)
  407. ctx.strokeStyle = color + '30'
  408. ctx.lineWidth = 1
  409. ctx.stroke()
  410. // 方位标签
  411. const labelR = outerR * 0.7
  412. const lx = cx + labelR * Math.cos(d.angle)
  413. const ly = cy + labelR * Math.sin(d.angle)
  414. const isLucky = d.label === luckyDir
  415. if (isLucky) {
  416. // 吉位高亮圆
  417. ctx.beginPath()
  418. ctx.arc(lx, ly, 12, 0, Math.PI * 2)
  419. ctx.fillStyle = luckyColor + '20'
  420. ctx.fill()
  421. ctx.strokeStyle = luckyColor
  422. ctx.lineWidth = 1.5
  423. ctx.stroke()
  424. }
  425. ctx.fillStyle = isLucky ? luckyColor : '#9CA3AF'
  426. ctx.font = (isLucky ? 'bold ' : '') + '12px sans-serif'
  427. ctx.textAlign = 'center'
  428. ctx.textBaseline = 'middle'
  429. ctx.fillText(d.label, lx, ly)
  430. }
  431. // 中心点
  432. ctx.beginPath()
  433. ctx.arc(cx, cy, 5, 0, Math.PI * 2)
  434. ctx.fillStyle = luckyColor
  435. ctx.fill()
  436. ctx.strokeStyle = '#FFFFFF'
  437. ctx.lineWidth = 2
  438. ctx.stroke()
  439. },
  440. // 初始化周趋势图 Canvas
  441. initWeekChartCanvas() {
  442. const query = uni.createSelectorQuery().in(this)
  443. query.select('#weekTrendChart')
  444. .fields({ node: true, size: true })
  445. .exec((res) => {
  446. if (!res || !res[0] || !res[0].node) return
  447. const canvas = res[0].node
  448. const dpr = uni.getWindowInfo().pixelRatio || 2
  449. const w = res[0].width
  450. const h = res[0].height
  451. canvas.width = w * dpr
  452. canvas.height = h * dpr
  453. const ctx = canvas.getContext('2d')
  454. ctx.scale(dpr, dpr)
  455. this.weekChartCtx = ctx
  456. this.drawWeekTrendChart(ctx, w, h)
  457. })
  458. },
  459. // 绘制周趋势图
  460. drawWeekTrendChart(ctx, w, h) {
  461. const padding = 40
  462. const chartW = w - padding * 2
  463. const chartH = h - padding * 2
  464. // 清空
  465. ctx.clearRect(0, 0, w, h)
  466. // 背景
  467. ctx.fillStyle = '#FFFFFF'
  468. ctx.fillRect(padding, padding, chartW, chartH)
  469. ctx.strokeStyle = '#F3F4F6'
  470. ctx.lineWidth = 1
  471. ctx.strokeRect(padding, padding, chartW, chartH)
  472. if (this.weekFortunes.length === 0) return
  473. // 坐标系
  474. const maxScore = 100
  475. const minScore = 0
  476. const step = 20
  477. const scoreSteps = Math.ceil((maxScore - minScore) / step)
  478. // Y 轴刻度
  479. ctx.fillStyle = '#6B7280'
  480. ctx.font = '10px sans-serif'
  481. ctx.textAlign = 'right'
  482. ctx.textBaseline = 'middle'
  483. for (let i = 0; i <= scoreSteps; i++) {
  484. const y = padding + chartH - (i * chartH / scoreSteps)
  485. const score = minScore + i * step
  486. ctx.fillText(score, padding - 10, y)
  487. ctx.beginPath()
  488. ctx.moveTo(padding - 5, y)
  489. ctx.lineTo(padding, y)
  490. ctx.strokeStyle = '#E5E7EB'
  491. ctx.stroke()
  492. }
  493. // X 轴刻度
  494. const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
  495. const labels = this.weekFortunes.map((f, i) => weekDays[i] || '')
  496. ctx.fillStyle = '#6B7280'
  497. ctx.font = '10px sans-serif'
  498. ctx.textAlign = 'center'
  499. ctx.textBaseline = 'top'
  500. for (let i = 0; i < labels.length; i++) {
  501. const x = padding + i * (chartW / (labels.length - 1))
  502. ctx.fillText(labels[i], x, padding + chartH + 10)
  503. ctx.beginPath()
  504. ctx.moveTo(x, padding + chartH)
  505. ctx.lineTo(x, padding + chartH + 5)
  506. ctx.strokeStyle = '#E5E7EB'
  507. ctx.stroke()
  508. }
  509. // 绘制折线
  510. ctx.beginPath()
  511. for (let i = 0; i < this.weekFortunes.length; i++) {
  512. const f = this.weekFortunes[i]
  513. const x = padding + i * (chartW / (this.weekFortunes.length - 1))
  514. const y = padding + chartH - (f.fortuneScore - minScore) * chartH / (maxScore - minScore)
  515. if (i === 0) {
  516. ctx.moveTo(x, y)
  517. } else {
  518. ctx.lineTo(x, y)
  519. }
  520. // 数据点
  521. ctx.beginPath()
  522. ctx.arc(x, y, 4, 0, Math.PI * 2)
  523. ctx.fillStyle = this.getFortuneColor(f.fortuneLevel)
  524. ctx.fill()
  525. ctx.strokeStyle = '#FFFFFF'
  526. ctx.lineWidth = 2
  527. ctx.stroke()
  528. // 恢复路径
  529. ctx.beginPath()
  530. ctx.moveTo(x, y)
  531. if (i < this.weekFortunes.length - 1) {
  532. const nextF = this.weekFortunes[i + 1]
  533. const nextX = padding + (i + 1) * (chartW / (this.weekFortunes.length - 1))
  534. const nextY = padding + chartH - (nextF.fortuneScore - minScore) * chartH / (maxScore - minScore)
  535. ctx.lineTo(nextX, nextY)
  536. }
  537. }
  538. ctx.strokeStyle = this.elementColors[this.familyFortune.dominantElement]
  539. ctx.lineWidth = 2
  540. ctx.stroke()
  541. },
  542. // 格式化日期
  543. formatDate(date) {
  544. if (!date) return ''
  545. const d = parseDate(date)
  546. if (!d) return ''
  547. return `${d.getMonth() + 1}月${d.getDate()}日`
  548. },
  549. // 菜单点击处理
  550. onMenuItemClick(mode) {
  551. this.showMenu = false
  552. if (mode === 'export') {
  553. uni.showToast({ title: '周报生成中...', icon: 'loading' })
  554. this.exportWeeklyReport()
  555. return
  556. }
  557. this.viewMode = mode
  558. },
  559. // 返回上一页
  560. goBack() {
  561. uni.navigateBack()
  562. },
  563. // 获取五行名称
  564. getElementName(element) {
  565. const names = { 木: '木', 火: '火', 土: '土', 金: '金', 水: '水' }
  566. return names[element] || element
  567. },
  568. // 获取运势颜色
  569. getFortuneColor(level) {
  570. const colors = {
  571. '大吉': '#EF4444',
  572. '吉': '#F97316',
  573. '平': '#FBBF24',
  574. '凶': '#6366F1',
  575. '大凶': '#8B5CF6'
  576. }
  577. return colors[level] || '#666666'
  578. },
  579. // 获取运势等级数值(用于CSS class)
  580. getFortuneLevel(dayFortune) {
  581. if (!dayFortune) return 2
  582. var levelMap = { '大吉': 0, '吉': 1, '平': 2, '凶': 3, '大凶': 4 }
  583. var level = dayFortune.fortuneLevel || dayFortune.fortuneLevelText || ''
  584. return levelMap[level] !== undefined ? levelMap[level] : 2
  585. },
  586. // 获取日历日样式
  587. getDayClass(day) {
  588. const classes = []
  589. if (!day.date) {
  590. classes.push('empty-day')
  591. return classes
  592. }
  593. if (day.day === new Date().getDate() &&
  594. day.date.getMonth() === new Date().getMonth() &&
  595. day.date.getFullYear() === new Date().getFullYear()) {
  596. classes.push('current-day')
  597. }
  598. if (day.fortune) {
  599. classes.push('has-data')
  600. classes.push('fortune-lv-' + this.getFortuneLevel(day.fortune))
  601. }
  602. return classes
  603. },
  604. // 查看周报
  605. viewWeeklyReport(report) {
  606. this.currentWeeklyReport = report
  607. this.showWeeklyReport = true
  608. },
  609. // 下载周报
  610. downloadReport(report) {
  611. uni.showToast({ title: '周报生成中...', icon: 'loading' })
  612. // TODO: 对接后端生成PDF接口
  613. setTimeout(() => {
  614. uni.showToast({ title: '周报已生成,请在文件管理中查看', icon: 'none' })
  615. }, 2000)
  616. },
  617. // 导出本周周报
  618. exportWeeklyReport() {
  619. uni.showToast({ title: '正在生成本周周报', icon: 'loading' })
  620. // TODO: 将画布数据发送到后端生成PDF
  621. setTimeout(() => {
  622. uni.showToast({ title: '周报已生成,请在历史周报中下载', icon: 'none' })
  623. }, 3000)
  624. }
  625. }
  626. }
  627. </script>
  628. <style scoped>
  629. .family-dashboard {
  630. background-color: #F9FAFB;
  631. min-height: 100vh;
  632. padding-bottom: 40rpx;
  633. }
  634. /* ===== 头部导航 ===== */
  635. .dashboard-header {
  636. position: sticky;
  637. top: 0;
  638. z-index: 100;
  639. background-color: #FFFFFF;
  640. display: flex;
  641. flex-direction: row;
  642. align-items: center;
  643. justify-content: space-between;
  644. padding: 30rpx 30rpx 16rpx;
  645. border-bottom: 1rpx solid #F3F4F6;
  646. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
  647. }
  648. .dashboard-icon {
  649. width: 64rpx;
  650. height: 64rpx;
  651. border-radius: 32rpx;
  652. background-color: #FFF0F3;
  653. display: flex;
  654. align-items: center;
  655. justify-content: center;
  656. }
  657. .dashboard-icon-text {
  658. font-size: 32rpx;
  659. color: #FF6B9D;
  660. }
  661. .dashboard-title {
  662. font-size: 32rpx;
  663. font-weight: 700;
  664. color: #1E293B;
  665. }
  666. .dashboard-menu {
  667. position: relative;
  668. }
  669. .dashboard-menu-text {
  670. font-size: 36rpx;
  671. color: #9CA3AF;
  672. }
  673. .dashboard-menu-dropdown {
  674. position: absolute;
  675. right: 0;
  676. top: 60rpx;
  677. background: #FFFFFF;
  678. border-radius: 12rpx;
  679. box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
  680. padding: 12rpx 0;
  681. min-width: 200rpx;
  682. z-index: 10;
  683. }
  684. .menu-item {
  685. padding: 20rpx 24rpx;
  686. font-size: 28rpx;
  687. color: #374151;
  688. }
  689. .menu-item:active {
  690. background-color: #F9FAFB;
  691. }
  692. /* ===== 数据卡片 ===== */
  693. .dashboard-card {
  694. margin: 20rpx 30rpx;
  695. border-radius: 24rpx;
  696. overflow: hidden;
  697. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
  698. }
  699. .card-header {
  700. padding: 24rpx 30rpx 16rpx;
  701. border-bottom: 1rpx solid rgba(255, 255, 255, 0.2);
  702. }
  703. .card-title {
  704. font-size: 28rpx;
  705. font-weight: 600;
  706. color: #1E293B;
  707. }
  708. .card-subtitle {
  709. font-size: 22rpx;
  710. color: #64748B;
  711. margin-top: 4rpx;
  712. }
  713. .card-body {
  714. padding: 24rpx;
  715. }
  716. .card-footer {
  717. background-color: rgba(255, 255, 255, 0.8);
  718. padding: 16rpx 30rpx;
  719. border-top: 1rpx solid rgba(0, 0, 0, 0.05);
  720. }
  721. .footer-tip {
  722. font-size: 24rpx;
  723. color: #6B7280;
  724. }
  725. /* 运势数据项 */
  726. .fortune-data {
  727. display: flex;
  728. flex-direction: row;
  729. flex-wrap: wrap;
  730. margin-bottom: 24rpx;
  731. }
  732. .data-item {
  733. width: 50%;
  734. display: flex;
  735. flex-direction: row;
  736. align-items: center;
  737. margin-bottom: 16rpx;
  738. }
  739. .data-icon {
  740. width: 56rpx;
  741. height: 56rpx;
  742. border-radius: 16rpx;
  743. display: flex;
  744. align-items: center;
  745. justify-content: center;
  746. margin-right: 16rpx;
  747. }
  748. .data-icon-text {
  749. font-size: 28rpx;
  750. }
  751. .data-info {
  752. display: flex;
  753. flex-direction: column;
  754. }
  755. .data-label {
  756. font-size: 22rpx;
  757. color: #64748B;
  758. margin-bottom: 4rpx;
  759. }
  760. .data-value {
  761. font-size: 26rpx;
  762. font-weight: 600;
  763. color: #1E293B;
  764. }
  765. /* 心情部分 */
  766. .mood-section {
  767. margin: 24rpx 0;
  768. }
  769. .mood-emoji {
  770. font-size: 52rpx;
  771. margin-right: 16rpx;
  772. }
  773. .mood-text {
  774. font-size: 28rpx;
  775. font-weight: 600;
  776. color: #1E293B;
  777. vertical-align: bottom;
  778. }
  779. .mood-bar {
  780. margin-top: 16rpx;
  781. height: 12rpx;
  782. background-color: #E5E7EB;
  783. border-radius: 6rpx;
  784. overflow: hidden;
  785. }
  786. .mood-bar-fill {
  787. height: 100%;
  788. background: linear-gradient(90deg, #FF6B9D, #FF8FB1);
  789. border-radius: 6rpx;
  790. transition: width 0.5s;
  791. }
  792. /* 五行罗盘 */
  793. .compass-section {
  794. text-align: center;
  795. margin: 24rpx 0;
  796. }
  797. .dashboard-compass {
  798. width: 300rpx;
  799. height: 300rpx;
  800. margin: 0 auto;
  801. }
  802. /* ===== 数据趋势区块 ===== */
  803. .dashboard-section {
  804. margin: 20rpx 30rpx;
  805. background-color: #FFFFFF;
  806. border-radius: 24rpx;
  807. overflow: hidden;
  808. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
  809. }
  810. .section-header {
  811. padding: 20rpx 30rpx;
  812. border-bottom: 1rpx solid #F3F4F6;
  813. }
  814. .section-title {
  815. font-size: 28rpx;
  816. font-weight: 600;
  817. color: #1E293B;
  818. }
  819. .section-body {
  820. padding: 20rpx 30rpx 30rpx;
  821. }
  822. /* 周趋势图 */
  823. .trend-chart {
  824. height: 400rpx;
  825. }
  826. .trend-canvas {
  827. width: 100%;
  828. height: 400rpx;
  829. }
  830. /* 月趋势 - 日历 */
  831. .trend-calendar {
  832. font-size: 24rpx;
  833. }
  834. .calendar-header {
  835. text-align: center;
  836. margin-bottom: 20rpx;
  837. }
  838. .calendar-month {
  839. font-size: 28rpx;
  840. font-weight: 600;
  841. color: #1E293B;
  842. }
  843. .calendar-grid {
  844. display: grid;
  845. grid-template-columns: repeat(7, 1fr);
  846. gap: 8rpx;
  847. }
  848. .calendar-day {
  849. aspect-ratio: 1;
  850. display: flex;
  851. flex-direction: column;
  852. align-items: center;
  853. justify-content: center;
  854. border-radius: 8rpx;
  855. position: relative;
  856. }
  857. .calendar-day.empty-day {
  858. background-color: transparent;
  859. cursor: default;
  860. }
  861. .calendar-day.has-data {
  862. background-color: #F9FAFB;
  863. }
  864. .calendar-day.current-day {
  865. background-color: #FFF0F3;
  866. border: 1rpx solid #FF6B9D;
  867. }
  868. .day-number {
  869. font-size: 24rpx;
  870. }
  871. .day-fortune {
  872. width: 100%;
  873. height: 36rpx;
  874. display: flex;
  875. align-items: center;
  876. justify-content: center;
  877. }
  878. .fortune-bubble {
  879. width: 16rpx;
  880. height: 16rpx;
  881. border-radius: 50%;
  882. }
  883. .fortune-lv-0, .fortune-lv-1 { background-color: #EF4444; }
  884. .fortune-lv-2 { background-color: #FBBF24; }
  885. .fortune-lv-3, .fortune-lv-4 { background-color: #8B5CF6; }
  886. /* ===== 历史周报 ===== */
  887. .report-list {
  888. display: flex;
  889. flex-direction: column;
  890. gap: 20rpx;
  891. }
  892. .report-item {
  893. display: flex;
  894. flex-direction: row;
  895. align-items: center;
  896. background-color: #F9FAFB;
  897. padding: 20rpx;
  898. border-radius: 16rpx;
  899. }
  900. .report-item:active {
  901. opacity: 0.8;
  902. }
  903. .report-icon {
  904. width: 64rpx;
  905. height: 64rpx;
  906. background-color: #FFF0F3;
  907. border-radius: 16rpx;
  908. margin-right: 16rpx;
  909. display: flex;
  910. align-items: center;
  911. justify-content: center;
  912. font-size: 32rpx;
  913. color: #FF6B9D;
  914. }
  915. .report-info {
  916. flex: 1;
  917. display: flex;
  918. flex-direction: column;
  919. }
  920. .report-title {
  921. font-size: 28rpx;
  922. font-weight: 600;
  923. color: #1E293B;
  924. }
  925. .report-date {
  926. font-size: 22rpx;
  927. color: #64748B;
  928. margin-top: 4rpx;
  929. }
  930. .report-action {
  931. padding: 8rpx 16rpx;
  932. }
  933. .report-download {
  934. font-size: 24rpx;
  935. color: #FF6B9D;
  936. font-weight: 500;
  937. }
  938. /* ===== 周报模态框 ===== */
  939. .modal-overlay {
  940. position: fixed;
  941. top: 0;
  942. left: 0;
  943. right: 0;
  944. bottom: 0;
  945. background-color: rgba(0, 0, 0, 0.5);
  946. z-index: 1000;
  947. display: flex;
  948. align-items: center;
  949. justify-content: center;
  950. }
  951. .weekly-report-modal {
  952. width: 90%;
  953. max-width: 600rpx;
  954. max-height: 80vh;
  955. background: #FFFFFF;
  956. border-radius: 24rpx;
  957. overflow: hidden;
  958. }
  959. .modal-header {
  960. padding: 24rpx 30rpx;
  961. border-bottom: 1rpx solid #F3F4F6;
  962. display: flex;
  963. flex-direction: row;
  964. align-items: center;
  965. justify-content: space-between;
  966. }
  967. .modal-title {
  968. font-size: 28rpx;
  969. font-weight: 600;
  970. color: #1E293B;
  971. }
  972. .modal-close {
  973. font-size: 32rpx;
  974. color: #9CA3AF;
  975. }
  976. .modal-close:active {
  977. opacity: 0.7;
  978. }
  979. .modal-body {
  980. padding: 0;
  981. }
  982. .modal-content {
  983. max-height: 60vh;
  984. padding: 24rpx 30rpx;
  985. }
  986. .modal-footer {
  987. padding: 20rpx 30rpx;
  988. border-top: 1rpx solid #F3F4F6;
  989. text-align: center;
  990. }
  991. .modal-btn {
  992. background-color: #FF6B9D;
  993. color: #FFFFFF;
  994. border: none;
  995. border-radius: 8rpx;
  996. padding: 12rpx 24rpx;
  997. font-size: 26rpx;
  998. margin: 0;
  999. }
  1000. </style>