index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <template>
  2. <view class="member-page">
  3. <view v-if="loading" class="empty-tip">加载中…</view>
  4. <!-- 无会员:两档购买卡 -->
  5. <view v-else-if="member.status === 'none'">
  6. <view class="head-banner">
  7. <text class="head-title">年度会员</text>
  8. <text class="head-sub">一年免费沙龙 + 课程折扣,推荐立减 ¥100</text>
  9. </view>
  10. <view class="mcard experience-card">
  11. <view class="mcard-head">
  12. <text class="mcard-name">体验卡</text>
  13. <text class="mcard-price">¥<text class="price-num">365</text><text class="price-year">/年</text></text>
  14. </view>
  15. <view class="mcard-benefit" v-for="(b, idx) in experienceBenefits" :key="idx">
  16. <text class="benefit-dot">·</text>
  17. <text class="benefit-text">{{ b }}</text>
  18. </view>
  19. <text class="mcard-limit">限量 30 张 · 仅 L0 存量学员</text>
  20. <button class="buy-btn" :loading="buying" :disabled="buying" @click="buy('experience')">立即开通</button>
  21. </view>
  22. <view class="mcard companion-card">
  23. <view class="mcard-head">
  24. <text class="mcard-name">爱伴星伙</text>
  25. <text class="mcard-price">¥<text class="price-num">1280</text><text class="price-year">/年</text></text>
  26. </view>
  27. <view class="mcard-benefit" v-for="(b, idx) in companionBenefits" :key="idx">
  28. <text class="benefit-dot">·</text>
  29. <text class="benefit-text">{{ b }}</text>
  30. </view>
  31. <text class="mcard-limit">全年畅享 · 优先报名可携亲友</text>
  32. <button class="buy-btn" :loading="buying" :disabled="buying" @click="buy('companion')">立即开通</button>
  33. </view>
  34. </view>
  35. <!-- 有会员记录:状态卡 -->
  36. <view v-else class="has-member">
  37. <view class="status-card" :class="statusClass">
  38. <view class="status-head">
  39. <text class="status-name">{{ member.cardTypeName || '会员卡' }}</text>
  40. <text class="status-tag">{{ statusText }}</text>
  41. </view>
  42. <view class="status-row">
  43. <text class="status-label">生效时间</text>
  44. <text class="status-value">{{ formatDateTime(member.startDate) }}</text>
  45. </view>
  46. <view class="status-row">
  47. <text class="status-label">有效期至</text>
  48. <text class="status-value">{{ formatDateTime(member.validUntil) }}</text>
  49. </view>
  50. <view v-if="member.status === 'active'" class="status-row">
  51. <text class="status-label">免费沙龙</text>
  52. <text class="status-value">已用 {{ member.salonUsed || 0 }} / {{ member.salonQuota || 0 }} 次</text>
  53. </view>
  54. <view v-if="member.status === 'active' && member.noShowCount > 0" class="status-row">
  55. <text class="status-label">爽约记录</text>
  56. <text class="status-value warn-text">{{ member.noShowCount }} 次</text>
  57. </view>
  58. </view>
  59. <!-- 权益说明 -->
  60. <view class="benefit-card">
  61. <text class="benefit-title">会员权益</text>
  62. <view class="benefit-item" v-for="(b, idx) in member.benefits || []" :key="idx">
  63. <text class="benefit-dot">✓</text>
  64. <text class="benefit-text">{{ b }}</text>
  65. </view>
  66. </view>
  67. <!-- 免费沙龙入口 -->
  68. <button v-if="member.status === 'active' && member.salonRemain > 0" class="salon-btn" @click="goSalonList">
  69. 免费沙龙报名(剩余 {{ member.salonRemain }} 次)
  70. </button>
  71. <button v-else-if="member.status === 'active'" class="salon-btn disabled-btn" disabled>
  72. 免费沙龙次数已用完
  73. </button>
  74. <!-- 续费 / 退卡 -->
  75. <view class="action-row">
  76. <button v-if="member.status !== 'pending'" class="renew-btn" :loading="buying" :disabled="buying" @click="buy(member.cardType)">
  77. {{ member.status === 'active' ? '续费' : '重新购买' }}
  78. </button>
  79. <view v-if="member.refundable" class="refund-link" @click="doRefund">7 日无理由退卡</view>
  80. <text v-if="!member.refundable && member.status === 'active'" class="refund-tip">已过 7 日退卡期</text>
  81. </view>
  82. </view>
  83. </view>
  84. </template>
  85. <script>
  86. import { getMyMember, createMemberOrder, refundMember } from '@/utils/api.js'
  87. import { formatDateTime } from '@/utils/format.js'
  88. import { guardPage } from '@/utils/guard.js'
  89. export default {
  90. data() {
  91. return {
  92. loading: false,
  93. buying: false,
  94. member: { status: 'none' },
  95. experienceBenefits: ['年度免费沙龙 4 次', 'L1 课程 95 折', '会员社群 / 资料库'],
  96. companionBenefits: ['年度免费沙龙 12 次', 'L1 课程 9 折', 'L2 课程 95 折', '专家诊室会员价(即将开放)', '会员社群、优先报名']
  97. }
  98. },
  99. computed: {
  100. statusText() {
  101. var map = {
  102. active: this.member.effective ? '生效中' : '已过期',
  103. pending: '待支付',
  104. expired: '已过期',
  105. refunded: '已退卡'
  106. }
  107. return map[this.member.status] || this.member.status || ''
  108. },
  109. statusClass() {
  110. if (this.member.status === 'active') {
  111. return this.member.effective ? 'status-active' : 'status-expired'
  112. }
  113. var cls = { pending: 'status-pending', expired: 'status-expired', refunded: 'status-refunded' }
  114. return cls[this.member.status] || ''
  115. }
  116. },
  117. onLoad() {
  118. if (!guardPage()) return
  119. this.loadMember()
  120. },
  121. methods: {
  122. formatDateTime: formatDateTime,
  123. loadMember() {
  124. var self = this
  125. self.loading = true
  126. getMyMember().then(function(resp) {
  127. self.member = resp.data || { status: 'none' }
  128. }).catch(function(err) {
  129. // 未登录等情况由 guard/全局处理;静默兜底为空态
  130. self.member = { status: 'none' }
  131. console.error('[member] loadMember fail', err)
  132. }).finally(function() {
  133. self.loading = false
  134. })
  135. },
  136. buy(cardType) {
  137. var self = this
  138. if (self.buying) return
  139. var name = cardType === 'experience' ? '体验卡 ¥365/年' : '爱伴星伙卡 ¥1280/年'
  140. var tip = self.member.status === 'active' ? '续费将衔接延长有效期,确认开通 ' + name + ' 吗?' : '确认开通 ' + name + ' 吗?'
  141. uni.showModal({
  142. title: '开通年度会员',
  143. content: tip,
  144. success: function(res) {
  145. if (!res.confirm) return
  146. self.buying = true
  147. uni.showLoading({ title: '下单中…' })
  148. createMemberOrder(cardType).then(function(resp) {
  149. var data = resp.data || {}
  150. uni.hideLoading()
  151. if (data.status === 'paid') {
  152. // test-mode 直付成功即已激活
  153. uni.showToast({ title: '开通成功', icon: 'success' })
  154. self.loadMember()
  155. } else {
  156. uni.showToast({ title: '订单创建成功,等待支付', icon: 'none' })
  157. }
  158. }).catch(function(err) {
  159. uni.hideLoading()
  160. uni.showToast({ title: (err && err.message) || '开通失败', icon: 'none' })
  161. }).finally(function() {
  162. self.buying = false
  163. })
  164. }
  165. })
  166. },
  167. doRefund() {
  168. var self = this
  169. uni.showModal({
  170. title: '退卡确认',
  171. content: '7 日内退卡:退款金额 = 实付 − 已用沙龙次数 × ¥68,确认退卡吗?',
  172. confirmColor: '#EF5350',
  173. success: function(res) {
  174. if (!res.confirm) return
  175. refundMember().then(function(resp) {
  176. uni.showModal({
  177. title: '退卡成功',
  178. content: '退款金额:¥' + (resp.data && resp.data.refundAmount !== undefined ? ((resp.data.refundAmount / 100).toFixed(2)) : '') + ',原路退回',
  179. showCancel: false,
  180. success: function() {
  181. self.loadMember()
  182. }
  183. })
  184. }).catch(function(err) {
  185. uni.showToast({ title: (err && err.message) || '退卡失败', icon: 'none' })
  186. })
  187. }
  188. })
  189. },
  190. goSalonList() {
  191. uni.navigateTo({ url: '/pages/salon/list' })
  192. }
  193. }
  194. }
  195. </script>
  196. <style scoped>
  197. .member-page { min-height: 100vh; background: #F5F5F5; padding: 32rpx; }
  198. .empty-tip { font-size: 26rpx; color: #94877F; padding: 64rpx 0; text-align: center; }
  199. .head-banner { background: linear-gradient(135deg, #FC7A57, #F9494F); border-radius: 16rpx; padding: 40rpx 32rpx; margin-bottom: 24rpx; }
  200. .head-title { display: block; font-size: 40rpx; font-weight: 700; color: #FFF; margin-bottom: 8rpx; }
  201. .head-sub { display: block; font-size: 26rpx; color: rgba(255,255,255,0.9); }
  202. .mcard { background: #FFF; border-radius: 16rpx; padding: 32rpx; margin-bottom: 24rpx; border: 2rpx solid #E2E8F0; }
  203. .companion-card { border: 2rpx solid #FED7AA; }
  204. .mcard-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20rpx; }
  205. .mcard-name { font-size: 34rpx; font-weight: 700; color: #2A2326; }
  206. .mcard-price { font-size: 26rpx; color: #FC7A57; }
  207. .price-num { font-size: 56rpx; font-weight: 700; }
  208. .price-year { font-size: 24rpx; color: #94877F; }
  209. .mcard-benefit { display: flex; align-items: center; margin-bottom: 10rpx; }
  210. .benefit-dot { font-size: 26rpx; color: #FC7A57; margin-right: 10rpx; }
  211. .benefit-text { font-size: 26rpx; color: #475569; }
  212. .mcard-limit { display: block; font-size: 22rpx; color: #94877F; margin-top: 8rpx; }
  213. .buy-btn { width: 100%; height: 84rpx; line-height: 84rpx; background: #FC7A57; color: #FFF; font-size: 30rpx; font-weight: 600; border-radius: 42rpx; border: none; margin-top: 24rpx; }
  214. .buy-btn:active { opacity: 0.85; }
  215. .status-card { border-radius: 16rpx; padding: 32rpx; margin-bottom: 24rpx; }
  216. .status-active { background: linear-gradient(135deg, #22C55E, #16A34A); }
  217. .status-pending { background: linear-gradient(135deg, #F59E0B, #D97706); }
  218. .status-expired { background: linear-gradient(135deg, #94A3B8, #64748B); }
  219. .status-refunded { background: linear-gradient(135deg, #94A3B8, #64748B); }
  220. .status-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
  221. .status-name { font-size: 36rpx; font-weight: 700; color: #FFF; }
  222. .status-tag { font-size: 24rpx; color: #FFF; background: rgba(255,255,255,0.25); padding: 6rpx 20rpx; border-radius: 24rpx; }
  223. .status-row { display: flex; align-items: center; margin-bottom: 12rpx; }
  224. .status-label { font-size: 25rpx; color: rgba(255,255,255,0.85); width: 140rpx; flex-shrink: 0; }
  225. .status-value { font-size: 26rpx; color: #FFF; font-weight: 600; }
  226. .warn-text { color: #FFF0E8; }
  227. .benefit-card { background: #FFF; border-radius: 16rpx; padding: 28rpx 32rpx; margin-bottom: 24rpx; }
  228. .benefit-title { display: block; font-size: 30rpx; font-weight: 700; color: #2A2326; margin-bottom: 16rpx; }
  229. .benefit-item { display: flex; align-items: center; margin-bottom: 10rpx; }
  230. .benefit-dot { font-size: 26rpx; color: #22C55E; margin-right: 10rpx; }
  231. .salon-btn { width: 100%; height: 88rpx; line-height: 88rpx; background: #FC7A57; color: #FFF; font-size: 30rpx; font-weight: 600; border-radius: 44rpx; border: none; margin-bottom: 24rpx; }
  232. .salon-btn:active { opacity: 0.85; }
  233. .disabled-btn { background: #CBD5E1; }
  234. .action-row { display: flex; flex-direction: column; align-items: center; }
  235. .renew-btn { width: 100%; height: 84rpx; line-height: 84rpx; background: #FFF; color: #FC7A57; font-size: 30rpx; font-weight: 600; border-radius: 42rpx; border: 2rpx solid #FED7AA; }
  236. .renew-btn:active { background: #FFF0E8; }
  237. .refund-link { font-size: 26rpx; color: #EF5350; text-decoration: underline; margin-top: 24rpx; padding: 8rpx 16rpx; }
  238. .refund-tip { font-size: 24rpx; color: #94877F; margin-top: 24rpx; }
  239. </style>