benefits.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <template>
  2. <scroll-view class="container" scroll-y :style="{ '--font-scale': fontScale }">
  3. <view class="header">
  4. <text class="header-title">我的权益</text>
  5. <text class="header-subtitle">Subscription Benefits</text>
  6. </view>
  7. <view class="subscription-status" v-if="subscription && subscription.level">
  8. <view class="status-badge" :class="'level-' + subscription.level.toLowerCase()">
  9. <text class="badge-icon">{{ levelIcon }}</text>
  10. <text class="badge-text">{{ levelName }}</text>
  11. </view>
  12. <view class="status-info" v-if="subscription.expireTime">
  13. <text class="expire-label">有效期至</text>
  14. <text class="expire-value">{{ formatDate(subscription.expireTime) }}</text>
  15. </view>
  16. </view>
  17. <view class="benefits-tabs">
  18. <view
  19. class="tab-item"
  20. :class="activeTab === 'available' ? 'tab-active' : ''"
  21. @click="activeTab = 'available'"
  22. >
  23. <text>可用权益</text>
  24. <text class="tab-count">{{ availableCount }}</text>
  25. </view>
  26. <view
  27. class="tab-item"
  28. :class="activeTab === 'used' ? 'tab-active' : ''"
  29. @click="activeTab = 'used'"
  30. >
  31. <text>已使用</text>
  32. <text class="tab-count">{{ usedCount }}</text>
  33. </view>
  34. </view>
  35. <view class="benefits-list" v-if="activeTab === 'available'">
  36. <view class="benefit-card" v-for="(item, idx) in availableBenefits" :key="idx">
  37. <view class="benefit-header">
  38. <view class="benefit-icon-wrap" :style="'background:' + item.bg">
  39. <text class="benefit-emoji">{{ item.icon }}</text>
  40. </view>
  41. <view class="benefit-info">
  42. <text class="benefit-name">{{ item.name }}</text>
  43. <text class="benefit-desc">{{ item.desc }}</text>
  44. </view>
  45. <view class="benefit-status available">
  46. <text>可用</text>
  47. </view>
  48. </view>
  49. <view class="benefit-detail" v-if="item.detail">
  50. <text class="detail-text">{{ item.detail }}</text>
  51. </view>
  52. </view>
  53. <view class="empty-state" v-if="availableBenefits.length === 0">
  54. <text class="empty-icon">📦</text>
  55. <text class="empty-text">暂无可用权益</text>
  56. <text class="empty-hint">开通会员解锁更多权益</text>
  57. </view>
  58. </view>
  59. <view class="benefits-list" v-if="activeTab === 'used'">
  60. <view class="benefit-card used" v-for="(item, idx) in usedBenefits" :key="idx">
  61. <view class="benefit-header">
  62. <view class="benefit-icon-wrap" :style="'background:' + item.bg">
  63. <text class="benefit-emoji">{{ item.icon }}</text>
  64. </view>
  65. <view class="benefit-info">
  66. <text class="benefit-name">{{ item.name }}</text>
  67. <text class="benefit-desc">{{ item.desc }}</text>
  68. </view>
  69. <view class="benefit-status used">
  70. <text>已使用</text>
  71. </view>
  72. </view>
  73. <view class="benefit-used-info">
  74. <text class="used-time">使用时间:{{ formatDate(item.usedAt) }}</text>
  75. </view>
  76. </view>
  77. <view class="empty-state" v-if="usedBenefits.length === 0">
  78. <text class="empty-icon">✅</text>
  79. <text class="empty-text">暂无使用记录</text>
  80. </view>
  81. </view>
  82. <view class="benefits-summary">
  83. <text class="summary-title">权益使用统计</text>
  84. <view class="summary-stats">
  85. <view class="stat-item">
  86. <text class="stat-value">{{ totalBenefits }}</text>
  87. <text class="stat-label">总权益数</text>
  88. </view>
  89. <view class="stat-divider"></view>
  90. <view class="stat-item">
  91. <text class="stat-value">{{ availableCount }}</text>
  92. <text class="stat-label">可用</text>
  93. </view>
  94. <view class="stat-divider"></view>
  95. <view class="stat-item">
  96. <text class="stat-value">{{ usedCount }}</text>
  97. <text class="stat-label">已用</text>
  98. </view>
  99. </view>
  100. </view>
  101. </scroll-view>
  102. </template>
  103. <script>
  104. import { getMyMembership, getMembershipBenefits } from '../../utils/api.js'
  105. import { parseDate } from '../../utils/format.js'
  106. import fontScaleMixin from '../../mixins/font-scale.js'
  107. export default {
  108. mixins: [fontScaleMixin],
  109. data() {
  110. return {
  111. subscription: null,
  112. activeTab: 'available',
  113. benefits: []
  114. }
  115. },
  116. computed: {
  117. levelIcon() {
  118. if (!this.subscription || !this.subscription.level) return '○'
  119. var map = { BASIC: '🧡', FAMILY: '👑', PREMIUM: '💎', LIFETIME: '🌟' }
  120. return map[this.subscription.level] || '○'
  121. },
  122. levelName() {
  123. if (!this.subscription || !this.subscription.level) return '未开通'
  124. var map = { FREE: '免费用户', BASIC: '体验会员', FAMILY: '家庭会员', PREMIUM: '高级会员', LIFETIME: '终身会员' }
  125. return map[this.subscription.level] || this.subscription.level
  126. },
  127. availableBenefits() {
  128. return this.benefits.filter(function(b) { return !b.used })
  129. },
  130. usedBenefits() {
  131. return this.benefits.filter(function(b) { return b.used })
  132. },
  133. availableCount() {
  134. return this.availableBenefits.length
  135. },
  136. usedCount() {
  137. return this.usedBenefits.length
  138. },
  139. totalBenefits() {
  140. return this.benefits.length
  141. }
  142. },
  143. onLoad() {
  144. this.loadData()
  145. },
  146. methods: {
  147. // 读取当前用户实际有效会员等级(getMyMembership 返回 memberLevel=个人与家庭身份取高者),
  148. // 免费用户展示体验会员权益(与会员中心 index.vue 一致)。
  149. loadData() {
  150. var self = this
  151. getMyMembership().then(function(res) {
  152. var m = res.data || {}
  153. var levelCode = m.memberLevel || m.levelCode || 'FREE'
  154. if (levelCode !== 'FREE') {
  155. self.subscription = {
  156. level: levelCode,
  157. expireTime: m.memberExpireTime || (m.membership && m.membership.endDate) || null
  158. }
  159. self.loadBenefits(levelCode)
  160. } else {
  161. self.subscription = { level: 'FREE', expireTime: null }
  162. self.loadBenefits('BASIC')
  163. }
  164. }).catch(function() {
  165. self.subscription = { level: 'FREE', expireTime: null }
  166. self.loadBenefits('BASIC')
  167. })
  168. },
  169. loadBenefits: function(levelCode) {
  170. var self = this
  171. getMembershipBenefits(levelCode).then(function(res) {
  172. var benefitsData = (res && res.data) || []
  173. if (benefitsData.length > 0) {
  174. self.benefits = benefitsData.map(function(b) {
  175. return {
  176. code: b.benefitCode,
  177. name: b.benefitName,
  178. desc: b.benefitDesc || '',
  179. icon: b.icon || '🎁',
  180. bg: b.bgColor || '#E0E7FF',
  181. canUse: false, // membership_benefits 为展示型权益,无单独使用入口
  182. used: false,
  183. usedAt: null,
  184. detail: b.benefitDesc || ''
  185. }
  186. }).filter(function(b) { return b.code !== 'NO_BENEFIT' && b.code !== 'UPGRADE_HINT' })
  187. } else {
  188. self.benefits = []
  189. }
  190. }).catch(function(e) {
  191. console.error('加载权益失败', e)
  192. self.benefits = []
  193. })
  194. },
  195. formatDate(date) {
  196. if (!date) return ''
  197. var d = parseDate(date)
  198. if (!d) return ''
  199. var year = d.getFullYear()
  200. var month = String(d.getMonth() + 1).padStart(2, '0')
  201. var day = String(d.getDate()).padStart(2, '0')
  202. var hour = String(d.getHours()).padStart(2, '0')
  203. var minute = String(d.getMinutes()).padStart(2, '0')
  204. return year + '-' + month + '-' + day + ' ' + hour + ':' + minute
  205. }
  206. }
  207. }
  208. </script>
  209. <style scoped>
  210. .container {
  211. padding: 30rpx;
  212. min-height: 100vh;
  213. background: #FFF7ED;
  214. }
  215. .header {
  216. text-align: center;
  217. padding: 40rpx 0 30rpx;
  218. }
  219. .header-title {
  220. font-size: calc(40rpx * var(--font-scale, 1));
  221. font-weight: bold;
  222. color: #1E293B;
  223. display: block;
  224. }
  225. .header-subtitle {
  226. font-size: calc(24rpx * var(--font-scale, 1));
  227. color: #94A3B8;
  228. margin-top: 8rpx;
  229. display: block;
  230. }
  231. .subscription-status {
  232. background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
  233. border-radius: 20rpx;
  234. padding: 30rpx;
  235. margin-bottom: 30rpx;
  236. display: flex;
  237. justify-content: space-between;
  238. align-items: center;
  239. }
  240. .status-badge {
  241. display: flex;
  242. align-items: center;
  243. background: rgba(255,255,255,0.2);
  244. padding: 12rpx 24rpx;
  245. border-radius: 9999rpx;
  246. }
  247. .badge-icon {
  248. font-size: 28rpx;
  249. margin-right: 8rpx;
  250. }
  251. .badge-text {
  252. font-size: calc(26rpx * var(--font-scale, 1));
  253. color: #fff;
  254. font-weight: 600;
  255. }
  256. .status-info {
  257. text-align: right;
  258. }
  259. .expire-label {
  260. font-size: 22rpx;
  261. color: rgba(255,255,255,0.8);
  262. display: block;
  263. }
  264. .expire-value {
  265. font-size: 26rpx;
  266. color: #FEF3C7;
  267. font-weight: 600;
  268. }
  269. .benefits-tabs {
  270. display: flex;
  271. background: #fff;
  272. border-radius: 16rpx;
  273. padding: 8rpx;
  274. margin-bottom: 24rpx;
  275. box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
  276. }
  277. .tab-item {
  278. flex: 1;
  279. display: flex;
  280. justify-content: center;
  281. align-items: center;
  282. padding: 16rpx 0;
  283. border-radius: 12rpx;
  284. font-size: 28rpx;
  285. color: #64748B;
  286. }
  287. .tab-active {
  288. background: #F97316;
  289. color: #fff;
  290. font-weight: 600;
  291. }
  292. .tab-count {
  293. margin-left: 8rpx;
  294. font-size: 22rpx;
  295. background: rgba(255,255,255,0.3);
  296. padding: 2rpx 10rpx;
  297. border-radius: 9999rpx;
  298. }
  299. .benefits-list {
  300. margin-bottom: 30rpx;
  301. }
  302. .benefit-card {
  303. background: #fff;
  304. border-radius: 20rpx;
  305. padding: 24rpx;
  306. margin-bottom: 20rpx;
  307. box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
  308. }
  309. .benefit-card.used {
  310. opacity: 0.7;
  311. }
  312. .benefit-header {
  313. display: flex;
  314. align-items: center;
  315. }
  316. .benefit-icon-wrap {
  317. width: 72rpx;
  318. height: 72rpx;
  319. border-radius: 50%;
  320. display: flex;
  321. align-items: center;
  322. justify-content: center;
  323. margin-right: 20rpx;
  324. flex-shrink: 0;
  325. }
  326. .benefit-emoji {
  327. font-size: 36rpx;
  328. }
  329. .benefit-info {
  330. flex: 1;
  331. margin-right: 16rpx;
  332. }
  333. .benefit-name {
  334. font-size: calc(28rpx * var(--font-scale, 1));
  335. font-weight: 600;
  336. color: #1E293B;
  337. display: block;
  338. margin-bottom: 6rpx;
  339. }
  340. .benefit-desc {
  341. font-size: 24rpx;
  342. color: #94A3B8;
  343. display: block;
  344. }
  345. .benefit-status {
  346. padding: 8rpx 20rpx;
  347. border-radius: 9999rpx;
  348. font-size: 22rpx;
  349. font-weight: 600;
  350. }
  351. .benefit-status.available {
  352. background: #D1FAE5;
  353. color: #059669;
  354. }
  355. .benefit-status.used {
  356. background: #E5E7EB;
  357. color: #6B7280;
  358. }
  359. .benefit-detail {
  360. margin-top: 16rpx;
  361. padding-top: 16rpx;
  362. border-top: 1rpx solid #F1F5F9;
  363. }
  364. .detail-text {
  365. font-size: 24rpx;
  366. color: #64748B;
  367. line-height: 1.6;
  368. display: block;
  369. }
  370. .benefit-used-info {
  371. margin-top: 16rpx;
  372. padding-top: 16rpx;
  373. border-top: 1rpx solid #F1F5F9;
  374. }
  375. .used-time {
  376. font-size: 22rpx;
  377. color: #94A3B8;
  378. display: block;
  379. }
  380. .btn-use {
  381. margin-top: 16rpx;
  382. background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
  383. color: #fff;
  384. font-size: 26rpx;
  385. font-weight: 600;
  386. padding: 16rpx 0;
  387. border-radius: 12rpx;
  388. border: none;
  389. }
  390. .btn-use::after {
  391. border: none;
  392. }
  393. .empty-state {
  394. text-align: center;
  395. padding: 80rpx 0;
  396. }
  397. .empty-icon {
  398. font-size: 80rpx;
  399. display: block;
  400. margin-bottom: 20rpx;
  401. }
  402. .empty-text {
  403. font-size: 28rpx;
  404. color: #64748B;
  405. display: block;
  406. margin-bottom: 12rpx;
  407. }
  408. .empty-hint {
  409. font-size: 24rpx;
  410. color: #94A3B8;
  411. display: block;
  412. }
  413. .benefits-summary {
  414. background: #fff;
  415. border-radius: 20rpx;
  416. padding: 30rpx;
  417. box-shadow: 0 2rpx 12rpx rgba(249,115,22,0.08);
  418. }
  419. .summary-title {
  420. font-size: 28rpx;
  421. font-weight: 600;
  422. color: #1E293B;
  423. display: block;
  424. margin-bottom: 24rpx;
  425. }
  426. .summary-stats {
  427. display: flex;
  428. align-items: center;
  429. justify-content: space-around;
  430. }
  431. .stat-item {
  432. text-align: center;
  433. flex: 1;
  434. }
  435. .stat-value {
  436. font-size: 44rpx;
  437. font-weight: bold;
  438. color: #F97316;
  439. display: block;
  440. }
  441. .stat-label {
  442. font-size: 22rpx;
  443. color: #94A3B8;
  444. margin-top: 8rpx;
  445. display: block;
  446. }
  447. .stat-divider {
  448. width: 1rpx;
  449. height: 60rpx;
  450. background: #F1F5F9;
  451. }
  452. </style>