records.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. <template>
  2. <view class="page">
  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 in memberList" :key="item.id || item.userId"
  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. export default {
  172. data() {
  173. return {
  174. memberId: null,
  175. currentMemberId: null,
  176. memberList: [],
  177. selectedDate: this.formatDate(new Date()),
  178. weekDays: [],
  179. records: [],
  180. dailyStats: {},
  181. weeklySummary: {},
  182. alertData: { overConsumed: [], underConsumed: [] },
  183. loading: false,
  184. _lastLoadDate: null
  185. }
  186. },
  187. onLoad: function(options) {
  188. this.memberId = options.memberId ? parseInt(options.memberId) : null
  189. this.currentMemberId = this.memberId
  190. this.loadMemberList()
  191. this.initWeekDays()
  192. this.loadWeekly()
  193. this.loadAlerts()
  194. this.loadDayRecords()
  195. },
  196. methods: {
  197. goBack: function() { uni.navigateBack() },
  198. mealLabel: function(t) {
  199. var m = { breakfast: '早餐', lunch: '午餐', dinner: '晚餐', snack: '加餐' }
  200. return m[t] || t || '饮食'
  201. },
  202. switchMember: function(item) {
  203. this.currentMemberId = item.id || item.userId
  204. this.loadWeekly()
  205. this.loadAlerts()
  206. this.loadDayRecords()
  207. },
  208. async loadMemberList() {
  209. try {
  210. var res = await getCautionFamilyMembers()
  211. if (res && res.code === 200 && res.data && res.data.length > 0) {
  212. var allItem = { id: -1, userId: -1, nickname: '全部', roleLabel: '全部' }
  213. this.memberList = [allItem].concat(res.data)
  214. if (!this.currentMemberId) this.currentMemberId = -1
  215. }
  216. } catch(e) {}
  217. },
  218. initWeekDays: function() {
  219. var self = this
  220. var today = new Date()
  221. var dayOfWeek = today.getDay() || 7 // 1=周一 7=周日
  222. var monday = new Date(today)
  223. monday.setDate(today.getDate() - (dayOfWeek - 1))
  224. var days = []
  225. var labels = ['一', '二', '三', '四', '五', '六', '日']
  226. for (var i = 0; i < 7; i++) {
  227. var d = new Date(monday)
  228. d.setDate(monday.getDate() + i)
  229. days.push({
  230. date: self.formatDate(d),
  231. label: '周' + labels[i],
  232. num: d.getDate(),
  233. today: self.formatDate(today) === self.formatDate(d),
  234. hasRecords: false,
  235. count: 0
  236. })
  237. }
  238. this.weekDays = days
  239. },
  240. updateWeekDots: function(records) {
  241. var self = this
  242. var countByDate = {}
  243. for (var i = 0; i < records.length; i++) {
  244. var r = records[i]
  245. var d = r.recordDate || self.formatDate(r.createdAt)
  246. countByDate[d] = (countByDate[d] || 0) + 1
  247. }
  248. for (var j = 0; j < self.weekDays.length; j++) {
  249. var wd = self.weekDays[j]
  250. wd.hasRecords = (countByDate[wd.date] || 0) > 0
  251. wd.count = countByDate[wd.date] || 0
  252. }
  253. self.$forceUpdate()
  254. },
  255. loadWeekly: function() {
  256. var self = this
  257. var mid = self.currentMemberId > 0 ? self.currentMemberId : null
  258. getMealWeekly({ memberId: mid, startDate: self.weekDays[0].date, endDate: self.weekDays[6].date })
  259. .then(function(res) {
  260. if (res && res.code === 200 && res.data) {
  261. self.weeklySummary = res.data
  262. self.updateWeekDots([])
  263. }
  264. }).catch(function() {})
  265. },
  266. loadAlerts: function() {
  267. var self = this
  268. var mid = self.currentMemberId > 0 ? self.currentMemberId : null
  269. getMealAlerts({ memberId: mid, startDate: self.weekDays[0].date, endDate: self.weekDays[6].date })
  270. .then(function(res) {
  271. if (res && res.code === 200 && res.data) self.alertData = res.data
  272. }).catch(function() {})
  273. },
  274. loadDayRecords: function() {
  275. var self = this
  276. self.loading = true
  277. var mid = self.currentMemberId > 0 ? self.currentMemberId : null
  278. getMealDaily({ memberId: mid, date: self.selectedDate })
  279. .then(function(res) {
  280. self.loading = false
  281. if (res && res.code === 200 && res.data) {
  282. self.records = (res.data || [])
  283. var stats = self._calcDailyStats(self.records)
  284. self.dailyStats = stats
  285. } else {
  286. self.records = []
  287. self.dailyStats = {}
  288. }
  289. })
  290. .catch(function() { self.loading = false })
  291. },
  292. _calcDailyStats: function(records) {
  293. var s = { totalCalories: 0, totalProtein: 0, totalFat: 0, totalCarbs: 0, totalFiber: 0 }
  294. for (var i = 0; i < records.length; i++) {
  295. var r = records[i]
  296. if (r.totalCalories != null) s.totalCalories += r.totalCalories
  297. if (r.proteinG != null) s.totalProtein += r.proteinG
  298. if (r.fatG != null) s.totalFat += r.fatG
  299. if (r.carbsG != null) s.totalCarbs += r.carbsG
  300. if (r.fiberG != null) s.totalFiber += r.fiberG
  301. }
  302. return s
  303. },
  304. prevDay: function() {
  305. var d = parseDate(this.selectedDate)
  306. if (!d) return
  307. d.setDate(d.getDate() - 1)
  308. this.selectedDate = this.formatDate(d)
  309. this.loadDayRecords()
  310. },
  311. nextDay: function() {
  312. var d = parseDate(this.selectedDate)
  313. if (!d) return
  314. d.setDate(d.getDate() + 1)
  315. this.selectedDate = this.formatDate(d)
  316. this.loadDayRecords()
  317. },
  318. addRecord: function() {
  319. var url = '/pages/growth/diet-checkin'
  320. if (this.selectedDate) url += '?date=' + this.selectedDate
  321. uni.navigateTo({ url: url })
  322. },
  323. goDetail: function(record) {
  324. // 饮食记录详情暂时直接展示,不跳转
  325. var foods = this.formatFoodItems(record.foodItems)
  326. var msg = (this.mealLabel(record.mealType) || '') + ' · ' + this.formatTime(record.createdAt)
  327. if (foods) msg += '\n' + foods
  328. if (record.remark) msg += '\n' + record.remark
  329. if (record.isBackfill) msg += '\n(补打,无能量值)'
  330. uni.showModal({ title: msg, content: '', showCancel: false })
  331. },
  332. formatFoodItems: function(txt) {
  333. if (!txt) return ''
  334. var clean = txt.replace(/^\[|\]$/g, '').replace(/"/g, '')
  335. // 尝试解析 JSON
  336. try {
  337. var arr = JSON.parse(txt)
  338. if (Array.isArray(arr)) {
  339. return arr.map(function(f) { return f.name || f }).join('、')
  340. }
  341. } catch(e) {}
  342. // 纯文本:去掉数量单位
  343. return clean.replace(/\d+[克斤两碗份盘个]/g, '').replace(/[、,,;;]/g, '、').replace(/\s+/g, ' ').trim()
  344. },
  345. formatTime: function(ts) {
  346. if (!ts) return ''
  347. var d = parseDate(ts)
  348. if (!d) return ''
  349. return (d.getHours() || 0).toString().padStart(2, '0') + ':' + (d.getMinutes() || 0).toString().padStart(2, '0')
  350. },
  351. formatDate: function(d) {
  352. if (!d) return ''
  353. var date = typeof d === 'string' ? parseDate(d) : d
  354. if (!date) return ''
  355. var m = (date.getMonth() + 1).toString().padStart(2, '0')
  356. var day = date.getDate().toString().padStart(2, '0')
  357. return date.getFullYear() + '-' + m + '-' + day
  358. }
  359. }
  360. }
  361. </script>
  362. <style scoped>
  363. .page { min-height: 100vh; background: #FFF7ED; display: flex; flex-direction: column; }
  364. /* 成员切换栏 */
  365. .member-bar {
  366. display: flex; overflow-x: auto; padding: 16rpx 30rpx 8rpx; background: #fff;
  367. border-bottom: 1rpx solid #f0f0f0; gap: 16rpx; -webkit-overflow-scrolling: touch;
  368. }
  369. .member-tab {
  370. flex-shrink: 0; padding: 8rpx 20rpx; border-radius: 20rpx; background: #f5f5f5; color: #666;
  371. font-size: 26rpx; font-weight: 500;
  372. }
  373. .member-tab.active { background: linear-gradient(135deg, #FF8C42, #FFB366); color: #fff; }
  374. /* 周历条 */
  375. .week-strip {
  376. display: flex; align-items: center; justify-content: space-around;
  377. background: #fff; padding: 12rpx 20rpx 16rpx; border-bottom: 1rpx solid #f0f0f0;
  378. }
  379. .week-day {
  380. display: flex; flex-direction: column; align-items: center;
  381. padding: 8rpx 12rpx; border-radius: 16rpx; min-width: 68rpx;
  382. }
  383. .week-day-today .week-day-label { color: #FF8C42; font-weight: 700; }
  384. .week-day-active { background: linear-gradient(135deg, #FF8C42, #FFB366); }
  385. .week-day-active .week-day-label,
  386. .week-day-active .week-day-num { color: #fff; }
  387. .week-day-has .week-dot-wrap { margin-top: 6rpx; }
  388. .week-day-label { font-size: 20rpx; color: #999; }
  389. .week-day-num { font-size: 28rpx; font-weight: 600; color: #333; }
  390. .week-dot-wrap { display: flex; align-items: center; gap: 6rpx; }
  391. .week-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #FF8C42; }
  392. .week-day-count { font-size: 18rpx; color: #C2410C; }
  393. /* 内容区 */
  394. .content { flex: 1; padding: 24rpx 24rpx 180rpx; overflow-y: auto; }
  395. /* 通用卡片 */
  396. .card {
  397. display: flex; background: #fff; border-radius: 20rpx; margin-bottom: 24rpx;
  398. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05); overflow: hidden;
  399. }
  400. .card-accent { width: 8rpx; background: linear-gradient(180deg, #FF8C42, #FFB366); flex-shrink: 0; }
  401. .card-body { flex: 1; padding: 24rpx 28rpx; }
  402. .card-title { font-size: 28rpx; font-weight: 600; color: #333; margin-bottom: 16rpx; display: block; }
  403. /* 统计行 */
  404. .stat-row { display: flex; justify-content: space-around; }
  405. .stat-item { text-align: center; }
  406. .stat-val { font-size: 36rpx; font-weight: 700; color: #FF8C42; display: block; }
  407. .stat-unit { font-size: 20rpx; color: #999; }
  408. .stat-label { font-size: 22rpx; color: #666; display: block; margin-top: 4rpx; }
  409. /* 食材提示 */
  410. .alert-card {
  411. background: #fff; border-radius: 20rpx; margin-bottom: 24rpx;
  412. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05); overflow: hidden;
  413. }
  414. .alert-header { display: flex; align-items: center; gap: 12rpx; padding: 24rpx 28rpx 16rpx; border-bottom: 1rpx solid #FFF3E0; }
  415. .alert-icon { font-size: 36rpx; }
  416. .alert-title { font-size: 28rpx; font-weight: 600; color: #333; }
  417. .alert-section { padding: 16rpx 28rpx; }
  418. .alert-label { font-size: 24rpx; font-weight: 600; display: block; margin-bottom: 12rpx; }
  419. .alert-label.over { color: #C62828; }
  420. .alert-label.under { color: #1565C0; }
  421. .alert-tags { display: flex; flex-wrap: wrap; gap: 12rpx; }
  422. .alert-tag { display: flex; align-items: center; gap: 6rpx; padding: 8rpx 16rpx; border-radius: 20rpx; font-size: 24rpx; }
  423. .tag-over { background: #FFEBEE; color: #C62828; }
  424. .tag-under { background: #E3F2FD; color: #1565C0; }
  425. .tag-count { font-size: 20rpx; opacity: 0.8; }
  426. .alert-hint { font-size: 22rpx; color: #888; margin-top: 8rpx; display: block; }
  427. /* 高频食材 */
  428. .top-foods-list { display: flex; flex-direction: column; gap: 12rpx; }
  429. .top-food-item { display: flex; align-items: center; gap: 16rpx; }
  430. .top-food-rank {
  431. width: 36rpx; height: 36rpx; border-radius: 50%; background: #FFF1E6; color: #FF8C42;
  432. font-size: 22rpx; font-weight: 700; display: flex; align-items: center; justify-content: center;
  433. }
  434. .top-food-name { flex: 1; font-size: 26rpx; color: #333; }
  435. .top-food-count { font-size: 24rpx; color: #999; }
  436. /* 日期切换栏 */
  437. .date-bar {
  438. display: flex; align-items: center; justify-content: space-between;
  439. background: #fff; border-radius: 16rpx; padding: 16rpx 24rpx; margin-bottom: 20rpx;
  440. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  441. }
  442. .date-prev, .date-next { font-size: 26rpx; color: #FF8C42; font-weight: 600; padding: 8rpx 16rpx; }
  443. .date-display { font-size: 28rpx; font-weight: 600; color: #333; }
  444. /* 记录卡片 */
  445. .records-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
  446. .records-label { font-size: 28rpx; font-weight: 600; color: #333; }
  447. .records-count { font-size: 24rpx; color: #999; }
  448. .record-card {
  449. background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; margin-bottom: 16rpx;
  450. box-shadow: 0 2rpx 8rpx rgba(255,140,66,0.06); border-left: 6rpx solid #FF8C42;
  451. }
  452. .record-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10rpx; }
  453. .meal-badge { padding: 6rpx 18rpx; border-radius: 20rpx; font-size: 24rpx; font-weight: 600; }
  454. .meal-breakfast { background: #FFF3E0; color: #E65100; }
  455. .meal-lunch { background: #FFF8E1; color: #F57C00; }
  456. .meal-dinner { background: #F3E5F5; color: #7B1FA2; }
  457. .meal-snack { background: #E8F5E9; color: #2E7D32; }
  458. .record-meta { display: flex; align-items: center; gap: 12rpx; }
  459. .record-time { font-size: 24rpx; color: #999; }
  460. .record-backfill { font-size: 20rpx; padding: 2rpx 10rpx; background: #FFF3E0; color: #E65100; border-radius: 8rpx; }
  461. .record-body { margin-bottom: 8rpx; }
  462. .record-foods { font-size: 26rpx; color: #333; line-height: 1.5; }
  463. .record-footer { display: flex; gap: 20rpx; margin-bottom: 6rpx; }
  464. .record-cal { font-size: 22rpx; color: #FF8C42; font-weight: 600; }
  465. .record-nut { font-size: 22rpx; color: #999; }
  466. .record-remark { font-size: 24rpx; color: #888; font-style: italic; }
  467. /* 空态 */
  468. .empty-state { text-align: center; padding: 80rpx 40rpx; }
  469. .empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
  470. .empty-text { font-size: 28rpx; color: #333; font-weight: 500; display: block; margin-bottom: 8rpx; }
  471. .empty-hint { font-size: 24rpx; color: #999; display: block; }
  472. .loading-state { text-align: center; padding: 80rpx 0; }
  473. .loading-text { font-size: 26rpx; color: #999; }
  474. .bottom-spacer { height: 40rpx; }
  475. /* 浮动按钮 */
  476. .fab {
  477. position: fixed; right: 40rpx; bottom: 60rpx; width: 100rpx; height: 100rpx;
  478. background: linear-gradient(135deg, #FF8C42, #FFB366); border-radius: 50%;
  479. display: flex; align-items: center; justify-content: center;
  480. box-shadow: 0 4rpx 20rpx rgba(255,140,66,0.4);
  481. }
  482. .fab-icon { font-size: 48rpx; color: #fff; font-weight: 300; }
  483. </style>