爱简收旧衣按件回收前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

860 lines
25 KiB

2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
  1. <template>
  2. <view class="pickup-container" :style="{paddingTop: navBarHeightRpx + 'rpx'}">
  3. <!-- 顶部导航栏 -->
  4. <view class="nav-bar" :style="{height: (statusBarHeight + 88) + 'rpx', paddingTop: statusBarHeight + 'px'}">
  5. <view class="back" @tap="goBack">
  6. <uni-icons type="left" size="20"></uni-icons>
  7. </view>
  8. <text class="title">免费上门取件预约</text>
  9. </view>
  10. <!-- 内容区域 -->
  11. <view class="content">
  12. <!-- 回收流程卡片 -->
  13. <view class="card process-card">
  14. <view class="card-title process-title">回收流程</view>
  15. <view class="process-steps">
  16. <view
  17. class="process-step-card"
  18. v-for="(step, i) in steps"
  19. :key="i"
  20. >
  21. <image :src="step.icon" class="step-icon" mode="aspectFit" />
  22. <view v-if="i === 0" class="step-bottom-bar">
  23. <view class="step-num-bar">
  24. <text class="text-main">{{ step.text }}</text>
  25. </view>
  26. </view>
  27. <view v-else class="step-label-gray">
  28. <text class="text-gray">{{ step.text }}</text>
  29. </view>
  30. </view>
  31. </view>
  32. <view class="divider"></view>
  33. <!-- 取件信息 -->
  34. <view class="pickup-info">
  35. <view class="info-item" @tap="selectAddress">
  36. <text class="label">取件地址</text>
  37. <view class="value">
  38. <text class="text" :class="{placeholder: !displayAddress}">{{ displayAddress || '请选择' }}</text>
  39. <text class="arrow">></text>
  40. </view>
  41. </view>
  42. <view class="info-item" @tap="openTimePicker">
  43. <text class="label">上门时间</text>
  44. <view class="value">
  45. <text class="text" :class="{placeholder: !selectedTime}">{{ selectedTime || '请选择' }}</text>
  46. <text class="arrow">></text>
  47. </view>
  48. </view>
  49. </view>
  50. </view>
  51. <!-- 订单详情卡片 -->
  52. <view class="card order-card">
  53. <view class="card-title process-title">订单详情</view>
  54. <view class="order-items">
  55. <view class="order-item" v-for="(item, index) in showAllItems ? selectedItems : selectedItems.slice(0, 3)" :key="index">
  56. <view class="item-left">
  57. <image :src="item.icon" mode="aspectFit"></image>
  58. <image v-if="item.brandImage" :src="item.brandImage" class="brand-logo" mode="aspectFit"></image>
  59. </view>
  60. <view class="item-info">
  61. <view class="name-brand-row">
  62. <text class="name">{{ item.name }}</text>
  63. <text v-if="item.brandName" class="brand-tag">{{ item.brandName }}</text>
  64. </view>
  65. <!-- <view class="desc">{{ item.desc }}</view> -->
  66. <view class="price-row">
  67. <text class="price" v-if="!item.maxPrice || item.maxPrice == (item.price || item.unitPrice)">{{ item.price || item.unitPrice }}/{{item.unit}}</text>
  68. <text class="price" v-else>{{ item.price || item.unitPrice }}~{{ item.maxPrice }}/{{item.unit}}</text>
  69. <text class="count">x{{ item.quantity }}</text>
  70. <text class="amount" v-if="!item.maxPrice || item.maxPrice == (item.price || item.unitPrice)">{{ ((item.price || item.unitPrice) * item.quantity).toFixed(2) }}</text>
  71. <text class="amount" v-else>{{ ((item.price || item.unitPrice) * item.quantity).toFixed(2) }}~{{ ((item.maxPrice) * item.quantity).toFixed(2) }}</text>
  72. </view>
  73. </view>
  74. </view>
  75. </view>
  76. <view v-if="selectedItems.length > 3" class="expand-btn" @tap="toggleExpandOrder">
  77. <text>{{ showAllItems ? '收起' : `展开(共${selectedItems.length}件)` }}</text>
  78. <text class="arrow">{{ showAllItems ? '▲' : '▼' }}</text>
  79. </view>
  80. </view>
  81. </view>
  82. <!-- 订单说明 -->
  83. <view class="order-desc">
  84. <view>1. 当前回收快递免费上门由于快递成本较高为避免不必要的成本及资源二次浪费不属于回收品类或不符合回收标准的物品请勿寄出</view>
  85. <view>2. 已通过的回收物品将正常结算不符合回收要求的物品可选择安排取回逾期未联系将默认捐赠无法再次取回</view>
  86. <view>3. 若用户寄出大量不可回收的物品平台有权限制下次回收权限或取消下次包邮服务</view>
  87. <view>4. 对于合格率高的回收订单平台将根据实际情况给予额外回收奖励</view>
  88. </view>
  89. <!-- 底部提交栏 -->
  90. <view class="agreement-bar">
  91. <view class="checkbox" :class="{active: agreed}" @tap="toggleAgreement">
  92. <text v-if="agreed"></text>
  93. </view>
  94. <text>我已阅读并同意</text>
  95. <text class="link" @tap="showServiceAgreement">回收服务协议</text>
  96. <text></text>
  97. <text class="link" @tap="showPrivacyPolicy">隐私政策</text>
  98. </view>
  99. <view class="bottom-bar">
  100. <view class="summary">
  101. <text>已选 {{ totalCount }} 预估回收可得</text>
  102. <text class="amount">{{ totalPriceRange }}</text>
  103. </view>
  104. <button class="main-btn" @tap="submitOrder">预约上门取件</button>
  105. </view>
  106. <!-- 时间选择弹窗 -->
  107. <view class="time-picker" v-if="showTimePicker">
  108. <view class="mask" @tap="closeTimePicker"></view>
  109. <view class="picker-content">
  110. <view class="picker-header">
  111. <text class="reset" @tap="resetPicker">重置</text>
  112. <text class="title">预约上门时间</text>
  113. </view>
  114. <view class="picker-section">
  115. <view class="section-title">选择日期</view>
  116. <view class="date-btns">
  117. <view
  118. v-for="(tab, index) in dateTabs"
  119. :key="index"
  120. :class="['date-btn', {active: currentDateTab === index}]"
  121. @tap="selectDateTab(index)"
  122. >
  123. {{ tab.label }}
  124. </view>
  125. </view>
  126. </view>
  127. <view class="picker-section">
  128. <view class="section-title">选择时间</view>
  129. <view class="time-btns">
  130. <view
  131. v-for="(slot, idx) in timeSlots"
  132. :key="idx"
  133. :class="['time-btn', {active: selectedTimeSlot === idx, disabled: !availableTimeSlots[idx]}]"
  134. @tap="availableTimeSlots[idx] ? selectTimeSlot(idx) : null"
  135. >
  136. {{ slot }}
  137. </view>
  138. </view>
  139. </view>
  140. <view class="confirm-btn" @tap="confirmTime">确认</view>
  141. </view>
  142. </view>
  143. </view>
  144. </template>
  145. <script>
  146. import pullRefreshMixin from '@/pages/mixins/pullRefreshMixin.js'
  147. export default {
  148. mixins: [pullRefreshMixin],
  149. data() {
  150. return {
  151. statusBarHeight: 0,
  152. navBarHeight: 0, // px
  153. navBarHeightRpx: 0, // rpx
  154. fromRecycle: false,
  155. address: '',
  156. selectedAddress: null,
  157. selectedTime: '',
  158. agreed: false,
  159. selectedItems: [],
  160. showTimePicker: false,
  161. currentDateTab: 0,
  162. dateTabs: [], // 动态生成
  163. timeSlots: ['09:00~11:00', '11:00~13:00', '13:00~15:00', '15:00~17:00', '17:00~19:00'],
  164. selectedTimeSlot: -1,
  165. steps: [], // 改为空数组,由接口获取
  166. showAllItems: false,
  167. addressId: ''
  168. }
  169. },
  170. onShow() {
  171. // 页面显示时触发,包括从地址选择页面返回时
  172. console.log('当前选中的地址:', this.selectedAddress)
  173. if (this.selectedAddress) {
  174. // 确保地址信息被正确更新
  175. this.address = this.selectedAddress.address
  176. this.addressId = this.selectedAddress.id
  177. // 强制更新视图
  178. this.$forceUpdate()
  179. }
  180. },
  181. onLoad(options) {
  182. // 判断是否从回收页面跳转而来
  183. this.fromRecycle = options.fromRecycle === 'true'
  184. // 如果是从回收页面跳转来的,解析传递的衣物信息
  185. if (this.fromRecycle && options.items) {
  186. try {
  187. this.selectedItems = JSON.parse(decodeURIComponent(options.items))
  188. } catch (e) {
  189. console.error('解析衣物信息失败:', e)
  190. }
  191. }
  192. // 监听地址选择事件
  193. uni.$on('addressSelected', (address) => {
  194. this.selectedAddress = address
  195. this.address = address.address
  196. this.addressId = address.id
  197. // 兼容 addressDetails
  198. if (address.addressDetails) this.selectedAddress.addressDetails = address.addressDetails
  199. this.$forceUpdate()
  200. })
  201. // 监听从订单详情页面返回的事件
  202. uni.$on('clearRecycleData', () => {
  203. // 清除回收页面的订单数据
  204. uni.$emit('clearRecycleOrderData')
  205. })
  206. const sysInfo = uni.getSystemInfoSync()
  207. this.statusBarHeight = sysInfo.statusBarHeight
  208. let navBarHeight = 44
  209. try {
  210. const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
  211. navBarHeight = menuButtonInfo.bottom + menuButtonInfo.top - sysInfo.statusBarHeight
  212. } catch (e) {}
  213. this.navBarHeight = navBarHeight
  214. this.navBarHeightRpx = Math.round(navBarHeight * 750 / sysInfo.windowWidth)
  215. this.getAddressList();
  216. // 动态生成时间tab
  217. this.dateTabs = this.generateDateTabs();
  218. // 获取步骤条数据
  219. this.getAreaList();
  220. },
  221. onUnload() {
  222. // 页面卸载时移除事件监听
  223. uni.$off('addressSelected')
  224. uni.$off('clearRecycleData')
  225. },
  226. computed: {
  227. availableTimeSlots() {
  228. const tab = this.dateTabs[this.currentDateTab];
  229. if (!tab) return this.timeSlots.map(() => true);
  230. const dateObj = tab.date;
  231. const now = new Date();
  232. return this.timeSlots.map(slot => {
  233. const startTime = slot.split('~')[0];
  234. const [h, m] = startTime.split(':');
  235. const slotDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), h, m);
  236. return slotDate > now;
  237. });
  238. },
  239. totalCount() {
  240. return this.selectedItems.reduce((sum, item) => sum + item.quantity, 0)
  241. },
  242. totalPriceRange() {
  243. if (this.selectedItems.length === 0) return '0.0'
  244. let minTotal = 0
  245. let maxTotal = 0
  246. this.selectedItems.forEach(item => {
  247. const minPrice = item.price || item.unitPrice || 0
  248. const maxPrice = item.maxPrice || minPrice || 0
  249. minTotal += minPrice * item.quantity
  250. maxTotal += maxPrice * item.quantity
  251. })
  252. // 如果最小值和最大值相等,只显示一个值
  253. if (minTotal === maxTotal) {
  254. return minTotal.toFixed(2)
  255. }
  256. return `${minTotal.toFixed(2)}~${maxTotal.toFixed(2)}`
  257. },
  258. canSubmit() {
  259. return this.agreed && this.selectedItems.length > 0 && this.selectedTime && this.displayAddress
  260. },
  261. displayAddress() {
  262. if (this.selectedAddress) {
  263. // 拼接 address 和 addressDetails
  264. return (this.selectedAddress.address || '') + (this.selectedAddress.addressDetails ? ' ' + this.selectedAddress.addressDetails : '')
  265. }
  266. return ''
  267. }
  268. },
  269. methods: {
  270. async onRefresh() {
  271. // 模拟刷新数据
  272. await new Promise(resolve => setTimeout(resolve, 1000))
  273. uni.stopPullRefresh()
  274. },
  275. goBack() {
  276. uni.navigateBack()
  277. },
  278. showMoreMenu() {
  279. uni.showModal({ title: '更多', content: '这里可以放更多操作' })
  280. },
  281. showScan() {
  282. uni.showModal({ title: '扫码', content: '这里可以实现扫码功能' })
  283. },
  284. selectAddress() {
  285. uni.navigateTo({ url: '/pages/subcomponent/select?mode=select' })
  286. },
  287. openTimePicker() {
  288. this.showTimePicker = true
  289. },
  290. closeTimePicker() {
  291. this.showTimePicker = false
  292. },
  293. selectDateTab(index) {
  294. this.currentDateTab = index
  295. },
  296. selectTimeSlot(index) {
  297. if (this.availableTimeSlots[index]) {
  298. this.selectedTimeSlot = index;
  299. }
  300. },
  301. confirmTime() {
  302. if (this.selectedTimeSlot === -1) {
  303. uni.showToast({ title: '请选择可用时间段', icon: 'none' });
  304. return;
  305. }
  306. const tab = this.dateTabs[this.currentDateTab];
  307. const dateObj = tab.date;
  308. const timeStr = this.timeSlots[this.selectedTimeSlot];
  309. const startTime = timeStr.split('~')[0];
  310. const yyyy = dateObj.getFullYear();
  311. const mm = (dateObj.getMonth() + 1).toString().padStart(2, '0');
  312. const dd = dateObj.getDate().toString().padStart(2, '0');
  313. this.selectedTime = `${yyyy}-${mm}-${dd} ${startTime}:00`;
  314. this.closeTimePicker();
  315. },
  316. resetPicker() {
  317. this.currentDateTab = 0;
  318. this.selectedTimeSlot = -1;
  319. },
  320. toggleAgreement() {
  321. this.agreed = !this.agreed
  322. },
  323. showServiceAgreement() {
  324. uni.showModal({ title: '回收服务协议', content: '这里展示回收服务协议内容' })
  325. },
  326. showPrivacyPolicy() {
  327. uni.showModal({ title: '隐私政策', content: '这里展示隐私政策内容' })
  328. },
  329. submitOrder() {
  330. if (!this.agreed) {
  331. uni.showToast({ title: '请先同意服务协议', icon: 'none' })
  332. return
  333. }
  334. if (!this.displayAddress || this.displayAddress === '请选择取件地址') {
  335. uni.showToast({ title: '请选择取件地址', icon: 'none' })
  336. return
  337. }
  338. if (!this.selectedTime) {
  339. uni.showToast({ title: '请选择上门时间', icon: 'none' })
  340. return
  341. }
  342. if (this.selectedItems.length === 0) {
  343. uni.showToast({ title: '请选择回收物品', icon: 'none' })
  344. return
  345. }
  346. const list = this.selectedItems.map(item => {
  347. const orderItem = {
  348. shopId: item.id,
  349. num: item.quantity
  350. };
  351. // 如果有品牌ID,添加到订单项中
  352. if (item.brandId) {
  353. orderItem.pinId = item.brandId;
  354. } else if (item.pinId) {
  355. orderItem.pinId = item.pinId;
  356. }
  357. return orderItem;
  358. });
  359. console.log({
  360. addressId: this.addressId,
  361. strTime: this.selectedTime,
  362. list: list
  363. },'createOrder');
  364. // 校验通过,提交
  365. uni.showLoading({ title: '提交中...' })
  366. this.$api('createOrder', {
  367. addressId: this.addressId,
  368. strTime: this.selectedTime,
  369. list: JSON.stringify(list)
  370. }, (res) => {
  371. if (res && res.success) {
  372. console.log(res,'createOrder-res');
  373. uni.showToast({ title: '预约成功', icon: 'success' })
  374. uni.redirectTo({
  375. url: `/pages/subcomponent/detail?id=${res.result.id}`
  376. })
  377. }
  378. })
  379. // setTimeout(() => {
  380. // uni.hideLoading()
  381. // uni.showToast({ title: '预约成功', icon: 'success' })
  382. // setTimeout(() => {
  383. // uni.navigateBack()
  384. // }, 1500)
  385. // }, 1000)
  386. },
  387. toggleExpandOrder() {
  388. this.showAllItems = !this.showAllItems
  389. },
  390. async getAddressList() {
  391. const res = await this.$api('getAddressList', {});
  392. if (res && res.code === 200 && res.result && res.result.records) {
  393. const defaultAddr = res.result.records.find(item => item.defaultFlag === 'Y');
  394. if (defaultAddr) {
  395. this.selectedAddress = defaultAddr;
  396. this.address = defaultAddr.address;
  397. this.addressId = defaultAddr.id;
  398. // 兼容 addressDetails
  399. if (defaultAddr.addressDetails) this.selectedAddress.addressDetails = defaultAddr.addressDetails
  400. } else {
  401. this.selectedAddress = null;
  402. this.address = '';
  403. }
  404. }
  405. },
  406. generateDateTabs() {
  407. const weekMap = ['日', '一', '二', '三', '四', '五', '六']
  408. const result = []
  409. const today = new Date()
  410. for (let i = 0; i < 6; i++) {
  411. const d = new Date(today)
  412. d.setDate(today.getDate() + i)
  413. const mm = (d.getMonth() + 1).toString().padStart(2, '0')
  414. const dd = d.getDate().toString().padStart(2, '0')
  415. let label = ''
  416. if (i === 0) label = `今天 ${mm}-${dd}`
  417. else if (i === 1) label = `明天 ${mm}-${dd}`
  418. else if (i === 2) label = `后天 ${mm}-${dd}`
  419. else label = `${weekMap[d.getDay()]} ${mm}-${dd}`
  420. result.push({ label, date: new Date(d) })
  421. }
  422. return result
  423. },
  424. getAreaList() {
  425. this.$api('getAreaList', {}, (res) => {
  426. if (res.code == 200 && Array.isArray(res.result)) {
  427. // 按sort升序排序
  428. const sorted = res.result.slice().sort((a, b) => a.sort - b.sort)
  429. this.steps = sorted.map(item => ({
  430. icon: item.image,
  431. text: item.title
  432. }))
  433. }
  434. })
  435. }
  436. }
  437. }
  438. </script>
  439. <style lang="scss" scoped>
  440. .pickup-container {
  441. min-height: 100vh;
  442. background: #f8f8f8;
  443. }
  444. .nav-bar {
  445. position: fixed;
  446. top: 0;
  447. left: 0;
  448. right: 0;
  449. z-index: 999;
  450. display: flex;
  451. align-items: center;
  452. background: #fff;
  453. padding: 0 30rpx;
  454. .back {
  455. padding: 20rpx;
  456. margin-left: -20rpx;
  457. }
  458. .title {
  459. flex: 1;
  460. text-align: center;
  461. font-size: 34rpx;
  462. font-weight: 500;
  463. color: #222;
  464. }
  465. .right-btns {
  466. display: flex;
  467. gap: 30rpx;
  468. .more, .scan {
  469. font-size: 40rpx;
  470. color: #333;
  471. }
  472. }
  473. }
  474. .content {
  475. padding: 20rpx;
  476. }
  477. .card {
  478. background: linear-gradient(to bottom,#fff3db 0%,#fffefb 40%);
  479. border-radius: 20rpx;
  480. margin-bottom: 20rpx;
  481. overflow: hidden;
  482. box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.03);
  483. }
  484. .process-card {
  485. background: #fff;
  486. border-radius: 24rpx;
  487. box-shadow: 0 8rpx 32rpx rgba(255, 149, 0, 0.08);
  488. padding: 0 0 20rpx 0;
  489. }
  490. .process-steps {
  491. display: flex;
  492. justify-content: space-between;
  493. align-items: flex-start;
  494. padding: 0 30rpx 30rpx;
  495. .process-step-card {
  496. background: #FFF8ED;
  497. border-radius: 24rpx;
  498. min-width: 140rpx;
  499. min-height: 180rpx;
  500. display: flex;
  501. flex-direction: column;
  502. align-items: center;
  503. margin-right: 24rpx;
  504. position: relative;
  505. .step-icon {
  506. width: 64rpx;
  507. height: 64rpx;
  508. margin: 24rpx 0 18rpx 0;
  509. }
  510. .step-bottom-bar {
  511. position: absolute;
  512. left: 0;
  513. right: 0;
  514. bottom: 0;
  515. height: 56rpx;
  516. background: #FFB74D;
  517. border-radius: 0 0 24rpx 24rpx;
  518. display: flex;
  519. align-items: center;
  520. justify-content: center;
  521. .step-num-bar {
  522. display: flex;
  523. flex-direction: row;
  524. align-items: center;
  525. margin-top: 8rpx;
  526. .num-main {
  527. width: 32rpx;
  528. height: 32rpx;
  529. border-radius: 50%;
  530. background: #fff;
  531. color: #FFB74D;
  532. font-size: 22rpx;
  533. display: flex;
  534. align-items: center;
  535. justify-content: center;
  536. font-weight: 600;
  537. margin-right: 10rpx;
  538. }
  539. .text-main {
  540. color: #fff;
  541. font-size: 26rpx;
  542. font-weight: 500;
  543. }
  544. }
  545. }
  546. .step-label-gray {
  547. position: absolute;
  548. left: 0;
  549. right: 0;
  550. bottom: 0;
  551. height: 56rpx;
  552. background: #FFF8ED;
  553. border-radius: 0 0 24rpx 24rpx;
  554. display: flex;
  555. align-items: center;
  556. justify-content: center;
  557. .num-gray {
  558. width: 32rpx;
  559. height: 32rpx;
  560. border-radius: 50%;
  561. background: #eee;
  562. color: #bbb;
  563. font-size: 22rpx;
  564. display: flex;
  565. align-items: center;
  566. justify-content: center;
  567. font-weight: 600;
  568. margin-right: 10rpx;
  569. }
  570. .text-gray {
  571. color: #bbb;
  572. font-size: 26rpx;
  573. font-weight: 500;
  574. }
  575. }
  576. }
  577. .process-step-card:last-child {
  578. margin-right: 0;
  579. }
  580. }
  581. .divider {
  582. height: 1rpx;
  583. background: rgba(0, 0, 0, 0.05);
  584. margin: 0 30rpx;
  585. }
  586. .pickup-info {
  587. padding: 0 30rpx;
  588. .info-item {
  589. padding: 30rpx 0;
  590. border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
  591. &:last-child { border-bottom: none; }
  592. .label {
  593. font-size: 28rpx;
  594. color: #333;
  595. margin-bottom: 16rpx;
  596. display: block;
  597. }
  598. .value {
  599. display: flex;
  600. justify-content: space-between;
  601. align-items: center;
  602. .text {
  603. flex: 1;
  604. font-size: 28rpx;
  605. color: #333;
  606. overflow: hidden;
  607. text-overflow: ellipsis;
  608. white-space: nowrap;
  609. }
  610. .text.placeholder { color: #ccc; }
  611. .arrow {
  612. color: #999;
  613. font-size: 28rpx;
  614. margin-left: 10rpx;
  615. }
  616. }
  617. }
  618. }
  619. .order-card {
  620. background: #fff;
  621. border-radius: 24rpx;
  622. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.04);
  623. margin-bottom: 20rpx;
  624. .order-items {
  625. padding: 0 30rpx;
  626. .order-item {
  627. display: flex;
  628. align-items: flex-start;
  629. padding: 30rpx 0;
  630. border-bottom: 1rpx solid #f5f5f5;
  631. &:last-child { border-bottom: none; }
  632. .item-left {
  633. position: relative;
  634. margin-right: 20rpx;
  635. image {
  636. width: 80rpx;
  637. height: 80rpx;
  638. border-radius: 8rpx;
  639. }
  640. .brand-logo {
  641. position: absolute;
  642. bottom: -8rpx;
  643. right: -8rpx;
  644. width: 32rpx;
  645. height: 32rpx;
  646. border-radius: 50%;
  647. border: 2rpx solid #fff;
  648. background: #fff;
  649. }
  650. }
  651. .item-info {
  652. flex: 1;
  653. .name-brand-row {
  654. display: flex;
  655. align-items: center;
  656. flex-wrap: wrap;
  657. margin-bottom: 4rpx;
  658. .name {
  659. font-size: 30rpx;
  660. color: #333;
  661. font-weight: 500;
  662. margin-right: 12rpx;
  663. }
  664. .brand-tag {
  665. background: #FFE8CC;
  666. color: #FF9500;
  667. font-size: 20rpx;
  668. padding: 4rpx 8rpx;
  669. border-radius: 8rpx;
  670. border: 1rpx solid #FFD4A0;
  671. }
  672. }
  673. .desc { font-size: 24rpx; color: #999; margin: 4rpx 0 8rpx 0; }
  674. .price-row {
  675. display: flex;
  676. align-items: center;
  677. .price { color: #FF9500; font-size: 26rpx; margin-right: 10rpx; }
  678. .count { color: #999; font-size: 24rpx; margin-right: 10rpx; }
  679. .amount { color: #333; font-size: 28rpx; margin-left: auto; }
  680. }
  681. }
  682. }
  683. }
  684. .expand-btn {
  685. text-align: center;
  686. color: #999;
  687. font-size: 24rpx;
  688. padding: 10rpx 0;
  689. .arrow { font-size: 20rpx; }
  690. }
  691. }
  692. .agreement-bar {
  693. display: flex;
  694. align-items: center;
  695. background: #fffbe6;
  696. padding: 20rpx 30rpx;
  697. font-size: 24rpx;
  698. .checkbox {
  699. width: 32rpx; height: 32rpx; border-radius: 50%; border: 2rpx solid #FFB74D;
  700. margin-right: 10rpx; display: flex; align-items: center; justify-content: center;
  701. background: #fff;
  702. &.active { background: #FFB74D; color: #fff; }
  703. }
  704. .link { color: #FFB74D; }
  705. }
  706. .bottom-bar {
  707. display: flex;
  708. align-items: center;
  709. justify-content: space-between;
  710. background: #fff;
  711. padding: 20rpx 30rpx calc(40rpx + env(safe-area-inset-bottom));
  712. .summary { color: #666; font-size: 26rpx; }
  713. .amount { color: #FF9500; font-size: 32rpx; font-weight: bold; margin-left: 10rpx; }
  714. .main-btn {
  715. background: #FFB74D;
  716. color: #fff;
  717. font-size: 28rpx;
  718. border-radius: 40rpx;
  719. padding: 0 40rpx;
  720. width: 60%;
  721. height: 80rpx;
  722. display: flex;
  723. justify-content: center;
  724. &[disabled] { opacity: 0.5; }
  725. }
  726. }
  727. .order-desc {
  728. color: #999;
  729. font-size: 22rpx;
  730. padding: 0 30rpx 20rpx 30rpx;
  731. line-height: 1.7;
  732. }
  733. .time-picker {
  734. position: fixed;
  735. left: 0;
  736. right: 0;
  737. top: 0;
  738. bottom: 0;
  739. z-index: 1000;
  740. .mask {
  741. position: absolute;
  742. left: 0;
  743. right: 0;
  744. top: 0;
  745. bottom: 0;
  746. background: rgba(0, 0, 0, 0.5);
  747. }
  748. .picker-content {
  749. position: absolute;
  750. left: 0;
  751. right: 0;
  752. bottom: 0;
  753. background: #fff;
  754. border-radius: 20rpx 20rpx 0 0;
  755. padding-bottom: env(safe-area-inset-bottom);
  756. .picker-header {
  757. padding: 30rpx 0 0 0;
  758. display: flex;
  759. align-items: center;
  760. border-bottom: 1rpx solid #eee;
  761. .reset {
  762. color: #bbb;
  763. font-size: 28rpx;
  764. margin-left: 30rpx;
  765. }
  766. .title {
  767. flex: 1;
  768. text-align: center;
  769. font-size: 32rpx;
  770. font-weight: 500;
  771. color: #222;
  772. margin-right: 60rpx;
  773. }
  774. }
  775. .picker-section {
  776. padding: 30rpx 30rpx 0 30rpx;
  777. .section-title {
  778. font-size: 28rpx;
  779. color: #222;
  780. margin-bottom: 20rpx;
  781. }
  782. .date-btns, .time-btns {
  783. display: flex;
  784. flex-wrap: wrap;
  785. gap: 20rpx 20rpx;
  786. }
  787. .date-btn, .time-btn {
  788. width: 200rpx;
  789. height: 70rpx;
  790. background: #f5f5f5;
  791. color: #999;
  792. border-radius: 18rpx;
  793. display: flex;
  794. align-items: center;
  795. justify-content: center;
  796. font-size: 28rpx;
  797. border: 2rpx solid transparent;
  798. margin-bottom: 10rpx;
  799. }
  800. .date-btn.active, .time-btn.active {
  801. background: #fff;
  802. color: #FFB74D;
  803. border: 2rpx solid #FFB74D;
  804. font-weight: 500;
  805. }
  806. }
  807. .confirm-btn {
  808. margin: 40rpx 30rpx 30rpx 30rpx;
  809. height: 90rpx;
  810. background: linear-gradient(90deg, #FFB74D 0%, #FF9500 100%);
  811. color: #fff;
  812. font-size: 32rpx;
  813. border-radius: 45rpx;
  814. display: flex;
  815. align-items: center;
  816. justify-content: center;
  817. box-shadow: 0 4rpx 16rpx rgba(255, 149, 0, 0.08);
  818. }
  819. }
  820. }
  821. .process-title {
  822. font-size: 32rpx;
  823. font-weight: bold;
  824. background: linear-gradient(to bottom,#fff3db 0%,#fffefb 40%);
  825. color: #222;
  826. text-align: left;
  827. padding: 36rpx 0 24rpx 30rpx;
  828. letter-spacing: 1rpx;
  829. }
  830. .time-btn.disabled {
  831. color: #ccc;
  832. background: #f0f0f0;
  833. border-color: #eee;
  834. pointer-events: none;
  835. }
  836. </style>