records.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <template>
  2. <view class="page" :style="{ '--font-scale': fontScale }">
  3. <view class="member-bar" v-if="memberList.length > 1">
  4. <view class="member-tab" :class="{'active': currentMemberId === (item.id || item.userId)}"
  5. v-for="(item, mi) in memberList" :key="mi"
  6. @tap="switchMember(item)">
  7. <text class="member-name">{{ item.nickname || item.roleLabel || '全部' }}</text>
  8. </view>
  9. </view>
  10. <!-- 周历 -->
  11. <view class="week-strip">
  12. <view class="week-day" v-for="(d, i) in weekDays" :key="d.date"
  13. :class="{'week-day-today': d.today, 'week-day-active': d.date === selectedDate, 'week-day-has': d.hasRecords}">
  14. <text class="week-day-label">{{ d.label }}</text>
  15. <text class="week-day-num">{{ d.num }}</text>
  16. <view class="week-dot-wrap" v-if="d.hasRecords">
  17. <view class="week-dot"></view>
  18. <text class="week-day-count">{{ d.count }}</text>
  19. </view>
  20. </view>
  21. </view>
  22. <scroll-view class="content" scroll-y>
  23. <!-- 食材摄入提示 -->
  24. <view class="alert-card" v-if="alertData.overConsumed.length > 0 || alertData.underConsumed.length > 0">
  25. <view class="alert-header">
  26. <text class="alert-icon">⚠️</text>
  27. <text class="alert-title">饮食提示</text>
  28. </view>
  29. <view class="alert-section" v-if="alertData.overConsumed.length > 0">
  30. <text class="alert-label over">摄入偏多 / 需戒备</text>
  31. <view class="alert-tags">
  32. <view class="alert-tag tag-over" v-for="(item, i) in alertData.overConsumed" :key="i">
  33. <text>{{ item.foodName }}</text>
  34. <text class="tag-count">{{ item.count }}次</text>
  35. </view>
  36. </view>
  37. <text class="alert-hint">{{ alertData.overConsumed[0] && alertData.overConsumed[0].reason || '' }}</text>
  38. </view>
  39. <view class="alert-section" v-if="alertData.underConsumed.length > 0">
  40. <text class="alert-label under">摄入偏少</text>
  41. <view class="alert-tags">
  42. <view class="alert-tag tag-under" v-for="(item, i) in alertData.underConsumed" :key="i">
  43. <text>{{ item.foodName }}</text>
  44. <text class="tag-count">{{ item.count }}次</text>
  45. </view>
  46. </view>
  47. </view>
  48. </view>
  49. <!-- 本周营养汇总 -->
  50. <view class="card" v-if="weeklySummary.recordCount > 0">
  51. <view class="card-accent"></view>
  52. <view class="card-body">
  53. <text class="card-title">本周饮食汇总({{ weeklySummary.startDate }} ~ {{ weeklySummary.endDate }})</text>
  54. <view class="stat-row">
  55. <view class="stat-item">
  56. <text class="stat-val">{{ weeklySummary.totalCalories || 0 }}</text>
  57. <text class="stat-unit">kcal</text>
  58. <text class="stat-label">总热量</text>
  59. </view>
  60. <view class="stat-item">
  61. <text class="stat-val">{{ weeklySummary.activeDays || 0 }}</text>
  62. <text class="stat-unit">天</text>
  63. <text class="stat-label">打卡天数</text>
  64. </view>
  65. <view class="stat-item">
  66. <text class="stat-val">{{ weeklySummary.totalProtein || 0 }}</text>
  67. <text class="stat-unit">g</text>
  68. <text class="stat-label">蛋白质</text>
  69. </view>
  70. <view class="stat-item">
  71. <text class="stat-val">{{ weeklySummary.totalFat || 0 }}</text>
  72. <text class="stat-unit">g</text>
  73. <text class="stat-label">脂肪</text>
  74. </view>
  75. </view>
  76. </view>
  77. </view>
  78. <!-- 高频食材 -->
  79. <view class="card" v-if="weeklySummary.topFoods.length > 0">
  80. <view class="card-accent" style="background: linear-gradient(180deg, #FF8C42, #FFB366);"></view>
  81. <view class="card-body">
  82. <text class="card-title">本周高频食材(前10)</text>
  83. <view class="top-foods-list">
  84. <view class="top-food-item" v-for="(f, i) in weeklySummary.topFoods" :key="f.foodName">
  85. <text class="top-food-rank">{{ i + 1 }}</text>
  86. <text class="top-food-name">{{ f.foodName }}</text>
  87. <text class="top-food-count">{{ f.count }}次</text>
  88. </view>
  89. </view>
  90. </view>
  91. </view>
  92. <!-- 日期切换 -->
  93. <view class="date-bar">
  94. <view class="date-prev" @tap="prevDay">‹ 前一天</view>
  95. <text class="date-display">{{ selectedDate }}</text>
  96. <view class="date-next" @tap="nextDay">下一天 ›</view>
  97. </view>
  98. <!-- 当日统计 -->
  99. <view class="card daily-summary-card" v-if="dailyStats.totalCalories > 0 || dailyStats.totalProtein > 0">
  100. <view class="card-accent"></view>
  101. <view class="card-body">
  102. <text class="card-title">{{ selectedDate }} 当日统计</text>
  103. <view class="stat-row">
  104. <view class="stat-item">
  105. <text class="stat-val">{{ dailyStats.totalCalories || 0 }}</text>
  106. <text class="stat-unit">kcal</text>
  107. <text class="stat-label">热量</text>
  108. </view>
  109. <view class="stat-item">
  110. <text class="stat-val">{{ dailyStats.totalProtein || 0 }}</text>
  111. <text class="stat-unit">g</text>
  112. <text class="stat-label">蛋白</text>
  113. </view>
  114. <view class="stat-item">
  115. <text class="stat-val">{{ dailyStats.totalFat || 0 }}</text>
  116. <text class="stat-unit">g</text>
  117. <text class="stat-label">脂肪</text>
  118. </view>
  119. <view class="stat-item">
  120. <text class="stat-val">{{ dailyStats.totalCarbs || 0 }}</text>
  121. <text class="stat-unit">g</text>
  122. <text class="stat-label">碳水</text>
  123. </view>
  124. </view>
  125. </view>
  126. </view>
  127. <!-- 记录列表(按日期分组) -->
  128. <view class="records-section" v-if="records.length > 0">
  129. <view class="records-title">
  130. <text class="records-label">{{ selectedDate }} 饮食记录</text>
  131. <text class="records-count">{{ records.length }} 条</text>
  132. </view>
  133. <view class="record-card" v-for="r in records" :key="r.id" @tap="goDetail(r)">
  134. <view class="record-card-header">
  135. <view class="meal-badge" :class="'meal-' + r.mealType">
  136. <text>{{ mealLabel(r.mealType) }}</text>
  137. </view>
  138. <view class="record-meta">
  139. <text class="record-time">{{ formatTime(r.createdAt) }}</text>
  140. <text class="record-backfill" v-if="r.isBackfill">补打</text>
  141. </view>
  142. </view>
  143. <view class="record-body">
  144. <text class="record-foods">{{ formatFoodItems(r.foodItems) }}</text>
  145. </view>
  146. <view class="record-footer" v-if="r.totalCalories > 0 || r.proteinG > 0">
  147. <text class="record-cal">🔥 {{ r.totalCalories }}kcal</text>
  148. <text class="record-nut" v-if="r.proteinG > 0">蛋白 {{ r.proteinG }}g</text>
  149. </view>
  150. <view class="record-remark" v-if="r.remark">{{ r.remark }}</view>
  151. </view>
  152. </view>
  153. <view class="empty-state" v-else-if="!loading">
  154. <text class="empty-icon">🍽️</text>
  155. <text class="empty-text">当天暂无饮食记录</text>
  156. <text class="empty-hint">点击右下角「+」打卡记录</text>
  157. </view>
  158. <view class="loading-state" v-if="loading">
  159. <text class="loading-text">加载中...</text>
  160. </view>
  161. <view class="bottom-spacer"></view>
  162. </scroll-view>
  163. <view class="fab" @tap="addRecord">
  164. <text class="fab-icon">+</text>
  165. </view>
  166. </view>
  167. </template>
  168. <script>
  169. import { getMealList, getMealDaily, getMealWeekly, getMealAlerts, getCautionFamilyMembers, createMealRecord } from '../../utils/api.js'
  170. import { parseDate } from '../../utils/format.js'
  171. import shareMixin from '../../components/share-mixin.js'
  172. import fontScaleMixin from '../../mixins/font-scale.js'
  173. export default {
  174. mixins: [shareMixin, fontScaleMixin],
  175. data() {
  176. return {
  177. memberId: null,
  178. currentMemberId: null,
  179. memberList: [],
  180. selectedDate: this.formatDate(new Date()),
  181. weekDays: [],
  182. records: [],
  183. dailyStats: {},
  184. weeklySummary: {},
  185. alertData: { overConsumed: [], underConsumed: [] },
  186. loading: false,
  187. _lastLoadDate: null
  188. }
  189. },
  190. onLoad: function(options) {
  191. this.memberId = options.memberId ? parseInt(options.memberId) : null
  192. this.currentMemberId = this.memberId
  193. this.loadMemberList()
  194. this.initWeekDays()
  195. this.loadWeekly()
  196. this.loadAlerts()
  197. this.loadDayRecords()
  198. this.setShareInfo('孩子的每日饮食打卡记录', '/pages/diet/records')
  199. },
  200. methods: {
  201. goBack: function() { uni.navigateBack() },
  202. mealLabel: function(t) {
  203. var m = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' }
  204. return m[t] || t || '饮食'
  205. },
  206. switchMember: function(item) {
  207. this.currentMemberId = item.id || item.userId
  208. this.loadWeekly()
  209. this.loadAlerts()
  210. this.loadDayRecords()
  211. },
  212. async loadMemberList() {
  213. try {
  214. var res = await getCautionFamilyMembers()
  215. if (res && res.code === 200 && res.data && res.data.length > 0) {
  216. var allItem = { id: -1, userId: -1, nickname: '全部', roleLabel: '全部' }
  217. this.memberList = [allItem].concat(res.data)
  218. if (!this.currentMemberId) this.currentMemberId = -1
  219. }
  220. } catch(e) {}
  221. },
  222. initWeekDays: function() {
  223. var self = this
  224. var today = new Date()
  225. var dayOfWeek = today.getDay() || 7 // 1=周一 7=周日
  226. var monday = new Date(today)
  227. monday.setDate(today.getDate() - (dayOfWeek - 1))
  228. var days = []
  229. var labels = ['一', '二', '三', '四', '五', '六', '日']
  230. for (var i = 0; i < 7; i++) {
  231. var d = new Date(monday)
  232. d.setDate(monday.getDate() + i)
  233. days.push({
  234. date: self.formatDate(d),
  235. label: '周' + labels[i],
  236. num: d.getDate(),
  237. today: self.formatDate(today) === self.formatDate(d),
  238. hasRecords: false,
  239. count: 0
  240. })
  241. }
  242. this.weekDays = days
  243. },
  244. updateWeekDots: function(records) {
  245. var self = this
  246. var countByDate = {}
  247. for (var i = 0; i < records.length; i++) {
  248. var r = records[i]
  249. var d = r.recordDate || self.formatDate(r.createdAt)
  250. countByDate[d] = (countByDate[d] || 0) + 1
  251. }
  252. for (var j = 0; j < self.weekDays.length; j++) {
  253. var wd = self.weekDays[j]
  254. wd.hasRecords = (countByDate[wd.date] || 0) > 0
  255. wd.count = countByDate[wd.date] || 0
  256. }
  257. self.$forceUpdate()
  258. },
  259. loadWeekly: function() {
  260. var self = this
  261. var mid = self.currentMemberId > 0 ? self.currentMemberId : null
  262. getMealWeekly({ memberId: mid, startDate: self.weekDays[0].date, endDate: self.weekDays[6].date })
  263. .then(function(res) {
  264. if (res && res.code === 200 && res.data) {
  265. self.weeklySummary = res.data
  266. self.updateWeekDots([])
  267. }
  268. }).catch(function() {})
  269. },
  270. loadAlerts: function() {
  271. var self = this
  272. var mid = self.currentMemberId > 0 ? self.currentMemberId : null
  273. getMealAlerts({ memberId: mid, startDate: self.weekDays[0].date, endDate: self.weekDays[6].date })
  274. .then(function(res) {
  275. if (res && res.code === 200 && res.data) self.alertData = res.data
  276. }).catch(function() {})
  277. },
  278. loadDayRecords: function() {
  279. var self = this
  280. self.loading = true
  281. var mid = self.currentMemberId > 0 ? self.currentMemberId : null
  282. getMealDaily({ memberId: mid, date: self.selectedDate })
  283. .then(function(res) {
  284. self.loading = false
  285. if (res && res.code === 200 && res.data) {
  286. self.records = (res.data || [])
  287. var stats = self._calcDailyStats(self.records)
  288. self.dailyStats = stats
  289. } else {
  290. self.records = []
  291. self.dailyStats = {}
  292. }
  293. })
  294. .catch(function() { self.loading = false })
  295. },
  296. _calcDailyStats: function(records) {
  297. var s = { totalCalories: 0, totalProtein: 0, totalFat: 0, totalCarbs: 0, totalFiber: 0 }
  298. for (var i = 0; i < records.length; i++) {
  299. var r = records[i]
  300. if (r.totalCalories != null) s.totalCalories += r.totalCalories
  301. if (r.proteinG != null) s.totalProtein += r.proteinG
  302. if (r.fatG != null) s.totalFat += r.fatG
  303. if (r.carbsG != null) s.totalCarbs += r.carbsG
  304. if (r.fiberG != null) s.totalFiber += r.fiberG
  305. }
  306. return s
  307. },
  308. prevDay: function() {
  309. var d = parseDate(this.selectedDate)
  310. if (!d) return
  311. d.setDate(d.getDate() - 1)
  312. this.selectedDate = this.formatDate(d)
  313. this.loadDayRecords()
  314. },
  315. nextDay: function() {
  316. var d = parseDate(this.selectedDate)
  317. if (!d) return
  318. d.setDate(d.getDate() + 1)
  319. this.selectedDate = this.formatDate(d)
  320. this.loadDayRecords()
  321. },
  322. addRecord: function() {
  323. var url = '/pages/growth/diet-checkin'
  324. if (this.selectedDate) url += '?date=' + this.selectedDate
  325. uni.navigateTo({ url: url })
  326. },
  327. goDetail: function(record) {
  328. // 饮食记录详情暂时直接展示,不跳转
  329. var foods = this.formatFoodItems(record.foodItems)
  330. var msg = (this.mealLabel(record.mealType) || '') + ' · ' + this.formatTime(record.createdAt)
  331. if (foods) msg += '\n' + foods
  332. if (record.remark) msg += '\n' + record.remark
  333. if (record.isBackfill) msg += '\n(补打,无能量值)'
  334. uni.showModal({ title: msg, content: '', showCancel: false })
  335. },
  336. formatFoodItems: function(txt) {
  337. if (!txt) return ''
  338. var clean = txt.replace(/^\[|\]$/g, '').replace(/"/g, '')
  339. // 尝试解析 JSON
  340. try {
  341. var arr = JSON.parse(txt)
  342. if (Array.isArray(arr)) {
  343. return arr.map(function(f) { return f.name || f }).join('、')
  344. }
  345. } catch(e) {}
  346. // 纯文本:去掉数量单位
  347. return clean.replace(/\d+[克斤两碗份盘个]/g, '').replace(/[、,,;;]/g, '、').replace(/\s+/g, ' ').trim()
  348. },
  349. formatTime: function(ts) {
  350. if (!ts) return ''
  351. var d = parseDate(ts)
  352. if (!d) return ''
  353. return (d.getHours() || 0).toString().padStart(2, '0') + ':' + (d.getMinutes() || 0).toString().padStart(2, '0')
  354. },
  355. formatDate: function(d) {
  356. if (!d) return ''
  357. var date = typeof d === 'string' ? parseDate(d) : d
  358. if (!date) return ''
  359. var m = (date.getMonth() + 1).toString().padStart(2, '0')
  360. var day = date.getDate().toString().padStart(2, '0')
  361. return date.getFullYear() + '-' + m + '-' + day
  362. }
  363. }
  364. }
  365. </script>
  366. <style scoped>
  367. .page { min-height: 100vh; background: #FFF7ED; display: flex; flex-direction: column; }
  368. /* 成员切换栏 */
  369. .member-bar {
  370. display: flex; overflow-x: auto; padding: 16rpx 30rpx 8rpx; background: #fff;
  371. border-bottom: 1rpx solid #f0f0f0; gap: 16rpx; -webkit-overflow-scrolling: touch;
  372. }
  373. .member-tab {
  374. flex-shrink: 0; padding: 8rpx 20rpx; border-radius: 20rpx; background: #f5f5f5; color: #666;
  375. font-size: 26rpx; font-weight: 500;
  376. }
  377. .member-tab.active { background: linear-gradient(135deg, #FF8C42, #FFB366); color: #fff; }
  378. /* 周历条 */
  379. .week-strip {
  380. display: flex; align-items: center; justify-content: space-around;
  381. background: #fff; padding: 12rpx 20rpx 16rpx; border-bottom: 1rpx solid #f0f0f0;
  382. }
  383. .week-day {
  384. display: flex; flex-direction: column; align-items: center;
  385. padding: 8rpx 12rpx; border-radius: 16rpx; min-width: 68rpx;
  386. }
  387. .week-day-today .week-day-label { color: #FF8C42; font-weight: 700; }
  388. .week-day-active { background: linear-gradient(135deg, #FF8C42, #FFB366); }
  389. .week-day-active .week-day-label,
  390. .week-day-active .week-day-num { color: #fff; }
  391. .week-day-has .week-dot-wrap { margin-top: 6rpx; }
  392. .week-day-label { font-size: 20rpx; color: #999; }
  393. .week-day-num { font-size: 28rpx; font-weight: 600; color: #333; }
  394. .week-dot-wrap { display: flex; align-items: center; gap: 6rpx; }
  395. .week-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #FF8C42; }
  396. .week-day-count { font-size: 18rpx; color: #C2410C; }
  397. /* 内容区 */
  398. .content { flex: 1; padding: 24rpx 24rpx 180rpx; overflow-y: auto; }
  399. /* 通用卡片 */
  400. .card {
  401. display: flex; background: #fff; border-radius: 20rpx; margin-bottom: 24rpx;
  402. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05); overflow: hidden;
  403. }
  404. .card-accent { width: 8rpx; background: linear-gradient(180deg, #FF8C42, #FFB366); flex-shrink: 0; }
  405. .card-body { flex: 1; padding: 24rpx 28rpx; }
  406. .card-title { font-size: calc(28rpx * var(--font-scale, 1)); font-weight: 600; color: #333; margin-bottom: 16rpx; display: block; }
  407. /* 统计行 */
  408. .stat-row { display: flex; justify-content: space-around; }
  409. .stat-item { text-align: center; }
  410. .stat-val { font-size: 36rpx; font-weight: 700; color: #FF8C42; display: block; }
  411. .stat-unit { font-size: 20rpx; color: #999; }
  412. .stat-label { font-size: 22rpx; color: #666; display: block; margin-top: 4rpx; }
  413. /* 食材提示 */
  414. .alert-card {
  415. background: #fff; border-radius: 20rpx; margin-bottom: 24rpx;
  416. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05); overflow: hidden;
  417. }
  418. .alert-header { display: flex; align-items: center; gap: 12rpx; padding: 24rpx 28rpx 16rpx; border-bottom: 1rpx solid #FFF3E0; }
  419. .alert-icon { font-size: 36rpx; }
  420. .alert-title { font-size: 28rpx; font-weight: 600; color: #333; }
  421. .alert-section { padding: 16rpx 28rpx; }
  422. .alert-label { font-size: 24rpx; font-weight: 600; display: block; margin-bottom: 12rpx; }
  423. .alert-label.over { color: #C62828; }
  424. .alert-label.under { color: #1565C0; }
  425. .alert-tags { display: flex; flex-wrap: wrap; gap: 12rpx; }
  426. .alert-tag { display: flex; align-items: center; gap: 6rpx; padding: 8rpx 16rpx; border-radius: 20rpx; font-size: 24rpx; }
  427. .tag-over { background: #FFEBEE; color: #C62828; }
  428. .tag-under { background: #E3F2FD; color: #1565C0; }
  429. .tag-count { font-size: 20rpx; opacity: 0.8; }
  430. .alert-hint { font-size: 22rpx; color: #888; margin-top: 8rpx; display: block; }
  431. /* 高频食材 */
  432. .top-foods-list { display: flex; flex-direction: column; gap: 12rpx; }
  433. .top-food-item { display: flex; align-items: center; gap: 16rpx; }
  434. .top-food-rank {
  435. width: 36rpx; height: 36rpx; border-radius: 50%; background: #FFF1E6; color: #FF8C42;
  436. font-size: 22rpx; font-weight: 700; display: flex; align-items: center; justify-content: center;
  437. }
  438. .top-food-name { flex: 1; font-size: 26rpx; color: #333; }
  439. .top-food-count { font-size: 24rpx; color: #999; }
  440. /* 日期切换栏 */
  441. .date-bar {
  442. display: flex; align-items: center; justify-content: space-between;
  443. background: #fff; border-radius: 16rpx; padding: 16rpx 24rpx; margin-bottom: 20rpx;
  444. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  445. }
  446. .date-prev, .date-next { font-size: 26rpx; color: #FF8C42; font-weight: 600; padding: 8rpx 16rpx; }
  447. .date-display { font-size: 28rpx; font-weight: 600; color: #333; }
  448. /* 记录卡片 */
  449. .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
  450. .records-label { font-size: 28rpx; font-weight: 600; color: #333; }
  451. .records-count { font-size: 24rpx; color: #999; }
  452. .record-card {
  453. background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 16rpx;
  454. box-shadow: 0 2rpx 8rpx rgba(255,140,66,0.06); border-left: 6rpx solid #FF8C42;
  455. }
  456. .record-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10rpx; }
  457. .meal-badge { padding: 6rpx 18rpx; border-radius: 20rpx; font-size: 24rpx; font-weight: 600; }
  458. .meal-breakfast { background: #FFF3E0; color: #E65100; }
  459. .meal-lunch { background: #FFF8E1; color: #F57C00; }
  460. .meal-dinner { background: #F3E5F5; color: #7B1FA2; }
  461. .meal-snack { background: #E8F5E9; color: #2E7D32; }
  462. .record-meta { display: flex; align-items: center; gap: 12rpx; }
  463. .record-time { font-size: 24rpx; color: #999; }
  464. .record-backfill { font-size: 20rpx; padding: 2rpx 10rpx; background: #FFF3E0; color: #E65100; border-radius: 8rpx; }
  465. .record-body { margin-bottom: 8rpx; }
  466. .record-foods { font-size: calc(26rpx * var(--font-scale, 1)); color: #333; line-height: 1.5; }
  467. .record-footer { display: flex; gap: 20rpx; margin-bottom: 6rpx; }
  468. .record-cal { font-size: 22rpx; color: #FF8C42; font-weight: 600; }
  469. .record-nut { font-size: 22rpx; color: #999; }
  470. .record-remark { font-size: 24rpx; color: #888; font-style: italic; }
  471. /* 空态 */
  472. .empty-state { text-align: center; padding: 80rpx 40rpx; }
  473. .empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
  474. .empty-text { font-size: 28rpx; color: #333; font-weight: 500; display: block; margin-bottom: 8rpx; }
  475. .empty-hint { font-size: 24rpx; color: #999; display: block; }
  476. .loading-state { text-align: center; padding: 80rpx 0; }
  477. .loading-text { font-size: 26rpx; color: #999; }
  478. .bottom-spacer { height: 40rpx; }
  479. /* 浮动按钮 */
  480. .fab {
  481. position: fixed; right: 40rpx; bottom: 60rpx; width: 100rpx; height: 100rpx;
  482. background: linear-gradient(135deg, #FF8C42, #FFB366); border-radius: 50%;
  483. display: flex; align-items: center; justify-content: center;
  484. box-shadow: 0 4rpx 20rpx rgba(255,140,66,0.4);
  485. }
  486. .fab-icon { font-size: 48rpx; color: #fff; font-weight: 300; }
  487. </style>