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

925 lines
23 KiB

1 week ago
5 days ago
1 week ago
1 week ago
1 week ago
  1. <template>
  2. <view class="container">
  3. <!-- 顶部导航 -->
  4. <view class="nav-bar">
  5. <view class="back" @tap="goBack">
  6. <text class="iconfont"></text>
  7. </view>
  8. <text class="title">修改订单</text>
  9. <view class="right-btns">
  10. <text class="more"></text>
  11. <text class="share"></text>
  12. </view>
  13. </view>
  14. <!-- 内容区域 -->
  15. <view class="content">
  16. <!-- 回收流程卡片 -->
  17. <view class="card process-card">
  18. <view class="card-title">回收流程</view>
  19. <view class="process-steps">
  20. <view class="step-item">
  21. <image src="/static/pickup/note.png" mode="aspectFit"></image>
  22. <view class="step-label">
  23. <text class="step-num"></text>
  24. <text>在线预约</text>
  25. </view>
  26. </view>
  27. <view class="step-item">
  28. <image src="/static/pickup/box.png" mode="aspectFit"></image>
  29. <view class="step-label">
  30. <text class="step-num"></text>
  31. <text>快递上门</text>
  32. </view>
  33. </view>
  34. <view class="step-item">
  35. <image src="/static/pickup/search.png" mode="aspectFit"></image>
  36. <view class="step-label">
  37. <text class="step-num"></text>
  38. <text>透明质检</text>
  39. </view>
  40. </view>
  41. <view class="step-item">
  42. <image src="/static/pickup/money.png" mode="aspectFit"></image>
  43. <view class="step-label">
  44. <text class="step-num"></text>
  45. <text>现金打款</text>
  46. </view>
  47. </view>
  48. </view>
  49. <view class="divider"></view>
  50. <!-- 取件信息 -->
  51. <view class="pickup-info">
  52. <view class="info-item" @tap="selectAddress">
  53. <text class="label">取件地址</text>
  54. <view class="value">
  55. <text class="text">{{ displayAddress }}</text>
  56. <text class="arrow">></text>
  57. </view>
  58. </view>
  59. <view class="info-item" @tap="openTimePicker">
  60. <text class="label">上门时间</text>
  61. <view class="value">
  62. <text class="text">{{ appointmentTime }}</text>
  63. <text class="arrow">></text>
  64. </view>
  65. </view>
  66. </view>
  67. </view>
  68. <!-- 订单详情卡片 -->
  69. <view class="card">
  70. <view class="card-header">
  71. <text class="title">订单详情</text>
  72. </view>
  73. <view class="order-items">
  74. <view class="order-item" v-for="(item, index) in selectedItems" :key="index">
  75. <image :src="item.icon" mode="aspectFit"></image>
  76. <view class="item-info">
  77. <view class="name-row">
  78. <text class="name">{{ item.name }}</text>
  79. <text class="rules" @tap="showRules">回收规则 ></text>
  80. </view>
  81. <text class="desc">{{ item.desc }}</text>
  82. <text class="price">¥ {{ item.unitPrice }}/</text>
  83. </view>
  84. <view class="quantity-control">
  85. <text class="btn minus" @tap="decreaseQuantity(index)">-</text>
  86. <text class="quantity">{{ item.quantity }}</text>
  87. <text class="btn plus" @tap="increaseQuantity(index)">+</text>
  88. </view>
  89. </view>
  90. </view>
  91. </view>
  92. </view>
  93. <!-- 底部提交栏 -->
  94. <view class="bottom-bar">
  95. <view class="agreement">
  96. <view class="custom-checkbox" :class="{ active: agreed }" @tap="toggleAgreement">
  97. <text v-if="agreed" class="checkbox-icon"></text>
  98. </view>
  99. <text>我已阅读并同意</text>
  100. <text class="link">回收服务协议</text>
  101. <text></text>
  102. <text class="link">隐私政策</text>
  103. </view>
  104. <view class="submit-info">
  105. <view class="submit-infos">
  106. <view class="count">
  107. <text>已选</text>
  108. <text class="num">{{ totalCount }}</text>
  109. <text></text>
  110. <text class="estimate">预估回收可得</text>
  111. </view>
  112. <view class="price">
  113. <!-- <text class="symbol">¥</text> -->
  114. <text class="value">{{ totalPriceRange }}</text>
  115. </view>
  116. </view>
  117. <view class="price-submit">
  118. <button class="submit-btn" :disabled="!canSubmit" @tap="submitOrder">
  119. 预约上门取件
  120. </button>
  121. </view>
  122. </view>
  123. </view>
  124. <!-- 时间选择器弹窗 -->
  125. <view class="time-picker" v-if="showTimePicker">
  126. <view class="mask" @tap="closeTimePicker"></view>
  127. <view class="picker-content">
  128. <view class="picker-header">
  129. <text class="reset" @tap="resetPicker">重置</text>
  130. <text class="title">预约上门时间</text>
  131. </view>
  132. <view class="date-tabs">
  133. <view
  134. class="date-tab"
  135. v-for="(tab, index) in ['今天', '明天', '后天', '自定义']"
  136. :key="index"
  137. :class="{ active: currentDateTab === index }"
  138. @tap="selectDateTab(index)"
  139. >
  140. {{ tab }}
  141. </view>
  142. </view>
  143. <picker-view
  144. class="time-picker-view"
  145. :value="pickerValue"
  146. @change="onPickerChange"
  147. >
  148. <picker-view-column>
  149. <view class="picker-item" v-for="year in years" :key="year">{{year}}</view>
  150. </picker-view-column>
  151. <picker-view-column>
  152. <view class="picker-item" v-for="month in months" :key="month">{{month}}</view>
  153. </picker-view-column>
  154. <picker-view-column>
  155. <view class="picker-item" v-for="day in days" :key="day">{{day}}</view>
  156. </picker-view-column>
  157. </picker-view>
  158. <view class="confirm-btn" @tap="confirmTime">确认</view>
  159. </view>
  160. </view>
  161. </view>
  162. </template>
  163. <script>
  164. import pullRefreshMixin from '@/pages/mixins/pullRefreshMixin.js'
  165. export default {
  166. mixins: [pullRefreshMixin],
  167. data() {
  168. return {
  169. address: '',
  170. selectedAddress: null,
  171. displayAddress: '请选择取件地址',
  172. addressDetail: {
  173. province: '',
  174. city: '',
  175. district: '',
  176. street: '',
  177. address: '',
  178. name: '',
  179. phone: '',
  180. isDefault: false
  181. },
  182. appointmentTime: '预约 周四 11:00~13:00',
  183. timeDetail: {
  184. date: '',
  185. time: '',
  186. displayText: ''
  187. },
  188. totalCount: 16,
  189. estimatePrice: '73.6-75.8',
  190. agreed: true,
  191. showTimePicker: false,
  192. currentDateTab: 0,
  193. pickerValue: [0, 0, 0],
  194. years: [],
  195. months: [],
  196. days: [],
  197. selectedDate: '',
  198. timeSlot: '11:00~13:00', // 固定时间段
  199. selectedItems: [
  200. {
  201. name: '羽绒服',
  202. desc: '允许脏破烂,160码以上',
  203. unitPrice: 8,
  204. quantity: 8
  205. },
  206. {
  207. name: '品牌羽绒服',
  208. desc: '允许脏破烂,160码以上',
  209. unitPrice: 10,
  210. quantity: 1
  211. }
  212. ],
  213. totalPriceRange: '¥ 73.6-75.8'
  214. }
  215. },
  216. computed: {
  217. displayAddress() {
  218. if (this.selectedAddress) {
  219. return this.selectedAddress.address
  220. }
  221. return '请选择取件地址'
  222. },
  223. totalPriceRange() {
  224. const totalPrice = this.selectedItems.reduce((total, item) => total + item.unitPrice * item.quantity, 0)
  225. return `¥ ${totalPrice.toFixed(2)}-${(totalPrice + 2).toFixed(2)}`
  226. },
  227. canSubmit() {
  228. return this.agreed &&
  229. this.selectedAddress &&
  230. this.timeDetail.date &&
  231. this.selectedItems.length > 0
  232. }
  233. },
  234. methods: {
  235. async onRefresh() {
  236. // 模拟刷新数据
  237. await new Promise(resolve => setTimeout(resolve, 1000))
  238. this.stopPullRefresh()
  239. },
  240. goBack() {
  241. uni.navigateBack()
  242. },
  243. selectAddress() {
  244. uni.navigateTo({
  245. url: '/pages/component/select'
  246. })
  247. },
  248. openTimePicker() {
  249. this.showTimePicker = true
  250. if (!this.years.length) {
  251. this.initDatePicker()
  252. }
  253. },
  254. closeTimePicker() {
  255. this.showTimePicker = false
  256. },
  257. initDatePicker() {
  258. const currentDate = new Date()
  259. const currentYear = currentDate.getFullYear()
  260. // 生成年份数据(从当年开始)
  261. this.years = Array.from({length: 5}, (_, i) => currentYear + i)
  262. // 生成月份数据
  263. this.months = Array.from({length: 12}, (_, i) => i + 1)
  264. // 生成日期数据
  265. this.updateDays(currentYear, currentDate.getMonth() + 1)
  266. // 设置默认值为当前日期
  267. this.pickerValue = [0, currentDate.getMonth(), currentDate.getDate() - 1]
  268. },
  269. updateDays(year, month) {
  270. const daysInMonth = new Date(year, month, 0).getDate()
  271. this.days = Array.from({length: daysInMonth}, (_, i) => i + 1)
  272. },
  273. selectDateTab(index) {
  274. this.currentDateTab = index
  275. if (index < 3) { // 今天、明天、后天
  276. const date = new Date()
  277. date.setDate(date.getDate() + index)
  278. // 找到对应的年份索引
  279. const yearIndex = this.years.findIndex(year => year === date.getFullYear())
  280. this.pickerValue = [
  281. yearIndex,
  282. date.getMonth(),
  283. date.getDate() - 1
  284. ]
  285. // 更新天数
  286. this.updateDays(date.getFullYear(), date.getMonth() + 1)
  287. }
  288. },
  289. onPickerChange(e) {
  290. // 切换到自定义选项
  291. this.currentDateTab = 3
  292. const values = e.detail.value
  293. const year = this.years[values[0]]
  294. const month = this.months[values[1]]
  295. // 更新天数
  296. this.updateDays(year, month)
  297. // 如果选择的日期超过了当月的最大天数,调整为最后一天
  298. if (values[2] >= this.days.length) {
  299. values[2] = this.days.length - 1
  300. }
  301. this.pickerValue = values
  302. },
  303. resetPicker() {
  304. this.initDatePicker()
  305. this.currentDateTab = 0
  306. },
  307. confirmTime() {
  308. const year = this.years[this.pickerValue[0]]
  309. const month = this.months[this.pickerValue[1]]
  310. const day = this.days[this.pickerValue[2]]
  311. // 创建日期对象
  312. const date = new Date(year, month - 1, day)
  313. // 获取周几
  314. const weekDays = ['日', '一', '二', '三', '四', '五', '六']
  315. const weekDay = weekDays[date.getDay()]
  316. // 设置格式化后的时间
  317. this.timeDetail = {
  318. date: `${year}-${month}-${day}`,
  319. time: this.timeSlot,
  320. displayText: `预约 周${weekDay} ${this.timeSlot}`
  321. }
  322. this.appointmentTime = this.timeDetail.displayText
  323. this.closeTimePicker()
  324. },
  325. handleAddressSelected(address) {
  326. console.log('接收到选中的地址:', address)
  327. this.selectedAddress = address
  328. this.address = address.address
  329. this.$forceUpdate()
  330. },
  331. handleTimeSelected(time) {
  332. this.timeDetail = time
  333. this.appointmentTime = time.displayText
  334. },
  335. showRules() {
  336. uni.navigateTo({
  337. url: '/pages/rules/recycle'
  338. })
  339. },
  340. toggleAgreement() {
  341. this.agreed = !this.agreed
  342. },
  343. viewAgreement() {
  344. uni.navigateTo({
  345. url: '/pages/agreement/service'
  346. })
  347. },
  348. viewPrivacy() {
  349. uni.navigateTo({
  350. url: '/pages/agreement/privacy'
  351. })
  352. },
  353. submitOrder() {
  354. // 表单验证
  355. if (!this.agreed) {
  356. return uni.showToast({
  357. title: '请先同意服务协议',
  358. icon: 'none'
  359. })
  360. }
  361. if (!this.selectedAddress) {
  362. return uni.showToast({
  363. title: '请选择取件地址',
  364. icon: 'none'
  365. })
  366. }
  367. if (!this.timeDetail.date) {
  368. return uni.showToast({
  369. title: '请选择上门时间',
  370. icon: 'none'
  371. })
  372. }
  373. if (this.selectedItems.length === 0) {
  374. return uni.showToast({
  375. title: '请选择回收物品',
  376. icon: 'none'
  377. })
  378. }
  379. // 提交订单
  380. uni.showLoading({
  381. title: '提交中...'
  382. })
  383. // 构建订单数据
  384. const orderData = {
  385. address: this.selectedAddress,
  386. time: this.timeDetail,
  387. items: this.selectedItems,
  388. totalCount: this.totalCount,
  389. totalPrice: this.totalPriceRange
  390. }
  391. // 模拟提交
  392. setTimeout(() => {
  393. uni.hideLoading()
  394. uni.showToast({
  395. title: '修改成功',
  396. icon: 'success'
  397. })
  398. setTimeout(() => {
  399. uni.navigateBack()
  400. }, 1500)
  401. }, 1000)
  402. },
  403. decreaseQuantity(index) {
  404. if (this.selectedItems[index].quantity > 1) {
  405. this.selectedItems[index].quantity--
  406. }
  407. },
  408. increaseQuantity(index) {
  409. this.selectedItems[index].quantity++
  410. }
  411. },
  412. onLoad(options) {
  413. if (options.id) {
  414. this.addressDetail = {
  415. province: '海南省',
  416. city: '海口市',
  417. district: '秀英区',
  418. street: '秀英街道',
  419. address: '5单元1...',
  420. name: '张三',
  421. phone: '13800138000'
  422. }
  423. this.timeDetail = {
  424. date: '2024-03-21',
  425. time: '11:00-13:00',
  426. displayText: '周四 11:00~13:00'
  427. }
  428. }
  429. // 监听地址选择事件
  430. uni.$on('addressSelected', (address) => {
  431. console.log('接收到选中的地址:', address)
  432. this.selectedAddress = address
  433. this.address = address.address
  434. this.$forceUpdate()
  435. })
  436. },
  437. onUnload() {
  438. // 页面卸载时移除事件监听
  439. uni.$off('addressSelected')
  440. },
  441. onShow() {
  442. // 页面显示时触发,包括从地址选择页面返回时
  443. console.log('当前选中的地址:', this.selectedAddress)
  444. if (this.selectedAddress) {
  445. // 确保地址信息被正确更新
  446. this.address = this.selectedAddress.address
  447. // 强制更新视图
  448. this.$forceUpdate()
  449. }
  450. const selectedTime = getApp().globalData.selectedTime
  451. if (selectedTime) {
  452. this.handleTimeSelected(selectedTime)
  453. getApp().globalData.selectedTime = null
  454. }
  455. },
  456. created() {
  457. this.initDatePicker()
  458. },
  459. watch: {
  460. showTimePicker(val) {
  461. if (val) {
  462. this.updateDays(this.pickerValue[0], this.pickerValue[1])
  463. }
  464. }
  465. }
  466. }
  467. </script>
  468. <style lang="scss" scoped>
  469. .container {
  470. min-height: 100vh;
  471. background: #F7F8FA;
  472. padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
  473. }
  474. .nav-bar {
  475. display: flex;
  476. align-items: center;
  477. height: 88rpx;
  478. background: #fff;
  479. padding: 0 30rpx;
  480. .back {
  481. padding: 20rpx;
  482. margin-left: -20rpx;
  483. }
  484. .title {
  485. flex: 1;
  486. text-align: center;
  487. font-size: 34rpx;
  488. font-weight: 500;
  489. }
  490. .right-btns {
  491. display: flex;
  492. gap: 30rpx;
  493. .more, .share {
  494. font-size: 40rpx;
  495. color: #333;
  496. }
  497. }
  498. }
  499. .content {
  500. padding: 20rpx;
  501. }
  502. .card {
  503. background: #fff;
  504. border-radius: 20rpx;
  505. margin-bottom: 20rpx;
  506. overflow: hidden;
  507. &.process-card {
  508. background: #fffbf6;
  509. }
  510. .card-title {
  511. font-size: 32rpx;
  512. font-weight: bold;
  513. color: #333;
  514. padding: 30rpx;
  515. border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
  516. }
  517. }
  518. .process-steps {
  519. display: flex;
  520. justify-content: space-between;
  521. padding: 30rpx;
  522. .step-item {
  523. flex: 1;
  524. display: flex;
  525. flex-direction: column;
  526. align-items: center;
  527. image {
  528. width: 80rpx;
  529. height: 80rpx;
  530. margin-bottom: 12rpx;
  531. }
  532. .step-label {
  533. text-align: center;
  534. .step-num {
  535. color: #FF9500;
  536. font-size: 26rpx;
  537. margin-right: 4rpx;
  538. }
  539. text {
  540. font-size: 26rpx;
  541. color: #333;
  542. }
  543. }
  544. }
  545. }
  546. .divider {
  547. height: 1rpx;
  548. background: rgba(0, 0, 0, 0.05);
  549. margin: 0 30rpx;
  550. }
  551. .pickup-info {
  552. padding: 0 30rpx;
  553. .info-item {
  554. padding: 30rpx 0;
  555. border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
  556. &:last-child {
  557. border-bottom: none;
  558. }
  559. .label {
  560. font-size: 28rpx;
  561. color: #333;
  562. margin-bottom: 16rpx;
  563. display: block;
  564. }
  565. .value {
  566. display: flex;
  567. justify-content: space-between;
  568. align-items: center;
  569. .text {
  570. flex: 1;
  571. font-size: 28rpx;
  572. color: #333;
  573. overflow: hidden;
  574. text-overflow: ellipsis;
  575. white-space: nowrap;
  576. }
  577. .arrow {
  578. color: #999;
  579. font-size: 28rpx;
  580. margin-left: 10rpx;
  581. }
  582. }
  583. }
  584. }
  585. .order-items {
  586. padding: 0 30rpx;
  587. .order-item {
  588. display: flex;
  589. align-items: center;
  590. padding: 30rpx 0;
  591. border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
  592. &:last-child {
  593. border-bottom: none;
  594. }
  595. image {
  596. width: 120rpx;
  597. height: 120rpx;
  598. margin-right: 20rpx;
  599. }
  600. .item-info {
  601. flex: 1;
  602. .name-row {
  603. display: flex;
  604. justify-content: space-between;
  605. align-items: center;
  606. margin-bottom: 8rpx;
  607. .name {
  608. font-size: 30rpx;
  609. color: #333;
  610. }
  611. .rules {
  612. font-size: 24rpx;
  613. color: #999;
  614. }
  615. }
  616. .desc {
  617. font-size: 24rpx;
  618. color: #999;
  619. margin-bottom: 8rpx;
  620. }
  621. .price {
  622. font-size: 28rpx;
  623. color: #FF9500;
  624. }
  625. }
  626. .quantity-control {
  627. display: flex;
  628. align-items: center;
  629. margin: 0 20rpx;
  630. .btn {
  631. width: 60rpx;
  632. height: 60rpx;
  633. display: flex;
  634. align-items: center;
  635. justify-content: center;
  636. background: #f5f5f5;
  637. border-radius: 30rpx;
  638. font-size: 36rpx;
  639. color: #333;
  640. }
  641. .quantity {
  642. width: 80rpx;
  643. text-align: center;
  644. font-size: 28rpx;
  645. }
  646. }
  647. }
  648. }
  649. .bottom-bar {
  650. position: fixed;
  651. left: 0;
  652. right: 0;
  653. bottom: 0;
  654. background: #fff;
  655. padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
  656. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  657. .agreement {
  658. display: flex;
  659. align-items: center;
  660. margin-bottom: 20rpx;
  661. font-size: 24rpx;
  662. color: #666;
  663. .custom-checkbox {
  664. width: 32rpx;
  665. height: 32rpx;
  666. border-radius: 50%;
  667. border: 2rpx solid #ddd;
  668. margin-right: 10rpx;
  669. display: flex;
  670. align-items: center;
  671. justify-content: center;
  672. transition: all 0.2s;
  673. &.active {
  674. background: #FFB74D;
  675. border-color: #FFB74D;
  676. }
  677. .checkbox-icon {
  678. color: #fff;
  679. font-size: 24rpx;
  680. line-height: 1;
  681. }
  682. }
  683. .link {
  684. color: #FFB74D;
  685. margin: 0 4rpx;
  686. }
  687. }
  688. .submit-info {
  689. display: flex;
  690. justify-content: space-between;
  691. align-items: center;
  692. .price {
  693. margin-right: 20rpx;
  694. .symbol {
  695. font-size: 24rpx;
  696. color: #FF9500;
  697. }
  698. .value {
  699. font-size: 36rpx;
  700. color: #FF9500;
  701. font-weight: bold;
  702. }
  703. }
  704. .count {
  705. font-size: 26rpx;
  706. color: #666;
  707. .num {
  708. color: #FF9500;
  709. margin: 0 4rpx;
  710. }
  711. .estimate {
  712. margin-left: 20rpx;
  713. }
  714. }
  715. .price-submit {
  716. display: flex;
  717. align-items: center;
  718. .submit-btn {
  719. height: 80rpx;
  720. padding: 0 40rpx;
  721. background: #FFB74D;
  722. color: #fff;
  723. font-size: 28rpx;
  724. border-radius: 40rpx;
  725. display: flex;
  726. align-items: center;
  727. justify-content: center;
  728. border: none;
  729. &[disabled] {
  730. opacity: 0.5;
  731. }
  732. &::after {
  733. border: none;
  734. }
  735. }
  736. }
  737. }
  738. }
  739. .time-picker {
  740. position: fixed;
  741. left: 0;
  742. right: 0;
  743. top: 0;
  744. bottom: 0;
  745. z-index: 1000;
  746. .mask {
  747. position: absolute;
  748. left: 0;
  749. right: 0;
  750. top: 0;
  751. bottom: 0;
  752. background: rgba(0, 0, 0, 0.5);
  753. }
  754. .picker-content {
  755. position: absolute;
  756. left: 0;
  757. right: 0;
  758. bottom: 0;
  759. background: #fff;
  760. border-radius: 20rpx 20rpx 0 0;
  761. padding-bottom: env(safe-area-inset-bottom);
  762. .picker-header {
  763. padding: 30rpx;
  764. display: flex;
  765. align-items: center;
  766. border-bottom: 1rpx solid #eee;
  767. .reset {
  768. color: #666;
  769. font-size: 28rpx;
  770. }
  771. .title {
  772. flex: 1;
  773. text-align: center;
  774. font-size: 32rpx;
  775. font-weight: 500;
  776. }
  777. }
  778. .date-tabs {
  779. display: flex;
  780. padding: 20rpx;
  781. background: #f8f8f8;
  782. .date-tab {
  783. flex: 1;
  784. height: 70rpx;
  785. display: flex;
  786. align-items: center;
  787. justify-content: center;
  788. font-size: 28rpx;
  789. color: #333;
  790. background: #fff;
  791. margin: 0 10rpx;
  792. border-radius: 8rpx;
  793. &:last-child {
  794. color: #FF9500;
  795. background: rgba(255, 149, 0, 0.1);
  796. }
  797. &.active {
  798. background: #FF9500;
  799. color: #fff;
  800. }
  801. }
  802. }
  803. .time-picker-view {
  804. width: 100%;
  805. height: 400rpx;
  806. .picker-item {
  807. line-height: 80rpx;
  808. text-align: center;
  809. }
  810. }
  811. .confirm-btn {
  812. margin: 30rpx;
  813. height: 90rpx;
  814. background: #FF9500;
  815. color: #fff;
  816. font-size: 32rpx;
  817. border-radius: 45rpx;
  818. display: flex;
  819. align-items: center;
  820. justify-content: center;
  821. }
  822. }
  823. }
  824. </style>