upgrade.vue 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. <template>
  2. <view class="upgrade-page-wrapper">
  3. <scroll-view class="page" scroll-y>
  4. <!-- 当前会员状态卡片 -->
  5. <view class="status-card">
  6. <view class="status-badge" :class="isFree ? 'badge-free' : 'badge-family'">
  7. <text class="badge-icon">{{ isFree ? '○' : '●' }}</text>
  8. <text class="badge-text">{{ currentLevelName }}</text>
  9. </view>
  10. <view class="status-body" v-if="membership && membership.levelCode !== 'FREE'">
  11. <view class="expiry-row">
  12. <text class="expiry-label">到期时间</text>
  13. <text class="expiry-value">{{ formatDate(membership.endDate) }}</text>
  14. </view>
  15. <view class="days-remaining" v-if="membership.isActive">
  16. <text class="days-num">{{ membership.daysRemaining || 0 }}</text>
  17. <text class="days-unit">天剩余</text>
  18. </view>
  19. <view class="expired-msg" v-else>
  20. <text class="expired-text">会员已过期,续费恢复权益</text>
  21. </view>
  22. </view>
  23. <view class="status-body" v-else>
  24. <text class="free-hint">当前为免费版,开通会员解锁全部特权</text>
  25. </view>
  26. </view>
  27. <!-- 等级切换 tabs -->
  28. <view class="level-tabs" v-if="isFree">
  29. <view class="level-tab" :class="selectedLevel === 'FAMILY' ? 'level-tab-active' : ''"
  30. @click="selectLevel('FAMILY')">
  31. <text class="level-tab-name">家庭会员</text>
  32. <text class="level-tab-price">¥{{ getLevelYearlyPrice('FAMILY') }}/年</text>
  33. </view>
  34. <view class="level-tab" :class="selectedLevel === 'PREMIUM' ? 'level-tab-active' : ''"
  35. @click="selectLevel('PREMIUM')">
  36. <text class="level-tab-name">高级家庭会员</text>
  37. <text class="level-tab-price">¥{{ getLevelYearlyPrice('PREMIUM') }}/年</text>
  38. </view>
  39. </view>
  40. <!-- 家庭会员开通卡片 -->
  41. <view class="upgrade-card" v-if="isFree">
  42. <view class="upgrade-header">
  43. <text class="upgrade-title">{{ selectedLevel === 'FAMILY' ? '家庭会员' : '高级家庭会员' }}</text>
  44. <text class="upgrade-subtitle">{{ selectedLevel === 'FAMILY' ? 'FAMILY' : 'PREMIUM' }}</text>
  45. </view>
  46. <!-- 档位选择 -->
  47. <view class="period-tabs">
  48. <view
  49. class="period-tab"
  50. v-for="p in visiblePeriods"
  51. :key="p.key"
  52. :class="selectedPeriod === p.key ? 'period-tab-active' : ''"
  53. @click="selectPeriod(p.key)"
  54. >
  55. <text class="period-tab-label">{{ p.label }}</text>
  56. <text class="period-tab-price">¥{{ getPriceForPeriod(p.key) }}</text>
  57. </view>
  58. </view>
  59. <view class="upgrade-price">
  60. <text class="price-symbol">¥</text>
  61. <text class="price-amount">{{ getPriceForPeriod(selectedPeriod) }}</text>
  62. <text class="price-unit">{{ getPeriodUnit() }}</text>
  63. </view>
  64. <view class="feature-list">
  65. <view class="feature-item" v-for="(feature, idx) in features" :key="idx">
  66. <text class="feature-check">✓</text>
  67. <text class="feature-text">{{ feature }}</text>
  68. </view>
  69. </view>
  70. <!-- Coupon section -->
  71. <view class="coupon-row" @click="openCouponPicker">
  72. <text class="coupon-row-label">优惠券</text>
  73. <text v-if="selectedCoupon" class="coupon-row-discount">-{{ formatPriceWithSymbol(selectedCoupon.value) }} {{ selectedCoupon.name }}</text>
  74. <text v-else class="coupon-row-placeholder">选择优惠券</text>
  75. <text class="coupon-row-arrow">›</text>
  76. </view>
  77. <button class="btn-upgrade" @click="handleUpgrade">立即开通</button>
  78. </view>
  79. <!-- 续费卡片 -->
  80. <view class="upgrade-card" v-else>
  81. <view class="upgrade-header">
  82. <text class="upgrade-title">续费会员</text>
  83. <text class="upgrade-subtitle">保持权益不中断</text>
  84. </view>
  85. <!-- 档位选择 -->
  86. <view class="period-tabs">
  87. <view
  88. class="period-tab"
  89. v-for="p in visiblePeriods"
  90. :key="p.key"
  91. :class="selectedPeriod === p.key ? 'period-tab-active' : ''"
  92. @click="selectPeriod(p.key)"
  93. >
  94. <text class="period-tab-label">{{ p.label }}</text>
  95. <text class="period-tab-price">¥{{ getPriceForPeriod(p.key) }}</text>
  96. </view>
  97. </view>
  98. <view class="upgrade-price">
  99. <text class="price-symbol">¥</text>
  100. <text class="price-amount">{{ getPriceForPeriod(selectedPeriod) }}</text>
  101. <text class="price-unit">{{ getPeriodUnit() }}</text>
  102. </view>
  103. <!-- Coupon section -->
  104. <view class="coupon-row" @click="openCouponPicker">
  105. <text class="coupon-row-label">优惠券</text>
  106. <text v-if="selectedCoupon" class="coupon-row-discount">-{{ formatPriceWithSymbol(selectedCoupon.value) }} {{ selectedCoupon.name }}</text>
  107. <text v-else class="coupon-row-placeholder">选择优惠券</text>
  108. <text class="coupon-row-arrow">›</text>
  109. </view>
  110. <button class="btn-upgrade btn-renew" @click="handleRenew">立即续费</button>
  111. </view>
  112. <!-- 权益对比表 -->
  113. <view class="compare-section">
  114. <text class="section-title">权益对比</text>
  115. <view class="compare-table">
  116. <view class="compare-row header-row">
  117. <text class="compare-cell cell-feature">权益项</text>
  118. <text class="compare-cell cell-free">免费</text>
  119. <text class="compare-cell cell-family active">家庭会员</text>
  120. <text class="compare-cell cell-premium active">高级会员</text>
  121. </view>
  122. <view class="compare-row" v-for="(item, idx) in compareData" :key="idx">
  123. <text class="compare-cell cell-feature">{{ item.name }}</text>
  124. <text class="compare-cell cell-free">{{ item.free }}</text>
  125. <text class="compare-cell cell-family active">{{ item.family }}</text>
  126. <text class="compare-cell cell-premium active">{{ item.premium }}</text>
  127. </view>
  128. </view>
  129. </view>
  130. <!-- 开通赠券规则 -->
  131. <view class="join-rules-section" v-if="joinRules.length > 0">
  132. <text class="section-title">开通赠券</text>
  133. <view class="join-rule-item" v-for="(rule, idx) in joinRules" :key="getJoinRuleKey(rule, idx)">
  134. <text class="join-rule-text">加入{{ getLevelName(rule.grantLevelCode) }}会员,赠送{{ rule.grantQuantity || 1 }}张「{{ rule.name }}」</text>
  135. <text class="join-rule-desc">{{ getCouponDesc(rule) }}</text>
  136. </view>
  137. </view>
  138. <!-- 升级记录 -->
  139. <view class="records-section" v-if="records.length > 0">
  140. <text class="section-title">升级记录</text>
  141. <view class="record-item" v-for="record in records" :key="record.id">
  142. <view class="record-left">
  143. <text class="record-level">{{ record.levelName }}</text>
  144. <text class="record-time">{{ formatDate(record.createdAt) }}</text>
  145. </view>
  146. <text class="record-amount">{{ formatPriceWithSymbol(record.amount) }}</text>
  147. </view>
  148. </view>
  149. </scroll-view>
  150. <!-- Coupon Picker Bottom Sheet -->
  151. <view class="modal-mask" v-if="showCouponPicker" @click="showCouponPicker = false">
  152. <view class="coupon-picker" @click.stop>
  153. <view class="picker-header">
  154. <text class="picker-title">选择优惠券</text>
  155. <text class="picker-close" @click="showCouponPicker = false">关闭</text>
  156. </view>
  157. <scroll-view scroll-y class="picker-list">
  158. <view
  159. v-for="coupon in availableCoupons"
  160. :key="coupon.id"
  161. class="picker-item"
  162. :class="selectedCoupon && selectedCoupon.id === coupon.id ? 'picker-item-active' : ''"
  163. @click="selectCoupon(coupon)"
  164. >
  165. <view class="picker-item-left">
  166. <text class="picker-item-value">{{ formatPriceWithSymbol(coupon.value) }}</text>
  167. </view>
  168. <view class="picker-item-right">
  169. <text class="picker-item-name">{{ coupon.name }}</text>
  170. <text class="picker-item-condition">{{ getCouponCondition(coupon.minSpend) }}</text>
  171. </view>
  172. <view class="picker-item-check" v-if="selectedCoupon && selectedCoupon.id === coupon.id">
  173. <text>✓</text>
  174. </view>
  175. </view>
  176. <view class="picker-empty" v-if="availableCoupons.length === 0">
  177. <text>暂无可用优惠券</text>
  178. </view>
  179. </scroll-view>
  180. <view class="picker-footer" v-if="selectedCoupon">
  181. <button class="picker-btn-remove" @click="removeCoupon">不使用优惠券</button>
  182. </view>
  183. </view>
  184. </view>
  185. </view>
  186. </template>
  187. <script>
  188. import { getMyMembership, getMembershipPlans, getUpgradeRecords, upgradeMember, createOrder, getCouponList, requestVirtualPayment, nextOrder, getJoinCouponRules } from '../../utils/api.js'
  189. import { parseDate } from '../../utils/format.js'
  190. export default {
  191. data() {
  192. return {
  193. membership: null,
  194. levels: [],
  195. records: [],
  196. features: ['折扣购物', '家庭管理', '推荐佣金', '专属活动', '任务特权', '成长报告'],
  197. isFree: true,
  198. currentLevelName: '免费用户',
  199. compareData: [
  200. { name: '商品折扣', free: '普通价格', family: '9折', premium: '8折' },
  201. { name: '家庭管理', free: '基础功能', family: '全部功能', premium: '全部功能+优先响应' },
  202. { name: '管家服务', free: '—', family: '—', premium: '每周一次' },
  203. { name: '推荐佣金', free: '不可参与', family: '最高15%', premium: '最高20%' },
  204. { name: '专属活动', free: '部分参与', family: '全部参与', premium: '优先参与' },
  205. { name: '成长报告', free: '月度', family: '每周+深度', premium: '每日+深度+专家解读' },
  206. { name: '任务配额', free: '每日5个', family: '不限量', premium: '不限量+优先审核' }
  207. ],
  208. coupons: [],
  209. selectedCoupon: null,
  210. showCouponPicker: false,
  211. periods: [
  212. { key: 'monthly', label: '月度', unit: '/月' },
  213. { key: 'quarterly', label: '季度', unit: '/季' },
  214. { key: 'yearly', label: '年度', unit: '/年' }
  215. ],
  216. selectedPeriod: 'yearly',
  217. selectedLevel: 'FAMILY',
  218. joinRules: [],
  219. }
  220. },
  221. computed: {
  222. visiblePeriods() {
  223. var self = this
  224. return this.periods.filter(function(p) {
  225. if (self.selectedLevel === 'FAMILY') {
  226. return p.key === 'yearly'
  227. }
  228. if (self.selectedLevel === 'PREMIUM') {
  229. return p.key === 'monthly' || p.key === 'yearly'
  230. }
  231. return true
  232. })
  233. },
  234. availableCoupons() {
  235. var self = this
  236. return this.coupons.filter(function(c) {
  237. if (c.status && c.status !== 'AVAILABLE') return false
  238. if (c.applicableTo && c.applicableTo !== 'ALL' && c.applicableTo !== 'MEMBERSHIP') return false
  239. return true
  240. })
  241. }
  242. },
  243. onLoad() {
  244. this.loadData()
  245. this.loadJoinRules()
  246. },
  247. methods: {
  248. loadJoinRules() {
  249. var that = this
  250. getJoinCouponRules().then(function(res) {
  251. if (res.code === 200) {
  252. that.joinRules = res.data || []
  253. }
  254. }).catch(function(e) {
  255. console.error('加载开通赠券规则失败', e)
  256. })
  257. },
  258. getLevelName(code) {
  259. var map = {
  260. FREE: '免费版',
  261. FAMILY: '家庭会员',
  262. PREMIUM: '高级会员'
  263. }
  264. return map[code] || code || ''
  265. },
  266. getJoinRuleKey(rule, idx) {
  267. return rule.id + '-' + idx
  268. },
  269. getCouponDesc(item) {
  270. if (!item) return ''
  271. if (item.type === 'DISCOUNT') {
  272. return '折扣券 ' + (item.discountRate / 100).toFixed(1) + '折'
  273. }
  274. return '立减 ' + (item.value / 100).toFixed(2) + '元'
  275. },
  276. async loadData() {
  277. try {
  278. const membershipRes = await getMyMembership()
  279. this.membership = membershipRes.data
  280. if (this.membership && this.membership.levelCode && this.membership.levelCode !== 'FREE') {
  281. this.isFree = false
  282. this.currentLevelName = this.membership.levelName || '家庭会员'
  283. this.selectedLevel = this.membership.levelCode
  284. } else {
  285. this.isFree = true
  286. this.currentLevelName = '免费用户'
  287. }
  288. } catch (e) {
  289. console.error('加载会员信息失败', e)
  290. }
  291. try {
  292. const levelsRes = await getMembershipPlans()
  293. this.levels = levelsRes.data || []
  294. } catch (e) {
  295. console.error('加载会员等级失败', e)
  296. }
  297. try {
  298. const recordsRes = await getUpgradeRecords()
  299. if (recordsRes.data && recordsRes.data.records) {
  300. this.records = recordsRes.data.records
  301. }
  302. } catch (e) {
  303. console.error('加载升级记录失败', e)
  304. }
  305. try {
  306. const couponsRes = await getCouponList()
  307. this.coupons = couponsRes.data || []
  308. } catch (e) {
  309. console.error('加载优惠券失败', e)
  310. }
  311. },
  312. openCouponPicker() {
  313. if (this.availableCoupons.length === 0 && !this.selectedCoupon) {
  314. uni.showToast({ title: '暂无可用优惠券', icon: 'none' })
  315. return
  316. }
  317. this.showCouponPicker = true
  318. },
  319. selectCoupon(coupon) {
  320. this.selectedCoupon = coupon
  321. this.showCouponPicker = false
  322. },
  323. removeCoupon() {
  324. this.selectedCoupon = null
  325. this.showCouponPicker = false
  326. },
  327. getCouponCondition(minSpend) {
  328. if (!minSpend || minSpend <= 0) return '无门槛'
  329. return '满' + (minSpend / 100).toFixed(2) + '元可用'
  330. },
  331. selectLevel: function(code) {
  332. this.selectedLevel = code
  333. this.selectedPeriod = 'yearly'
  334. },
  335. getLevelYearlyPrice: function(code) {
  336. for (var i = 0; i < this.levels.length; i++) {
  337. if (this.levels[i].levelCode === code) {
  338. return this.levels[i].yearly ? (this.levels[i].yearly / 100).toFixed(0) : '0'
  339. }
  340. }
  341. return '0'
  342. },
  343. selectPeriod: function(key) {
  344. this.selectedPeriod = key
  345. },
  346. getPriceForPeriod: function(periodKey) {
  347. var plan = null
  348. for (var i = 0; i < this.levels.length; i++) {
  349. if (this.levels[i].levelCode === this.selectedLevel) {
  350. plan = this.levels[i]
  351. break
  352. }
  353. }
  354. if (!plan) return '0'
  355. if (periodKey === 'monthly') return plan.monthly ? (plan.monthly / 100).toFixed(0) : '0'
  356. if (periodKey === 'quarterly') return plan.quarterly ? (plan.quarterly / 100).toFixed(0) : '0'
  357. if (periodKey === 'yearly') return plan.yearly ? (plan.yearly / 100).toFixed(0) : '0'
  358. return '0'
  359. },
  360. getPeriodUnit: function() {
  361. for (var i = 0; i < this.periods.length; i++) {
  362. if (this.periods[i].key === this.selectedPeriod) {
  363. return this.periods[i].unit
  364. }
  365. }
  366. return '/年'
  367. },
  368. handleUpgrade: function() {
  369. var self = this
  370. var priceText = '¥' + this.getPriceForPeriod(this.selectedPeriod) + this.getPeriodUnit()
  371. var levelName = this.selectedLevel === 'FAMILY' ? '家庭会员' : '高级家庭会员'
  372. uni.showModal({
  373. title: '确认开通',
  374. content: '确认开通' + levelName + '(' + priceText + ')?',
  375. success: function(res) {
  376. if (!res.confirm) return
  377. self.doUpgrade()
  378. }
  379. })
  380. },
  381. doUpgrade: function() {
  382. var self = this
  383. createOrder(self.selectedLevel, 'pay', self.selectedPeriod).then(function(res) {
  384. var data = res.data || {}
  385. if (data.orderInfo && data.sign) {
  386. self.payInstallments(data)
  387. } else {
  388. uni.showToast({ title: '开通成功', icon: 'success' })
  389. self.loadData()
  390. }
  391. }).catch(function(e) {
  392. if (e.code === 46001) {
  393. uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
  394. } else {
  395. uni.showToast({ title: e.message || '开通失败', icon: 'none' })
  396. }
  397. })
  398. },
  399. pollOrderStatus: function() {
  400. var self = this
  401. var times = 0
  402. var timer = setInterval(function() {
  403. times++
  404. getMyMembership().then(function(res) {
  405. var m = res.data || {}
  406. if (m.membership && m.membership.isActive) {
  407. clearInterval(timer)
  408. uni.showToast({ title: '开通成功', icon: 'success' })
  409. self.loadData()
  410. } else if (times >= 5) {
  411. clearInterval(timer)
  412. self.loadData()
  413. }
  414. }).catch(function() {
  415. if (times >= 5) { clearInterval(timer); self.loadData() }
  416. })
  417. }, 2000)
  418. },
  419. payInstallments: function(dto) {
  420. var self = this
  421. var current = (dto.currentPeriod || 1)
  422. var total = (dto.totalPeriods || 1)
  423. requestVirtualPayment(dto, {
  424. success: function() {
  425. if (current < total) {
  426. uni.showToast({ title: '第' + current + '期支付成功', icon: 'none' })
  427. nextOrder({ orderNo: dto.orderNo }).then(function(res) {
  428. var nd = res.data
  429. if (nd && nd.done) {
  430. self.pollOrderStatus()
  431. } else if (nd && nd.orderInfo && nd.sign) {
  432. self.payInstallments(nd)
  433. } else {
  434. self.pollOrderStatus()
  435. }
  436. }).catch(function() {
  437. self.pollOrderStatus()
  438. })
  439. } else {
  440. uni.showToast({ title: '支付成功,开通中…', icon: 'none' })
  441. self.pollOrderStatus()
  442. }
  443. },
  444. fail: function(err) {
  445. var errCode = err && err.errCode
  446. var msg = ''
  447. if (errCode === -15007 || errCode === -15005) msg = '登录态已过期,请重新登录后支付'
  448. else if (errCode === -15010) msg = '商品未发布,请联系客服'
  449. else if (errCode === -15013) msg = '商品价格异常'
  450. else if (errCode === -2) msg = '支付已取消'
  451. else if (err && err.errMsg) msg = err.errMsg
  452. uni.showToast({ title: msg || '支付未完成', icon: 'none' })
  453. }
  454. })
  455. },
  456. handleRenew: function() {
  457. var self = this
  458. var priceText = '¥' + this.getPriceForPeriod(this.selectedPeriod) + this.getPeriodUnit()
  459. var levelName = this.selectedLevel === 'FAMILY' ? '家庭会员' : '高级家庭会员'
  460. uni.showModal({
  461. title: '确认续费',
  462. content: '确认续费' + levelName + '(' + priceText + ')?',
  463. success: function(res) {
  464. if (!res.confirm) return
  465. self.doRenew()
  466. }
  467. })
  468. },
  469. doRenew: function() {
  470. var self = this
  471. createOrder(self.selectedLevel, 'pay', this.selectedPeriod).then(function(res) {
  472. var data = res.data || {}
  473. if (data.orderInfo && data.sign) {
  474. self.payInstallments(data)
  475. } else {
  476. uni.showToast({ title: '续费成功', icon: 'success' })
  477. self.loadData()
  478. }
  479. }).catch(function(e) {
  480. if (e.code === 46001) {
  481. uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
  482. } else {
  483. uni.showToast({ title: e.message || '续费失败', icon: 'none' })
  484. }
  485. })
  486. },
  487. formatDate(date) {
  488. if (!date) return ''
  489. var d = parseDate(date)
  490. if (!d) return ''
  491. return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
  492. }
  493. }
  494. }
  495. </script>
  496. <style scoped>
  497. .page {
  498. min-height: 100vh;
  499. background: #FFF7ED;
  500. padding: 30rpx;
  501. box-sizing: border-box;
  502. }
  503. /* ===== 状态卡片 ===== */
  504. .status-card {
  505. background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
  506. border-radius: 20rpx;
  507. padding: 40rpx;
  508. margin-bottom: 30rpx;
  509. }
  510. .status-badge {
  511. display: inline-flex;
  512. align-items: center;
  513. padding: 10rpx 24rpx;
  514. border-radius: 9999rpx;
  515. font-size: 24rpx;
  516. }
  517. .badge-free {
  518. background: rgba(255,255,255,0.2);
  519. color: #fff;
  520. }
  521. .badge-family {
  522. background: rgba(255,255,255,0.3);
  523. color: #fff;
  524. }
  525. .badge-icon {
  526. font-size: 20rpx;
  527. margin-right: 8rpx;
  528. }
  529. .badge-text {
  530. font-weight: 600;
  531. }
  532. .status-body {
  533. margin-top: 24rpx;
  534. }
  535. .expiry-row {
  536. display: flex;
  537. justify-content: space-between;
  538. align-items: center;
  539. }
  540. .expiry-label {
  541. font-size: 26rpx;
  542. color: rgba(255,255,255,0.8);
  543. }
  544. .expiry-value {
  545. font-size: 28rpx;
  546. color: #fff;
  547. font-weight: 500;
  548. }
  549. .days-remaining {
  550. display: flex;
  551. align-items: baseline;
  552. justify-content: center;
  553. margin-top: 20rpx;
  554. }
  555. .days-num {
  556. font-size: 56rpx;
  557. font-weight: bold;
  558. color: #FEF3C7;
  559. }
  560. .days-unit {
  561. font-size: 26rpx;
  562. color: rgba(255,255,255,0.7);
  563. margin-left: 8rpx;
  564. }
  565. .expired-msg {
  566. text-align: center;
  567. margin-top: 16rpx;
  568. }
  569. .expired-text {
  570. font-size: 26rpx;
  571. color: #FEF3C7;
  572. }
  573. .free-hint {
  574. font-size: 26rpx;
  575. color: rgba(255,255,255,0.85);
  576. text-align: center;
  577. display: block;
  578. }
  579. /* ===== 升级/续费卡片 ===== */
  580. .upgrade-card {
  581. background: #FFFFFF;
  582. border-radius: 20rpx;
  583. padding: 40rpx;
  584. margin-bottom: 30rpx;
  585. box-shadow: 0 4rpx 24rpx rgba(249, 115, 22, 0.08);
  586. }
  587. .upgrade-header {
  588. text-align: center;
  589. margin-bottom: 20rpx;
  590. }
  591. .upgrade-title {
  592. font-size: 36rpx;
  593. font-weight: bold;
  594. color: #1E293B;
  595. display: block;
  596. }
  597. .upgrade-subtitle {
  598. font-size: 24rpx;
  599. color: #94A3B8;
  600. margin-top: 6rpx;
  601. display: block;
  602. }
  603. .upgrade-price {
  604. text-align: center;
  605. margin: 24rpx 0;
  606. }
  607. .price-symbol {
  608. font-size: 28rpx;
  609. color: #F97316;
  610. vertical-align: top;
  611. }
  612. .price-amount {
  613. font-size: 64rpx;
  614. font-weight: bold;
  615. color: #F97316;
  616. line-height: 1;
  617. }
  618. .price-unit {
  619. font-size: 26rpx;
  620. color: #94A3B8;
  621. margin-left: 6rpx;
  622. }
  623. .feature-list {
  624. margin: 24rpx 0;
  625. }
  626. .feature-item {
  627. display: flex;
  628. align-items: center;
  629. padding: 12rpx 0;
  630. }
  631. .feature-check {
  632. width: 40rpx;
  633. height: 40rpx;
  634. line-height: 40rpx;
  635. text-align: center;
  636. background: #F97316;
  637. color: #fff;
  638. border-radius: 50%;
  639. font-size: 22rpx;
  640. margin-right: 16rpx;
  641. flex-shrink: 0;
  642. }
  643. .feature-text {
  644. font-size: 28rpx;
  645. color: #1E293B;
  646. }
  647. .btn-upgrade {
  648. width: 100%;
  649. height: 88rpx;
  650. line-height: 88rpx;
  651. background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
  652. color: #fff;
  653. font-size: 32rpx;
  654. font-weight: 600;
  655. border-radius: 44rpx;
  656. margin-top: 20rpx;
  657. border: none;
  658. }
  659. .btn-upgrade::after {
  660. border: none;
  661. }
  662. .btn-upgrade:active {
  663. opacity: 0.9;
  664. }
  665. .btn-renew {
  666. background: linear-gradient(135deg, #0EA5E9 0%, #0284C7 100%);
  667. }
  668. /* ===== 权益对比 ===== */
  669. .compare-section {
  670. background: #FFFFFF;
  671. border-radius: 20rpx;
  672. padding: 30rpx;
  673. margin-bottom: 30rpx;
  674. box-shadow: 0 4rpx 24rpx rgba(249, 115, 22, 0.08);
  675. }
  676. .section-title {
  677. font-size: 32rpx;
  678. font-weight: bold;
  679. color: #1E293B;
  680. display: block;
  681. margin-bottom: 24rpx;
  682. }
  683. .compare-table {
  684. border-radius: 16rpx;
  685. overflow: hidden;
  686. border: 1rpx solid #FED7AA;
  687. }
  688. .compare-row {
  689. display: flex;
  690. border-bottom: 1rpx solid #FED7AA;
  691. }
  692. .compare-row:last-child {
  693. border-bottom: none;
  694. }
  695. .header-row {
  696. background: #FFF7ED;
  697. }
  698. .compare-cell {
  699. flex: 1;
  700. padding: 20rpx 16rpx;
  701. font-size: 26rpx;
  702. text-align: center;
  703. display: flex;
  704. align-items: center;
  705. justify-content: center;
  706. }
  707. .cell-feature {
  708. flex: 1.5;
  709. color: #1E293B;
  710. font-weight: 500;
  711. justify-content: flex-start;
  712. }
  713. .cell-free {
  714. color: #94A3B8;
  715. }
  716. .cell-family.active {
  717. color: #F97316;
  718. font-weight: 600;
  719. }
  720. .cell-premium.active {
  721. color: #8B5CF6;
  722. font-weight: 600;
  723. }
  724. .header-row .compare-cell {
  725. font-size: 24rpx;
  726. font-weight: 600;
  727. color: #64748B;
  728. }
  729. .header-row .cell-family.active {
  730. color: #F97316;
  731. }
  732. .header-row .cell-premium.active {
  733. color: #8B5CF6;
  734. }
  735. /* ===== 等级切换 Tabs ===== */
  736. .level-tabs {
  737. display: flex;
  738. margin-bottom: 30rpx;
  739. border-radius: 16rpx;
  740. overflow: hidden;
  741. border: 2rpx solid #FED7AA;
  742. }
  743. .level-tab {
  744. flex: 1;
  745. padding: 24rpx;
  746. text-align: center;
  747. background: #FFF7ED;
  748. cursor: pointer;
  749. }
  750. .level-tab-active {
  751. background: #fff;
  752. border-bottom: 4rpx solid #F97316;
  753. }
  754. .level-tab-name {
  755. font-size: 28rpx;
  756. font-weight: 600;
  757. color: #1E293B;
  758. display: block;
  759. }
  760. .level-tab-price {
  761. font-size: 22rpx;
  762. color: #F97316;
  763. margin-top: 6rpx;
  764. display: block;
  765. }
  766. .level-tab-active .level-tab-name {
  767. color: #F97316;
  768. }
  769. /* ===== 开通赠券 ===== */
  770. .join-rules-section {
  771. background: #FFFFFF;
  772. border-radius: 20rpx;
  773. padding: 30rpx;
  774. margin-bottom: 30rpx;
  775. box-shadow: 0 4rpx 24rpx rgba(249, 115, 22, 0.08);
  776. }
  777. .join-rule-item {
  778. display: flex;
  779. align-items: center;
  780. padding: 16rpx 0;
  781. border-bottom: 1rpx solid #FED7AA;
  782. }
  783. .join-rule-item:last-child {
  784. border-bottom: none;
  785. }
  786. .join-rule-text {
  787. flex: 1;
  788. font-size: 26rpx;
  789. color: #1E293B;
  790. }
  791. .join-rule-desc {
  792. font-size: 24rpx;
  793. color: #F97316;
  794. font-weight: 600;
  795. }
  796. /* ===== 升级记录 ===== */
  797. .records-section {
  798. background: #FFFFFF;
  799. border-radius: 20rpx;
  800. padding: 30rpx;
  801. margin-bottom: 30rpx;
  802. box-shadow: 0 4rpx 24rpx rgba(249, 115, 22, 0.08);
  803. }
  804. .record-item {
  805. display: flex;
  806. justify-content: space-between;
  807. align-items: center;
  808. padding: 20rpx 0;
  809. border-bottom: 1rpx solid #FED7AA;
  810. }
  811. .record-item:last-child {
  812. border-bottom: none;
  813. }
  814. .record-left {
  815. flex: 1;
  816. }
  817. .record-level {
  818. font-size: 28rpx;
  819. color: #1E293B;
  820. display: block;
  821. }
  822. .record-time {
  823. font-size: 22rpx;
  824. color: #94A3B8;
  825. margin-top: 4rpx;
  826. display: block;
  827. }
  828. .record-amount {
  829. font-size: 30rpx;
  830. font-weight: bold;
  831. color: #F97316;
  832. }
  833. /* ===== Coupon Row ===== */
  834. .coupon-row {
  835. display: flex;
  836. align-items: center;
  837. padding: 24rpx 0;
  838. border-top: 1rpx solid #f5f5f5;
  839. margin-top: 16rpx;
  840. }
  841. .coupon-row:active {
  842. opacity: 0.7;
  843. }
  844. .coupon-row-label {
  845. font-size: 26rpx;
  846. color: #666;
  847. margin-right: 16rpx;
  848. }
  849. .coupon-row-discount {
  850. flex: 1;
  851. font-size: 26rpx;
  852. color: #F97316;
  853. font-weight: 600;
  854. text-align: right;
  855. }
  856. .coupon-row-placeholder {
  857. flex: 1;
  858. font-size: 26rpx;
  859. color: #ccc;
  860. text-align: right;
  861. }
  862. .coupon-row-arrow {
  863. font-size: 32rpx;
  864. color: #ccc;
  865. margin-left: 8rpx;
  866. }
  867. /* ===== Coupon Picker Modal ===== */
  868. .modal-mask {
  869. position: fixed;
  870. top: 0;
  871. left: 0;
  872. right: 0;
  873. bottom: 0;
  874. background: rgba(0,0,0,0.5);
  875. z-index: 999;
  876. display: flex;
  877. align-items: flex-end;
  878. }
  879. .coupon-picker {
  880. background: #fff;
  881. border-radius: 24rpx 24rpx 0 0;
  882. width: 100%;
  883. max-height: 70vh;
  884. display: flex;
  885. flex-direction: column;
  886. }
  887. .picker-header {
  888. display: flex;
  889. justify-content: space-between;
  890. align-items: center;
  891. padding: 30rpx;
  892. border-bottom: 1rpx solid #eee;
  893. }
  894. .picker-title {
  895. font-size: 30rpx;
  896. font-weight: 600;
  897. color: #333;
  898. }
  899. .picker-close {
  900. font-size: 26rpx;
  901. color: #999;
  902. }
  903. .picker-list {
  904. max-height: 50vh;
  905. padding: 0 20rpx;
  906. }
  907. .picker-item {
  908. display: flex;
  909. align-items: center;
  910. padding: 24rpx 16rpx;
  911. border-bottom: 1rpx solid #f5f5f5;
  912. }
  913. .picker-item:active {
  914. background: #fafafa;
  915. }
  916. .picker-item-active {
  917. background: #FFF7ED;
  918. }
  919. .picker-item-left {
  920. width: 120rpx;
  921. text-align: center;
  922. flex-shrink: 0;
  923. }
  924. .picker-item-value {
  925. font-size: 32rpx;
  926. font-weight: bold;
  927. color: #F97316;
  928. }
  929. .picker-item-right {
  930. flex: 1;
  931. margin: 0 16rpx;
  932. }
  933. .picker-item-name {
  934. font-size: 26rpx;
  935. color: #333;
  936. display: block;
  937. margin-bottom: 4rpx;
  938. }
  939. .picker-item-condition {
  940. font-size: 22rpx;
  941. color: #999;
  942. }
  943. .picker-item-check {
  944. width: 40rpx;
  945. height: 40rpx;
  946. background: #F97316;
  947. border-radius: 50%;
  948. display: flex;
  949. align-items: center;
  950. justify-content: center;
  951. flex-shrink: 0;
  952. color: #fff;
  953. font-size: 24rpx;
  954. font-weight: bold;
  955. }
  956. .picker-empty {
  957. text-align: center;
  958. padding: 60rpx 0;
  959. font-size: 26rpx;
  960. color: #999;
  961. }
  962. .picker-footer {
  963. padding: 20rpx;
  964. border-top: 1rpx solid #eee;
  965. }
  966. .picker-btn-remove {
  967. background: #f5f5f5;
  968. color: #666;
  969. font-size: 26rpx;
  970. border-radius: 12rpx;
  971. border: none;
  972. }
  973. .picker-btn-remove::after {
  974. border: none;
  975. }
  976. /* ===== 档位选择 ===== */
  977. .period-tabs {
  978. display: flex;
  979. justify-content: space-between;
  980. margin: 24rpx 0;
  981. gap: 16rpx;
  982. }
  983. .period-tab {
  984. flex: 1;
  985. display: flex;
  986. flex-direction: column;
  987. align-items: center;
  988. padding: 20rpx 0;
  989. border: 2rpx solid #FED7AA;
  990. border-radius: 16rpx;
  991. background: #FFFBF5;
  992. }
  993. .period-tab-active {
  994. border-color: #F97316;
  995. background: #FFF7ED;
  996. }
  997. .period-tab-label {
  998. font-size: 26rpx;
  999. color: #64748B;
  1000. font-weight: 500;
  1001. }
  1002. .period-tab-active .period-tab-label {
  1003. color: #F97316;
  1004. font-weight: 600;
  1005. }
  1006. .period-tab-price {
  1007. font-size: 28rpx;
  1008. font-weight: bold;
  1009. color: #F97316;
  1010. margin-top: 8rpx;
  1011. }
  1012. </style>