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

10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months 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 }}/{{item.unit}}</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. uni.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>