index.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083
  1. <template>
  2. <view class="page">
  3. <!-- 加载状态 -->
  4. <BaseLoading :loading="loading" text="加载心愿中..." />
  5. <!-- 积分卡片 -->
  6. <view class="points-card" v-if="!loading">
  7. <view class="points-card__glow"></view>
  8. <view class="points-card__inner">
  9. <view class="points-card__header">
  10. <text class="points-card__icon">⭐</text>
  11. <text class="points-card__label">我的积分</text>
  12. </view>
  13. <text class="points-card__value">{{ formattedPoints }}</text>
  14. <view class="milestone">
  15. <view class="milestone__track">
  16. <view
  17. class="milestone__fill"
  18. :style="{ width: milestoneInfo.progress + '%' }"
  19. ></view>
  20. </view>
  21. <text class="milestone__text">🎯 距离下一个奖励还差 {{ milestoneInfo.remaining }} 分</text>
  22. </view>
  23. </view>
  24. </view>
  25. <!-- 筛选标签 -->
  26. <scroll-view
  27. class="filter-tabs"
  28. scroll-x="true"
  29. show-scrollbar="false"
  30. v-if="!loading && wishes.length > 0"
  31. >
  32. <view class="filter-tabs__inner">
  33. <view
  34. v-for="tab in filterTabs"
  35. :key="tab.key"
  36. class="filter-tab"
  37. :class="{ 'filter-tab--active': activeFilter === tab.key }"
  38. @click="onFilterChange(tab.key)"
  39. >
  40. <text class="filter-tab__label">{{ tab.emoji }} {{ tab.label }}</text>
  41. <view class="filter-tab__count" v-if="getFilterCount(tab.key) > 0">{{ getFilterCount(tab.key) }}</view>
  42. </view>
  43. </view>
  44. </scroll-view>
  45. <!-- 空状态:无任何心愿 -->
  46. <BaseEmpty
  47. v-if="!loading && wishes.length === 0"
  48. icon="🗺️"
  49. title="还没有心愿呢"
  50. description="点击 + 添加第一个心愿吧!"
  51. >
  52. <template v-slot:action>
  53. <PlayfulButton variant="clay" size="lg" @click="showAddModal = true">
  54. ✨ 添加第一个心愿
  55. </PlayfulButton>
  56. </template>
  57. </BaseEmpty>
  58. <!-- 空状态:筛选后无结果 -->
  59. <BaseEmpty
  60. v-else-if="!loading && filteredWishes.length === 0 && wishes.length > 0"
  61. icon="🔍"
  62. title="没有找到"
  63. :description="'还没有' + getActiveTabLabel + '的心愿哦'"
  64. />
  65. <!-- 心愿列表(宝箱卡片) -->
  66. <view class="wish-list" v-if="!loading && wishes.length > 0">
  67. <view
  68. v-for="(wish, index) in filteredWishes"
  69. :key="wish.id"
  70. class="wish-item"
  71. :class="['animate-slide-up', 'animate-stagger-' + (index + 1)]"
  72. >
  73. <PlayfulCard variant="clay" padding="0" :clickable="false">
  74. <view class="wish-card">
  75. <!-- 左侧 emoji 图标 -->
  76. <view class="wish-card__icon-wrap">
  77. <text class="wish-card__emoji">{{ getCategoryEmoji(wish.category) }}</text>
  78. </view>
  79. <!-- 中间内容 -->
  80. <view class="wish-card__body">
  81. <view class="wish-card__top">
  82. <text class="wish-card__title">{{ wish.title }}</text>
  83. <BaseBadge :variant="getStatusBadgeVariant(wish.status)" size="xs">
  84. <text>{{ getStatusEmoji(wish.status) }} {{ wish.statusText || wish.status }}</text>
  85. </BaseBadge>
  86. </view>
  87. <view class="wish-card__points-row">
  88. <text class="wish-card__points-label">需要</text>
  89. <text class="wish-card__points-value">{{ wish.pointsRequired || '???' }}</text>
  90. <text class="wish-card__points-unit">⭐</text>
  91. </view>
  92. <!-- 根据不同状态显示操作区域 -->
  93. <view class="wish-card__actions">
  94. <!-- draft:等待定价 -->
  95. <view class="wish-card__status-text" v-if="wish.status === 'draft'">
  96. <text class="wish-card__status-emoji">⏳</text>
  97. <text class="wish-card__status-msg">等待妈妈定价中</text>
  98. </view>
  99. <!-- active + 积分足够 → 兑换按钮 -->
  100. <PlayfulButton
  101. v-if="wish.status === 'active'"
  102. :variant="canRedeem(wish) ? 'primary' : 'secondary'"
  103. size="sm"
  104. :disabled="!canRedeem(wish)"
  105. @click="onRedeem(wish)"
  106. >
  107. {{ canRedeem(wish) ? '🎁 兑换' : '还差 ' + (wish.pointsRequired - currentPoints) + ' ⭐' }}
  108. </PlayfulButton>
  109. <!-- pending_exchange:审批中 -->
  110. <view class="wish-card__status-text wish-card__status-text--warning" v-if="wish.status === 'pending_exchange'">
  111. <text class="wish-card__status-emoji">⏳</text>
  112. <text class="wish-card__status-msg">等待妈妈审批</text>
  113. </view>
  114. <!-- approved:已批准 -->
  115. <view class="wish-card__status-text wish-card__status-text--info" v-if="wish.status === 'approved'">
  116. <text class="wish-card__status-emoji">📦</text>
  117. <text class="wish-card__status-msg">妈妈准备中</text>
  118. </view>
  119. <!-- exchanging:确认收货 -->
  120. <PlayfulButton
  121. v-if="wish.status === 'exchanging'"
  122. variant="success"
  123. size="sm"
  124. @click="onConfirmReceived(wish)"
  125. >
  126. 📦 确认收货
  127. </PlayfulButton>
  128. <!-- fulfilled:已完成 -->
  129. <view class="wish-card__status-text wish-card__status-text--success" v-if="wish.status === 'fulfilled'">
  130. <text class="wish-card__status-emoji">🎉</text>
  131. <text class="wish-card__status-msg">已兑换</text>
  132. </view>
  133. <!-- rejected:未通过 -->
  134. <view class="wish-card__status-text wish-card__status-text--error" v-if="wish.status === 'rejected'">
  135. <text class="wish-card__status-emoji">😢</text>
  136. <text class="wish-card__status-msg">未通过</text>
  137. </view>
  138. </view>
  139. </view>
  140. <!-- 右侧删除按钮(仅可取消状态) -->
  141. <view
  142. class="wish-card__delete"
  143. v-if="wish.status === 'draft' || wish.status === 'active'"
  144. @click.stop="onDeleteWish(wish)"
  145. >
  146. <text class="wish-card__delete-icon">✕</text>
  147. </view>
  148. </view>
  149. </PlayfulCard>
  150. </view>
  151. <!-- 底部间距(给 FAB 让位) -->
  152. <view class="wish-list__spacer"></view>
  153. </view>
  154. <!-- 浮动操作按钮 -->
  155. <view class="fab" @click="showAddModal = true">
  156. <view class="fab__pulse"></view>
  157. <text class="fab__icon">+</text>
  158. </view>
  159. <!-- 添加心愿弹窗 -->
  160. <view class="modal-overlay" v-if="showAddModal" @click="closeAddModal">
  161. <view class="modal" @click.stop>
  162. <view class="modal__header">
  163. <text class="modal__title">✨ 添加新心愿</text>
  164. <text class="modal__close" @click="closeAddModal">✕</text>
  165. </view>
  166. <!-- 表情/分类选择 -->
  167. <view class="form-group">
  168. <text class="form-group__label">选择分类</text>
  169. <view class="emoji-grid">
  170. <view
  171. v-for="cat in categories"
  172. :key="cat.value"
  173. class="emoji-item"
  174. :class="{ 'emoji-item--active': addForm.category === cat.value }"
  175. @click="addForm.category = cat.value; addForm.emoji = cat.emoji"
  176. >
  177. <text class="emoji-item__icon">{{ cat.emoji }}</text>
  178. <text class="emoji-item__label">{{ cat.label }}</text>
  179. </view>
  180. </view>
  181. </view>
  182. <!-- 心愿名称 -->
  183. <view class="form-group">
  184. <text class="form-group__label">心愿名称</text>
  185. <input
  186. class="form-input"
  187. v-model="addForm.title"
  188. placeholder="写下你的心愿吧~"
  189. maxlength="20"
  190. @input="onTitleInput"
  191. />
  192. <text class="form-input__counter">{{ addForm.title.length }}/20</text>
  193. </view>
  194. <!-- 所需积分 -->
  195. <view class="form-group">
  196. <text class="form-group__label">所需积分(给妈妈参考)</text>
  197. <view class="form-input__wrapper">
  198. <input
  199. class="form-input form-input--number"
  200. v-model="addForm.pointsRequired"
  201. placeholder="建议积分"
  202. type="number"
  203. maxlength="4"
  204. />
  205. <text class="form-input__suffix">⭐</text>
  206. </view>
  207. </view>
  208. <!-- 提交按钮 -->
  209. <PlayfulButton variant="clay" size="lg" block :disabled="!canSubmit" @click="onAddWish">
  210. ✨ 许下心愿
  211. </PlayfulButton>
  212. </view>
  213. </view>
  214. </view>
  215. </template>
  216. <script>
  217. import {
  218. getWishes,
  219. createWish,
  220. requestWishExchange,
  221. confirmWishReceived,
  222. cancelWish,
  223. getPointsBalance
  224. } from '../../utils/api.js'
  225. import PlayfulCard from '../../components/PlayfulCard.vue'
  226. import PlayfulButton from '../../components/PlayfulButton.vue'
  227. import BaseBadge from '../../components/BaseBadge.vue'
  228. import BaseEmpty from '../../components/BaseEmpty.vue'
  229. import BaseLoading from '../../components/BaseLoading.vue'
  230. export default {
  231. components: {
  232. PlayfulCard,
  233. PlayfulButton,
  234. BaseBadge,
  235. BaseEmpty,
  236. BaseLoading
  237. },
  238. data() {
  239. return {
  240. wishes: [],
  241. currentPoints: 0,
  242. memberId: '',
  243. loading: true,
  244. activeFilter: 'all',
  245. // 添加心愿弹窗
  246. showAddModal: false,
  247. addForm: {
  248. title: '',
  249. category: '',
  250. emoji: '🎁',
  251. pointsRequired: ''
  252. },
  253. submittingWish: false,
  254. // 筛选标签配置
  255. filterTabs: [
  256. { key: 'all', emoji: '📋', label: '全部' },
  257. { key: 'redeemable', emoji: '🎯', label: '可兑换' },
  258. { key: 'pending', emoji: '⏳', label: '审核中' },
  259. { key: 'completed', emoji: '✅', label: '已兑换' },
  260. { key: 'rejected', emoji: '❌', label: '未通过' }
  261. ],
  262. // 分类配置
  263. categories: [
  264. { value: '玩具', emoji: '🎮', label: '玩具' },
  265. { value: '图书', emoji: '📚', label: '图书' },
  266. { value: '美食', emoji: '🍕', label: '美食' },
  267. { value: '娱乐', emoji: '🎪', label: '娱乐' },
  268. { value: '活动', emoji: '🏕️', label: '活动' },
  269. { value: '其他', emoji: '🎁', label: '其他' }
  270. ],
  271. // 状态 → badge 颜色映射
  272. statusBadgeMap: {
  273. draft: 'warning',
  274. active: 'accent',
  275. pending_exchange: 'warning',
  276. approved: 'accent',
  277. exchanging: 'primary',
  278. fulfilled: 'success',
  279. rejected: 'error'
  280. },
  281. // 状态 → emoji 映射
  282. statusEmojiMap: {
  283. draft: '⏳',
  284. active: '🎯',
  285. pending_exchange: '⏳',
  286. approved: '📦',
  287. exchanging: '🚚',
  288. fulfilled: '✅',
  289. rejected: '😢'
  290. },
  291. // 分类 → emoji 映射
  292. categoryEmojiMap: {
  293. '玩具': '🎮',
  294. '图书': '📚',
  295. '美食': '🍕',
  296. '娱乐': '🎪',
  297. '活动': '🏕️',
  298. '其他': '🎁'
  299. },
  300. // 用于触发列表重绘(筛选切换时重新触发动画)
  301. listKey: 0
  302. }
  303. },
  304. computed: {
  305. formattedPoints() {
  306. if (!this.currentPoints) return '0'
  307. return this.currentPoints.toLocaleString()
  308. },
  309. milestoneInfo() {
  310. const step = 200
  311. const current = this.currentPoints || 0
  312. const next = Math.ceil(current / step) * step
  313. const remaining = next - current
  314. const progress = step > 0 ? (current % step) / step * 100 : 0
  315. return { nextMilestone: next, remaining, progress }
  316. },
  317. filteredWishes() {
  318. return this.getFilteredWishes(this.activeFilter)
  319. },
  320. getActiveTabLabel() {
  321. var activeFilter = this.activeFilter
  322. var tab = this.filterTabs.find(function(t) { return t.key === activeFilter })
  323. return tab ? tab.label : ''
  324. },
  325. canSubmit() {
  326. var title = this.addForm.title || ''
  327. return title.trim().length > 0 && !this.submittingWish
  328. }
  329. },
  330. onShow() {
  331. this.loadData()
  332. },
  333. methods: {
  334. async loadData() {
  335. this.loading = true
  336. var currentRole
  337. var results
  338. try {
  339. currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role')
  340. if (currentRole !== 'child') {
  341. uni.reLaunch({ url: '/pages/index-home/index' })
  342. return
  343. }
  344. this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('userId')
  345. // 并行获取心愿列表和积分
  346. results = await Promise.all([
  347. getWishes({ memberId: this.memberId }),
  348. getPointsBalance(this.memberId)
  349. ])
  350. this.wishes = (results[0] && results[0].data) || []
  351. this.currentPoints = (results[1] && results[1].data && results[1].data.balance) || 0
  352. } catch (e) {
  353. console.error('加载心愿列表失败', e)
  354. uni.showToast({ title: '加载失败', icon: 'none' })
  355. } finally {
  356. this.loading = false
  357. }
  358. },
  359. // 筛选切换(触发重动画)
  360. onFilterChange(key) {
  361. this.activeFilter = key
  362. this.listKey++
  363. },
  364. getFilteredWishes(filterKey) {
  365. var list = this.wishes || []
  366. if (filterKey === 'all') return list
  367. if (filterKey === 'redeemable') {
  368. return list.filter(function(w) {
  369. return w.status === 'active'
  370. })
  371. }
  372. if (filterKey === 'pending') {
  373. return list.filter(function(w) {
  374. return w.status === 'draft' || w.status === 'pending_exchange'
  375. })
  376. }
  377. if (filterKey === 'completed') {
  378. return list.filter(function(w) {
  379. return w.status === 'fulfilled' || w.status === 'exchanging' || w.status === 'approved'
  380. })
  381. }
  382. if (filterKey === 'rejected') {
  383. return list.filter(function(w) {
  384. return w.status === 'rejected'
  385. })
  386. }
  387. return list
  388. },
  389. getFilterCount(filterKey) {
  390. return this.getFilteredWishes(filterKey).length
  391. },
  392. canRedeem(wish) {
  393. return this.currentPoints >= (wish.pointsRequired || 0)
  394. },
  395. getCategoryEmoji(category) {
  396. var map = this.categoryEmojiMap
  397. if (category && map[category]) {
  398. return map[category]
  399. }
  400. return '🎁'
  401. },
  402. getStatusBadgeVariant(status) {
  403. var map = this.statusBadgeMap
  404. return (status && map[status]) || 'muted'
  405. },
  406. getStatusEmoji(status) {
  407. var map = this.statusEmojiMap
  408. return (status && map[status]) || '📋'
  409. },
  410. // 兑换操作
  411. onRedeem(wish) {
  412. var self = this
  413. uni.showModal({
  414. title: '兑换心愿',
  415. content: '确定要兑换"' + wish.title + '"吗?\n将消耗 ' + wish.pointsRequired + ' 积分',
  416. success: function(res) {
  417. if (res.confirm) {
  418. self.doRedeem(wish)
  419. }
  420. }
  421. })
  422. },
  423. async doRedeem(wish) {
  424. try {
  425. await requestWishExchange(wish.id)
  426. uni.showToast({ title: '已提交申请 🎉', icon: 'success' })
  427. this.loadData()
  428. } catch (e) {
  429. uni.showToast({ title: e.message || '申请失败', icon: 'none' })
  430. }
  431. },
  432. // 确认收货
  433. onConfirmReceived(wish) {
  434. var self = this
  435. uni.showModal({
  436. title: '确认收货',
  437. content: '确定已收到"' + wish.title + '"吗?',
  438. success: function(res) {
  439. if (res.confirm) {
  440. self.doConfirmReceived(wish)
  441. }
  442. }
  443. })
  444. },
  445. async doConfirmReceived(wish) {
  446. try {
  447. await confirmWishReceived(wish.id)
  448. uni.showToast({ title: '心愿完成!🎉', icon: 'success' })
  449. this.loadData()
  450. } catch (e) {
  451. uni.showToast({ title: '确认失败', icon: 'none' })
  452. }
  453. },
  454. // 删除/取消心愿
  455. onDeleteWish(wish) {
  456. var self = this
  457. uni.showModal({
  458. title: '取消心愿',
  459. content: '确定要取消"' + wish.title + '"吗?',
  460. success: function(res) {
  461. if (res.confirm) {
  462. self.doDeleteWish(wish)
  463. }
  464. }
  465. })
  466. },
  467. async doDeleteWish(wish) {
  468. try {
  469. await cancelWish(wish.id, '不想兑换了')
  470. uni.showToast({ title: '已取消', icon: 'success' })
  471. this.loadData()
  472. } catch (e) {
  473. uni.showToast({ title: '取消失败', icon: 'none' })
  474. }
  475. },
  476. // 添加心愿弹窗
  477. closeAddModal() {
  478. this.showAddModal = false
  479. this.resetAddForm()
  480. },
  481. resetAddForm() {
  482. this.addForm = {
  483. title: '',
  484. category: '',
  485. emoji: '🎁',
  486. pointsRequired: ''
  487. }
  488. },
  489. onTitleInput() {
  490. // 仅用于触发字符计数更新(vue 双向绑定已处理)
  491. },
  492. async onAddWish() {
  493. if (!this.canSubmit) return
  494. var title = (this.addForm.title || '').trim()
  495. if (!title) {
  496. uni.showToast({ title: '请输入心愿名称', icon: 'none' })
  497. return
  498. }
  499. this.submittingWish = true
  500. var data
  501. try {
  502. data = {
  503. memberId: this.memberId,
  504. title: title,
  505. category: this.addForm.category || '其他'
  506. }
  507. // 如果填写了积分建议,一并提交
  508. if (this.addForm.pointsRequired) {
  509. data.pointsRequired = parseInt(this.addForm.pointsRequired, 10) || 0
  510. }
  511. await createWish(data)
  512. uni.showToast({ title: '心愿已许下 ✨', icon: 'success' })
  513. this.closeAddModal()
  514. this.loadData()
  515. } catch (e) {
  516. uni.showToast({ title: e.message || '添加失败', icon: 'none' })
  517. } finally {
  518. this.submittingWish = false
  519. }
  520. }
  521. }
  522. }
  523. </script>
  524. <style scoped>
  525. /* ============================================================
  526. 页面容器
  527. ============================================================ */
  528. .page {
  529. min-height: 100vh;
  530. background: var(--bg, #F5F9FC);
  531. padding-bottom: 180rpx;
  532. }
  533. /* ============================================================
  534. 积分卡片
  535. ============================================================ */
  536. .points-card {
  537. position: relative;
  538. margin: 24rpx 32rpx 16rpx;
  539. border-radius: 32rpx;
  540. overflow: hidden;
  541. }
  542. .points-card__glow {
  543. position: absolute;
  544. top: -50%;
  545. right: -30%;
  546. width: 300rpx;
  547. height: 300rpx;
  548. background: radial-gradient(circle, rgba(91, 155, 213, 0.15), transparent 70%);
  549. pointer-events: none;
  550. }
  551. .points-card__inner {
  552. position: relative;
  553. padding: 32rpx 36rpx 28rpx;
  554. border-radius: 32rpx;
  555. border: 4rpx solid var(--border, #CBD5E1);
  556. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  557. box-shadow:
  558. inset -4rpx -4rpx 12rpx rgba(91, 155, 213, 0.05),
  559. 6rpx 6rpx 18rpx rgba(91, 155, 213, 0.10);
  560. }
  561. .points-card__header {
  562. display: flex;
  563. align-items: center;
  564. margin-bottom: 12rpx;
  565. }
  566. .points-card__icon {
  567. font-size: 36rpx;
  568. margin-right: 12rpx;
  569. }
  570. .points-card__label {
  571. font-size: 28rpx;
  572. font-weight: 600;
  573. color: var(--text-secondary, #64748B);
  574. }
  575. .points-card__value {
  576. display: block;
  577. font-size: 72rpx;
  578. font-weight: 800;
  579. color: var(--color-primary, #5B9BD5);
  580. line-height: 1.1;
  581. margin-bottom: 20rpx;
  582. letter-spacing: -2rpx;
  583. }
  584. /* 里程碑进度条 */
  585. .milestone {
  586. display: flex;
  587. flex-direction: column;
  588. }
  589. .milestone__track {
  590. height: 12rpx;
  591. background: var(--border-light, #E2E8F0);
  592. border-radius: 999rpx;
  593. overflow: hidden;
  594. margin-bottom: 12rpx;
  595. }
  596. .milestone__fill {
  597. height: 100%;
  598. border-radius: 999rpx;
  599. background: linear-gradient(90deg, var(--color-primary-light, #8FC5E8), var(--color-primary, #5B9BD5));
  600. transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
  601. }
  602. .milestone__text {
  603. font-size: 24rpx;
  604. color: var(--text-secondary, #64748B);
  605. line-height: 1.5;
  606. }
  607. /* ============================================================
  608. 筛选标签
  609. ============================================================ */
  610. .filter-tabs {
  611. margin: 8rpx 0 16rpx;
  612. white-space: nowrap;
  613. padding: 0 24rpx;
  614. }
  615. .filter-tabs__inner {
  616. display: flex;
  617. gap: 16rpx;
  618. padding: 4rpx 8rpx;
  619. }
  620. .filter-tab {
  621. position: relative;
  622. display: inline-flex;
  623. align-items: center;
  624. padding: 16rpx 28rpx;
  625. border-radius: 999rpx;
  626. background: var(--surface, #FFFFFF);
  627. border: 2rpx solid var(--border-light, #E2E8F0);
  628. font-size: 26rpx;
  629. font-weight: 500;
  630. color: var(--text-secondary, #64748B);
  631. transition: all 0.25s ease;
  632. flex-shrink: 0;
  633. }
  634. .filter-tab--active {
  635. background: var(--color-primary, #5B9BD5);
  636. border-color: var(--color-primary, #5B9BD5);
  637. color: #FFFFFF;
  638. box-shadow: 0 4rpx 16rpx rgba(91, 155, 213, 0.25);
  639. }
  640. .filter-tab:active {
  641. transform: scale(0.96);
  642. }
  643. .filter-tab__count {
  644. display: inline-flex;
  645. align-items: center;
  646. justify-content: center;
  647. min-width: 32rpx;
  648. height: 32rpx;
  649. padding: 0 8rpx;
  650. margin-left: 8rpx;
  651. border-radius: 999rpx;
  652. background: rgba(255, 255, 255, 0.25);
  653. color: inherit;
  654. font-size: 20rpx;
  655. font-weight: 700;
  656. }
  657. .filter-tab--active .filter-tab__count {
  658. background: rgba(255, 255, 255, 0.3);
  659. color: #FFFFFF;
  660. }
  661. /* ============================================================
  662. 心愿列表
  663. ============================================================ */
  664. .wish-list {
  665. padding: 8rpx 32rpx 0;
  666. }
  667. .wish-item {
  668. margin-bottom: 24rpx;
  669. }
  670. .wish-list__spacer {
  671. height: 160rpx;
  672. }
  673. /* ============================================================
  674. 心愿卡片(内部布局)
  675. ============================================================ */
  676. .wish-card {
  677. display: flex;
  678. align-items: stretch;
  679. padding: 24rpx;
  680. min-height: 160rpx;
  681. }
  682. /* 左侧 emoji 区域 */
  683. .wish-card__icon-wrap {
  684. display: flex;
  685. align-items: center;
  686. justify-content: center;
  687. width: 96rpx;
  688. min-height: 120rpx;
  689. margin-right: 20rpx;
  690. flex-shrink: 0;
  691. }
  692. .wish-card__emoji {
  693. font-size: 56rpx;
  694. line-height: 1;
  695. }
  696. /* 中间内容区域 */
  697. .wish-card__body {
  698. flex: 1;
  699. display: flex;
  700. flex-direction: column;
  701. justify-content: center;
  702. min-width: 0;
  703. padding-right: 8rpx;
  704. }
  705. .wish-card__top {
  706. display: flex;
  707. align-items: center;
  708. justify-content: space-between;
  709. margin-bottom: 8rpx;
  710. gap: 12rpx;
  711. }
  712. .wish-card__title {
  713. font-size: 32rpx;
  714. font-weight: 700;
  715. color: var(--text, #1E293B);
  716. flex: 1;
  717. overflow: hidden;
  718. text-overflow: ellipsis;
  719. white-space: nowrap;
  720. line-height: 1.4;
  721. }
  722. /* 积分行 */
  723. .wish-card__points-row {
  724. display: flex;
  725. align-items: baseline;
  726. margin-bottom: 12rpx;
  727. gap: 6rpx;
  728. }
  729. .wish-card__points-label {
  730. font-size: 24rpx;
  731. color: var(--text-secondary, #64748B);
  732. }
  733. .wish-card__points-value {
  734. font-size: 40rpx;
  735. font-weight: 800;
  736. color: var(--color-accent, #0EA5E9);
  737. line-height: 1;
  738. }
  739. .wish-card__points-unit {
  740. font-size: 28rpx;
  741. color: var(--color-accent, #0EA5E9);
  742. font-weight: 600;
  743. }
  744. /* 操作区域 */
  745. .wish-card__actions {
  746. display: flex;
  747. align-items: center;
  748. }
  749. /* 状态文本 */
  750. .wish-card__status-text {
  751. display: flex;
  752. align-items: center;
  753. gap: 8rpx;
  754. }
  755. .wish-card__status-emoji {
  756. font-size: 28rpx;
  757. }
  758. .wish-card__status-msg {
  759. font-size: 24rpx;
  760. font-weight: 500;
  761. color: var(--text-secondary, #64748B);
  762. }
  763. .wish-card__status-text--warning .wish-card__status-msg {
  764. color: var(--color-warning, #F59E0B);
  765. }
  766. .wish-card__status-text--info .wish-card__status-msg {
  767. color: var(--color-accent, #0EA5E9);
  768. }
  769. .wish-card__status-text--success .wish-card__status-msg {
  770. color: var(--color-success, #22C55E);
  771. }
  772. .wish-card__status-text--error .wish-card__status-msg {
  773. color: var(--color-error, #EF4444);
  774. }
  775. /* 右侧删除按钮 */
  776. .wish-card__delete {
  777. display: flex;
  778. align-items: center;
  779. justify-content: center;
  780. width: 48rpx;
  781. flex-shrink: 0;
  782. margin-left: 8rpx;
  783. }
  784. .wish-card__delete-icon {
  785. font-size: 28rpx;
  786. color: var(--muted, #94A3B8);
  787. padding: 12rpx;
  788. }
  789. .wish-card__delete-icon:active {
  790. opacity: 0.5;
  791. }
  792. /* ============================================================
  793. 浮动操作按钮 (FAB)
  794. ============================================================ */
  795. .fab {
  796. position: fixed;
  797. right: 40rpx;
  798. bottom: 80rpx;
  799. width: 110rpx;
  800. height: 110rpx;
  801. border-radius: 50%;
  802. display: flex;
  803. align-items: center;
  804. justify-content: center;
  805. background: linear-gradient(145deg, var(--color-primary-light, #8FC5E8), var(--color-primary, #5B9BD5));
  806. border: 4rpx solid var(--border, #CBD5E1);
  807. box-shadow:
  808. inset -3rpx -3rpx 10rpx rgba(255, 255, 255, 0.3),
  809. 4rpx 4rpx 16rpx rgba(91, 155, 213, 0.3);
  810. z-index: 100;
  811. transition: transform 0.15s ease;
  812. }
  813. .fab:active {
  814. transform: scale(0.92);
  815. }
  816. .fab__pulse {
  817. position: absolute;
  818. inset: -8rpx;
  819. border-radius: 50%;
  820. background: rgba(91, 155, 213, 0.15);
  821. animation: pulseGlow 2s ease-in-out infinite;
  822. pointer-events: none;
  823. }
  824. .fab__icon {
  825. font-size: 56rpx;
  826. color: #FFFFFF;
  827. font-weight: 300;
  828. line-height: 1;
  829. margin-top: -4rpx;
  830. }
  831. /* ============================================================
  832. 添加心愿弹窗
  833. ============================================================ */
  834. .modal-overlay {
  835. position: fixed;
  836. inset: 0;
  837. background: rgba(0, 0, 0, 0.45);
  838. z-index: 999;
  839. display: flex;
  840. align-items: flex-end;
  841. justify-content: center;
  842. animation: fadeIn 0.2s ease both;
  843. }
  844. .modal {
  845. width: 100%;
  846. max-height: 80vh;
  847. background: var(--surface, #FFFFFF);
  848. border-radius: 40rpx 40rpx 0 0;
  849. padding: 40rpx 36rpx;
  850. padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
  851. animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
  852. overflow-y: auto;
  853. }
  854. .modal__header {
  855. display: flex;
  856. align-items: center;
  857. justify-content: space-between;
  858. margin-bottom: 36rpx;
  859. }
  860. .modal__title {
  861. font-size: 36rpx;
  862. font-weight: 700;
  863. color: var(--text, #1E293B);
  864. }
  865. .modal__close {
  866. font-size: 36rpx;
  867. color: var(--muted, #94A3B8);
  868. padding: 12rpx;
  869. min-width: 60rpx;
  870. text-align: center;
  871. }
  872. .modal__close:active {
  873. opacity: 0.5;
  874. }
  875. /* 表单组 */
  876. .form-group {
  877. margin-bottom: 32rpx;
  878. }
  879. .form-group__label {
  880. display: block;
  881. font-size: 26rpx;
  882. font-weight: 600;
  883. color: var(--text, #1E293B);
  884. margin-bottom: 16rpx;
  885. }
  886. /* 分类/表情选择器 */
  887. .emoji-grid {
  888. display: grid;
  889. grid-template-columns: repeat(3, 1fr);
  890. gap: 16rpx;
  891. }
  892. .emoji-item {
  893. display: flex;
  894. flex-direction: column;
  895. align-items: center;
  896. padding: 20rpx 12rpx;
  897. border-radius: 20rpx;
  898. background: var(--bg, #F5F9FC);
  899. border: 2rpx solid var(--border-light, #E2E8F0);
  900. transition: all 0.2s ease;
  901. }
  902. .emoji-item--active {
  903. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  904. border-color: var(--color-primary, #5B9BD5);
  905. box-shadow: 0 4rpx 12rpx rgba(91, 155, 213, 0.15);
  906. }
  907. .emoji-item:active {
  908. transform: scale(0.95);
  909. }
  910. .emoji-item__icon {
  911. font-size: 48rpx;
  912. margin-bottom: 8rpx;
  913. line-height: 1;
  914. }
  915. .emoji-item__label {
  916. font-size: 24rpx;
  917. color: var(--text-secondary, #64748B);
  918. font-weight: 500;
  919. }
  920. .emoji-item--active .emoji-item__label {
  921. color: var(--color-primary, #5B9BD5);
  922. font-weight: 600;
  923. }
  924. /* 输入框 */
  925. .form-input {
  926. width: 100%;
  927. background: var(--bg, #F5F9FC);
  928. border: 2rpx solid var(--border-light, #E2E8F0);
  929. border-radius: 20rpx;
  930. padding: 24rpx 28rpx;
  931. font-size: 30rpx;
  932. color: var(--text, #1E293B);
  933. box-sizing: border-box;
  934. }
  935. .form-input--number {
  936. padding-right: 80rpx;
  937. }
  938. .form-input__wrapper {
  939. position: relative;
  940. }
  941. .form-input__suffix {
  942. position: absolute;
  943. right: 28rpx;
  944. top: 50%;
  945. transform: translateY(-50%);
  946. font-size: 32rpx;
  947. color: var(--color-accent, #0EA5E9);
  948. font-weight: 700;
  949. }
  950. .form-input__counter {
  951. display: block;
  952. text-align: right;
  953. font-size: 22rpx;
  954. color: var(--muted, #94A3B8);
  955. margin-top: 8rpx;
  956. }
  957. </style>