index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. <template>
  2. <!-- 优惠券页面 -->
  3. <view class="page">
  4. <!-- 主 Tab + 右侧「全部」类型筛选 -->
  5. <view class="tabs-wrap">
  6. <view class="tabs-row">
  7. <view class="tabs-main">
  8. <view
  9. v-for="(tab, index) in tabs"
  10. :key="index"
  11. :class="['tab-item', { 'tab-item--active': currentTab === index }]"
  12. @click="handleTabChange(index)"
  13. >
  14. {{ tab }}
  15. </view>
  16. </view>
  17. <view class="tabs-trigger-gap" />
  18. <view class="tabs-trigger" @click="toggleTypePanel">
  19. <text
  20. :class="[
  21. 'tabs-trigger__text',
  22. {
  23. 'tabs-trigger__text--open': typePanelOpen,
  24. 'tabs-trigger__text--filtered': !typePanelOpen && typeFilter !== 'all'
  25. }
  26. ]"
  27. >{{ typeTriggerLabel }}</text>
  28. <view
  29. :class="['tabs-trigger__chevron', { 'tabs-trigger__chevron--open': typePanelOpen }]"
  30. />
  31. </view>
  32. </view>
  33. <view v-if="typePanelOpen" class="type-filter-row">
  34. <view
  35. v-for="opt in typeFilterOptions"
  36. :key="opt.value"
  37. :class="['type-pill', { 'type-pill--active': typeFilter === opt.value }]"
  38. @click="selectTypeFilter(opt.value)"
  39. >
  40. {{ opt.label }}
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 列表区域:展开类型筛选时盖半透明遮罩,点击关闭 -->
  45. <view class="page-body">
  46. <view v-if="typePanelOpen" class="type-mask" @tap="closeTypePanel" />
  47. <scroll-view class="content" scroll-y :class="{ 'content--dimmed': typePanelOpen }">
  48. <view class="coupon-list" v-if="filteredCoupons.length > 0">
  49. <view v-for="(coupon, index) in filteredCoupons" :key="coupon.id || index"
  50. :class="['coupon-card', getCouponCardClass(coupon)]">
  51. <image :src="getFileUrl('img/personal/coupon.png')" mode="widthFix" class="coupon-card-bg"></image>
  52. <image :src="getFileUrl('img/personal/couponLeft.png')" mode="heightFix" class="coupon-card-bgLeft"></image>
  53. <view class="coupon-card-content">
  54. <!-- 左侧金额区域 -->
  55. <view class="coupon-card__left">
  56. <view class="amount-wrapper">
  57. <text class="amount-number">{{ coupon.amount }}</text>
  58. <text class="amount-unit">{{ coupon.amountUnit || '元' }}</text>
  59. </view>
  60. <text class="condition-text">{{ coupon.conditionText || (coupon.minAmount > 0 ? '满' + coupon.minAmount + '可用' : '无门槛') }}</text>
  61. </view>
  62. <!-- 右侧信息区域 -->
  63. <view class="coupon-card__right">
  64. <view class="coupon-info">
  65. <text class="coupon-name">{{ coupon.name }}</text>
  66. <view class="coupon-rules" @click="handleShowRules(coupon)">
  67. 使用规则
  68. <text class="arrow">›</text>
  69. </view>
  70. <text class="coupon-expire">{{ coupon.expireDate ? coupon.expireDate + '到期' : '' }}</text>
  71. </view>
  72. </view>
  73. </view>
  74. </view>
  75. </view>
  76. <!-- 空状态 -->
  77. <view class="empty-state" v-else>
  78. <image :src="getFileUrl('img/icon/noCoupon.png')" mode="widthFix" class="empty-icon"></image>
  79. <text class="empty-text">暂无优惠券</text>
  80. </view>
  81. </scroll-view>
  82. </view>
  83. <!-- 使用规则弹窗 -->
  84. <RulesModal v-model:open="showRulesModal" :couponData="selectedCoupon" />
  85. </view>
  86. </template>
  87. <script setup>
  88. import { onShow } from "@dcloudio/uni-app";
  89. import { ref, computed } from "vue";
  90. import { getFileUrl } from "@/utils/file.js";
  91. import RulesModal from "./components/RulesModal.vue";
  92. import * as diningApi from "@/api/dining.js";
  93. // 标签页:0未使用 1即将过期 2已使用 3已过期
  94. const tabs = ['未使用', '即将过期', '已使用', '已过期'];
  95. const currentTab = ref(0);
  96. /** 右侧:展开折扣券 / 满减券筛选(couponType 1 满减 2 折扣) */
  97. const typePanelOpen = ref(false);
  98. const typeFilter = ref('all');
  99. const typeFilterOptions = [
  100. { value: 'all', label: '全部' },
  101. { value: 'discount', label: '折扣券' },
  102. { value: 'reduction', label: '满减券' }
  103. ];
  104. /** 右上角文案与当前选中的券类型一致 */
  105. const typeTriggerLabel = computed(() => {
  106. const hit = typeFilterOptions.find((o) => o.value === typeFilter.value);
  107. return hit?.label ?? '全部';
  108. });
  109. function toggleTypePanel() {
  110. typePanelOpen.value = !typePanelOpen.value;
  111. }
  112. function closeTypePanel() {
  113. typePanelOpen.value = false;
  114. }
  115. function selectTypeFilter(value) {
  116. typeFilter.value = value;
  117. // 仅在「切换券类型」流程中展示选项,选完后收起
  118. typePanelOpen.value = false;
  119. }
  120. // 弹窗控制
  121. const showRulesModal = ref(false);
  122. const selectedCoupon = ref({
  123. amount: 0,
  124. amountUnit: '元',
  125. minAmount: 0,
  126. name: '',
  127. expireDate: '',
  128. specifiedDay: '',
  129. supplementaryInstruction: '',
  130. conditionText: '',
  131. qrCodeUrl: '',
  132. verificationCode: ''
  133. });
  134. // 优惠券数据(接口返回)
  135. const couponList = ref([]);
  136. const loading = ref(false);
  137. // 规范化接口返回的优惠券项
  138. function normalizeCouponItem(raw) {
  139. if (!raw || typeof raw !== 'object') return null;
  140. const couponType = Number(raw.couponType) ?? 1;
  141. const nominalValue = Number(raw.nominalValue ?? raw.amount ?? 0) || 0;
  142. const discountRate = ((Number(raw.discountRate ?? 0) || 0) / 10) || 0;
  143. const minAmount = Number(raw.minimumSpendingAmount ?? raw.minAmount ?? 0) || 0;
  144. let amount = nominalValue;
  145. let amountUnit = '元';
  146. let conditionText = minAmount > 0 ? `满${minAmount}可用` : '无门槛';
  147. if (couponType === 2 && discountRate > 0) {
  148. amount = discountRate;
  149. amountUnit = '折';
  150. conditionText = minAmount > 0 ? `满${minAmount}可用` : '无门槛';
  151. }
  152. return {
  153. id: raw.id ?? raw.userCouponId ?? raw.couponId ?? '',
  154. amount,
  155. amountUnit,
  156. minAmount,
  157. name: raw.name ?? raw.title ?? raw.couponName ?? '',
  158. expireDate: raw.expirationTime ?? raw.endGetDate ?? raw.expireDate ?? '',
  159. status: currentTab.value,
  160. couponType,
  161. nominalValue,
  162. discountRate,
  163. conditionText,
  164. specifiedDay: raw.specifiedDay ?? raw.validDays ?? '',
  165. supplementaryInstruction: raw.supplementaryInstruction ?? raw.description ?? '',
  166. qrCodeUrl: raw.qrCodeUrl ?? raw.qrcodeUrl ?? raw.qrUrl ?? '',
  167. verificationCode:
  168. raw.verificationCode ?? raw.verifyCode ?? raw.couponCode ?? raw.pickUpCode ?? ''
  169. };
  170. }
  171. // 列表:主 Tab 数据 + 类型筛选(全部 / 折扣券 / 满减券)
  172. const filteredCoupons = computed(() => {
  173. const list = couponList.value;
  174. const t = typeFilter.value;
  175. if (t === 'discount') return list.filter((c) => Number(c.couponType) === 2);
  176. if (t === 'reduction') return list.filter((c) => Number(c.couponType) === 1);
  177. return list;
  178. });
  179. // 切换标签页
  180. const handleTabChange = (index) => {
  181. currentTab.value = index;
  182. typePanelOpen.value = false;
  183. fetchCouponList();
  184. };
  185. // 拉取优惠券列表(coupon/getUserCouponList)
  186. const fetchCouponList = async () => {
  187. loading.value = true;
  188. try {
  189. const storeId = uni.getStorageSync('currentStoreId') || '';
  190. const res = await diningApi.GetUserCouponList({ storeId, tabType: currentTab.value, page: 1, size: 20 });
  191. const list = Array.isArray(res) ? res : (res?.data ?? res?.records ?? res?.list ?? []);
  192. const arr = Array.isArray(list) ? list : [];
  193. couponList.value = arr.map(normalizeCouponItem).filter(Boolean);
  194. } catch (err) {
  195. console.error('获取优惠券列表失败:', err);
  196. uni.showToast({ title: '加载失败', icon: 'none' });
  197. couponList.value = [];
  198. } finally {
  199. loading.value = false;
  200. }
  201. };
  202. // 获取优惠券卡片样式类
  203. const getCouponCardClass = (coupon) => {
  204. if (coupon?.status === 2) return 'coupon-card--used';
  205. if (coupon?.status === 3) return 'coupon-card--expired';
  206. return '';
  207. };
  208. // 查看使用规则
  209. const handleShowRules = (coupon) => {
  210. selectedCoupon.value = { ...coupon };
  211. showRulesModal.value = true;
  212. };
  213. // 使用 onShow 拉取数据(onLoad 在 uni-app Vue3 组合式 API 下可能不触发,onShow 更可靠)
  214. onShow(() => {
  215. fetchCouponList();
  216. });
  217. </script>
  218. <style lang="scss" scoped>
  219. .page {
  220. height: 100vh;
  221. min-height: 100vh;
  222. background: #F5F5F5;
  223. display: flex;
  224. flex-direction: column;
  225. overflow: hidden;
  226. }
  227. .header {
  228. background: #FFFFFF;
  229. padding: 20rpx 30rpx;
  230. padding-top: calc(20rpx + env(safe-area-inset-top));
  231. .header-title {
  232. font-size: 36rpx;
  233. font-weight: bold;
  234. color: #151515;
  235. text-align: center;
  236. }
  237. }
  238. .tabs-wrap {
  239. flex-shrink: 0;
  240. background: #ffffff;
  241. position: relative;
  242. z-index: 20;
  243. }
  244. .page-body {
  245. flex: 1;
  246. min-height: 0;
  247. position: relative;
  248. display: flex;
  249. flex-direction: column;
  250. overflow: hidden;
  251. }
  252. .type-mask {
  253. position: absolute;
  254. left: 0;
  255. right: 0;
  256. top: 0;
  257. bottom: 0;
  258. z-index: 10;
  259. background: rgba(0, 0, 0, 0.45);
  260. }
  261. .tabs-row {
  262. display: flex;
  263. flex-direction: row;
  264. align-items: flex-end;
  265. padding: 0 24rpx 0 20rpx;
  266. box-sizing: border-box;
  267. }
  268. .tabs-main {
  269. flex: 1;
  270. min-width: 0;
  271. display: flex;
  272. flex-direction: row;
  273. align-items: flex-end;
  274. }
  275. .tabs-trigger-gap {
  276. flex-shrink: 0;
  277. width: 24rpx;
  278. }
  279. .tabs-trigger {
  280. flex-shrink: 0;
  281. display: flex;
  282. flex-direction: row;
  283. align-items: center;
  284. justify-content: flex-end;
  285. padding: 28rpx 0 24rpx 8rpx;
  286. box-sizing: border-box;
  287. }
  288. .tabs-trigger__text {
  289. font-size: 26rpx;
  290. color: #151515;
  291. line-height: 1;
  292. }
  293. .tabs-trigger__text--open {
  294. color: #f47d1f;
  295. }
  296. /* 已选折扣券/满减券且面板收起时,右上角保持主题色提示当前筛选 */
  297. .tabs-trigger__text--filtered {
  298. color: #f47d1f;
  299. }
  300. .tabs-trigger__chevron {
  301. width: 0;
  302. height: 0;
  303. margin-left: 8rpx;
  304. border-left: 8rpx solid transparent;
  305. border-right: 8rpx solid transparent;
  306. border-top: 10rpx solid #999999;
  307. transform: translateY(2rpx);
  308. }
  309. .tabs-trigger__chevron--open {
  310. border-top: none;
  311. border-bottom: 10rpx solid #f47d1f;
  312. border-left: 8rpx solid transparent;
  313. border-right: 8rpx solid transparent;
  314. transform: translateY(-2rpx);
  315. }
  316. .type-filter-row {
  317. display: flex;
  318. flex-direction: row;
  319. align-items: center;
  320. flex-wrap: wrap;
  321. gap: 16rpx;
  322. padding: 8rpx 30rpx 20rpx;
  323. box-sizing: border-box;
  324. background: #ffffff;
  325. }
  326. .type-pill {
  327. padding: 14rpx 32rpx;
  328. border-radius: 999rpx;
  329. font-size: 24rpx;
  330. color: #333333;
  331. background: #f2f3f5;
  332. line-height: 1.2;
  333. }
  334. .type-pill--active {
  335. background: #fff4e6;
  336. color: #f47d1f;
  337. font-weight: 500;
  338. }
  339. .tab-item {
  340. flex: 1;
  341. min-width: 0;
  342. text-align: center;
  343. font-size: 26rpx;
  344. color: #666666;
  345. padding: 28rpx 4rpx 24rpx;
  346. position: relative;
  347. transition: color 0.2s;
  348. box-sizing: border-box;
  349. &--active {
  350. color: #151515;
  351. font-weight: bold;
  352. &::after {
  353. content: '';
  354. position: absolute;
  355. bottom: 16rpx;
  356. left: 50%;
  357. transform: translateX(-50%);
  358. width: 48rpx;
  359. height: 6rpx;
  360. background: linear-gradient(90deg, #ff8a57 0%, #f47d1f 100%);
  361. border-radius: 3rpx;
  362. }
  363. }
  364. }
  365. .content {
  366. flex: 1;
  367. min-height: 0;
  368. position: relative;
  369. z-index: 1;
  370. padding: 24rpx 30rpx;
  371. padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
  372. box-sizing: border-box;
  373. }
  374. .content--dimmed {
  375. pointer-events: none;
  376. }
  377. .coupon-list {
  378. position: relative;
  379. .coupon-card-bg {
  380. position: absolute;
  381. top: 0;
  382. left: 0;
  383. width: 100%;
  384. height: 100%;
  385. z-index: 1;
  386. }
  387. .coupon-card-bgLeft {
  388. position: absolute;
  389. top: 0;
  390. left: 0;
  391. width: auto;
  392. height: 190rpx;
  393. z-index: 1;
  394. }
  395. .coupon-card-content {
  396. position: relative;
  397. z-index: 3;
  398. display: flex;
  399. align-items: center;
  400. }
  401. .coupon-card {
  402. display: flex;
  403. align-items: center;
  404. margin-bottom: 24rpx;
  405. overflow: hidden;
  406. position: relative;
  407. box-sizing: border-box;
  408. position: relative;
  409. z-index: 3;
  410. &:last-child {
  411. margin-bottom: 0;
  412. }
  413. &__left {
  414. width: 200rpx;
  415. padding: 32rpx 0;
  416. display: flex;
  417. flex-direction: column;
  418. align-items: center;
  419. justify-content: center;
  420. .amount-wrapper {
  421. display: flex;
  422. align-items: baseline;
  423. margin-bottom: 8rpx;
  424. .amount-number {
  425. font-size: 64rpx;
  426. font-weight: bold;
  427. color: #F47D1F;
  428. line-height: 1;
  429. }
  430. .amount-unit {
  431. font-size: 28rpx;
  432. color: #F47D1F;
  433. margin-left: 4rpx;
  434. }
  435. }
  436. .condition-text {
  437. font-size: 22rpx;
  438. color: #F47D1F;
  439. }
  440. }
  441. &__divider {
  442. width: 2rpx;
  443. height: 100%;
  444. position: relative;
  445. .dash-line {
  446. width: 2rpx;
  447. height: 100%;
  448. // background-image: linear-gradient(to bottom, #FFD9C2 0%, #FFD9C2 50%, transparent 50%, transparent 100%);
  449. background-size: 2rpx 12rpx;
  450. background-repeat: repeat-y;
  451. }
  452. }
  453. &__right {
  454. flex: 1;
  455. display: flex;
  456. align-items: center;
  457. justify-content: space-between;
  458. padding: 32rpx 24rpx;
  459. .coupon-info {
  460. flex: 1;
  461. display: flex;
  462. flex-direction: column;
  463. .coupon-name {
  464. font-size: 28rpx;
  465. font-weight: bold;
  466. color: #151515;
  467. margin-bottom: 12rpx;
  468. }
  469. .coupon-rules {
  470. font-size: 22rpx;
  471. color: #999999;
  472. margin-bottom: 12rpx;
  473. display: flex;
  474. align-items: center;
  475. .arrow {
  476. font-size: 28rpx;
  477. margin-left: 4rpx;
  478. }
  479. }
  480. .coupon-expire {
  481. font-size: 22rpx;
  482. color: #999999;
  483. }
  484. }
  485. }
  486. // 已使用状态
  487. &--used {
  488. background: #F8F8F8;
  489. .coupon-card__left {
  490. .amount-number,
  491. .amount-unit,
  492. .condition-text {
  493. color: #CCCCCC;
  494. }
  495. }
  496. .coupon-card__right {
  497. .coupon-info {
  498. .coupon-name,
  499. .coupon-rules,
  500. .coupon-expire {
  501. color: #CCCCCC;
  502. }
  503. }
  504. }
  505. }
  506. // 已过期状态
  507. &--expired {
  508. background: #F8F8F8;
  509. .coupon-card__left {
  510. .amount-number,
  511. .amount-unit,
  512. .condition-text {
  513. color: #CCCCCC;
  514. }
  515. }
  516. .coupon-card__right {
  517. .coupon-info {
  518. .coupon-name,
  519. .coupon-rules,
  520. .coupon-expire {
  521. color: #CCCCCC;
  522. }
  523. }
  524. }
  525. }
  526. }
  527. }
  528. .hover-active {
  529. opacity: 0.8;
  530. transform: scale(0.98);
  531. }
  532. .empty-state {
  533. display: flex;
  534. flex-direction: column;
  535. align-items: center;
  536. justify-content: center;
  537. padding-top: 200rpx;
  538. .empty-icon {
  539. width: 300rpx;
  540. height: 280rpx;
  541. margin-bottom: 40rpx;
  542. }
  543. .empty-text {
  544. font-size: 28rpx;
  545. color: #AAAAAA;
  546. }
  547. }
  548. </style>