records.vue 21 KB

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