checkout.vue 21 KB

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