checkout.vue 23 KB

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