爱简收旧衣按件回收前端代码仓库
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.

693 lines
20 KiB

1 week ago
1 week ago
1 week ago
1 week ago
5 days ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week ago
1 week 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.num }}{{ step.text }}</text>
  25. </view>
  26. </view>
  27. <view v-else class="step-label-gray">
  28. <text class="text-gray">{{ step.num }}{{ 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. <image :src="item.icon" mode="aspectFit"></image>
  57. <view class="item-info">
  58. <view class="name">{{ item.name }}</view>
  59. <view class="desc">{{ item.desc }}</view>
  60. <view class="price-row">
  61. <text class="price">{{ item.unitPrice }}/</text>
  62. <text class="count">x{{ item.quantity }}</text>
  63. <text class="amount">{{ item.unitPrice * item.quantity }}</text>
  64. </view>
  65. </view>
  66. </view>
  67. </view>
  68. <view v-if="selectedItems.length > 3" class="expand-btn" @tap="toggleExpandOrder">
  69. <text>{{ showAllItems ? '收起' : `展开(共${selectedItems.length}件)` }}</text>
  70. <text class="arrow">{{ showAllItems ? '▲' : '▼' }}</text>
  71. </view>
  72. </view>
  73. </view>
  74. <!-- 订单说明 -->
  75. <view class="order-desc">
  76. <view>1. 当前回收快递免费上门由于快递成本较高为避免不必要的成本及资源二次浪费不属于回收品类或不符合回收标准的物品请勿寄出</view>
  77. <view>2. 已通过的回收物品将正常结算不符合回收要求的物品可选择安排取回逾期未联系将默认捐赠无法再次取回</view>
  78. <view>3. 若用户寄出大量不可回收的物品平台有权限制下次回收权限或取消下次包邮服务</view>
  79. <view>4. 对于合格率高的回收订单平台将根据实际情况给予额外回收奖励</view>
  80. </view>
  81. <!-- 底部提交栏 -->
  82. <view class="agreement-bar">
  83. <view class="checkbox" :class="{active: agreed}" @tap="toggleAgreement">
  84. <text v-if="agreed"></text>
  85. </view>
  86. <text>我已阅读并同意</text>
  87. <text class="link" @tap="showServiceAgreement">回收服务协议</text>
  88. <text></text>
  89. <text class="link" @tap="showPrivacyPolicy">隐私政策</text>
  90. </view>
  91. <view class="bottom-bar">
  92. <view class="summary">
  93. <text>已选 {{ totalCount }} 预估回收可得</text>
  94. <text class="amount">{{ totalPriceRange }}</text>
  95. </view>
  96. <button class="main-btn" @tap="submitOrder">预约上门取件</button>
  97. </view>
  98. <!-- 时间选择弹窗 -->
  99. <view class="time-picker" v-if="showTimePicker">
  100. <view class="mask" @tap="closeTimePicker"></view>
  101. <view class="picker-content">
  102. <view class="picker-header">
  103. <text class="reset" @tap="resetPicker">重置</text>
  104. <text class="title">预约上门时间</text>
  105. </view>
  106. <view class="picker-section">
  107. <view class="section-title">选择日期</view>
  108. <view class="date-btns">
  109. <view
  110. v-for="(tab, index) in dateTabs"
  111. :key="index"
  112. :class="['date-btn', {active: currentDateTab === index}]"
  113. @tap="selectDateTab(index)"
  114. >
  115. {{ tab }}
  116. </view>
  117. </view>
  118. </view>
  119. <view class="picker-section">
  120. <view class="section-title">选择时间</view>
  121. <view class="time-btns">
  122. <view
  123. v-for="(slot, idx) in timeSlots"
  124. :key="idx"
  125. :class="['time-btn', {active: selectedTimeSlot === idx}]"
  126. @tap="selectTimeSlot(idx)"
  127. >
  128. {{ slot }}
  129. </view>
  130. </view>
  131. </view>
  132. <view class="confirm-btn" @tap="confirmTime">确认</view>
  133. </view>
  134. </view>
  135. </view>
  136. </template>
  137. <script>
  138. import pullRefreshMixin from '@/pages/mixins/pullRefreshMixin.js'
  139. export default {
  140. mixins: [pullRefreshMixin],
  141. data() {
  142. return {
  143. statusBarHeight: 0,
  144. navBarHeight: 0, // px
  145. navBarHeightRpx: 0, // rpx
  146. fromRecycle: false,
  147. address: '',
  148. selectedAddress: null,
  149. selectedTime: '',
  150. agreed: false,
  151. selectedItems: [],
  152. showTimePicker: false,
  153. currentDateTab: 0,
  154. dateTabs: ['今天 04-20', '明天 04-21', '后天 04-22', '周一 04-23', '周二 04-24', '周三 04-25'],
  155. timeSlots: ['11:00~13:00', '13:00~15:00', '15:00~17:00'],
  156. selectedTimeSlot: 0,
  157. steps: [
  158. { icon: '/static/home/① 在线预约.png', num: '①', text: '在线预约' },
  159. { icon: '/static/home/② 快递上门.png', num: '②', text: '快递上门' },
  160. { icon: '/static/home/③ 透明质检.png', num: '③', text: '透明质检' },
  161. { icon: '/static/home/④ 现金打款.png', num: '④', text: '现金打款' }
  162. ],
  163. showAllItems: false
  164. }
  165. },
  166. onShow() {
  167. // 页面显示时触发,包括从地址选择页面返回时
  168. console.log('当前选中的地址:', this.selectedAddress)
  169. if (this.selectedAddress) {
  170. // 确保地址信息被正确更新
  171. this.address = this.selectedAddress.address
  172. // 强制更新视图
  173. this.$forceUpdate()
  174. }
  175. },
  176. onLoad(options) {
  177. // 判断是否从回收页面跳转而来
  178. this.fromRecycle = options.fromRecycle === 'true'
  179. // 如果是从回收页面跳转来的,解析传递的衣物信息
  180. if (this.fromRecycle && options.items) {
  181. try {
  182. this.selectedItems = JSON.parse(decodeURIComponent(options.items))
  183. } catch (e) {
  184. console.error('解析衣物信息失败:', e)
  185. }
  186. }
  187. // 监听地址选择事件
  188. uni.$on('addressSelected', (address) => {
  189. this.selectedAddress = address
  190. this.address = address.address
  191. // 兼容 addressDetails
  192. if (address.addressDetails) this.selectedAddress.addressDetails = address.addressDetails
  193. this.$forceUpdate()
  194. })
  195. const sysInfo = uni.getSystemInfoSync()
  196. this.statusBarHeight = sysInfo.statusBarHeight
  197. let navBarHeight = 44
  198. try {
  199. const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
  200. navBarHeight = menuButtonInfo.bottom + menuButtonInfo.top - sysInfo.statusBarHeight
  201. } catch (e) {}
  202. this.navBarHeight = navBarHeight
  203. this.navBarHeightRpx = Math.round(navBarHeight * 750 / sysInfo.windowWidth)
  204. this.getAddressList();
  205. },
  206. onUnload() {
  207. // 页面卸载时移除事件监听
  208. uni.$off('addressSelected')
  209. },
  210. computed: {
  211. totalCount() {
  212. return this.selectedItems.reduce((sum, item) => sum + item.quantity, 0)
  213. },
  214. totalPriceRange() {
  215. if (this.selectedItems.length === 0) return '0-0'
  216. const total = this.selectedItems.reduce((sum, item) => sum + (item.unitPrice * item.quantity), 0)
  217. return `${(total * 0.92).toFixed(2)}~${(total * 1.1).toFixed(2)}`
  218. },
  219. canSubmit() {
  220. return this.agreed && this.selectedItems.length > 0 && this.selectedTime && this.displayAddress
  221. },
  222. displayAddress() {
  223. if (this.selectedAddress) {
  224. // 拼接 address 和 addressDetails
  225. return (this.selectedAddress.address || '') + (this.selectedAddress.addressDetails ? ' ' + this.selectedAddress.addressDetails : '')
  226. }
  227. return ''
  228. }
  229. },
  230. methods: {
  231. async onRefresh() {
  232. // 模拟刷新数据
  233. await new Promise(resolve => setTimeout(resolve, 1000))
  234. uni.stopPullRefresh()
  235. },
  236. goBack() {
  237. uni.navigateBack()
  238. },
  239. showMoreMenu() {
  240. uni.showModal({ title: '更多', content: '这里可以放更多操作' })
  241. },
  242. showScan() {
  243. uni.showModal({ title: '扫码', content: '这里可以实现扫码功能' })
  244. },
  245. selectAddress() {
  246. uni.navigateTo({ url: '/pages/subcomponent/select?mode=select' })
  247. },
  248. openTimePicker() {
  249. this.showTimePicker = true
  250. },
  251. closeTimePicker() {
  252. this.showTimePicker = false
  253. },
  254. selectDateTab(index) {
  255. this.currentDateTab = index
  256. },
  257. selectTimeSlot(index) {
  258. this.selectedTimeSlot = index
  259. },
  260. confirmTime() {
  261. this.selectedTime = `${this.dateTabs[this.currentDateTab]} ${this.timeSlots[this.selectedTimeSlot]}`
  262. this.closeTimePicker()
  263. },
  264. resetPicker() {
  265. this.currentDateTab = 0
  266. this.selectedTimeSlot = 0
  267. },
  268. toggleAgreement() {
  269. this.agreed = !this.agreed
  270. },
  271. showServiceAgreement() {
  272. uni.showModal({ title: '回收服务协议', content: '这里展示回收服务协议内容' })
  273. },
  274. showPrivacyPolicy() {
  275. uni.showModal({ title: '隐私政策', content: '这里展示隐私政策内容' })
  276. },
  277. submitOrder() {
  278. if (!this.agreed) {
  279. uni.showToast({ title: '请先同意服务协议', icon: 'none' })
  280. return
  281. }
  282. if (!this.displayAddress || this.displayAddress === '请选择取件地址') {
  283. uni.showToast({ title: '请选择取件地址', icon: 'none' })
  284. return
  285. }
  286. if (!this.selectedTime) {
  287. uni.showToast({ title: '请选择上门时间', icon: 'none' })
  288. return
  289. }
  290. if (this.selectedItems.length === 0) {
  291. uni.showToast({ title: '请选择回收物品', icon: 'none' })
  292. return
  293. }
  294. // 校验通过,提交
  295. uni.showLoading({ title: '提交中...' })
  296. setTimeout(() => {
  297. uni.hideLoading()
  298. uni.showToast({ title: '预约成功', icon: 'success' })
  299. setTimeout(() => {
  300. uni.navigateBack()
  301. }, 1500)
  302. }, 1000)
  303. },
  304. toggleExpandOrder() {
  305. this.showAllItems = !this.showAllItems
  306. },
  307. async getAddressList() {
  308. const res = await this.$api('getAddressList', {});
  309. if (res && res.code === 200 && res.result && res.result.records) {
  310. const defaultAddr = res.result.records.find(item => item.defaultFlag === 'Y');
  311. if (defaultAddr) {
  312. this.selectedAddress = defaultAddr;
  313. this.address = defaultAddr.address;
  314. // 兼容 addressDetails
  315. if (defaultAddr.addressDetails) this.selectedAddress.addressDetails = defaultAddr.addressDetails
  316. } else {
  317. this.selectedAddress = null;
  318. this.address = '';
  319. }
  320. }
  321. }
  322. }
  323. }
  324. </script>
  325. <style lang="scss" scoped>
  326. .pickup-container {
  327. min-height: 100vh;
  328. background: #f8f8f8;
  329. }
  330. .nav-bar {
  331. position: fixed;
  332. top: 0;
  333. left: 0;
  334. right: 0;
  335. z-index: 999;
  336. display: flex;
  337. align-items: center;
  338. background: #fff;
  339. padding: 0 30rpx;
  340. .back {
  341. padding: 20rpx;
  342. margin-left: -20rpx;
  343. }
  344. .title {
  345. flex: 1;
  346. text-align: center;
  347. font-size: 34rpx;
  348. font-weight: 500;
  349. color: #222;
  350. }
  351. .right-btns {
  352. display: flex;
  353. gap: 30rpx;
  354. .more, .scan {
  355. font-size: 40rpx;
  356. color: #333;
  357. }
  358. }
  359. }
  360. .content {
  361. padding: 20rpx;
  362. }
  363. .card {
  364. background: linear-gradient(to bottom,#fff3db 0%,#fffefb 40%);
  365. border-radius: 20rpx;
  366. margin-bottom: 20rpx;
  367. overflow: hidden;
  368. box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.03);
  369. }
  370. .process-card {
  371. background: #fff;
  372. border-radius: 24rpx;
  373. box-shadow: 0 8rpx 32rpx rgba(255, 149, 0, 0.08);
  374. padding: 0 0 20rpx 0;
  375. }
  376. .process-steps {
  377. display: flex;
  378. justify-content: space-between;
  379. align-items: flex-start;
  380. padding: 0 30rpx 30rpx;
  381. .process-step-card {
  382. background: #FFF8ED;
  383. border-radius: 24rpx;
  384. min-width: 140rpx;
  385. min-height: 180rpx;
  386. display: flex;
  387. flex-direction: column;
  388. align-items: center;
  389. margin-right: 24rpx;
  390. position: relative;
  391. .step-icon {
  392. width: 64rpx;
  393. height: 64rpx;
  394. margin: 24rpx 0 18rpx 0;
  395. }
  396. .step-bottom-bar {
  397. position: absolute;
  398. left: 0;
  399. right: 0;
  400. bottom: 0;
  401. height: 56rpx;
  402. background: #FFB74D;
  403. border-radius: 0 0 24rpx 24rpx;
  404. display: flex;
  405. align-items: center;
  406. justify-content: center;
  407. .step-num-bar {
  408. display: flex;
  409. flex-direction: row;
  410. align-items: center;
  411. margin-top: 8rpx;
  412. .num-main {
  413. width: 32rpx;
  414. height: 32rpx;
  415. border-radius: 50%;
  416. background: #fff;
  417. color: #FFB74D;
  418. font-size: 22rpx;
  419. display: flex;
  420. align-items: center;
  421. justify-content: center;
  422. font-weight: 600;
  423. margin-right: 10rpx;
  424. }
  425. .text-main {
  426. color: #fff;
  427. font-size: 26rpx;
  428. font-weight: 500;
  429. }
  430. }
  431. }
  432. .step-label-gray {
  433. position: absolute;
  434. left: 0;
  435. right: 0;
  436. bottom: 0;
  437. height: 56rpx;
  438. background: #FFF8ED;
  439. border-radius: 0 0 24rpx 24rpx;
  440. display: flex;
  441. align-items: center;
  442. justify-content: center;
  443. .num-gray {
  444. width: 32rpx;
  445. height: 32rpx;
  446. border-radius: 50%;
  447. background: #eee;
  448. color: #bbb;
  449. font-size: 22rpx;
  450. display: flex;
  451. align-items: center;
  452. justify-content: center;
  453. font-weight: 600;
  454. margin-right: 10rpx;
  455. }
  456. .text-gray {
  457. color: #bbb;
  458. font-size: 26rpx;
  459. font-weight: 500;
  460. }
  461. }
  462. }
  463. .process-step-card:last-child {
  464. margin-right: 0;
  465. }
  466. }
  467. .divider {
  468. height: 1rpx;
  469. background: rgba(0, 0, 0, 0.05);
  470. margin: 0 30rpx;
  471. }
  472. .pickup-info {
  473. padding: 0 30rpx;
  474. .info-item {
  475. padding: 30rpx 0;
  476. border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
  477. &:last-child { border-bottom: none; }
  478. .label {
  479. font-size: 28rpx;
  480. color: #333;
  481. margin-bottom: 16rpx;
  482. display: block;
  483. }
  484. .value {
  485. display: flex;
  486. justify-content: space-between;
  487. align-items: center;
  488. .text {
  489. flex: 1;
  490. font-size: 28rpx;
  491. color: #333;
  492. overflow: hidden;
  493. text-overflow: ellipsis;
  494. white-space: nowrap;
  495. }
  496. .text.placeholder { color: #ccc; }
  497. .arrow {
  498. color: #999;
  499. font-size: 28rpx;
  500. margin-left: 10rpx;
  501. }
  502. }
  503. }
  504. }
  505. .order-card {
  506. background: #fff;
  507. border-radius: 24rpx;
  508. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.04);
  509. margin-bottom: 20rpx;
  510. .order-items {
  511. padding: 0 30rpx;
  512. .order-item {
  513. display: flex;
  514. align-items: center;
  515. padding: 30rpx 0;
  516. border-bottom: 1rpx solid #f5f5f5;
  517. &:last-child { border-bottom: none; }
  518. image { width: 80rpx; height: 80rpx; margin-right: 20rpx; }
  519. .item-info {
  520. flex: 1;
  521. .name { font-size: 30rpx; color: #333; font-weight: 500; }
  522. .desc { font-size: 24rpx; color: #999; margin: 4rpx 0 8rpx 0; }
  523. .price-row {
  524. display: flex;
  525. align-items: center;
  526. .price { color: #FF9500; font-size: 26rpx; margin-right: 10rpx; }
  527. .count { color: #999; font-size: 24rpx; margin-right: 10rpx; }
  528. .amount { color: #333; font-size: 28rpx; margin-left: auto; }
  529. }
  530. }
  531. }
  532. }
  533. .expand-btn {
  534. text-align: center;
  535. color: #999;
  536. font-size: 24rpx;
  537. padding: 10rpx 0;
  538. .arrow { font-size: 20rpx; }
  539. }
  540. }
  541. .agreement-bar {
  542. display: flex;
  543. align-items: center;
  544. background: #fffbe6;
  545. padding: 20rpx 30rpx;
  546. font-size: 24rpx;
  547. .checkbox {
  548. width: 32rpx; height: 32rpx; border-radius: 50%; border: 2rpx solid #FFB74D;
  549. margin-right: 10rpx; display: flex; align-items: center; justify-content: center;
  550. background: #fff;
  551. &.active { background: #FFB74D; color: #fff; }
  552. }
  553. .link { color: #FFB74D; }
  554. }
  555. .bottom-bar {
  556. display: flex;
  557. align-items: center;
  558. justify-content: space-between;
  559. background: #fff;
  560. padding: 20rpx 30rpx calc(40rpx + env(safe-area-inset-bottom));
  561. .summary { color: #666; font-size: 26rpx; }
  562. .amount { color: #FF9500; font-size: 32rpx; font-weight: bold; margin-left: 10rpx; }
  563. .main-btn {
  564. background: #FFB74D;
  565. color: #fff;
  566. font-size: 28rpx;
  567. border-radius: 40rpx;
  568. padding: 0 40rpx;
  569. width: 60%;
  570. height: 80rpx;
  571. display: flex;
  572. justify-content: center;
  573. &[disabled] { opacity: 0.5; }
  574. }
  575. }
  576. .order-desc {
  577. color: #999;
  578. font-size: 22rpx;
  579. padding: 0 30rpx 20rpx 30rpx;
  580. line-height: 1.7;
  581. }
  582. .time-picker {
  583. position: fixed;
  584. left: 0;
  585. right: 0;
  586. top: 0;
  587. bottom: 0;
  588. z-index: 1000;
  589. .mask {
  590. position: absolute;
  591. left: 0;
  592. right: 0;
  593. top: 0;
  594. bottom: 0;
  595. background: rgba(0, 0, 0, 0.5);
  596. }
  597. .picker-content {
  598. position: absolute;
  599. left: 0;
  600. right: 0;
  601. bottom: 0;
  602. background: #fff;
  603. border-radius: 20rpx 20rpx 0 0;
  604. padding-bottom: env(safe-area-inset-bottom);
  605. .picker-header {
  606. padding: 30rpx 0 0 0;
  607. display: flex;
  608. align-items: center;
  609. border-bottom: 1rpx solid #eee;
  610. .reset {
  611. color: #bbb;
  612. font-size: 28rpx;
  613. margin-left: 30rpx;
  614. }
  615. .title {
  616. flex: 1;
  617. text-align: center;
  618. font-size: 32rpx;
  619. font-weight: 500;
  620. color: #222;
  621. margin-right: 60rpx;
  622. }
  623. }
  624. .picker-section {
  625. padding: 30rpx 30rpx 0 30rpx;
  626. .section-title {
  627. font-size: 28rpx;
  628. color: #222;
  629. margin-bottom: 20rpx;
  630. }
  631. .date-btns, .time-btns {
  632. display: flex;
  633. flex-wrap: wrap;
  634. gap: 20rpx 20rpx;
  635. }
  636. .date-btn, .time-btn {
  637. width: 200rpx;
  638. height: 70rpx;
  639. background: #f5f5f5;
  640. color: #999;
  641. border-radius: 18rpx;
  642. display: flex;
  643. align-items: center;
  644. justify-content: center;
  645. font-size: 28rpx;
  646. border: 2rpx solid transparent;
  647. margin-bottom: 10rpx;
  648. }
  649. .date-btn.active, .time-btn.active {
  650. background: #fff;
  651. color: #FFB74D;
  652. border: 2rpx solid #FFB74D;
  653. font-weight: 500;
  654. }
  655. }
  656. .confirm-btn {
  657. margin: 40rpx 30rpx 30rpx 30rpx;
  658. height: 90rpx;
  659. background: linear-gradient(90deg, #FFB74D 0%, #FF9500 100%);
  660. color: #fff;
  661. font-size: 32rpx;
  662. border-radius: 45rpx;
  663. display: flex;
  664. align-items: center;
  665. justify-content: center;
  666. box-shadow: 0 4rpx 16rpx rgba(255, 149, 0, 0.08);
  667. }
  668. }
  669. }
  670. .process-title {
  671. font-size: 32rpx;
  672. font-weight: bold;
  673. background: linear-gradient(to bottom,#fff3db 0%,#fffefb 40%);
  674. color: #222;
  675. text-align: left;
  676. padding: 36rpx 0 24rpx 30rpx;
  677. letter-spacing: 1rpx;
  678. }
  679. </style>