food-query.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. <template>
  2. <view class="food-query-page">
  3. <!-- 成员切换条 -->
  4. <view class="member-bar" v-if="memberList.length > 1 || familyMode">
  5. <view class="member-tab"
  6. :class="{'active': currentView === 'family'}"
  7. @tap="switchView('family')">
  8. <text class="member-name">👨‍👩‍👧 家庭总览</text>
  9. </view>
  10. <view class="member-tab"
  11. :class="{'active': currentView === 'personal'}"
  12. @tap="switchView('personal')">
  13. <text class="member-name">👤 个人</text>
  14. </view>
  15. <view class="member-tab"
  16. v-for="(m, mi) in memberList"
  17. :key="m.userId"
  18. :class="{'active': currentMemberId === m.userId}"
  19. @tap="switchMember(m)">
  20. <text class="member-name">{{ m.nickname || m.roleLabel || '成员' }}</text>
  21. </view>
  22. </view>
  23. <!-- 模式切换(仅家庭模式展示) -->
  24. <view class="mode-bar" v-if="familyMode">
  25. <view class="mode-btn" :class="{'active': sortType === 'desc'}" @tap="setSort('desc')">
  26. <text>推荐值 ↓</text>
  27. </view>
  28. <view class="mode-btn" :class="{'active': sortType === 'asc'}" @tap="setSort('asc')">
  29. <text>推荐值 ↑</text>
  30. </view>
  31. </view>
  32. <!-- 搜索栏(固定顶部,不随列表滚动) -->
  33. <view class="search-bar">
  34. <input class="search-input" placeholder="搜索食材..." v-model="searchQuery" @confirm="searchFood" />
  35. <view class="search-btn" @tap="searchFood">搜索</view>
  36. </view>
  37. <!-- 分类标签 -->
  38. <view class="category-tabs">
  39. <view class="tab-item" :class="{'tab-active': activeCategory === 'all'}" @tap="switchCategory('all')">
  40. <text>全部</text>
  41. </view>
  42. <view class="tab-item" :class="{'tab-active': activeCategory === '蔬菜'}" @tap="switchCategory('蔬菜')">
  43. <text>蔬菜</text>
  44. </view>
  45. <view class="tab-item" :class="{'tab-active': activeCategory === '水果'}" @tap="switchCategory('水果')">
  46. <text>水果</text>
  47. </view>
  48. <view class="tab-item" :class="{'tab-active': activeCategory === '肉禽'}" @tap="switchCategory('肉禽')">
  49. <text>肉禽</text>
  50. </view>
  51. <view class="tab-item" :class="{'tab-active': activeCategory === '水产'}" @tap="switchCategory('水产')">
  52. <text>水产</text>
  53. </view>
  54. <view class="tab-item" :class="{'tab-active': activeCategory === '谷物'}" @tap="switchCategory('谷物')">
  55. <text>谷物</text>
  56. </view>
  57. <view class="tab-item" :class="{'tab-active': activeCategory === '蛋奶'}" @tap="switchCategory('蛋奶')">
  58. <text>蛋奶</text>
  59. </view>
  60. </view>
  61. <!-- 筛选栏 -->
  62. <view class="filter-bar">
  63. <view class="filter-item" :class="{'filter-active': filterType === 'suitable'}" @tap="setFilter('suitable')">
  64. <text>✅ 适合</text>
  65. </view>
  66. <view class="filter-item" :class="{'filter-active': filterType === 'unsuitable'}" @tap="setFilter('unsuitable')">
  67. <text>❌ 戒备</text>
  68. </view>
  69. <view class="filter-item" :class="{'filter-active': filterType === 'all'}" @tap="setFilter('all')">
  70. <text>📋 全部</text>
  71. </view>
  72. </view>
  73. <!-- 列表区(仅此区域滚动) -->
  74. <scroll-view class="content" scroll-y :refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
  75. <!-- 家庭总览 -->
  76. <view class="family-overview" v-if="currentView === 'family' && familyOverview && familyOverview.length > 0">
  77. <view class="overview-title">
  78. <text>🏠 家庭推荐概况(共 {{ familyMembers.length }} 人)</text>
  79. </view>
  80. <view class="overview-item" v-for="(item, idx) in familyOverview" :key="idx">
  81. <view class="overview-header">
  82. <text class="food-name">{{ item.name }}</text>
  83. <view class="score-badge" :class="item.color">
  84. <text>{{ item.avgScore }}</text>
  85. </view>
  86. </view>
  87. <view class="overview-detail">
  88. <text class="category-label">{{ item.category }}</text>
  89. <view class="member-scores">
  90. <text v-for="(s, mi) in item.memberScores" :key="mi"
  91. class="member-score"
  92. :class="s.score >= 0 ? 'score-green' : 'score-red'">
  93. {{ s.name }}:{{ s.score >= 0 ? '+' : '' }}{{ s.score }}
  94. </text>
  95. </view>
  96. </view>
  97. </view>
  98. </view>
  99. <!-- 食材列表 -->
  100. <view class="food-list" v-if="currentView === 'personal'">
  101. <view class="food-card" v-for="(item, idx) in filteredFoods" :key="getFoodKey(item, idx)">
  102. <view class="food-left">
  103. <view class="ingredient-badge" :class="item.scoreClass">
  104. <text class="ingredient-badge-text">{{ idx + 1 }}</text>
  105. </view>
  106. <view class="food-info">
  107. <text class="food-name">{{ item.name }}</text>
  108. <view class="food-meta" v-if="item.category">
  109. <text class="meta-item">{{ item.category }}</text>
  110. </view>
  111. </view>
  112. </view>
  113. <view class="food-right">
  114. <text class="score-value" :class="item.scoreClass">{{ item.score || '--' }}</text>
  115. <view class="status-badge" :class="'badge-' + item.status">
  116. <text>{{ item.statusText }}</text>
  117. </view>
  118. </view>
  119. </view>
  120. </view>
  121. <view class="empty-state" v-if="!loading && filteredFoods.length === 0 && currentView === 'personal'">
  122. <text class="empty-icon">🥗</text>
  123. <text class="empty-text">暂无匹配食材</text>
  124. </view>
  125. <view class="loading-state" v-if="loading">
  126. <text class="loading-text">加载中...</text>
  127. </view>
  128. <view class="bottom-spacer"></view>
  129. </scroll-view>
  130. </view>
  131. </template>
  132. <script>
  133. import config from '@/config.js'
  134. export default {
  135. data() {
  136. return {
  137. searchQuery: '',
  138. activeCategory: 'all',
  139. filterType: 'suitable',
  140. sortType: 'desc',
  141. currentView: 'personal',
  142. foods: [],
  143. allFoods: [],
  144. familyData: null,
  145. familyMembers: [],
  146. familyOverview: [],
  147. currentMemberId: null,
  148. memberList: [],
  149. loading: false,
  150. refreshing: false,
  151. reportId: null
  152. }
  153. },
  154. computed: {
  155. filteredFoods: function() {
  156. var self = this
  157. var result = self.currentView === 'family'
  158. ? self.familyOverview
  159. : self.allFoods
  160. // 分类筛选(食材分类为中文,用关键词集合匹配不同数据源的分类命名)
  161. if (self.activeCategory !== 'all') {
  162. var keywords = self.getCategoryKeywords(self.activeCategory)
  163. result = result.filter(function(f) {
  164. if (!f.category) return false
  165. for (var ki = 0; ki < keywords.length; ki++) {
  166. if (f.category.indexOf(keywords[ki]) !== -1) return true
  167. }
  168. return false
  169. })
  170. }
  171. // 搜索筛选
  172. if (self.searchQuery) {
  173. result = result.filter(function(f) {
  174. return (f.name && f.name.indexOf(self.searchQuery) !== -1) ||
  175. (f.category && f.category.indexOf(self.searchQuery) !== -1)
  176. })
  177. }
  178. // 适合/戒备筛选(戒备 = 推荐指数 < 0)
  179. if (self.filterType === 'suitable') {
  180. result = result.filter(function(f) {
  181. return f.score >= 0
  182. })
  183. } else if (self.filterType === 'unsuitable') {
  184. result = result.filter(function(f) {
  185. return f.score < 0
  186. })
  187. }
  188. // 排序(戒备模式默认升序,见 setFilter / onLoad 中 sortType 默认值)
  189. if (self.sortType === 'asc') {
  190. result = result.slice().sort(function(a, b) {
  191. return (a.score || 0) - (b.score || 0)
  192. })
  193. } else {
  194. result = result.slice().sort(function(a, b) {
  195. return (b.score || 0) - (a.score || 0)
  196. })
  197. }
  198. return result
  199. }
  200. },
  201. onLoad: function(options) {
  202. this.reportId = options.reportId ? parseInt(options.reportId) : null
  203. this.currentMemberId = options.memberId ? parseInt(options.memberId) : null
  204. // 从报告入口进来时,默认显示戒备模式(升序,数值最小的排最前)
  205. if (this.reportId) {
  206. this.filterType = 'unsuitable'
  207. this.sortType = 'asc'
  208. }
  209. this.loadMembers()
  210. this.loadFoods()
  211. this.loadFamilyData()
  212. },
  213. methods: {
  214. getFoodKey: function(item, idx) {
  215. return item.id || idx
  216. },
  217. goBack: function() {
  218. uni.navigateBack()
  219. },
  220. loadMembers: function() {
  221. var self = this
  222. var token = uni.getStorageSync('token')
  223. if (!token) return
  224. uni.request({
  225. url: config.API_BASE_URL + '/api/food/recommendation/members',
  226. method: 'POST',
  227. header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
  228. success: function(res) {
  229. if (res.data && res.data.code === 200 && res.data.data) {
  230. self.memberList = res.data.data
  231. if (!self.currentMemberId && self.memberList.length > 0) {
  232. self.currentMemberId = self.memberList[0].userId
  233. }
  234. }
  235. }
  236. })
  237. },
  238. loadFoods: function() {
  239. var self = this
  240. var token = uni.getStorageSync('token')
  241. if (!token) return
  242. self.loading = true
  243. var url = config.API_BASE_URL + '/api/health/foods'
  244. var data = {}
  245. if (self.reportId) data.reportId = self.reportId
  246. if (self.currentMemberId) data.memberId = self.currentMemberId
  247. uni.request({
  248. url: url,
  249. method: 'POST',
  250. data: data,
  251. header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
  252. success: function(res) {
  253. self.loading = false
  254. if (res.data && res.data.code === 200 && res.data.data) {
  255. self.allFoods = res.data.data.map(function(f) {
  256. f.scoreClass = self.getScoreClass(f.score)
  257. f.statusText = self.getStatusText(f.score)
  258. f.status = f.score >= 0 ? 'suitable' : 'unsuitable'
  259. return f
  260. })
  261. }
  262. },
  263. fail: function() {
  264. self.loading = false
  265. }
  266. })
  267. },
  268. loadFamilyData: function() {
  269. var self = this
  270. var token = uni.getStorageSync('token')
  271. if (!token) return
  272. var familyId = uni.getStorageSync('familyId')
  273. if (!familyId) return
  274. uni.request({
  275. url: config.API_BASE_URL + '/api/health/foods/by-family',
  276. method: 'POST',
  277. header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
  278. success: function(res) {
  279. if (res.data && res.data.code === 200 && res.data.data) {
  280. self.familyData = res.data.data
  281. self.familyMembers = Object.keys(res.data.data.byMember || {}).map(function(key) {
  282. return { userId: key, nickname: res.data.data.memberNames[key] || '成员' }
  283. })
  284. self.buildFamilyOverview(res.data.data.totalList, res.data.data.byMember, res.data.data.memberNames)
  285. }
  286. }
  287. })
  288. },
  289. buildFamilyOverview: function(totalList, byMember, memberNames) {
  290. var self = this
  291. var overview = []
  292. for (var i = 0; i < totalList.length; i++) {
  293. var food = totalList[i]
  294. var memberScores = []
  295. var totalScore = 0
  296. var count = 0
  297. for (var j = 0; j < self.familyMembers.length; j++) {
  298. var member = self.familyMembers[j]
  299. var memberFoods = byMember[member.userId]
  300. if (memberFoods) {
  301. for (var k = 0; k < memberFoods.length; k++) {
  302. if (memberFoods[k].foodId === food.id) {
  303. var score = memberFoods[k].indexScore || 0
  304. memberScores.push({ name: member.nickname, score: score })
  305. totalScore += score
  306. count++
  307. break
  308. }
  309. }
  310. }
  311. }
  312. if (memberScores.length > 0) {
  313. var avgScore = Math.round(totalScore / memberScores.length)
  314. overview.push({
  315. id: food.id,
  316. name: food.name,
  317. category: food.category,
  318. score: avgScore,
  319. scoreClass: self.getScoreClass(avgScore),
  320. statusText: self.getStatusText(avgScore),
  321. status: avgScore >= 0 ? 'suitable' : 'unsuitable',
  322. memberScores: memberScores
  323. })
  324. }
  325. }
  326. // 按平均推荐值排序(从高到低)
  327. overview.sort(function(a, b) {
  328. return b.score - a.score
  329. })
  330. self.familyOverview = overview
  331. },
  332. getScoreClass: function(score) {
  333. if (score === undefined || score === null) return 'neutral'
  334. if (score >= 0) return 'good'
  335. return 'bad'
  336. },
  337. getStatusText: function(score) {
  338. if (score === undefined || score === null) return '未知'
  339. if (score >= 0) return '适合'
  340. return '戒备'
  341. },
  342. searchFood: function() {
  343. // 搜索由 computed 自动处理
  344. },
  345. switchCategory: function(category) {
  346. this.activeCategory = category
  347. },
  348. getCategoryKeywords: function(category) {
  349. var map = {
  350. '蔬菜': ['蔬菜'],
  351. '水果': ['水果'],
  352. '肉禽': ['肉类', '肉禽'],
  353. '水产': ['水产', '水产品'],
  354. '谷物': ['谷物', '主食', '全谷物'],
  355. '蛋奶': ['蛋类', '蛋奶', '乳制品']
  356. }
  357. return map[category] || []
  358. },
  359. setFilter: function(type) {
  360. this.filterType = type
  361. // 适合/全部:倒序,推荐值大的排前面;戒备:升序,推荐值小的排前面
  362. if (type === 'unsuitable') {
  363. this.sortType = 'asc'
  364. } else {
  365. this.sortType = 'desc'
  366. }
  367. },
  368. setSort: function(type) {
  369. this.sortType = type
  370. },
  371. switchView: function(view) {
  372. this.currentView = view
  373. },
  374. switchMember: function(member) {
  375. this.currentMemberId = member.userId
  376. this.currentView = 'personal'
  377. this.loadFoods()
  378. },
  379. onRefresh: function() {
  380. this.refreshing = true
  381. this.loadFoods()
  382. this.loadFamilyData()
  383. setTimeout(function() {
  384. this.refreshing = false
  385. }.bind(this), 500)
  386. }
  387. }
  388. }
  389. </script>
  390. <style scoped>
  391. .food-query-page {
  392. height: 100vh;
  393. overflow: hidden;
  394. background-color: #F5F7FA;
  395. display: flex;
  396. flex-direction: column;
  397. }
  398. /* 成员切换条 */
  399. .member-bar {
  400. display: flex;
  401. overflow-x: auto;
  402. padding: 16rpx 30rpx 8rpx;
  403. background: #fff;
  404. border-bottom: 1rpx solid #f0f0f0;
  405. gap: 16rpx;
  406. -webkit-overflow-scrolling: touch;
  407. }
  408. .member-tab {
  409. flex-shrink: 0;
  410. padding: 8rpx 20rpx;
  411. border-radius: 20rpx;
  412. background: #f5f5f5;
  413. color: #666;
  414. font-size: 26rpx;
  415. font-weight: 500;
  416. }
  417. .member-tab.active {
  418. background: linear-gradient(135deg, #FF8C42, #FFB366);
  419. color: #fff;
  420. }
  421. /* 模式切换(排序) */
  422. .mode-bar {
  423. display: flex;
  424. padding: 16rpx 30rpx;
  425. background: #fff;
  426. border-bottom: 1rpx solid #f0f0f0;
  427. gap: 20rpx;
  428. }
  429. .mode-btn {
  430. flex: 1;
  431. padding: 12rpx 0;
  432. text-align: center;
  433. border-radius: 20rpx;
  434. background: #f5f5f5;
  435. color: #666;
  436. font-size: 26rpx;
  437. }
  438. .mode-btn.active {
  439. background: linear-gradient(135deg, #10B981, #34D399);
  440. color: #fff;
  441. }
  442. /* 搜索栏 */
  443. .search-bar {
  444. display: flex;
  445. padding: 20rpx 30rpx;
  446. background-color: #FFFFFF;
  447. }
  448. .search-input {
  449. flex: 1;
  450. border: 1rpx solid #ddd;
  451. border-radius: 30rpx;
  452. padding: 16rpx 24rpx;
  453. font-size: 26rpx;
  454. background-color: #FAFAFA;
  455. }
  456. .search-btn {
  457. margin-left: 20rpx;
  458. padding: 16rpx 32rpx;
  459. background-color: #FF8C42;
  460. color: #FFFFFF;
  461. border-radius: 30rpx;
  462. font-size: 26rpx;
  463. }
  464. .content {
  465. flex: 1;
  466. min-height: 0;
  467. overflow: hidden;
  468. padding: 20rpx 30rpx;
  469. box-sizing: border-box;
  470. }
  471. /* 分类标签 */
  472. .category-tabs {
  473. display: flex;
  474. flex-wrap: wrap;
  475. gap: 16rpx;
  476. margin-bottom: 20rpx;
  477. }
  478. .tab-item {
  479. padding: 12rpx 24rpx;
  480. border: 1rpx solid #ddd;
  481. border-radius: 30rpx;
  482. font-size: 24rpx;
  483. color: #666;
  484. background-color: #FFFFFF;
  485. }
  486. .tab-active {
  487. border-color: #FF8C42;
  488. color: #FF8C42;
  489. background-color: #FFF5EB;
  490. }
  491. /* 筛选栏 */
  492. .filter-bar {
  493. display: flex;
  494. gap: 20rpx;
  495. margin-bottom: 20rpx;
  496. }
  497. .filter-item {
  498. padding: 12rpx 24rpx;
  499. border: 1rpx solid #ddd;
  500. border-radius: 30rpx;
  501. font-size: 24rpx;
  502. color: #666;
  503. background-color: #FFFFFF;
  504. }
  505. .filter-active {
  506. border-color: #4CAF50;
  507. color: #4CAF50;
  508. background-color: #E8F5E9;
  509. }
  510. /* 家庭总览 */
  511. .family-overview {
  512. background: #fff;
  513. border-radius: 16rpx;
  514. padding: 20rpx;
  515. margin-bottom: 20rpx;
  516. }
  517. .overview-title {
  518. font-size: 28rpx;
  519. font-weight: 600;
  520. color: #333;
  521. margin-bottom: 16rpx;
  522. padding-bottom: 16rpx;
  523. border-bottom: 1rpx solid #f0f0f0;
  524. }
  525. .overview-item {
  526. padding: 16rpx 0;
  527. border-bottom: 1rpx solid #f5f5f5;
  528. }
  529. .overview-item:last-child {
  530. border-bottom: none;
  531. }
  532. .overview-header {
  533. display: flex;
  534. align-items: center;
  535. justify-content: space-between;
  536. margin-bottom: 8rpx;
  537. }
  538. .overview-detail {
  539. display: flex;
  540. flex-direction: column;
  541. gap: 8rpx;
  542. }
  543. .category-label {
  544. font-size: 22rpx;
  545. color: #999;
  546. }
  547. .member-scores {
  548. display: flex;
  549. flex-wrap: wrap;
  550. gap: 12rpx;
  551. }
  552. .member-score {
  553. font-size: 20rpx;
  554. padding: 4rpx 12rpx;
  555. border-radius: 12rpx;
  556. background: #f5f5f5;
  557. }
  558. .score-green {
  559. color: #10B981;
  560. background: #E8F5E9;
  561. }
  562. .score-red {
  563. color: #EF4444;
  564. background: #FEE2E2;
  565. }
  566. /* 食材列表 */
  567. .food-list {
  568. display: flex;
  569. flex-direction: column;
  570. gap: 16rpx;
  571. }
  572. .food-card {
  573. background: #fff;
  574. border-radius: 16rpx;
  575. padding: 18rpx 20rpx;
  576. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
  577. display: flex;
  578. align-items: center;
  579. justify-content: space-between;
  580. width: 100%;
  581. box-sizing: border-box;
  582. }
  583. .food-left {
  584. display: flex;
  585. align-items: center;
  586. flex: 1;
  587. min-width: 0;
  588. }
  589. .ingredient-badge {
  590. width: 36rpx;
  591. height: 36rpx;
  592. border-radius: 10rpx;
  593. background: linear-gradient(135deg, #FF8C42, #FFB366);
  594. display: flex;
  595. align-items: center;
  596. justify-content: center;
  597. margin-right: 16rpx;
  598. flex-shrink: 0;
  599. }
  600. .ingredient-badge.good {
  601. background: linear-gradient(135deg, #10B981, #34D399);
  602. }
  603. .ingredient-badge.bad {
  604. background: linear-gradient(135deg, #EF4444, #DC2626);
  605. }
  606. .ingredient-badge-text {
  607. font-size: 18rpx;
  608. color: #FFFFFF;
  609. font-weight: 600;
  610. }
  611. .food-info {
  612. display: flex;
  613. flex-direction: column;
  614. flex: 1;
  615. min-width: 0;
  616. overflow: hidden;
  617. }
  618. .food-name {
  619. font-size: 28rpx;
  620. font-weight: 600;
  621. color: #1a1a1a;
  622. display: block;
  623. white-space: nowrap;
  624. overflow: hidden;
  625. text-overflow: ellipsis;
  626. }
  627. .food-meta {
  628. display: flex;
  629. flex-wrap: wrap;
  630. gap: 8rpx;
  631. margin-top: 6rpx;
  632. }
  633. .meta-item {
  634. flex-shrink: 0;
  635. font-size: 20rpx;
  636. color: #888;
  637. background: #f5f5f5;
  638. padding: 4rpx 14rpx;
  639. border-radius: 8rpx;
  640. }
  641. .food-right {
  642. display: flex;
  643. align-items: center;
  644. gap: 12rpx;
  645. flex-shrink: 0;
  646. }
  647. .score-value {
  648. font-size: 28rpx;
  649. font-weight: 700;
  650. min-width: 60rpx;
  651. text-align: right;
  652. }
  653. .score-value.good { color: #10B981; }
  654. .score-value.bad { color: #EF4444; }
  655. .status-badge {
  656. padding: 6rpx 16rpx;
  657. border-radius: 20rpx;
  658. font-size: 22rpx;
  659. font-weight: 500;
  660. }
  661. .badge-suitable {
  662. background: #E8F5E9;
  663. color: #4CAF50;
  664. }
  665. .badge-unsuitable {
  666. background: #FFEBEE;
  667. color: #F44336;
  668. }
  669. /* 空状态 */
  670. .empty-state {
  671. text-align: center;
  672. padding: 100rpx 40rpx;
  673. }
  674. .empty-icon {
  675. font-size: 80rpx;
  676. display: block;
  677. margin-bottom: 20rpx;
  678. }
  679. .empty-text {
  680. font-size: 30rpx;
  681. color: #333;
  682. font-weight: 500;
  683. display: block;
  684. margin-bottom: 12rpx;
  685. }
  686. .loading-state {
  687. text-align: center;
  688. padding: 80rpx 0;
  689. }
  690. .loading-text {
  691. font-size: 26rpx;
  692. color: #999;
  693. }
  694. .bottom-spacer { height: 40rpx; }
  695. </style>