checkout.vue 25 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. <template>
  2. <view class="container">
  3. <view v-if="loading" class="loading-wrap">
  4. <text class="loading-text">加载中...</text>
  5. </view>
  6. <scroll-view v-else scroll-y class="content">
  7. <!-- Consignee Section -->
  8. <view class="section address-section" @click="openConsigneePicker">
  9. <view v-if="selectedConsignee" class="address-content">
  10. <view class="address-top">
  11. <text class="receiver-name">{{ selectedConsignee.name }}</text>
  12. <text class="receiver-phone">{{ formatPhone(selectedConsignee.phone) }}</text>
  13. <text v-if="selectedConsignee.isDefault === 1" class="default-badge">默认</text>
  14. </view>
  15. <text v-if="selectedConsignee.idCard" class="address-detail">身份证: {{ maskField(selectedConsignee.idCard) }}</text>
  16. <text v-if="selectedConsignee.ethnicity" class="address-detail">民族: {{ selectedConsignee.ethnicity }}</text>
  17. <text v-if="selectedConsignee.bloodType" class="address-detail">血型: {{ selectedConsignee.bloodType }}</text>
  18. <text class="address-arrow">›</text>
  19. </view>
  20. <view v-else class="address-empty" @click.stop="goAddConsignee">
  21. <text class="address-empty-icon">📍</text>
  22. <text class="address-empty-text">请选择收货人</text>
  23. <text class="address-arrow">›</text>
  24. </view>
  25. </view>
  26. <!-- Purchase Info Section (when product requires it) -->
  27. <view class="section" v-if="requiredFields.length > 0">
  28. <text class="section-title">购买信息</text>
  29. <view
  30. v-for="field in requiredFields"
  31. :key="field.fieldKey"
  32. class="purchase-field-row"
  33. >
  34. <text class="purchase-field-label">
  35. {{ field.fieldName }}
  36. <text v-if="field.isRequired" class="required-mark">*</text>
  37. </text>
  38. <input
  39. v-model="purchaseForm[field.fieldKey]"
  40. class="purchase-field-input"
  41. :placeholder="'请输入' + field.fieldName + (field.isRequired ? '' : '(选填)')"
  42. :disabled="!!field.prefillValue"
  43. placeholder-style="color: #ccc; font-size: 26rpx;"
  44. />
  45. <text v-if="field.prefillValue" class="purchase-field-prefill">已填写</text>
  46. </view>
  47. </view>
  48. <!-- Order Items Section -->
  49. <view class="section">
  50. <text class="section-title">商品信息</text>
  51. <view
  52. v-for="item in items"
  53. :key="item.productId"
  54. class="order-item"
  55. >
  56. <image
  57. class="item-image"
  58. :src="item.coverImage || '/static/default-product.png'"
  59. mode="aspectFill"
  60. />
  61. <view class="item-info">
  62. <text class="item-name">{{ item.productName }}</text>
  63. <text v-if="item.specDesc" class="item-spec-desc">{{ item.specDesc }}</text>
  64. <text v-if="item.supplierName" class="item-supplier-name">供应商: {{ item.supplierName }}</text>
  65. <text class="item-qty">x{{ item.quantity }}</text>
  66. </view>
  67. <text class="item-price">{{ formatPriceWithSymbol(item.unitPrice) }}</text>
  68. </view>
  69. </view>
  70. <!-- Freight Section -->
  71. <view class="section">
  72. <view class="info-row">
  73. <text class="info-label">运费</text>
  74. <text class="info-value">{{ formatPriceWithSymbol(freight) }}</text>
  75. </view>
  76. </view>
  77. <!-- Member Upgrade Hint -->
  78. <view v-if="!isMember" class="section member-upgrade-section" @click="goUpgrade">
  79. <view class="member-upgrade-row">
  80. <text class="member-upgrade-icon">👑</text>
  81. <text class="member-upgrade-text">开通家庭会员享全场商品会员专享价</text>
  82. <text class="member-upgrade-arrow">›</text>
  83. </view>
  84. </view>
  85. <!-- Coupon Section -->
  86. <view class="section coupon-section" @click="openCouponPicker">
  87. <view class="info-row">
  88. <text class="info-label">优惠券</text>
  89. <view class="coupon-selected" v-if="selectedCoupon">
  90. <text class="coupon-discount">-{{ formatPriceWithSymbol(selectedCoupon.value) }}</text>
  91. <text class="coupon-name-tag">{{ selectedCoupon.name }}</text>
  92. </view>
  93. <view class="coupon-placeholder" v-else>
  94. <text class="placeholder-text">选择优惠券</text>
  95. </view>
  96. <text class="address-arrow">›</text>
  97. </view>
  98. </view>
  99. <!-- Points Deduction Section -->
  100. <view class="section" v-if="userTotalPoints > 0">
  101. <view class="info-row">
  102. <text class="info-label">积分抵扣</text>
  103. <text class="points-balance">可用 {{ userTotalPoints }} 积分</text>
  104. </view>
  105. <view class="points-input-row">
  106. <input
  107. class="points-input"
  108. type="number"
  109. :value="pointsUsed"
  110. @input="onPointsInput"
  111. placeholder="输入使用积分"
  112. placeholder-style="color: #ccc; font-size: 26rpx;"
  113. />
  114. <text class="points-hint">≈ ¥{{ pointsYuanEquivalent }}</text>
  115. </view>
  116. <view class="points-tip">
  117. <text class="points-tip-text">1积分 = ¥0.01,最多使用 {{ maxPoints }} 积分</text>
  118. </view>
  119. </view>
  120. <!-- Remark Section -->
  121. <view class="section">
  122. <view class="info-row">
  123. <text class="info-label">备注</text>
  124. <input
  125. class="remark-input"
  126. v-model="remark"
  127. placeholder="选填,给商家留言"
  128. placeholder-style="color: #ccc; font-size: 26rpx;"
  129. />
  130. </view>
  131. </view>
  132. <view class="bottom-spacer"></view>
  133. <!-- Coupon Picker Bottom Sheet -->
  134. <view class="modal-mask" v-if="showCouponPicker" @click="closeCouponPicker">
  135. <view class="coupon-picker" @click.stop>
  136. <view class="picker-header">
  137. <text class="picker-title">选择优惠券</text>
  138. <text class="picker-close" @click="closeCouponPicker">关闭</text>
  139. </view>
  140. <scroll-view scroll-y class="picker-list">
  141. <view
  142. v-for="coupon in availableCoupons"
  143. :key="coupon.id"
  144. class="picker-item"
  145. :class="selectedCoupon && selectedCoupon.id === coupon.id ? 'picker-item-active' : ''"
  146. @click="selectCoupon(coupon)"
  147. >
  148. <view class="picker-item-left">
  149. <text class="picker-item-value">{{ formatPriceWithSymbol(coupon.value) }}</text>
  150. </view>
  151. <view class="picker-item-right">
  152. <text class="picker-item-name">{{ coupon.name }}</text>
  153. <text class="picker-item-condition">{{ getConditionText(coupon.minSpend) }}</text>
  154. </view>
  155. <view class="picker-item-check" v-if="selectedCoupon && selectedCoupon.id === coupon.id">
  156. <text class="check-icon">✓</text>
  157. </view>
  158. </view>
  159. <view class="picker-empty" v-if="availableCoupons.length === 0">
  160. <text>暂无可用优惠券</text>
  161. </view>
  162. </scroll-view>
  163. <view class="picker-footer" v-if="selectedCoupon">
  164. <button class="picker-btn-remove" @click="removeCoupon">不使用优惠券</button>
  165. </view>
  166. </view>
  167. </view>
  168. </scroll-view>
  169. <view class="bottom-bar">
  170. <view class="bottom-left">
  171. <text class="total-label">合计: </text>
  172. <text class="total-amount">{{ formatPriceWithSymbol(finalAmount) }}</text>
  173. <text v-if="selectedCoupon" class="total-original">{{ formatPriceWithSymbol(actualAmount + freight) }}</text>
  174. </view>
  175. <button
  176. :class="['submit-btn', submitting ? 'disabled' : '']"
  177. :disabled="submitting"
  178. @click="onSubmit"
  179. >提交订单</button>
  180. </view>
  181. </view>
  182. </template>
  183. <script>
  184. import config from '@/config.js'
  185. import { getCouponList, consigneeList, getProductRequiredFields, getMyMembership } from '../../../utils/api.js'
  186. export default {
  187. data() {
  188. return {
  189. items: [],
  190. selectedConsignee: null,
  191. requiredFields: [], // 需要的购买信息字段
  192. purchaseForm: {}, // 用户填写的购买信息
  193. consigneeList: [],
  194. showConsigneePicker: false,
  195. freight: 0,
  196. remark: '',
  197. loading: false,
  198. submitting: false,
  199. actualAmount: 0,
  200. coupons: [],
  201. selectedCoupon: null,
  202. showCouponPicker: false,
  203. pointsUsed: 0,
  204. userTotalPoints: 0,
  205. pointsLoading: false,
  206. isMember: false
  207. }
  208. },
  209. computed: {
  210. finalAmount() {
  211. var total = (this.actualAmount || 0) + (this.freight || 0)
  212. if (this.selectedCoupon) {
  213. total = total - this.selectedCoupon.value
  214. }
  215. total = total - (this.pointsUsed || 0)
  216. if (total < 0) total = 0
  217. return total
  218. },
  219. maxPoints() {
  220. var total = (this.actualAmount || 0) + (this.freight || 0)
  221. if (this.selectedCoupon) {
  222. total = total - this.selectedCoupon.value
  223. }
  224. if (total < 0) total = 0
  225. if (this.userTotalPoints < total) {
  226. return this.userTotalPoints
  227. }
  228. return total
  229. },
  230. pointsYuanEquivalent() {
  231. return (this.pointsUsed / 100).toFixed(2)
  232. },
  233. availableCoupons() {
  234. var self = this
  235. return this.coupons.filter(function(c) {
  236. if (c.status && c.status !== 'AVAILABLE') return false
  237. if (c.applicableTo && c.applicableTo !== 'ALL') return false
  238. if (c.minSpend && c.minSpend > self.actualAmount) return false
  239. return true
  240. })
  241. }
  242. },
  243. onLoad(options) {
  244. var that = this
  245. var stored = uni.getStorageSync('checkoutItems')
  246. if (stored && stored.length > 0) {
  247. this.items = stored
  248. this.calcAmount()
  249. } else if (options.productId) {
  250. // Load product directly from detail page
  251. var productData = {
  252. productId: parseInt(options.productId),
  253. productName: options.productName || '商品',
  254. coverImage: options.coverImage || '',
  255. unitPrice: parseInt(options.price) || 0,
  256. quantity: parseInt(options.quantity) || 1
  257. }
  258. if (options.skuId) {
  259. productData.skuId = parseInt(options.skuId)
  260. }
  261. this.items = [productData]
  262. this.calcAmount()
  263. }
  264. this.checkMemberStatus()
  265. this.loadDefaultConsignee()
  266. this.loadRequiredFields()
  267. this.loadCoupons()
  268. this.loadUserPoints()
  269. },
  270. methods: {
  271. calcAmount() {
  272. var total = 0
  273. for (var i = 0; i < this.items.length; i++) {
  274. total = total + (this.items[i].unitPrice * this.items[i].quantity)
  275. }
  276. this.actualAmount = total
  277. },
  278. async loadCoupons() {
  279. try {
  280. var res = await getCouponList()
  281. this.coupons = res.data || []
  282. } catch (e) {
  283. this.coupons = []
  284. }
  285. },
  286. openCouponPicker() {
  287. if (this.availableCoupons.length === 0 && !this.selectedCoupon) {
  288. uni.showToast({ title: '暂无可用优惠券', icon: 'none' })
  289. return
  290. }
  291. this.showCouponPicker = true
  292. },
  293. closeCouponPicker() {
  294. this.showCouponPicker = false
  295. },
  296. selectCoupon(coupon) {
  297. this.selectedCoupon = coupon
  298. this.showCouponPicker = false
  299. },
  300. removeCoupon() {
  301. this.selectedCoupon = null
  302. this.showCouponPicker = false
  303. },
  304. onPointsInput(e) {
  305. var val = parseInt(e.detail.value) || 0
  306. if (val < 0) val = 0
  307. var maxP = this.maxPoints
  308. if (val > maxP) {
  309. val = maxP
  310. uni.showToast({ title: '最多使用 ' + maxP + ' 积分', icon: 'none' })
  311. }
  312. this.pointsUsed = val
  313. },
  314. getConditionText(minSpend) {
  315. if (!minSpend || minSpend <= 0) return '无门槛'
  316. return '满' + (minSpend / 100).toFixed(2) + '元可用'
  317. },
  318. async loadDefaultConsignee() {
  319. this.loading = true
  320. try {
  321. var res = await consigneeList()
  322. if (res.code === 200) {
  323. var list = res.data || []
  324. this.consigneeList = list
  325. for (var i = 0; i < list.length; i++) {
  326. if (list[i].isDefault === 1) {
  327. this.selectedConsignee = list[i]
  328. this.autoFillPurchaseForm()
  329. break
  330. }
  331. }
  332. if (!this.selectedConsignee && list.length > 0) {
  333. this.selectedConsignee = list[0]
  334. this.autoFillPurchaseForm()
  335. }
  336. }
  337. } catch (e) {
  338. console.error(e)
  339. } finally {
  340. this.loading = false
  341. }
  342. },
  343. async loadRequiredFields() {
  344. if (this.items.length === 0) return
  345. var productId = this.items[0].productId
  346. try {
  347. var res = await getProductRequiredFields(productId)
  348. if (res.code === 200) {
  349. this.requiredFields = res.data || []
  350. this.autoFillPurchaseForm()
  351. }
  352. } catch (e) {
  353. console.error(e)
  354. }
  355. },
  356. autoFillPurchaseForm() {
  357. if (!this.selectedConsignee || this.requiredFields.length === 0) return
  358. var form = {}
  359. for (var i = 0; i < this.requiredFields.length; i++) {
  360. var field = this.requiredFields[i]
  361. if (field.prefillValue) {
  362. form[field.fieldKey] = field.prefillValue
  363. } else {
  364. form[field.fieldKey] = ''
  365. }
  366. }
  367. this.purchaseForm = form
  368. },
  369. openConsigneePicker() {
  370. var that = this
  371. if (this.consigneeList.length === 0) {
  372. uni.showToast({ title: '暂无收货人,请先添加', icon: 'none' })
  373. return
  374. }
  375. var items = this.consigneeList.map(function(c) {
  376. return (c.name || '') + ' ' + (c.phone || '')
  377. })
  378. uni.showActionSheet({
  379. itemList: items,
  380. success: function(res) {
  381. that.selectedConsignee = that.consigneeList[res.tapIndex]
  382. that.autoFillPurchaseForm()
  383. }
  384. })
  385. },
  386. checkMemberStatus() {
  387. var that = this
  388. getMyMembership().then(function(res) {
  389. if (res.data && res.data.memberLevel) {
  390. that.isMember = res.data.memberLevel !== 'FREE'
  391. }
  392. }).catch(function() {})
  393. },
  394. goUpgrade() {
  395. uni.navigateTo({ url: '/pages/membership/upgrade' })
  396. },
  397. goAddConsignee() {
  398. uni.navigateTo({ url: '/pages/shop/consignee-edit/consignee-edit' })
  399. },
  400. loadUserPoints() {
  401. var that = this
  402. uni.request({
  403. url: config.api('/api/user/info'),
  404. method: 'POST',
  405. data: {},
  406. header: {
  407. 'Content-Type': 'application/json',
  408. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  409. },
  410. success: function(res) {
  411. if (res.data && res.data.code === 200) {
  412. that.userTotalPoints = res.data.data.totalPoints || 0
  413. }
  414. }
  415. })
  416. },
  417. onSubmit() {
  418. if (!this.selectedConsignee) {
  419. uni.showToast({ title: '请选择收货人', icon: 'none' })
  420. return
  421. }
  422. if (this.items.length === 0) {
  423. uni.showToast({ title: '请选择商品', icon: 'none' })
  424. return
  425. }
  426. this.submitting = true
  427. var orderItems = []
  428. for (var i = 0; i < this.items.length; i++) {
  429. var itemData = {
  430. productId: this.items[i].productId,
  431. quantity: this.items[i].quantity
  432. }
  433. if (this.items[i].skuId) {
  434. itemData.skuId = this.items[i].skuId
  435. }
  436. orderItems.push(itemData)
  437. }
  438. var that = this
  439. uni.request({
  440. url: config.api('/api/product/order/create'),
  441. method: 'POST',
  442. data: {
  443. items: orderItems,
  444. remark: this.remark,
  445. consigneeId: that.selectedConsignee.id,
  446. purchaseInfo: that.purchaseForm,
  447. pointsUsed: that.pointsUsed
  448. },
  449. header: {
  450. 'Content-Type': 'application/json',
  451. 'Authorization': 'Bearer ' + uni.getStorageSync('token')
  452. },
  453. success: function(res) {
  454. that.submitting = false
  455. if (res.data && res.data.code === 200) {
  456. var order = res.data.data
  457. uni.showToast({ title: '下单成功', icon: 'success' })
  458. uni.removeStorageSync('checkoutItems')
  459. var moneyAmount = order.moneyAmount != null ? order.moneyAmount : order.totalAmount
  460. var pointsUsed = order.pointsUsed || 0
  461. setTimeout(function() {
  462. uni.navigateTo({
  463. url: '/pages/shop/payment/payment?orderNo=' + order.orderNo + '&amount=' + (moneyAmount / 100).toFixed(2) + '&pointsUsed=' + pointsUsed
  464. })
  465. }, 1000)
  466. } else {
  467. uni.showToast({ title: res.data.message || '下单失败', icon: 'none' })
  468. }
  469. },
  470. fail: function() {
  471. that.submitting = false
  472. uni.showToast({ title: '网络请求失败', icon: 'none' })
  473. }
  474. })
  475. },
  476. formatPrice(price) {
  477. if (price === null || price === undefined) return '0.00'
  478. return (Number(price) / 100).toFixed(2)
  479. },
  480. formatPhone(phone) {
  481. if (!phone) return ''
  482. if (phone.length === 11) {
  483. return phone.substr(0, 3) + '****' + phone.substr(7)
  484. }
  485. return phone
  486. },
  487. maskField(val) {
  488. if (!val) return ''
  489. if (val.length >= 8) {
  490. return val.substr(0, 4) + '****' + val.substr(val.length - 4)
  491. }
  492. return val
  493. }
  494. }
  495. }
  496. </script>
  497. <style scoped>
  498. .container {
  499. min-height: 100vh;
  500. background: #f5f5f5;
  501. }
  502. .loading-wrap {
  503. display: flex;
  504. justify-content: center;
  505. padding-top: 200rpx;
  506. }
  507. .loading-text {
  508. font-size: 28rpx;
  509. color: #999;
  510. }
  511. .content {
  512. height: calc(100vh - 120rpx);
  513. }
  514. .section {
  515. background: #fff;
  516. margin: 20rpx;
  517. border-radius: 16rpx;
  518. padding: 24rpx;
  519. }
  520. .section-title {
  521. font-size: 28rpx;
  522. font-weight: bold;
  523. color: #333;
  524. margin-bottom: 20rpx;
  525. display: block;
  526. }
  527. .address-section {
  528. position: relative;
  529. }
  530. .address-content {
  531. position: relative;
  532. }
  533. .address-top {
  534. display: flex;
  535. align-items: center;
  536. margin-bottom: 10rpx;
  537. }
  538. .receiver-name {
  539. font-size: 30rpx;
  540. font-weight: bold;
  541. color: #333;
  542. margin-right: 20rpx;
  543. }
  544. .receiver-phone {
  545. font-size: 26rpx;
  546. color: #666;
  547. }
  548. .default-badge {
  549. font-size: 20rpx;
  550. color: #F97316;
  551. background: #fff7e6;
  552. padding: 2rpx 12rpx;
  553. border-radius: 20rpx;
  554. margin-left: 12rpx;
  555. }
  556. .address-detail {
  557. font-size: 26rpx;
  558. color: #999;
  559. line-height: 1.5;
  560. }
  561. .address-arrow {
  562. position: absolute;
  563. right: 0;
  564. top: 50%;
  565. font-size: 40rpx;
  566. color: #ccc;
  567. transform: translateY(-50%);
  568. }
  569. .address-empty {
  570. display: flex;
  571. align-items: center;
  572. justify-content: center;
  573. padding: 30rpx 0;
  574. position: relative;
  575. }
  576. .address-empty-icon {
  577. font-size: 40rpx;
  578. margin-right: 12rpx;
  579. }
  580. .address-empty-text {
  581. font-size: 28rpx;
  582. color: #999;
  583. }
  584. .order-item {
  585. display: flex;
  586. align-items: center;
  587. padding: 16rpx 0;
  588. border-bottom: 1rpx solid #f5f5f5;
  589. }
  590. .order-item:last-child {
  591. border-bottom: none;
  592. }
  593. .item-image {
  594. width: 80rpx;
  595. height: 80rpx;
  596. border-radius: 8rpx;
  597. background: #f0f0f0;
  598. margin-right: 16rpx;
  599. }
  600. .item-info {
  601. flex: 1;
  602. display: flex;
  603. flex-direction: column;
  604. min-width: 0;
  605. }
  606. .item-name {
  607. font-size: 26rpx;
  608. color: #333;
  609. overflow: hidden;
  610. text-overflow: ellipsis;
  611. white-space: nowrap;
  612. margin-bottom: 6rpx;
  613. }
  614. .item-spec-desc {
  615. font-size: 22rpx;
  616. color: #999;
  617. display: block;
  618. margin-top: 2rpx;
  619. }
  620. .item-supplier-name {
  621. font-size: 20rpx;
  622. color: #0EA5E9;
  623. display: block;
  624. margin-top: 2rpx;
  625. }
  626. .item-qty {
  627. font-size: 22rpx;
  628. color: #999;
  629. }
  630. .item-price {
  631. font-size: 26rpx;
  632. color: #F97316;
  633. font-weight: bold;
  634. margin-left: 16rpx;
  635. }
  636. .info-row {
  637. display: flex;
  638. justify-content: space-between;
  639. align-items: center;
  640. padding: 8rpx 0;
  641. }
  642. .info-label {
  643. font-size: 26rpx;
  644. color: #999;
  645. }
  646. .info-value {
  647. font-size: 26rpx;
  648. color: #333;
  649. }
  650. .remark-input {
  651. flex: 1;
  652. text-align: right;
  653. font-size: 26rpx;
  654. color: #333;
  655. }
  656. /* ===== Coupon Section ===== */
  657. .coupon-section {
  658. position: relative;
  659. }
  660. .coupon-section:active {
  661. background: #fafafa;
  662. }
  663. .coupon-selected {
  664. display: flex;
  665. align-items: center;
  666. flex: 1;
  667. justify-content: flex-end;
  668. margin-right: 30rpx;
  669. }
  670. .coupon-discount {
  671. font-size: 26rpx;
  672. color: #F97316;
  673. font-weight: 600;
  674. margin-right: 12rpx;
  675. }
  676. .coupon-name-tag {
  677. font-size: 22rpx;
  678. color: #fff;
  679. background: #F97316;
  680. padding: 2rpx 12rpx;
  681. border-radius: 6rpx;
  682. max-width: 200rpx;
  683. overflow: hidden;
  684. text-overflow: ellipsis;
  685. white-space: nowrap;
  686. }
  687. .coupon-placeholder {
  688. flex: 1;
  689. text-align: right;
  690. margin-right: 30rpx;
  691. }
  692. .placeholder-text {
  693. font-size: 26rpx;
  694. color: #ccc;
  695. }
  696. .total-original {
  697. font-size: 22rpx;
  698. color: #bbb;
  699. text-decoration: line-through;
  700. margin-left: 8rpx;
  701. }
  702. /* ===== Member Upgrade Hint ===== */
  703. .member-upgrade-section {
  704. position: relative;
  705. }
  706. .member-upgrade-row {
  707. display: flex;
  708. align-items: center;
  709. }
  710. .member-upgrade-icon {
  711. font-size: 32rpx;
  712. margin-right: 12rpx;
  713. }
  714. .member-upgrade-text {
  715. flex: 1;
  716. font-size: 26rpx;
  717. color: #F97316;
  718. font-weight: 500;
  719. }
  720. .member-upgrade-arrow {
  721. font-size: 32rpx;
  722. color: #F97316;
  723. }
  724. /* ===== Coupon Picker Modal ===== */
  725. .modal-mask {
  726. position: fixed;
  727. top: 0;
  728. left: 0;
  729. right: 0;
  730. bottom: 0;
  731. background: rgba(0,0,0,0.5);
  732. z-index: 999;
  733. display: flex;
  734. align-items: flex-end;
  735. }
  736. .coupon-picker {
  737. background: #fff;
  738. border-radius: 24rpx 24rpx 0 0;
  739. width: 100%;
  740. max-height: 70vh;
  741. display: flex;
  742. flex-direction: column;
  743. }
  744. .picker-header {
  745. display: flex;
  746. justify-content: space-between;
  747. align-items: center;
  748. padding: 30rpx;
  749. border-bottom: 1rpx solid #eee;
  750. }
  751. .picker-title {
  752. font-size: 30rpx;
  753. font-weight: 600;
  754. color: #333;
  755. }
  756. .picker-close {
  757. font-size: 26rpx;
  758. color: #999;
  759. }
  760. .picker-list {
  761. max-height: 50vh;
  762. padding: 0 20rpx;
  763. }
  764. .picker-item {
  765. display: flex;
  766. align-items: center;
  767. padding: 24rpx 16rpx;
  768. border-bottom: 1rpx solid #f5f5f5;
  769. }
  770. .picker-item:active {
  771. background: #fafafa;
  772. }
  773. .picker-item-active {
  774. background: #FFF7ED;
  775. }
  776. .picker-item-left {
  777. width: 120rpx;
  778. text-align: center;
  779. flex-shrink: 0;
  780. }
  781. .picker-item-value {
  782. font-size: 32rpx;
  783. font-weight: bold;
  784. color: #F97316;
  785. }
  786. .picker-item-right {
  787. flex: 1;
  788. margin: 0 16rpx;
  789. }
  790. .picker-item-name {
  791. font-size: 26rpx;
  792. color: #333;
  793. display: block;
  794. margin-bottom: 4rpx;
  795. }
  796. .picker-item-condition {
  797. font-size: 22rpx;
  798. color: #999;
  799. }
  800. .picker-item-check {
  801. width: 40rpx;
  802. height: 40rpx;
  803. background: #F97316;
  804. border-radius: 50%;
  805. display: flex;
  806. align-items: center;
  807. justify-content: center;
  808. flex-shrink: 0;
  809. }
  810. .check-icon {
  811. color: #fff;
  812. font-size: 24rpx;
  813. font-weight: bold;
  814. }
  815. .picker-empty {
  816. text-align: center;
  817. padding: 60rpx 0;
  818. font-size: 26rpx;
  819. color: #999;
  820. }
  821. .picker-footer {
  822. padding: 20rpx;
  823. border-top: 1rpx solid #eee;
  824. }
  825. .picker-btn-remove {
  826. background: #f5f5f5;
  827. color: #666;
  828. font-size: 26rpx;
  829. border-radius: 12rpx;
  830. border: none;
  831. }
  832. .picker-btn-remove::after {
  833. border: none;
  834. }
  835. .bottom-spacer {
  836. height: 140rpx;
  837. }
  838. .bottom-bar {
  839. position: fixed;
  840. bottom: 0;
  841. left: 0;
  842. right: 0;
  843. height: 100rpx;
  844. background: #fff;
  845. border-top: 1rpx solid #eee;
  846. display: flex;
  847. align-items: center;
  848. justify-content: space-between;
  849. padding: 0 30rpx;
  850. z-index: 100;
  851. }
  852. .bottom-left {
  853. display: flex;
  854. align-items: baseline;
  855. }
  856. .total-label {
  857. font-size: 26rpx;
  858. color: #666;
  859. }
  860. .total-amount {
  861. font-size: 36rpx;
  862. color: #F97316;
  863. font-weight: bold;
  864. }
  865. .submit-btn {
  866. background: linear-gradient(135deg, #F97316, #FB923C);
  867. color: #fff;
  868. font-size: 28rpx;
  869. font-weight: bold;
  870. padding: 0 60rpx;
  871. height: 72rpx;
  872. line-height: 72rpx;
  873. border-radius: 36rpx;
  874. border: none;
  875. }
  876. .submit-btn::after {
  877. border: none;
  878. }
  879. .submit-btn.disabled {
  880. background: #ddd;
  881. color: #999;
  882. }
  883. .points-balance {
  884. font-size: 24rpx;
  885. color: #F97316;
  886. }
  887. .points-input-row {
  888. display: flex;
  889. align-items: center;
  890. margin-top: 16rpx;
  891. padding: 12rpx 0;
  892. border-top: 1rpx solid #f5f5f5;
  893. }
  894. .points-input {
  895. flex: 1;
  896. height: 60rpx;
  897. font-size: 28rpx;
  898. color: #333;
  899. background: #f9f9f9;
  900. border-radius: 8rpx;
  901. padding: 0 16rpx;
  902. }
  903. .points-hint {
  904. font-size: 24rpx;
  905. color: #999;
  906. margin-left: 12rpx;
  907. min-width: 100rpx;
  908. text-align: right;
  909. }
  910. .points-tip {
  911. margin-top: 8rpx;
  912. }
  913. .points-tip-text {
  914. font-size: 22rpx;
  915. color: #bbb;
  916. }
  917. .total-original {
  918. font-size: 24rpx;
  919. color: #bbb;
  920. text-decoration: line-through;
  921. margin-left: 8rpx;
  922. }
  923. /* ===== Purchase Info Fields ===== */
  924. .purchase-field-row {
  925. display: flex;
  926. align-items: center;
  927. padding: 16rpx 0;
  928. border-bottom: 1rpx solid #f5f5f5;
  929. }
  930. .purchase-field-row:last-child {
  931. border-bottom: none;
  932. }
  933. .purchase-field-label {
  934. font-size: 26rpx;
  935. color: #333;
  936. width: 160rpx;
  937. flex-shrink: 0;
  938. }
  939. .required-mark {
  940. color: #f56c6c;
  941. }
  942. .purchase-field-input {
  943. flex: 1;
  944. font-size: 26rpx;
  945. color: #333;
  946. text-align: right;
  947. padding: 8rpx 0;
  948. }
  949. .purchase-field-input[disabled] {
  950. background: transparent;
  951. color: #999;
  952. }
  953. .purchase-field-prefill {
  954. font-size: 22rpx;
  955. color: #67c23a;
  956. margin-left: 8rpx;
  957. }
  958. </style>