index.vue 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  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, reportAppEnter
  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. reportAppEnter('wish');
  332. this.loadData()
  333. },
  334. methods: {
  335. async loadData() {
  336. this.loading = true
  337. var currentRole
  338. var results
  339. try {
  340. currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role')
  341. if (currentRole !== 'child') {
  342. uni.reLaunch({ url: '/pages/index-home/index' })
  343. return
  344. }
  345. this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('userId')
  346. // 并行获取心愿列表和积分
  347. results = await Promise.all([
  348. getWishes({ memberId: this.memberId }),
  349. getPointsBalance(this.memberId)
  350. ])
  351. this.wishes = (results[0] && results[0].data) || []
  352. this.currentPoints = (results[1] && results[1].data && results[1].data.balance) || 0
  353. } catch (e) {
  354. console.error('加载心愿列表失败', e)
  355. uni.showToast({ title: '加载失败', icon: 'none' })
  356. } finally {
  357. this.loading = false
  358. }
  359. },
  360. // 筛选切换(触发重动画)
  361. onFilterChange(key) {
  362. this.activeFilter = key
  363. this.listKey++
  364. },
  365. getFilteredWishes(filterKey) {
  366. var list = this.wishes || []
  367. if (filterKey === 'all') return list
  368. if (filterKey === 'redeemable') {
  369. return list.filter(function(w) {
  370. return w.status === 'active'
  371. })
  372. }
  373. if (filterKey === 'pending') {
  374. return list.filter(function(w) {
  375. return w.status === 'draft' || w.status === 'pending_exchange'
  376. })
  377. }
  378. if (filterKey === 'completed') {
  379. return list.filter(function(w) {
  380. return w.status === 'fulfilled' || w.status === 'exchanging' || w.status === 'approved'
  381. })
  382. }
  383. if (filterKey === 'rejected') {
  384. return list.filter(function(w) {
  385. return w.status === 'rejected'
  386. })
  387. }
  388. return list
  389. },
  390. getFilterCount(filterKey) {
  391. return this.getFilteredWishes(filterKey).length
  392. },
  393. canRedeem(wish) {
  394. return this.currentPoints >= (wish.pointsRequired || 0)
  395. },
  396. getCategoryEmoji(category) {
  397. var map = this.categoryEmojiMap
  398. if (category && map[category]) {
  399. return map[category]
  400. }
  401. return '🎁'
  402. },
  403. getStatusBadgeVariant(status) {
  404. var map = this.statusBadgeMap
  405. return (status && map[status]) || 'muted'
  406. },
  407. getStatusEmoji(status) {
  408. var map = this.statusEmojiMap
  409. return (status && map[status]) || '📋'
  410. },
  411. // 兑换操作
  412. onRedeem(wish) {
  413. var self = this
  414. uni.showModal({
  415. title: '兑换心愿',
  416. content: '确定要兑换"' + wish.title + '"吗?\n将消耗 ' + wish.pointsRequired + ' 积分',
  417. success: function(res) {
  418. if (res.confirm) {
  419. self.doRedeem(wish)
  420. }
  421. }
  422. })
  423. },
  424. async doRedeem(wish) {
  425. try {
  426. await requestWishExchange(wish.id)
  427. uni.showToast({ title: '已提交申请 🎉', icon: 'success' })
  428. this.loadData()
  429. } catch (e) {
  430. uni.showToast({ title: e.message || '申请失败', icon: 'none' })
  431. }
  432. },
  433. // 确认收货
  434. onConfirmReceived(wish) {
  435. var self = this
  436. uni.showModal({
  437. title: '确认收货',
  438. content: '确定已收到"' + wish.title + '"吗?',
  439. success: function(res) {
  440. if (res.confirm) {
  441. self.doConfirmReceived(wish)
  442. }
  443. }
  444. })
  445. },
  446. async doConfirmReceived(wish) {
  447. try {
  448. await confirmWishReceived(wish.id)
  449. uni.showToast({ title: '心愿完成!🎉', icon: 'success' })
  450. this.loadData()
  451. } catch (e) {
  452. uni.showToast({ title: '确认失败', icon: 'none' })
  453. }
  454. },
  455. // 删除/取消心愿
  456. onDeleteWish(wish) {
  457. var self = this
  458. uni.showModal({
  459. title: '取消心愿',
  460. content: '确定要取消"' + wish.title + '"吗?',
  461. success: function(res) {
  462. if (res.confirm) {
  463. self.doDeleteWish(wish)
  464. }
  465. }
  466. })
  467. },
  468. async doDeleteWish(wish) {
  469. try {
  470. await cancelWish(wish.id, '不想兑换了')
  471. uni.showToast({ title: '已取消', icon: 'success' })
  472. this.loadData()
  473. } catch (e) {
  474. uni.showToast({ title: '取消失败', icon: 'none' })
  475. }
  476. },
  477. // 添加心愿弹窗
  478. closeAddModal() {
  479. this.showAddModal = false
  480. this.resetAddForm()
  481. },
  482. resetAddForm() {
  483. this.addForm = {
  484. title: '',
  485. category: '',
  486. emoji: '🎁',
  487. pointsRequired: ''
  488. }
  489. },
  490. onTitleInput() {
  491. // 仅用于触发字符计数更新(vue 双向绑定已处理)
  492. },
  493. async onAddWish() {
  494. if (!this.canSubmit) return
  495. var title = (this.addForm.title || '').trim()
  496. if (!title) {
  497. uni.showToast({ title: '请输入心愿名称', icon: 'none' })
  498. return
  499. }
  500. this.submittingWish = true
  501. var data
  502. try {
  503. data = {
  504. memberId: this.memberId,
  505. title: title,
  506. category: this.addForm.category || '其他'
  507. }
  508. // 如果填写了积分建议,一并提交
  509. if (this.addForm.pointsRequired) {
  510. data.pointsRequired = parseInt(this.addForm.pointsRequired, 10) || 0
  511. }
  512. await createWish(data)
  513. uni.showToast({ title: '心愿已许下 ✨', icon: 'success' })
  514. this.closeAddModal()
  515. this.loadData()
  516. } catch (e) {
  517. uni.showToast({ title: e.message || '添加失败', icon: 'none' })
  518. } finally {
  519. this.submittingWish = false
  520. }
  521. }
  522. }
  523. }
  524. </script>
  525. <style scoped>
  526. /* ============================================================
  527. 页面容器
  528. ============================================================ */
  529. .page {
  530. min-height: 100vh;
  531. background: var(--bg, #F5F9FC);
  532. padding-bottom: 180rpx;
  533. }
  534. /* ============================================================
  535. 积分卡片
  536. ============================================================ */
  537. .points-card {
  538. position: relative;
  539. margin: 24rpx 32rpx 16rpx;
  540. border-radius: 32rpx;
  541. overflow: hidden;
  542. }
  543. .points-card__glow {
  544. position: absolute;
  545. top: -50%;
  546. right: -30%;
  547. width: 300rpx;
  548. height: 300rpx;
  549. background: radial-gradient(circle, rgba(91, 155, 213, 0.15), transparent 70%);
  550. pointer-events: none;
  551. }
  552. .points-card__inner {
  553. position: relative;
  554. padding: 32rpx 36rpx 28rpx;
  555. border-radius: 32rpx;
  556. border: 4rpx solid var(--border, #CBD5E1);
  557. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  558. box-shadow:
  559. inset -4rpx -4rpx 12rpx rgba(91, 155, 213, 0.05),
  560. 6rpx 6rpx 18rpx rgba(91, 155, 213, 0.10);
  561. }
  562. .points-card__header {
  563. display: flex;
  564. align-items: center;
  565. margin-bottom: 12rpx;
  566. }
  567. .points-card__icon {
  568. font-size: 36rpx;
  569. margin-right: 12rpx;
  570. }
  571. .points-card__label {
  572. font-size: 28rpx;
  573. font-weight: 600;
  574. color: var(--text-secondary, #64748B);
  575. }
  576. .points-card__value {
  577. display: block;
  578. font-size: 72rpx;
  579. font-weight: 800;
  580. color: var(--color-primary, #5B9BD5);
  581. line-height: 1.1;
  582. margin-bottom: 20rpx;
  583. letter-spacing: -2rpx;
  584. }
  585. /* 里程碑进度条 */
  586. .milestone {
  587. display: flex;
  588. flex-direction: column;
  589. }
  590. .milestone__track {
  591. height: 12rpx;
  592. background: var(--border-light, #E2E8F0);
  593. border-radius: 999rpx;
  594. overflow: hidden;
  595. margin-bottom: 12rpx;
  596. }
  597. .milestone__fill {
  598. height: 100%;
  599. border-radius: 999rpx;
  600. background: linear-gradient(90deg, var(--color-primary-light, #8FC5E8), var(--color-primary, #5B9BD5));
  601. transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
  602. }
  603. .milestone__text {
  604. font-size: 24rpx;
  605. color: var(--text-secondary, #64748B);
  606. line-height: 1.5;
  607. }
  608. /* ============================================================
  609. 筛选标签
  610. ============================================================ */
  611. .filter-tabs {
  612. margin: 8rpx 0 16rpx;
  613. white-space: nowrap;
  614. padding: 0 24rpx;
  615. }
  616. .filter-tabs__inner {
  617. display: flex;
  618. gap: 16rpx;
  619. padding: 4rpx 8rpx;
  620. }
  621. .filter-tab {
  622. position: relative;
  623. display: inline-flex;
  624. align-items: center;
  625. padding: 16rpx 28rpx;
  626. border-radius: 999rpx;
  627. background: var(--surface, #FFFFFF);
  628. border: 2rpx solid var(--border-light, #E2E8F0);
  629. font-size: 26rpx;
  630. font-weight: 500;
  631. color: var(--text-secondary, #64748B);
  632. transition: all 0.25s ease;
  633. flex-shrink: 0;
  634. }
  635. .filter-tab--active {
  636. background: var(--color-primary, #5B9BD5);
  637. border-color: var(--color-primary, #5B9BD5);
  638. color: #FFFFFF;
  639. box-shadow: 0 4rpx 16rpx rgba(91, 155, 213, 0.25);
  640. }
  641. .filter-tab:active {
  642. transform: scale(0.96);
  643. }
  644. .filter-tab__count {
  645. display: inline-flex;
  646. align-items: center;
  647. justify-content: center;
  648. min-width: 32rpx;
  649. height: 32rpx;
  650. padding: 0 8rpx;
  651. margin-left: 8rpx;
  652. border-radius: 999rpx;
  653. background: rgba(255, 255, 255, 0.25);
  654. color: inherit;
  655. font-size: 20rpx;
  656. font-weight: 700;
  657. }
  658. .filter-tab--active .filter-tab__count {
  659. background: rgba(255, 255, 255, 0.3);
  660. color: #FFFFFF;
  661. }
  662. /* ============================================================
  663. 心愿列表
  664. ============================================================ */
  665. .wish-list {
  666. padding: 8rpx 32rpx 0;
  667. }
  668. .wish-item {
  669. margin-bottom: 24rpx;
  670. }
  671. .wish-list__spacer {
  672. height: 160rpx;
  673. }
  674. /* ============================================================
  675. 心愿卡片(内部布局)
  676. ============================================================ */
  677. .wish-card {
  678. display: flex;
  679. align-items: stretch;
  680. padding: 24rpx;
  681. min-height: 160rpx;
  682. }
  683. /* 左侧 emoji 区域 */
  684. .wish-card__icon-wrap {
  685. display: flex;
  686. align-items: center;
  687. justify-content: center;
  688. width: 96rpx;
  689. min-height: 120rpx;
  690. margin-right: 20rpx;
  691. flex-shrink: 0;
  692. }
  693. .wish-card__emoji {
  694. font-size: 56rpx;
  695. line-height: 1;
  696. }
  697. /* 中间内容区域 */
  698. .wish-card__body {
  699. flex: 1;
  700. display: flex;
  701. flex-direction: column;
  702. justify-content: center;
  703. min-width: 0;
  704. padding-right: 8rpx;
  705. }
  706. .wish-card__top {
  707. display: flex;
  708. align-items: center;
  709. justify-content: space-between;
  710. margin-bottom: 8rpx;
  711. gap: 12rpx;
  712. }
  713. .wish-card__title {
  714. font-size: 32rpx;
  715. font-weight: 700;
  716. color: var(--text, #1E293B);
  717. flex: 1;
  718. overflow: hidden;
  719. text-overflow: ellipsis;
  720. white-space: nowrap;
  721. line-height: 1.4;
  722. }
  723. /* 积分行 */
  724. .wish-card__points-row {
  725. display: flex;
  726. align-items: baseline;
  727. margin-bottom: 12rpx;
  728. gap: 6rpx;
  729. }
  730. .wish-card__points-label {
  731. font-size: 24rpx;
  732. color: var(--text-secondary, #64748B);
  733. }
  734. .wish-card__points-value {
  735. font-size: 40rpx;
  736. font-weight: 800;
  737. color: var(--color-accent, #0EA5E9);
  738. line-height: 1;
  739. }
  740. .wish-card__points-unit {
  741. font-size: 28rpx;
  742. color: var(--color-accent, #0EA5E9);
  743. font-weight: 600;
  744. }
  745. /* 操作区域 */
  746. .wish-card__actions {
  747. display: flex;
  748. align-items: center;
  749. }
  750. /* 状态文本 */
  751. .wish-card__status-text {
  752. display: flex;
  753. align-items: center;
  754. gap: 8rpx;
  755. }
  756. .wish-card__status-emoji {
  757. font-size: 28rpx;
  758. }
  759. .wish-card__status-msg {
  760. font-size: 24rpx;
  761. font-weight: 500;
  762. color: var(--text-secondary, #64748B);
  763. }
  764. .wish-card__status-text--warning .wish-card__status-msg {
  765. color: var(--color-warning, #F59E0B);
  766. }
  767. .wish-card__status-text--info .wish-card__status-msg {
  768. color: var(--color-accent, #0EA5E9);
  769. }
  770. .wish-card__status-text--success .wish-card__status-msg {
  771. color: var(--color-success, #22C55E);
  772. }
  773. .wish-card__status-text--error .wish-card__status-msg {
  774. color: var(--color-error, #EF4444);
  775. }
  776. /* 右侧删除按钮 */
  777. .wish-card__delete {
  778. display: flex;
  779. align-items: center;
  780. justify-content: center;
  781. width: 48rpx;
  782. flex-shrink: 0;
  783. margin-left: 8rpx;
  784. }
  785. .wish-card__delete-icon {
  786. font-size: 28rpx;
  787. color: var(--muted, #94A3B8);
  788. padding: 12rpx;
  789. }
  790. .wish-card__delete-icon:active {
  791. opacity: 0.5;
  792. }
  793. /* ============================================================
  794. 浮动操作按钮 (FAB)
  795. ============================================================ */
  796. .fab {
  797. position: fixed;
  798. right: 40rpx;
  799. bottom: 80rpx;
  800. width: 110rpx;
  801. height: 110rpx;
  802. border-radius: 50%;
  803. display: flex;
  804. align-items: center;
  805. justify-content: center;
  806. background: linear-gradient(145deg, var(--color-primary-light, #8FC5E8), var(--color-primary, #5B9BD5));
  807. border: 4rpx solid var(--border, #CBD5E1);
  808. box-shadow:
  809. inset -3rpx -3rpx 10rpx rgba(255, 255, 255, 0.3),
  810. 4rpx 4rpx 16rpx rgba(91, 155, 213, 0.3);
  811. z-index: 100;
  812. transition: transform 0.15s ease;
  813. }
  814. .fab:active {
  815. transform: scale(0.92);
  816. }
  817. .fab__pulse {
  818. position: absolute;
  819. inset: -8rpx;
  820. border-radius: 50%;
  821. background: rgba(91, 155, 213, 0.15);
  822. animation: pulseGlow 2s ease-in-out infinite;
  823. pointer-events: none;
  824. }
  825. .fab__icon {
  826. font-size: 56rpx;
  827. color: #FFFFFF;
  828. font-weight: 300;
  829. line-height: 1;
  830. margin-top: -4rpx;
  831. }
  832. /* ============================================================
  833. 添加心愿弹窗
  834. ============================================================ */
  835. .modal-overlay {
  836. position: fixed;
  837. inset: 0;
  838. background: rgba(0, 0, 0, 0.45);
  839. z-index: 999;
  840. display: flex;
  841. align-items: flex-end;
  842. justify-content: center;
  843. animation: fadeIn 0.2s ease both;
  844. }
  845. .modal {
  846. width: 100%;
  847. max-height: 80vh;
  848. background: var(--surface, #FFFFFF);
  849. border-radius: 40rpx 40rpx 0 0;
  850. padding: 40rpx 36rpx;
  851. padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
  852. animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
  853. overflow-y: auto;
  854. }
  855. .modal__header {
  856. display: flex;
  857. align-items: center;
  858. justify-content: space-between;
  859. margin-bottom: 36rpx;
  860. }
  861. .modal__title {
  862. font-size: 36rpx;
  863. font-weight: 700;
  864. color: var(--text, #1E293B);
  865. }
  866. .modal__close {
  867. font-size: 36rpx;
  868. color: var(--muted, #94A3B8);
  869. padding: 12rpx;
  870. min-width: 60rpx;
  871. text-align: center;
  872. }
  873. .modal__close:active {
  874. opacity: 0.5;
  875. }
  876. /* 表单组 */
  877. .form-group {
  878. margin-bottom: 32rpx;
  879. }
  880. .form-group__label {
  881. display: block;
  882. font-size: 26rpx;
  883. font-weight: 600;
  884. color: var(--text, #1E293B);
  885. margin-bottom: 16rpx;
  886. }
  887. /* 分类/表情选择器 */
  888. .emoji-grid {
  889. display: grid;
  890. grid-template-columns: repeat(3, 1fr);
  891. gap: 16rpx;
  892. }
  893. .emoji-item {
  894. display: flex;
  895. flex-direction: column;
  896. align-items: center;
  897. padding: 20rpx 12rpx;
  898. border-radius: 20rpx;
  899. background: var(--bg, #F5F9FC);
  900. border: 2rpx solid var(--border-light, #E2E8F0);
  901. transition: all 0.2s ease;
  902. }
  903. .emoji-item--active {
  904. background: linear-gradient(145deg, #FFFFFF, var(--bg-grey, #EDF2F7));
  905. border-color: var(--color-primary, #5B9BD5);
  906. box-shadow: 0 4rpx 12rpx rgba(91, 155, 213, 0.15);
  907. }
  908. .emoji-item:active {
  909. transform: scale(0.95);
  910. }
  911. .emoji-item__icon {
  912. font-size: 48rpx;
  913. margin-bottom: 8rpx;
  914. line-height: 1;
  915. }
  916. .emoji-item__label {
  917. font-size: 24rpx;
  918. color: var(--text-secondary, #64748B);
  919. font-weight: 500;
  920. }
  921. .emoji-item--active .emoji-item__label {
  922. color: var(--color-primary, #5B9BD5);
  923. font-weight: 600;
  924. }
  925. /* 输入框 */
  926. .form-input {
  927. width: 100%;
  928. background: var(--bg, #F5F9FC);
  929. border: 2rpx solid var(--border-light, #E2E8F0);
  930. border-radius: 20rpx;
  931. padding: 24rpx 28rpx;
  932. font-size: 30rpx;
  933. color: var(--text, #1E293B);
  934. box-sizing: border-box;
  935. }
  936. .form-input--number {
  937. padding-right: 80rpx;
  938. }
  939. .form-input__wrapper {
  940. position: relative;
  941. }
  942. .form-input__suffix {
  943. position: absolute;
  944. right: 28rpx;
  945. top: 50%;
  946. transform: translateY(-50%);
  947. font-size: 32rpx;
  948. color: var(--color-accent, #0EA5E9);
  949. font-weight: 700;
  950. }
  951. .form-input__counter {
  952. display: block;
  953. text-align: right;
  954. font-size: 22rpx;
  955. color: var(--muted, #94A3B8);
  956. margin-top: 8rpx;
  957. }
  958. </style>