| @ -0,0 +1,203 @@ | |||||
| <template> | |||||
| <view class="card"> | |||||
| <!-- todo: delete --> | |||||
| <view class="flex header"> | |||||
| <view class="flex left"> | |||||
| <view class="avatar"> | |||||
| <!-- todo: check key --> | |||||
| <image class="avatar-img" :src="data.user.avatar"></image> | |||||
| </view> | |||||
| <view class="info"> | |||||
| <view class="name">{{ data.user.name }}</view> | |||||
| <view>{{ $dayjs(data.createTime).format('YYYY-MM-DD') }}</view> | |||||
| <!-- todo: check key --> | |||||
| <!-- <view>{{ `${data.countDesc || ''} | ${$dayjs(data.createTime).format('YYYY-MM-DD')}` }}</view> --> | |||||
| </view> | |||||
| </view> | |||||
| <view class="right" v-if="mode == 'edit'"> | |||||
| <button class="btn" @click="onDelete"> | |||||
| <image class="btn-icon" src="@/pages_order/static/comment/icon-delete.png" mode="widthFix"></image> | |||||
| </button> | |||||
| </view> | |||||
| </view> | |||||
| <view class="section content">{{ data.content }}</view> | |||||
| <view class="flex section imgs"> | |||||
| <image class="img" | |||||
| v-for="(url, iIdx) in images" | |||||
| :key="iIdx" :src="url" | |||||
| mode="scaleToFill" | |||||
| ></image> | |||||
| </view> | |||||
| <view class="section score"> | |||||
| <view class="flex score-item"> | |||||
| <view class="score-item-label">产品服务度</view> | |||||
| <uv-rate :value="data.productNum" size="48rpx" gutter="16rpx" activeColor="#F7BA1E" :allowHalf="true" :minCount="0.5" readonly></uv-rate> | |||||
| </view> | |||||
| <view class="flex score-item"> | |||||
| <view class="score-item-label">问卷体验</view> | |||||
| <uv-rate :value="data.paperNum" size="48rpx" gutter="16rpx" activeColor="#F7BA1E" :allowHalf="true" :minCount="0.5" readonly></uv-rate> | |||||
| </view> | |||||
| <view class="flex score-item"> | |||||
| <view class="score-item-label">物流速度</view> | |||||
| <uv-rate :value="data.logisticsNum" size="48rpx" gutter="16rpx" activeColor="#F7BA1E" :allowHalf="true" :minCount="0.5" readonly></uv-rate> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| props: { | |||||
| data: { | |||||
| type: Object, | |||||
| default() { | |||||
| return {} | |||||
| } | |||||
| }, | |||||
| mode: { | |||||
| type: String, | |||||
| default: 'read' // read | edit | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| images() { | |||||
| const { image } = this.data || {} | |||||
| return image?.split?.(',') | |||||
| } | |||||
| }, | |||||
| methods: { | |||||
| async fetchDelete() { | |||||
| uni.showToast({ | |||||
| icon: 'loading', | |||||
| title: '正在删除', | |||||
| }); | |||||
| try { | |||||
| await this.$fetch('deleteEvaluate', { id: this.data.id }) | |||||
| uni.showToast({ | |||||
| icon: 'success', | |||||
| title: '删除成功', | |||||
| }); | |||||
| this.$emit('deleteSucc') | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| onDelete() { | |||||
| uni.showModal({ | |||||
| title: '确认删除?', | |||||
| success : e => { | |||||
| if(e.confirm){ | |||||
| this.fetchDelete() | |||||
| } | |||||
| } | |||||
| }) | |||||
| } | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .card { | |||||
| width: 100%; | |||||
| padding: 32rpx; | |||||
| box-sizing: border-box; | |||||
| background: #FAFAFF; | |||||
| border: 2rpx solid #FFFFFF; | |||||
| border-radius: 32rpx; | |||||
| } | |||||
| .header { | |||||
| .left { | |||||
| flex: 1; | |||||
| justify-content: flex-start; | |||||
| column-gap: 24rpx; | |||||
| } | |||||
| .avatar { | |||||
| width: 100rpx; | |||||
| height: 100rpx; | |||||
| border: 4rpx solid #FFFFFF; | |||||
| border-radius: 50%; | |||||
| overflow: hidden; | |||||
| &-img { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| .info { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.5; | |||||
| color: #8B8B8B; | |||||
| .name { | |||||
| font-weight: 600; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.2; | |||||
| color: #252545; | |||||
| margin-bottom: 8rpx; | |||||
| } | |||||
| } | |||||
| .btn { | |||||
| &-icon { | |||||
| width: 44rpx; | |||||
| height: auto; | |||||
| } | |||||
| } | |||||
| } | |||||
| .section { | |||||
| margin-top: 24rpx; | |||||
| } | |||||
| .content { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 32rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| } | |||||
| .imgs { | |||||
| justify-content: flex-start; | |||||
| flex-wrap: wrap; | |||||
| gap: 24rpx; | |||||
| .img { | |||||
| width: 190rpx; | |||||
| height: 190rpx; | |||||
| } | |||||
| } | |||||
| .score { | |||||
| &-item { | |||||
| padding: 12rpx 0; | |||||
| justify-content: space-between; | |||||
| & + & { | |||||
| margin-top: 4rpx; | |||||
| } | |||||
| &-label { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 26rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,130 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar :title="title" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="#FFFFFF" /> | |||||
| <view class="main"> | |||||
| <view class="tabs"> | |||||
| <uv-tabs | |||||
| :list="tabs" | |||||
| :scrollable="false" | |||||
| lineColor="#7451DE" | |||||
| lineWidth="48rpx" | |||||
| lineHeight="4rpx" | |||||
| :activeStyle="{ | |||||
| 'font-family': 'PingFang SC', | |||||
| 'font-weight': 500, | |||||
| 'font-size': '32rpx', | |||||
| 'line-height': 1.4, | |||||
| 'color': '#7451DE', | |||||
| }" | |||||
| :inactiveStyle="{ | |||||
| 'font-family': 'PingFang SC', | |||||
| 'font-weight': 400, | |||||
| 'font-size': '32rpx', | |||||
| 'line-height': 1.4, | |||||
| 'color': '#181818', | |||||
| }" | |||||
| @click="clickTabs" | |||||
| ></uv-tabs> | |||||
| </view> | |||||
| <view class="comment"> | |||||
| <view class="comment-item" v-for="item in list" :key="item.id"> | |||||
| <commentCard :data="item" mode="edit" @deleteSucc="getData"></commentCard> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import mixinsList from '@/mixins/list.js' | |||||
| import commentCard from '@/pages_order/comment/commentCard.vue' | |||||
| export default { | |||||
| mixins: [mixinsList], | |||||
| components: { | |||||
| commentCard, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| title: '我的评价', | |||||
| tabs: [ | |||||
| { name: '全部' }, | |||||
| { name: '有图/视频' }, | |||||
| { name: '最新' }, | |||||
| ], | |||||
| mixinsListApi: 'myEvaluate', | |||||
| } | |||||
| }, | |||||
| onShow() { | |||||
| console.log('onShow') | |||||
| }, | |||||
| onLoad() { | |||||
| this.getData() | |||||
| }, | |||||
| methods: { | |||||
| //点击tab栏 | |||||
| clickTabs({ index }) { | |||||
| // todo | |||||
| return | |||||
| if (index == 0) { | |||||
| delete this.queryParams.status | |||||
| } else { | |||||
| this.queryParams.status = index - 1 | |||||
| } | |||||
| this.getData() | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background-color: $uni-bg-color; | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| width: 100vw; | |||||
| padding-top: calc(var(--status-bar-height) + 204rpx); | |||||
| box-sizing: border-box; | |||||
| .tabs { | |||||
| position: fixed; | |||||
| top: calc(var(--status-bar-height) + 120rpx); | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 84rpx; | |||||
| background: #FFFFFF; | |||||
| /deep/ .uv-tabs__wrapper__nav__line { | |||||
| border-radius: 2rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| .comment { | |||||
| padding: 40rpx 32rpx; | |||||
| &-item { | |||||
| & + & { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,81 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar :title="title" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="#FFFFFF" /> | |||||
| <view class="main"> | |||||
| <view class="comment"> | |||||
| <view class="comment-item" v-for="item in list" :key="item.id"> | |||||
| <commentCard :data="item"></commentCard> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import mixinsList from '@/mixins/list.js' | |||||
| import commentCard from '@/pages_order/comment/commentCard.vue' | |||||
| export default { | |||||
| mixins: [mixinsList], | |||||
| components: { | |||||
| commentCard, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| title: '用户评价', | |||||
| mixinsListApi: 'productEvaluate', | |||||
| } | |||||
| }, | |||||
| onShow() { | |||||
| console.log('onShow') | |||||
| }, | |||||
| onLoad(arg) { | |||||
| console.log('onLoad') | |||||
| const { productId } = arg | |||||
| this.queryParams.productId = productId | |||||
| this.getData() | |||||
| }, | |||||
| methods: { | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background-color: $uni-bg-color; | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| width: 100vw; | |||||
| padding-top: calc(var(--status-bar-height) + 120rpx); | |||||
| box-sizing: border-box; | |||||
| } | |||||
| .comment { | |||||
| padding: 40rpx 32rpx; | |||||
| &-item { | |||||
| & + & { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,270 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar title="立即评价" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="#F3F2F7" /> | |||||
| <view class="main form"> | |||||
| <uv-form | |||||
| ref="form" | |||||
| :model="form" | |||||
| :rules="rules" | |||||
| errorType="toast" | |||||
| > | |||||
| <view class="card info"> | |||||
| <view class="card-header">评价信息</view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="content" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label">评价内容</view> | |||||
| <view class="form-item-content"> | |||||
| <formTextarea v-model="form.content"></formTextarea> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="images" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label">上传图片/视频(选填)</view> | |||||
| <view class="form-item-content"> | |||||
| <formUpload v-model="form.images"></formUpload> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| </view> | |||||
| <view class="card"> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="productNum" :customStyle="formItemStyle"> | |||||
| <view class="flex row"> | |||||
| <view class="form-item-label">产品服务度</view> | |||||
| <view class="form-item-content"> | |||||
| <uv-rate v-model="form.productNum" size="48rpx" gutter="16rpx" activeColor="#F7BA1E" :allowHalf="true" :minCount="0.5" ></uv-rate> | |||||
| </view> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="paperNum" :customStyle="formItemStyle"> | |||||
| <view class="flex row"> | |||||
| <view class="form-item-label">问卷体验</view> | |||||
| <view class="form-item-content"> | |||||
| <uv-rate v-model="form.paperNum" size="48rpx" gutter="16rpx" activeColor="#F7BA1E" :allowHalf="true" :minCount="0.5" ></uv-rate> | |||||
| </view> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="logisticsNum" :customStyle="formItemStyle"> | |||||
| <view class="flex row"> | |||||
| <view class="form-item-label">物流速度</view> | |||||
| <view class="form-item-content"> | |||||
| <uv-rate v-model="form.logisticsNum" size="48rpx" gutter="16rpx" activeColor="#F7BA1E" :allowHalf="true" :minCount="0.5" ></uv-rate> | |||||
| </view> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| </view> | |||||
| </uv-form> | |||||
| </view> | |||||
| <view class="bottom"> | |||||
| <button class="btn" @click="onSubmit">提交申请</button> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import { mapState } from 'vuex' | |||||
| import formTextarea from '@/pages_order/components/formTextarea.vue' | |||||
| import formUpload from '@/pages_order/components/formUpload.vue' | |||||
| export default { | |||||
| components: { | |||||
| formTextarea, | |||||
| formUpload, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| orderId: null, | |||||
| form: { | |||||
| content: null, | |||||
| images: [], | |||||
| productNum: null, | |||||
| paperNum: null, | |||||
| logisticsNum: null, | |||||
| }, | |||||
| rules: { | |||||
| 'content': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请输入评价', | |||||
| }, | |||||
| 'productNum': { | |||||
| type: 'number', | |||||
| required: true, | |||||
| message: '请为【产品服务度】打分', | |||||
| }, | |||||
| 'paperNum': { | |||||
| type: 'number', | |||||
| required: true, | |||||
| message: '请为【问卷体验】打分', | |||||
| }, | |||||
| 'logisticsNum': { | |||||
| type: 'number', | |||||
| required: true, | |||||
| message: '请为【物流速度】打分', | |||||
| }, | |||||
| }, | |||||
| formItemStyle: { padding: 0 }, | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| ...mapState(['userInfo']), | |||||
| }, | |||||
| onLoad(arg) { | |||||
| const { orderId } = arg | |||||
| this.orderId = orderId | |||||
| }, | |||||
| methods: { | |||||
| async onSubmit() { | |||||
| try { | |||||
| await this.$refs.form.validate() | |||||
| const { | |||||
| content, | |||||
| images, | |||||
| productNum, | |||||
| paperNum, | |||||
| logisticsNum, | |||||
| } = this.form | |||||
| const params = { | |||||
| orderId: this.orderId, | |||||
| content, | |||||
| image: images.join(','), | |||||
| productNum, | |||||
| paperNum, | |||||
| logisticsNum, | |||||
| } | |||||
| await this.$fetch('evaluateOrder', params) | |||||
| uni.showToast({ | |||||
| icon: 'success', | |||||
| title: '提交成功', | |||||
| }); | |||||
| setTimeout(() => { | |||||
| this.$utils.navigateBack() | |||||
| }, 800) | |||||
| } catch (err) { | |||||
| console.log('onSubmit err', err) | |||||
| } | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background-color: $uni-bg-color; | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| padding: calc(var(--status-bar-height) + 144rpx) 32rpx 236rpx 32rpx; | |||||
| } | |||||
| .card { | |||||
| padding: 32rpx; | |||||
| background: #FAFAFF; | |||||
| border: 2rpx solid #FFFFFF; | |||||
| border-radius: 32rpx; | |||||
| & + & { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| &-header { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #252545; | |||||
| margin-bottom: 32rpx; | |||||
| } | |||||
| } | |||||
| .form { | |||||
| &-item { | |||||
| border-bottom: 2rpx solid #EEEEEE; | |||||
| &:last-child { | |||||
| border: none; | |||||
| } | |||||
| & + & { | |||||
| margin-top: 32rpx; | |||||
| } | |||||
| &-label { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 26rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| } | |||||
| } | |||||
| } | |||||
| .info { | |||||
| .form-item + .form-item { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| .form-item-content { | |||||
| margin-top: 16rpx; | |||||
| } | |||||
| } | |||||
| .row { | |||||
| justify-content: space-between; | |||||
| } | |||||
| .bottom { | |||||
| position: fixed; | |||||
| left: 0; | |||||
| bottom: 0; | |||||
| width: 100vw; | |||||
| // height: 200rpx; | |||||
| padding: 24rpx 40rpx; | |||||
| padding-bottom: calc(env(safe-area-inset-bottom) + 24rpx); | |||||
| background: #FFFFFF; | |||||
| box-sizing: border-box; | |||||
| .btn { | |||||
| width: 100%; | |||||
| padding: 16rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1; | |||||
| color: #FFFFFF; | |||||
| background-image: linear-gradient(to right, #4B348F, #845CFA); | |||||
| border-radius: 41rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,371 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar title="填写订单" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="#F3F2F7" /> | |||||
| <view class="main"> | |||||
| <view class="card"> | |||||
| <productCard :data="orderInfo"></productCard> | |||||
| </view> | |||||
| <view class="order" v-if="orderData"> | |||||
| <view class="order-header"> | |||||
| 订单信息 | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">订单编号</view> | |||||
| <view class="row-content">{{ orderData.number }}</view> | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">下单时间</view> | |||||
| <view class="row-content">{{ $dayjs(orderData.createTime).format('YYYY-MM-DD HH:mm') }}</view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="notice"> | |||||
| <!-- <view class="notice-header">下单须知</view> --> | |||||
| <view class="notice-content"> | |||||
| <!-- todo: check key --> | |||||
| <!-- <uv-parse :content="configList['order_instructions']"></uv-parse> --> | |||||
| 如有特殊病史或有不宜参加的旅程(项目)、男女报名如无法同住、分开报名需安排同住同车等,请备注 | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="bottom"> | |||||
| <view class="agreement"> | |||||
| <uv-checkbox-group | |||||
| v-model="checkboxValue" | |||||
| shape="circle" | |||||
| > | |||||
| <uv-checkbox | |||||
| size="40rpx" | |||||
| icon-size="40rpx" | |||||
| activeColor="#7451DE" | |||||
| :name="1" | |||||
| ></uv-checkbox> | |||||
| </uv-checkbox-group> | |||||
| <view class="desc"> | |||||
| 我已阅读并同意 | |||||
| <!-- todo: 替换配置项key --> | |||||
| <text class="highlight" @click="$refs.modal.open('config_agreement', '退订政策')">《退订政策》</text> | |||||
| <!-- todo: 替换配置项key --> | |||||
| <text class="highlight" @click="$refs.modal.open('config_privacy', '合同范本')">《合同范本》</text> | |||||
| <!-- todo: 替换配置项key --> | |||||
| <text class="highlight" @click="$refs.modal.open('config_privacy', '预订须知')">《预订须知》</text> | |||||
| <!-- todo: 替换配置项key --> | |||||
| <text class="highlight" @click="$refs.modal.open('config_privacy', '安全提示')">《安全提示》</text> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex bar"> | |||||
| <view class="flex col price"> | |||||
| <view class="price-label"> | |||||
| 已选<view>{{ `${totolPeople}人` }}</view>总额 | |||||
| </view> | |||||
| <view class="price-unit">¥</view><view class="price-value">{{ totalPrice }}</view> | |||||
| </view> | |||||
| <button class="col btn" @click="onPay">立即支付</button> | |||||
| </view> | |||||
| </view> | |||||
| <agreementModal ref="modal" @confirm="onConfirmAgreement"></agreementModal> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import { mapState } from 'vuex' | |||||
| import productCard from './productCard.vue' | |||||
| import agreementModal from '@/pages_order/components/agreementModal.vue' | |||||
| export default { | |||||
| components: { | |||||
| productCard, | |||||
| agreementModal, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| id: null, | |||||
| defaultAddressInfo: null, | |||||
| orderData: null, | |||||
| productList: [], | |||||
| checkboxValue: [], | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| ...mapState(['configList', 'userInfo', 'orderInfo']), | |||||
| totolPeople() { | |||||
| const { adults, teenager, child } = this.orderInfo | |||||
| return (adults || 0) + (teenager || 0) + (child || 0) | |||||
| }, | |||||
| totalPrice() { | |||||
| const { time, adults, teenager, child, product } = this.orderInfo | |||||
| const { timeOptions } = product || {} | |||||
| const { adultsPrice, teenagerPrice, childPrice } = timeOptions?.find?.(item => item.id === time) || {} | |||||
| let total = 0 | |||||
| adults && (total += adults * (adultsPrice || 0)) | |||||
| teenager && (total += teenager * (teenagerPrice || 0)) | |||||
| child && (total += child * (childPrice || 0)) | |||||
| return total | |||||
| }, | |||||
| addressData() { | |||||
| return this.addressInfo || this.defaultAddressInfo || null | |||||
| }, | |||||
| }, | |||||
| onLoad(arg) { | |||||
| console.log('onLoad') | |||||
| console.log('payOrderProduct', this.payOrderProduct) | |||||
| this.productList = JSON.parse(JSON.stringify(this.payOrderProduct)) | |||||
| // todo: check include Overseas Product ? | |||||
| // this.$utils.navigateTo('/pages_order/order/userInfo/infoFill') | |||||
| return | |||||
| this.orderData = { | |||||
| id: '001', | |||||
| number: 'BH872381728321983929', | |||||
| createTime: '2025-04-28 08:14', | |||||
| } | |||||
| console.log('orderData', this.orderData) | |||||
| }, | |||||
| onUnload() { | |||||
| this.$store.commit('setAddressInfo', null) | |||||
| }, | |||||
| methods: { | |||||
| onConfirmAgreement(confirm) { | |||||
| if (confirm) { | |||||
| this.checkboxValue = [1] | |||||
| } else { | |||||
| this.checkboxValue = [] | |||||
| } | |||||
| }, | |||||
| async onPay() { | |||||
| if(!this.checkboxValue.length){ | |||||
| return uni.showToast({ | |||||
| title: '请先同意《用户协议》《隐私协议》《消费者告知》', | |||||
| icon:'none' | |||||
| }) | |||||
| } | |||||
| // todo | |||||
| return | |||||
| // const { id } = this.orderData | |||||
| const obj = { | |||||
| // // todo: check title | |||||
| // title: '营养套餐消费', | |||||
| // orderId: id, | |||||
| list: this.productList, | |||||
| addressId: this.addressData.id, | |||||
| amount: this.totalPrice, | |||||
| } | |||||
| this.$refs.payPopup.open(obj) | |||||
| }, | |||||
| onPaySuccess() { | |||||
| setTimeout(() => { | |||||
| // todo: check → jump to order list page ? | |||||
| uni.reLaunch({ | |||||
| url: `/pages_order/order/orderList/index` | |||||
| }); | |||||
| }, 700) | |||||
| }, | |||||
| onPayCancel() { | |||||
| // todo: check → jump to order list page? | |||||
| uni.redirectTo({ | |||||
| url: `/pages_order/order/orderList/index?index=1` | |||||
| }); | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background-color: $uni-bg-color; | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| padding: calc(var(--status-bar-height) + 144rpx) 32rpx 310rpx 32rpx; | |||||
| } | |||||
| .address { | |||||
| margin-bottom: 40rpx; | |||||
| justify-content: space-between; | |||||
| padding: 24rpx 32rpx; | |||||
| background: #FFFFFF; | |||||
| border-radius: 24rpx; | |||||
| } | |||||
| .card { | |||||
| & + & { | |||||
| margin-top: 32rpx; | |||||
| } | |||||
| } | |||||
| .order { | |||||
| margin-top: 40rpx; | |||||
| padding: 32rpx; | |||||
| background: #FAFAFF; | |||||
| border: 2rpx solid #FFFFFF; | |||||
| border-radius: 32rpx; | |||||
| &-header { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #252545; | |||||
| } | |||||
| .row { | |||||
| margin-top: 32rpx; | |||||
| justify-content: space-between; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| line-height: 1.4; | |||||
| &-label { | |||||
| font-size: 26rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| &-content { | |||||
| font-size: 28rpx; | |||||
| color: #393939; | |||||
| } | |||||
| } | |||||
| } | |||||
| .notice { | |||||
| margin-top: 40rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| &-header { | |||||
| font-size: 28rpx; | |||||
| line-height: 1.4; | |||||
| color: #393939; | |||||
| } | |||||
| &-content { | |||||
| margin-top: 24rpx; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #BABABA; | |||||
| } | |||||
| } | |||||
| .bottom { | |||||
| position: fixed; | |||||
| left: 0; | |||||
| bottom: 0; | |||||
| width: 100vw; | |||||
| // height: 270rpx; | |||||
| background: #FFFFFF; | |||||
| box-sizing: border-box; | |||||
| .agreement { | |||||
| display: flex; | |||||
| padding: 16rpx 40rpx; | |||||
| background: #EFEAFF; | |||||
| box-sizing: border-box; | |||||
| /deep/ .uv-checkbox-group { | |||||
| flex: none; | |||||
| } | |||||
| .desc { | |||||
| flex: 1; | |||||
| font-family: PingFang SC; | |||||
| font-size: 24rpx; | |||||
| font-weight: 400; | |||||
| line-height: 40rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| .highlight { | |||||
| color: $uni-color; | |||||
| } | |||||
| } | |||||
| .bar { | |||||
| padding: 24rpx 40rpx; | |||||
| padding-bottom: calc(env(safe-area-inset-bottom) + 24rpx); | |||||
| box-sizing: border-box; | |||||
| column-gap: 30rpx; | |||||
| .col { | |||||
| flex: 1; | |||||
| } | |||||
| .price { | |||||
| justify-content: flex-start; | |||||
| &-label { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #626262; | |||||
| } | |||||
| &-unit { | |||||
| margin: 0 8rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #7451DE; | |||||
| } | |||||
| &-value { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 40rpx; | |||||
| line-height: 1.4; | |||||
| color: #7451DE; | |||||
| } | |||||
| } | |||||
| .btn { | |||||
| width: 100%; | |||||
| padding: 16rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1; | |||||
| color: #FFFFFF; | |||||
| background-image: linear-gradient(to right, #4B348F, #845CFA); | |||||
| border-radius: 41rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,374 @@ | |||||
| <template> | |||||
| <view> | |||||
| <uv-popup ref="popup" mode="bottom" bgColor="none" @change="onPopupChange"> | |||||
| <view class="popup__view"> | |||||
| <view class="flex header"> | |||||
| 选择日期/套餐/人数 | |||||
| </view> | |||||
| <uv-form | |||||
| ref="form" | |||||
| :model="form" | |||||
| errorType="toast" | |||||
| > | |||||
| <view class="section"> | |||||
| <uv-form-item prop="time" :customStyle="formItemStyle"> | |||||
| <view class="flex section-header"> | |||||
| <view>选择团期</view> | |||||
| <button class="flex btn" @click="openTimePicker"> | |||||
| <view class="highlight">日历选择</view> | |||||
| <image class="img" src="@/static/image/icon-arrow-right.png" mode="widthFix"></image> | |||||
| </button> | |||||
| </view> | |||||
| <timeCalendarSelect ref="timeCalendarSelect" v-model="form.time" :options="data.timeOptions"></timeCalendarSelect> | |||||
| <view class="flex section-content"> | |||||
| <timeOptionsSelect style="width: calc(100vw - 40rpx*2);" | |||||
| v-model="form.time" | |||||
| :options="data.timeOptions" | |||||
| ></timeOptionsSelect> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="section"> | |||||
| <uv-form-item prop="adults" :customStyle="formItemStyle"> | |||||
| <view class="flex section-header"> | |||||
| <view>选择人数</view> | |||||
| </view> | |||||
| <view class="flex section-content"> | |||||
| <peopleNumberInput style="width: calc(100vw - 40rpx*2);" | |||||
| :adults.sync="form.adults" | |||||
| :teenager.sync="form.teenager" | |||||
| :child.sync="form.child" | |||||
| :adultsPrice="selectTimeObj.adultsPrice" | |||||
| :teenagerPrice="selectTimeObj.teenagerPrice" | |||||
| :childPrice="selectTimeObj.childPrice" | |||||
| ></peopleNumberInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="section"> | |||||
| <uv-form-item prop="members" :customStyle="formItemStyle"> | |||||
| <view class="flex section-header"> | |||||
| <view>选择人员</view> | |||||
| <button class="flex btn" @click="jumpToSelectMember"> | |||||
| <view>请选择出行人</view> | |||||
| <image class="img" src="@/static/image/icon-arrow-right.png" mode="widthFix"></image> | |||||
| </button> | |||||
| </view> | |||||
| <view class="flex section-content member"> | |||||
| <view class="member-item" v-for="item in form.members" :key="item.id"> | |||||
| {{ item.name }} | |||||
| </view> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| </uv-form> | |||||
| <view class="footer"> | |||||
| <button class="flex btn" @click="onConfirm">填写订单</button> | |||||
| </view> | |||||
| </view> | |||||
| </uv-popup> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import { mapState } from 'vuex' | |||||
| import timeOptionsSelect from '@/pages_order/order/orderConfirm/timeOptionsSelect.vue' | |||||
| import timeCalendarSelect from '@/pages_order/order/orderConfirm/timeCalendarSelect.vue' | |||||
| import peopleNumberInput from '@/pages_order/order/orderConfirm/peopleNumberInput.vue' | |||||
| export default { | |||||
| components: { | |||||
| timeOptionsSelect, | |||||
| timeCalendarSelect, | |||||
| peopleNumberInput, | |||||
| }, | |||||
| props: { | |||||
| data: { | |||||
| type: Object, | |||||
| default() { | |||||
| return {} | |||||
| } | |||||
| }, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| options: [], | |||||
| form: { | |||||
| time: null, | |||||
| adults: 0, | |||||
| teenager: 0, | |||||
| child: 0, | |||||
| members: [], | |||||
| }, | |||||
| formItemStyle: { padding: 0 }, | |||||
| } | |||||
| }, | |||||
| computed : { | |||||
| ...mapState(['configList', 'travelerList']), | |||||
| selectTimeObj() { | |||||
| const { time: id } = this.form | |||||
| const { timeOptions } = this.data | |||||
| if (id) { | |||||
| return timeOptions?.find?.(option => option.id === id) || {} | |||||
| } | |||||
| return timeOptions?.[0] || {} | |||||
| }, | |||||
| }, | |||||
| watch: { | |||||
| travelerList(val) { | |||||
| if (val?.length) { | |||||
| this.form.members = val | |||||
| this.$store.commit('setTravelerList', []) | |||||
| } | |||||
| }, | |||||
| form: { | |||||
| handler(val) { | |||||
| console.log('watch form', val) | |||||
| this.$refs.form.setRules(this.getRules()) | |||||
| }, | |||||
| deep: true | |||||
| } | |||||
| }, | |||||
| onReady() { | |||||
| this.$refs.form.setRules(this.getRules()) | |||||
| }, | |||||
| methods: { | |||||
| getRules() { | |||||
| const { adults, teenager, child } = this.form | |||||
| return { | |||||
| 'time': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请选择团期', | |||||
| }, | |||||
| 'adults': { | |||||
| type: 'number', | |||||
| required: true, | |||||
| message: '请选择人数', | |||||
| validator: (rule, value, callback) => { | |||||
| if (adults || teenager || child) { | |||||
| return true | |||||
| } | |||||
| return false | |||||
| }, | |||||
| }, | |||||
| 'members': { | |||||
| type: 'array', | |||||
| required: true, | |||||
| message: '请选择出行人', | |||||
| }, | |||||
| } | |||||
| }, | |||||
| openTimePicker() { | |||||
| this.$refs.timeCalendarSelect.open() | |||||
| }, | |||||
| async getDefaultMembers() { | |||||
| try { | |||||
| // todo: fetch defalt members | |||||
| return [ | |||||
| { | |||||
| id: '001', | |||||
| name: '李梓发', | |||||
| idNo: '430223********9999', | |||||
| type: 0, | |||||
| }, | |||||
| { | |||||
| id: '002', | |||||
| name: '吴彦谦', | |||||
| idNo: '430223********9999', | |||||
| type: 0, | |||||
| }, | |||||
| { | |||||
| id: '003', | |||||
| name: '冯云', | |||||
| idNo: '430223********9999', | |||||
| type: 1, | |||||
| }, | |||||
| { | |||||
| id: '004', | |||||
| name: '冯思钗', | |||||
| idNo: '430223********9999', | |||||
| type: 2, | |||||
| }, | |||||
| { | |||||
| id: '005', | |||||
| name: '李书萍', | |||||
| idNo: '430223********9999', | |||||
| type: 0, | |||||
| }, | |||||
| { | |||||
| id: '006', | |||||
| name: '冯艺莲', | |||||
| idNo: '430223********9999', | |||||
| type: 1, | |||||
| }, | |||||
| ] | |||||
| } catch (err) { | |||||
| return [] | |||||
| } | |||||
| }, | |||||
| jumpToSelectMember() { | |||||
| const { members } = this.form | |||||
| const selectIds = members.map(item => item.id).join(',') | |||||
| console.log('jumpToSelectMember', selectIds) | |||||
| this.$utils.navigateTo(`/pages_order/traveler/travelerList?selectIds=${selectIds}`) | |||||
| }, | |||||
| async open(data) { | |||||
| const { selectTime } = data || {} | |||||
| const defaultMembers = await this.getDefaultMembers() | |||||
| this.form.time = selectTime || null | |||||
| this.form.members = defaultMembers | |||||
| this.$refs.popup.open() | |||||
| }, | |||||
| close() { | |||||
| this.$refs.popup.close() | |||||
| }, | |||||
| async onConfirm() { | |||||
| try { | |||||
| await this.$refs.form.validate() | |||||
| const { | |||||
| time, | |||||
| adults, | |||||
| teenager, | |||||
| child, | |||||
| members, | |||||
| } = this.form | |||||
| const orderInfo = { | |||||
| product: this.data, | |||||
| time, | |||||
| adults, | |||||
| teenager, | |||||
| child, | |||||
| members, | |||||
| } | |||||
| this.$store.commit('setOrderInfo', orderInfo) | |||||
| uni.navigateTo({ | |||||
| url: '/pages_order/order/orderConfirm/index' | |||||
| }) | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| onPopupChange(e) { | |||||
| if (e.show) { | |||||
| return | |||||
| } | |||||
| this.$emit('timeChange', this.form.time) | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style lang="scss" scoped> | |||||
| .popup__view { | |||||
| width: 100vw; | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| line-height: 1.4; | |||||
| background: #FFFFFF; | |||||
| border-top-left-radius: 32rpx; | |||||
| border-top-right-radius: 32rpx; | |||||
| } | |||||
| .header { | |||||
| position: relative; | |||||
| width: 100%; | |||||
| padding: 24rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 34rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| border-bottom: 2rpx solid #EEEEEE; | |||||
| } | |||||
| .section { | |||||
| padding: 24rpx 40rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| &-header { | |||||
| justify-content: space-between; | |||||
| font-size: 32rpx; | |||||
| font-weight: 500; | |||||
| color: #181818; | |||||
| .btn { | |||||
| column-gap: 4rpx; | |||||
| font-size: 32rpx; | |||||
| font-weight: 400; | |||||
| color: #8B8B8B; | |||||
| .highlight { | |||||
| color: #181818; | |||||
| } | |||||
| .img { | |||||
| width: 32rpx; | |||||
| height: auto; | |||||
| } | |||||
| } | |||||
| } | |||||
| &-content { | |||||
| margin-top: 20rpx; | |||||
| } | |||||
| } | |||||
| .member { | |||||
| display: grid; | |||||
| grid-template-columns: repeat(3, 1fr); | |||||
| gap: 12rpx; | |||||
| &-item { | |||||
| padding: 16rpx; | |||||
| text-align: center; | |||||
| font-size: 28rpx; | |||||
| color: #181818; | |||||
| background: #F9F9F9; | |||||
| border-radius: 16rpx; | |||||
| } | |||||
| } | |||||
| .footer { | |||||
| width: 100%; | |||||
| // height: 214rpx; | |||||
| padding: 32rpx 40rpx; | |||||
| box-sizing: border-box; | |||||
| .btn { | |||||
| width: 100%; | |||||
| padding: 14rpx 0; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #FFFFFF; | |||||
| background-image: linear-gradient(to right, #21FEEC, #019AF9); | |||||
| border: 2rpx solid #00A9FF; | |||||
| border-radius: 41rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,193 @@ | |||||
| <template> | |||||
| <view class="input__view" :style="style"> | |||||
| <view class="flex row"> | |||||
| <view class="flex row-label"> | |||||
| <view class="title">成人</view> | |||||
| <view class="desc">(18周岁以上)</view> | |||||
| <view class="flex price"> | |||||
| <text>¥</text> | |||||
| <text class="highlight">{{ adultsPrice }}</text> | |||||
| </view> | |||||
| </view> | |||||
| <view class="row-content"> | |||||
| <uv-number-box | |||||
| v-model="adultsNum" | |||||
| :min="0" | |||||
| :integer="true" | |||||
| :inputWidth="68" | |||||
| bgColor="transparent" | |||||
| :iconStyle="{ | |||||
| background: '#F7F8FA', | |||||
| fontSize: '13px', | |||||
| lineHeight: 1, | |||||
| padding: '12px', | |||||
| borderRadius: '50%', | |||||
| }" | |||||
| ></uv-number-box> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="flex row-label"> | |||||
| <view class="title">青少年</view> | |||||
| <view class="desc">(14周岁以上)</view> | |||||
| <view class="flex price"> | |||||
| <text>¥</text> | |||||
| <text class="highlight">{{ teenagerPrice }}</text> | |||||
| </view> | |||||
| </view> | |||||
| <view class="row-content"> | |||||
| <uv-number-box | |||||
| v-model="teenagerNum" | |||||
| :min="0" | |||||
| :integer="true" | |||||
| :inputWidth="68" | |||||
| bgColor="transparent" | |||||
| :iconStyle="{ | |||||
| background: '#F7F8FA', | |||||
| fontSize: '13px', | |||||
| lineHeight: 1, | |||||
| padding: '12px', | |||||
| borderRadius: '50%', | |||||
| }" | |||||
| ></uv-number-box> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="flex row-label"> | |||||
| <view class="title">儿童</view> | |||||
| <view class="desc">(14周岁以下)</view> | |||||
| <view class="flex price"> | |||||
| <text>¥</text> | |||||
| <text class="highlight">{{ childPrice }}</text> | |||||
| </view> | |||||
| </view> | |||||
| <view class="row-content"> | |||||
| <uv-number-box | |||||
| v-model="childNum" | |||||
| :min="0" | |||||
| :integer="true" | |||||
| :inputWidth="68" | |||||
| bgColor="transparent" | |||||
| :iconStyle="{ | |||||
| background: '#F7F8FA', | |||||
| fontSize: '13px', | |||||
| lineHeight: 1, | |||||
| padding: '12px', | |||||
| borderRadius: '50%', | |||||
| }" | |||||
| ></uv-number-box> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| props: { | |||||
| adults: { | |||||
| type: Number, | |||||
| default: 0, | |||||
| }, | |||||
| teenager: { | |||||
| type: Number, | |||||
| default: 0, | |||||
| }, | |||||
| child: { | |||||
| type: Number, | |||||
| default: 0, | |||||
| }, | |||||
| adultsPrice: { | |||||
| type: Number, | |||||
| default: 0, | |||||
| }, | |||||
| teenagerPrice: { | |||||
| type: Number, | |||||
| default: 0, | |||||
| }, | |||||
| childPrice: { | |||||
| type: Number, | |||||
| default: 0, | |||||
| }, | |||||
| style: { | |||||
| type: String, | |||||
| default: '' | |||||
| }, | |||||
| }, | |||||
| computed: { | |||||
| adultsNum: { | |||||
| set(val) { | |||||
| this.$emit('update:adults', val) | |||||
| }, | |||||
| get() { | |||||
| return this.adults | |||||
| } | |||||
| }, | |||||
| teenagerNum: { | |||||
| set(val) { | |||||
| this.$emit('update:teenager', val) | |||||
| }, | |||||
| get() { | |||||
| return this.teenager | |||||
| } | |||||
| }, | |||||
| childNum: { | |||||
| set(val) { | |||||
| this.$emit('update:child', val) | |||||
| }, | |||||
| get() { | |||||
| return this.child | |||||
| } | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style lang="scss" scoped> | |||||
| .input__view { | |||||
| width: 100%; | |||||
| } | |||||
| .row { | |||||
| justify-content: space-between; | |||||
| padding: 24rpx 0; | |||||
| font-family: PingFang SC; | |||||
| font-size: 24rpx; | |||||
| font-weight: 400; | |||||
| line-height: 1.4; | |||||
| border-bottom: 2rpx solid #EEEEEE; | |||||
| & + & { | |||||
| margin-top: 20rpx; | |||||
| } | |||||
| &-label { | |||||
| justify-content: flex-start; | |||||
| column-gap: 4rpx; | |||||
| } | |||||
| &-content { | |||||
| /deep/ .uv-number-box__minus--disabled { | |||||
| background: transparent !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| .title { | |||||
| font-size: 28rpx; | |||||
| color: #000000; | |||||
| } | |||||
| .desc { | |||||
| color: #8B8B8B; | |||||
| } | |||||
| .price { | |||||
| font-weight: 500; | |||||
| color: #FF4800; | |||||
| .highlight { | |||||
| font-size: 32rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,145 @@ | |||||
| <template> | |||||
| <view class="card info"> | |||||
| <view class="card-header">{{ product.name }}</view> | |||||
| <view class="card-content"> | |||||
| <view class="row desc">{{ product.desc }}</view> | |||||
| <view class="flex row tags" v-if="product.tags && product.tags.length"> | |||||
| <view class="tag" v-for="(tag, tIdx) in product.tags" :key="tIdx"> | |||||
| {{ tag }} | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex row time"> | |||||
| <view class="time-item"> | |||||
| <view class="time-item-value">{{ $dayjs(productPackage.startDate).format('MM月DD日') }}</view> | |||||
| <view class="time-item-label">出发日期</view> | |||||
| </view> | |||||
| <view class="flex time-total"> | |||||
| <view class="time-total-line"></view> | |||||
| <view class="time-total-value">{{ `${days}天` }}</view> | |||||
| <view class="time-total-line"></view> | |||||
| </view> | |||||
| <view class="time-item"> | |||||
| <view class="time-item-value">{{ $dayjs(productPackage.endDate).format('MM月DD日') }}</view> | |||||
| <view class="time-item-label">结束日期</view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| props: { | |||||
| data: { | |||||
| type: Object, | |||||
| default() { | |||||
| return {} | |||||
| } | |||||
| }, | |||||
| }, | |||||
| computed: { | |||||
| product() { | |||||
| return this.data?.product || {} | |||||
| }, | |||||
| productPackage() { | |||||
| const { time, product } = this.data | |||||
| const { timeOptions } = product || {} | |||||
| return timeOptions?.find?.(item => item.id === time) || {} | |||||
| }, | |||||
| days() { | |||||
| console.log('productPackage', this.productPackage) | |||||
| const { startDate, endDate } = this.productPackage | |||||
| return this.$dayjs(endDate).diff(this.$dayjs(startDate), 'day') | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .card { | |||||
| width: 100%; | |||||
| padding: 32rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| line-height: 1.4; | |||||
| box-sizing: border-box; | |||||
| background: #FFFFFF; | |||||
| border-radius: 24rpx; | |||||
| &-header { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 32rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| } | |||||
| &-content { | |||||
| } | |||||
| } | |||||
| .row { | |||||
| margin-top: 16rpx; | |||||
| } | |||||
| .desc { | |||||
| font-size: 26rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| .tags { | |||||
| justify-content: flex-start; | |||||
| flex-wrap: wrap; | |||||
| gap: 16rpx; | |||||
| .tag { | |||||
| padding: 2rpx 14rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #00A9FF; | |||||
| background: #E9F8FF; | |||||
| border: 2rpx solid #00A9FF; | |||||
| border-radius: 8rpx; | |||||
| } | |||||
| } | |||||
| .time { | |||||
| justify-content: space-between; | |||||
| &-item { | |||||
| &-value { | |||||
| font-size: 32rpx; | |||||
| font-weight: 500; | |||||
| color: #000000; | |||||
| } | |||||
| &-label { | |||||
| margin-top: 4rpx; | |||||
| font-size: 26rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| } | |||||
| &-total { | |||||
| &-line { | |||||
| width: 64rpx; | |||||
| height: 2rpx; | |||||
| background: #8B8B8B; | |||||
| } | |||||
| &-value { | |||||
| padding: 6rpx 22rpx; | |||||
| font-size: 26rpx; | |||||
| color: #8B8B8B; | |||||
| border: 2rpx solid #8B8B8B; | |||||
| border-radius: 26rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,91 @@ | |||||
| <template> | |||||
| <view class="calendar"> | |||||
| <uv-calendar | |||||
| ref="calendar" | |||||
| title="出行日期" | |||||
| mode="single" | |||||
| rowHeight="110" | |||||
| :defaultDate="defaultDate" | |||||
| @confirm="confirm" | |||||
| ></uv-calendar> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| props: { | |||||
| value: { | |||||
| type: String, | |||||
| default: null | |||||
| }, | |||||
| options: { | |||||
| type: Array, | |||||
| default() { | |||||
| return [] | |||||
| } | |||||
| }, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| defaultDate: null, | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| selected: { | |||||
| set(val) { | |||||
| this.$emit('input', val) | |||||
| }, | |||||
| get() { | |||||
| return this.value | |||||
| } | |||||
| }, | |||||
| startDateList() { | |||||
| return this.options.map(item => { | |||||
| const { startDate } = item | |||||
| return this.$dayjs(`2025/${startDate}`).format('YYYY-MM-DD') | |||||
| }) | |||||
| }, | |||||
| }, | |||||
| onReady() { | |||||
| this.$refs.calendar.setFormatter(this.formatter); | |||||
| }, | |||||
| methods: { | |||||
| formatter(day) { | |||||
| const dateStr = this.$dayjs(day.date).format('YYYY-MM-DD') | |||||
| const index = this.startDateList.findIndex(startDate => startDate === dateStr) | |||||
| if (index === -1) { | |||||
| day.disabled = true | |||||
| return day; | |||||
| } | |||||
| const { startDate, endDate, currentPrice } = this.options[index] | |||||
| // day.topInfo = `${startDate}-${endDate}` | |||||
| day.topInfo = `~${endDate}` | |||||
| day.bottomInfo = `¥${currentPrice}` | |||||
| return day | |||||
| }, | |||||
| open() { | |||||
| if (this.selected) { | |||||
| const index = this.options.findIndex(option => option.id === this.selected) | |||||
| this.defaultDate = this.startDateList[index] | |||||
| } | |||||
| this.$refs.calendar.open(); | |||||
| }, | |||||
| confirm(e) { | |||||
| const dateStr = e[0] | |||||
| const index = this.startDateList.findIndex(startDate => startDate === dateStr) | |||||
| this.selected = this.options[index].id | |||||
| }, | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style lang="scss" scoped> | |||||
| </style> | |||||
| @ -0,0 +1,150 @@ | |||||
| <template> | |||||
| <view class="flex option" :style="style"> | |||||
| <view | |||||
| :class="['option-item', item.id === selected ? 'is-active' : '']" | |||||
| v-for="item in options" | |||||
| :key="item.id" | |||||
| @click="selected = item.id" | |||||
| > | |||||
| <view class="option-item-content"> | |||||
| <view class="flex time"> | |||||
| <view class="time-val">{{ item.startDate }}</view> | |||||
| <view class="time-split">-</view> | |||||
| <view class="time-val">{{ item.endDate }}</view> | |||||
| </view> | |||||
| <view class="flex price"> | |||||
| <view class="price-val"> | |||||
| <text>¥</text> | |||||
| <text class="highlight">{{ priceInt(item.currentPrice) }}</text> | |||||
| <text>{{ `${priceFrac(item.currentPrice)}起` }}</text> | |||||
| </view> | |||||
| <view class="price-bef" v-if="item.originalPrice">¥<text>{{ item.originalPrice }}</text></view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex option-item-bottom"> | |||||
| {{ item.id === selected ? '已选择' : '点击选择' }} | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| props: { | |||||
| value: { | |||||
| type: String | Number, | |||||
| default: null | |||||
| }, | |||||
| options: { | |||||
| type: Array, | |||||
| default() { | |||||
| return [] | |||||
| } | |||||
| }, | |||||
| style: { | |||||
| type: String, | |||||
| default: '' | |||||
| }, | |||||
| }, | |||||
| computed: { | |||||
| selected: { | |||||
| set(val) { | |||||
| this.$emit('input', val) | |||||
| }, | |||||
| get() { | |||||
| return this.value | |||||
| } | |||||
| }, | |||||
| }, | |||||
| methods: { | |||||
| priceInt(currentPrice) { | |||||
| return parseInt(currentPrice) | |||||
| }, | |||||
| priceFrac(currentPrice) { | |||||
| return (currentPrice % this.priceInt(currentPrice)).toFixed(2).slice(1) | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .option { | |||||
| width: 100%; | |||||
| overflow-x: auto; | |||||
| flex-wrap: nowrap; | |||||
| justify-content: flex-start; | |||||
| column-gap: 16rpx; | |||||
| &-item { | |||||
| min-width: 238rpx; | |||||
| flex: none; | |||||
| font-size: 0; | |||||
| border: 2rpx solid #EEEEEE; | |||||
| border-radius: 12rpx; | |||||
| overflow: hidden; | |||||
| &-content { | |||||
| padding: 16rpx 12rpx; | |||||
| } | |||||
| &-bottom { | |||||
| padding: 12rpx 0; | |||||
| font-size: 28rpx; | |||||
| font-weight: 500; | |||||
| color: #191919; | |||||
| background: #E4E7EB; | |||||
| } | |||||
| &.is-active { | |||||
| background: #E9F8FF; | |||||
| border-color: #00A9FF; | |||||
| .option-item-bottom { | |||||
| color: #FFFFFF; | |||||
| background: #00A9FF; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| .time { | |||||
| column-gap: 8rpx; | |||||
| &-val { | |||||
| font-size: 28rpx; | |||||
| font-weight: 500; | |||||
| color: #000000; | |||||
| } | |||||
| &-split { | |||||
| font-size: 24rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| } | |||||
| .price { | |||||
| margin-top: 4rpx; | |||||
| justify-content: flex-start; | |||||
| align-items: baseline; | |||||
| column-gap: 8rpx; | |||||
| white-space: nowrap; | |||||
| &-val { | |||||
| font-size: 24rpx; | |||||
| font-weight: 500; | |||||
| color: #FF4800; | |||||
| .highlight { | |||||
| font-size: 32rpx; | |||||
| } | |||||
| } | |||||
| &-bef { | |||||
| text-decoration: line-through; | |||||
| font-size: 24rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,652 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar title="订单详情" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="#FFFFFF" /> | |||||
| <view class="main"> | |||||
| <template v-if="orderData"> | |||||
| <view class="card detail"> | |||||
| <view class="flex card-top"> | |||||
| <view class="title">订单详情</view> | |||||
| <view :class="['flex', 'status', `status-${status}`]">{{ statusDesc }}</view> | |||||
| </view> | |||||
| <view class="card-main"> | |||||
| <view class="flex product" v-for="item in orderData.appletOrderProductList" :key="item.id"> | |||||
| <image class="img" :src="getCoverImg(item.image)" mode="scaleToFill"></image> | |||||
| <view class="info"> | |||||
| <view class="row">{{ item.productName }}</view> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">产品类型:</view> | |||||
| <!-- todo: check key --> | |||||
| <view class="row-content">{{ getTypeDesc(item.type) || '--' }}</view> | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">产品内容:</view> | |||||
| <!-- todo: check key --> | |||||
| <view class="row-content">{{ item.content || '--' }}</view> | |||||
| </view> | |||||
| <view class="flex price"> | |||||
| <text class="price-label">价格:</text> | |||||
| <text class="price-unit">¥</text><text class="price-value">{{ item.price }}</text> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex row card-bottom"> | |||||
| <view class="row-label">总价格</view> | |||||
| <view class="flex row-content price">¥<text class="price-value">{{ orderData.orderAmount }}</text></view> | |||||
| </view> | |||||
| </view> | |||||
| <view v-if="orderData.process.length" class="card service"> | |||||
| <view class="flex card-top"> | |||||
| <view class="title">售后信息</view> | |||||
| </view> | |||||
| <view class="card-main"> | |||||
| <uv-steps | |||||
| current="0" | |||||
| direction="column" | |||||
| dot | |||||
| activeColor="#10A934" | |||||
| inactiveColor="#C6C6C6" | |||||
| > | |||||
| <uv-steps-item | |||||
| v-for="(item, index) in orderData.process" | |||||
| :key="item.id" | |||||
| > | |||||
| <template #title> | |||||
| <view class="flex step-header"> | |||||
| <view :class="['step-title', index == 0 ? 'highlight' : '']">{{ item.title }}</view> | |||||
| <view class="step-time">{{ item.createTime }}</view> | |||||
| </view> | |||||
| </template> | |||||
| <template #desc> | |||||
| <view class="step-desc">{{ item.text }}</view> | |||||
| </template> | |||||
| </uv-steps-item> | |||||
| </uv-steps> | |||||
| </view> | |||||
| </view> | |||||
| <view class="card info"> | |||||
| <view class="flex card-top"> | |||||
| <view class="title">订单信息</view> | |||||
| </view> | |||||
| <view class="card-main"> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">订单编号</view> | |||||
| <view class="row-content">{{ orderData.orderNo }}</view> | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">下单时间</view> | |||||
| <view class="row-content">{{ $dayjs(orderData.orderDate).format('YYYY-MM-DD HH:mm') }}</view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <view class="notice"> | |||||
| <view class="notice-header">下单须知</view> | |||||
| <view class="notice-content"> | |||||
| <uv-parse :content="configList['order_instructions']"></uv-parse> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex bottom" v-if="[0, 1, 2, 3, 4].includes(status)"> | |||||
| <view class="flex bar"> | |||||
| <button plain class="flex flex-column btn btn-service" open-type="contact"> | |||||
| <image class="btn-service-icon" src="@/pages_order/static/order/icon-service.png" mode="widthFix"></image> | |||||
| <view>联系客服</view> | |||||
| </button> | |||||
| <view class="flex cols"> | |||||
| <!-- 待支付 --> | |||||
| <template v-if="status == 0"> | |||||
| <view class="flex col price"> | |||||
| <view class="price-label">合计</view> | |||||
| <text class="price-unit">¥</text><text class="price-value">{{ orderData.orderAmount }}</text> | |||||
| </view> | |||||
| <button class="flex col btn btn-primary" @click="onPay">立即支付</button> | |||||
| </template> | |||||
| <!-- 待发货 --> | |||||
| <template v-else-if="status == 1"> | |||||
| <button class="flex col btn" @click="onApplyService">申请售后</button> | |||||
| <!-- 自采检测 --> | |||||
| <template v-if="detectProduct && detectProduct.subscribeType == 0"> | |||||
| <button class="flex col btn btn-primary" @click="onDetectModify">修改</button> | |||||
| </template> | |||||
| </template> | |||||
| <!-- 待收货 --> | |||||
| <template v-else-if="status == 2"> | |||||
| <button class="flex col btn" @click="onApplyService">申请售后</button> | |||||
| <!-- 检测 subscribeType: 0自采,1上门,2到店,3已取消 --> | |||||
| <template v-if="detectProduct"> | |||||
| <!-- 自采检测 --> | |||||
| <template v-if="detectProduct.subscribeType == 0"> | |||||
| <button class="flex col btn btn-primary" @click="onDetectSendBack">线上回寄试剂盒</button> | |||||
| </template> | |||||
| <template v-else> | |||||
| <button class="flex col btn btn-primary" @click="onDetectBook">检测预约</button> | |||||
| </template> | |||||
| </template> | |||||
| <!-- 其他商品 --> | |||||
| <template v-else> | |||||
| <button class="flex col btn btn-primary" @click="onConfirmReceipt">确认收货</button> | |||||
| </template> | |||||
| </template> | |||||
| <!-- 待评价 --> | |||||
| <template v-else-if="status == 3"> | |||||
| <button class="flex col btn" @click="onApplyService">申请售后</button> | |||||
| <button class="flex col btn btn-primary" @click="onComment">立即评价</button> | |||||
| </template> | |||||
| <!-- 已完成 --> | |||||
| <template v-else-if="status == 4"> | |||||
| <button class="flex col btn" @click="onApplyService">申请售后</button> | |||||
| </template> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| // 订单状态 0待支付 1待发货 2待收货 3待评价 4已完成 | |||||
| const STATUS_AND_DESC_MAPPING = { | |||||
| 0: '待支付', | |||||
| 1: '待发货', | |||||
| 2: '待收货', | |||||
| 3: '待评价', | |||||
| 4: '已完成', | |||||
| 5: '售后', | |||||
| } | |||||
| // 产品类型(0营养剂,1预约,2课程) | |||||
| const TYPE_AND_DESC_MAPPING = { | |||||
| 0: '营养剂', | |||||
| 1: '检测', | |||||
| 2: '课程', | |||||
| } | |||||
| export default { | |||||
| data() { | |||||
| return { | |||||
| id: null, | |||||
| addressData: null, | |||||
| orderData: null, | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| status() { | |||||
| const { orderStatus, afterSales } = this.orderData || {} | |||||
| if (afterSales) { | |||||
| return 5 | |||||
| } | |||||
| return orderStatus | |||||
| }, | |||||
| statusDesc() { | |||||
| return STATUS_AND_DESC_MAPPING[this.status] | |||||
| }, | |||||
| detectProduct() { | |||||
| const { appletOrderProductList } = this.orderData || {} | |||||
| if (appletOrderProductList?.length == 1 && appletOrderProductList?.[0]?.type == 1) { // type: 产品类型(0营养剂,1预约,2课程) | |||||
| return appletOrderProductList[0] | |||||
| } | |||||
| return null | |||||
| }, | |||||
| }, | |||||
| onShow() { | |||||
| console.log('onShow') | |||||
| if (!this.id) { | |||||
| return | |||||
| } | |||||
| this.getData() | |||||
| }, | |||||
| onLoad(arg) { | |||||
| this.id = arg.id | |||||
| this.getData() | |||||
| }, | |||||
| onPullDownRefresh() { | |||||
| this.getData() | |||||
| }, | |||||
| methods: { | |||||
| async getData() { | |||||
| try { | |||||
| const result = await this.$fetch('detailOrder', { id: this.id }) | |||||
| const { | |||||
| customerName, | |||||
| customerPhone, | |||||
| deliveryAddressDetail, | |||||
| ...orderData | |||||
| } = result | |||||
| this.addressData = { | |||||
| name: customerName, | |||||
| phone: customerPhone, | |||||
| detail: deliveryAddressDetail, | |||||
| } | |||||
| this.orderData = orderData | |||||
| } catch (err) { | |||||
| } | |||||
| uni.stopPullDownRefresh() | |||||
| }, | |||||
| getTypeDesc(type) { | |||||
| return TYPE_AND_DESC_MAPPING[type] | |||||
| }, | |||||
| getCoverImg(image) { | |||||
| return image?.split?.(',')?.[0] || '' | |||||
| }, | |||||
| onPay() { | |||||
| const { | |||||
| id, | |||||
| title, | |||||
| orderAmount | |||||
| } = this.orderData | |||||
| const obj = { | |||||
| id, | |||||
| title, | |||||
| orderAmount, | |||||
| } | |||||
| this.$refs.payPopup.open(obj) | |||||
| }, | |||||
| async onConfirmReceipt() { | |||||
| try { | |||||
| await this.$fetch('confirmOrder', { id: this.orderData.id }) | |||||
| uni.showToast({ | |||||
| icon: 'success', | |||||
| title: '确认收货成功', | |||||
| }); | |||||
| this.getData() | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| onComment() { | |||||
| this.$utils.navigateTo(`/pages_order/comment/commentWrite?orderId=${this.orderData.id}`) | |||||
| }, | |||||
| onApplyService() { | |||||
| console.log('orderData', this.orderData) | |||||
| const { | |||||
| id, | |||||
| appletOrderProductList, | |||||
| } = this.orderData | |||||
| const obj = { | |||||
| id, | |||||
| appletOrderProductList: appletOrderProductList.map(item => ({ ...item, statusDesc: this.statusDesc })), | |||||
| } | |||||
| this.$refs.serviceSelectPopup.open(obj) | |||||
| }, | |||||
| onDetectModify() { | |||||
| // todo | |||||
| }, | |||||
| onDetectSendBack() { | |||||
| // todo | |||||
| }, | |||||
| onDetectBook() { | |||||
| // todo | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background-color: $uni-bg-color; | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| padding: calc(var(--status-bar-height) + 144rpx) 32rpx 224rpx 32rpx; | |||||
| } | |||||
| .address { | |||||
| padding: 24rpx 32rpx; | |||||
| background: #FFFFFF; | |||||
| border-radius: 24rpx; | |||||
| justify-content: flex-start; | |||||
| } | |||||
| .card { | |||||
| margin-top: 40rpx; | |||||
| padding: 32rpx; | |||||
| background: #FAFAFF; | |||||
| border: 2rpx solid #FFFFFF; | |||||
| border-radius: 32rpx; | |||||
| &-top { | |||||
| margin-bottom: 32rpx; | |||||
| justify-content: space-between; | |||||
| .title { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #252545; | |||||
| } | |||||
| .status { | |||||
| display: inline-flex; | |||||
| min-width: 120rpx; | |||||
| padding: 6rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #252545; | |||||
| background: #F3F3F3; | |||||
| border-radius: 12rpx; | |||||
| &-0 { | |||||
| color: #FF860E; | |||||
| background: #FFF4E9; | |||||
| } | |||||
| &-1 { | |||||
| color: #2799E0; | |||||
| background: #EEF7FD; | |||||
| } | |||||
| &-2 { | |||||
| color: #7D27E0; | |||||
| background: #F5EEFD; | |||||
| } | |||||
| &-5 { | |||||
| color: #E53C29; | |||||
| background: #FDE7E5; | |||||
| } | |||||
| } | |||||
| } | |||||
| .row { | |||||
| justify-content: space-between; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 28rpx; | |||||
| line-height: 1.4; | |||||
| &-label { | |||||
| flex: none; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| &-content { | |||||
| color: #393939; | |||||
| } | |||||
| } | |||||
| &.detail { | |||||
| .product { | |||||
| margin-bottom: 32rpx; | |||||
| column-gap: 24rpx; | |||||
| .img { | |||||
| flex: none; | |||||
| width: 120rpx; | |||||
| height: 120rpx; | |||||
| } | |||||
| .info { | |||||
| flex: 1; | |||||
| padding: 24rpx 32rpx; | |||||
| background: #FFFFFF; | |||||
| border-radius: 32rpx; | |||||
| .row { | |||||
| margin-bottom: 16rpx; | |||||
| justify-content: flex-start; | |||||
| column-gap: 4rpx; | |||||
| } | |||||
| .price { | |||||
| justify-content: flex-start; | |||||
| column-gap: 8rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| line-height: 1.4; | |||||
| &-label { | |||||
| font-weight: 400; | |||||
| font-size: 26rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| &-unit { | |||||
| font-size: 24rpx; | |||||
| color: #7451DE; | |||||
| } | |||||
| &-value { | |||||
| font-size: 32rpx; | |||||
| color: #7451DE; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| .card-bottom { | |||||
| .price { | |||||
| column-gap: 8rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #7451DE; | |||||
| &-value { | |||||
| font-size: 32rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| &.info { | |||||
| .row + .row { | |||||
| margin-top: 32rpx; | |||||
| } | |||||
| } | |||||
| &.service { | |||||
| .step { | |||||
| &-header { | |||||
| justify-content: flex-start; | |||||
| column-gap: 24rpx; | |||||
| padding-left: 24rpx; | |||||
| } | |||||
| &-title { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 30rpx; | |||||
| line-height: 1.4; | |||||
| color: #000000; | |||||
| &.highlight { | |||||
| font-weight: 500; | |||||
| color: #10A934; | |||||
| } | |||||
| } | |||||
| &-time { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| &-desc { | |||||
| padding: 16rpx 0 16rpx 24rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #777777; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| .notice { | |||||
| margin-top: 40rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| &-header { | |||||
| font-size: 28rpx; | |||||
| line-height: 1.4; | |||||
| color: #393939; | |||||
| } | |||||
| &-content { | |||||
| margin-top: 24rpx; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #BABABA; | |||||
| } | |||||
| } | |||||
| .bottom { | |||||
| position: fixed; | |||||
| left: 0; | |||||
| bottom: 0; | |||||
| z-index: 2; | |||||
| width: 100vw; | |||||
| // height: 200rpx; | |||||
| padding: 24rpx 40rpx; | |||||
| padding-bottom: calc(env(safe-area-inset-bottom) + 24rpx); | |||||
| background: #FFFFFF; | |||||
| box-sizing: border-box; | |||||
| align-items: flex-start; | |||||
| .bar { | |||||
| width: 100%; | |||||
| column-gap: 32rpx; | |||||
| } | |||||
| .btn { | |||||
| background: transparent; | |||||
| border: none; | |||||
| &-service { | |||||
| flex: none; | |||||
| row-gap: 4rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 22rpx; | |||||
| line-height: 1.1; | |||||
| color: #999999; | |||||
| &-icon { | |||||
| width: 52rpx; | |||||
| height: auto; | |||||
| } | |||||
| } | |||||
| } | |||||
| .cols { | |||||
| flex: 1; | |||||
| column-gap: 32rpx; | |||||
| .col { | |||||
| flex: 1; | |||||
| } | |||||
| .btn { | |||||
| padding: 14rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #252545; | |||||
| border: 2rpx solid #252545; | |||||
| border-radius: 41rpx; | |||||
| &-primary { | |||||
| padding: 16rpx 0; | |||||
| color: #FFFFFF; | |||||
| background-image: linear-gradient(to right, #4B348F, #845CFA); | |||||
| border: none; | |||||
| } | |||||
| } | |||||
| .price { | |||||
| column-gap: 8rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| &-label { | |||||
| color: #626262; | |||||
| } | |||||
| &-unit, | |||||
| &-value { | |||||
| font-weight: 500; | |||||
| color: #7451DE; | |||||
| .highlight { | |||||
| font-size: 40rpx; | |||||
| } | |||||
| } | |||||
| &-value { | |||||
| font-size: 40rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,156 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar title="订单管理" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="#FFFFFF" /> | |||||
| <view class="main"> | |||||
| <view class="tabs"> | |||||
| <uv-tabs | |||||
| :list="tabs" | |||||
| :current="current" | |||||
| :scrollable="false" | |||||
| lineColor="#7451DE" | |||||
| lineWidth="48rpx" | |||||
| lineHeight="4rpx" | |||||
| :activeStyle="{ | |||||
| 'font-family': 'PingFang SC', | |||||
| 'font-weight': 500, | |||||
| 'font-size': '32rpx', | |||||
| 'line-height': 1.4, | |||||
| 'color': '#7451DE', | |||||
| }" | |||||
| :inactiveStyle="{ | |||||
| 'font-family': 'PingFang SC', | |||||
| 'font-weight': 400, | |||||
| 'font-size': '32rpx', | |||||
| 'line-height': 1.4, | |||||
| 'color': '#181818', | |||||
| }" | |||||
| @click="clickTabs" | |||||
| ></uv-tabs> | |||||
| </view> | |||||
| <view class="card" v-for="item in list" :key="item.id"> | |||||
| <orderCard | |||||
| :data="item" | |||||
| @pay="onPay(item)" | |||||
| @applyService="onApplyService" | |||||
| @statusChange="getData" | |||||
| ></orderCard> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import mixinsList from '@/mixins/list.js' | |||||
| import orderCard from './orderCard.vue' | |||||
| export default { | |||||
| mixins: [mixinsList], | |||||
| components: { | |||||
| orderCard, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| // 订单状态 0待支付 1待发货 2待收货 3待评价 4已完成 | |||||
| tabs: [ | |||||
| { name: '全部' }, | |||||
| { name: '待支付' }, | |||||
| { name: '待发货' }, | |||||
| { name: '待收货' }, | |||||
| { name: '待评价' }, | |||||
| ], | |||||
| mixinsListApi: 'getOrderList', | |||||
| current: 0, | |||||
| } | |||||
| }, | |||||
| onShow() { | |||||
| console.log('onShow') | |||||
| }, | |||||
| onLoad(arg) { | |||||
| this.clickTabs({ index: arg.index || 0 }) | |||||
| }, | |||||
| methods: { | |||||
| //点击tab栏 | |||||
| clickTabs({ index }) { | |||||
| console.log('clickTabs') | |||||
| this.current = index | |||||
| if (index == 0) { | |||||
| delete this.queryParams.status | |||||
| } else { | |||||
| this.queryParams.status = index - 1 | |||||
| } | |||||
| this.getData() | |||||
| }, | |||||
| onPay(data) { | |||||
| const { | |||||
| id, | |||||
| title, | |||||
| orderAmount | |||||
| } = data | |||||
| const obj = { | |||||
| id, | |||||
| title, | |||||
| orderAmount, | |||||
| } | |||||
| this.$refs.payPopup.open(obj) | |||||
| }, | |||||
| onApplyService(obj) { | |||||
| this.$refs.serviceSelectPopup.open(obj) | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background-color: $uni-bg-color; | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| padding: calc(var(--status-bar-height) + 244rpx) 32rpx 40rpx 32rpx; | |||||
| .tabs { | |||||
| position: fixed; | |||||
| top: calc(var(--status-bar-height) + 120rpx); | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 84rpx; | |||||
| background: #FFFFFF; | |||||
| z-index: 1; | |||||
| /deep/ .uv-tabs__wrapper__nav__line { | |||||
| border-radius: 2rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| .card { | |||||
| & + & { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,320 @@ | |||||
| <template> | |||||
| <view class="card" @click="jumpToOrderDetail"> | |||||
| <view class="flex top"> | |||||
| <view class="title">{{ data.title }}</view> | |||||
| <view :class="['flex', 'status', `status-${status}`]">{{ statusDesc }}</view> | |||||
| </view> | |||||
| <view class="flex main"> | |||||
| <image class="img" :src="coverImg" mode="scaleToFill"></image> | |||||
| <view class="info"> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">客户姓名:</view> | |||||
| <view class="row-content">{{ data.customerName }}</view> | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">下单时间:</view> | |||||
| <view class="row-content">{{ data.orderDate }}</view> | |||||
| </view> | |||||
| <view class="flex row"> | |||||
| <view class="row-label">联系电话:</view> | |||||
| <view class="row-content">{{ data.customerPhone }}</view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex bottom"> | |||||
| <view class="flex price"> | |||||
| <text class="price-label">总价格:</text> | |||||
| <text class="price-unit">¥</text><text class="price-value">{{ data.orderAmount }}</text> | |||||
| </view> | |||||
| <view class="flex btns"> | |||||
| <!-- 待支付 --> | |||||
| <template v-if="status == 0"> | |||||
| <button class="btn" @click.stop="onPay">立即支付</button> | |||||
| </template> | |||||
| <!-- 待发货 --> | |||||
| <template v-else-if="status == 1"> | |||||
| <button class="btn" @click.stop="onApplyService">申请售后</button> | |||||
| <!-- 自采检测 | |||||
| <template v-if="detectProduct && detectProduct.subscribeType == 0"> | |||||
| <button class="btn" @click="onDetectModify">修改</button> | |||||
| </template> --> | |||||
| </template> | |||||
| <!-- 待收货 --> | |||||
| <template v-else-if="status == 2"> | |||||
| <button class="btn" @click.stop="onApplyService">申请售后</button> | |||||
| <button class="btn" @click.stop="onConfirmReceipt">确认收货</button> | |||||
| <!-- 检测 subscribeType: 0自采,1上门,2到店,3已取消 | |||||
| <template v-if="detectProduct"> | |||||
| <template v-if="detectProduct.subscribeType == 0"> | |||||
| <button class="btn" @click="onDetectSendBack">线上回寄试剂盒</button> | |||||
| </template> | |||||
| <template v-else> | |||||
| <button class="btn" @click="onDetectBook">检测预约</button> | |||||
| </template> | |||||
| </template> | |||||
| 其他商品 | |||||
| <template v-else> | |||||
| <button class="btn" @click.stop="onConfirmReceipt">确认收货</button> | |||||
| </template> --> | |||||
| </template> | |||||
| <!-- 待评价 --> | |||||
| <template v-else-if="status == 3"> | |||||
| <button class="btn" @click.stop="onApplyService">申请售后</button> | |||||
| <button class="btn" @click.stop="onComment">立即评价</button> | |||||
| </template> | |||||
| <!-- 已完成 --> | |||||
| <template v-else-if="status == 4"> | |||||
| <button class="btn" @click.stop="onApplyService">申请售后</button> | |||||
| </template> | |||||
| <!-- 售后 --> | |||||
| <template v-else-if="status == 5"> | |||||
| </template> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| // 订单状态 0待支付 1待发货 2待收货 3待评价 4已完成 | |||||
| const STATUS_AND_DESC_MAPPING = { | |||||
| 0: '待支付', | |||||
| 1: '待发货', | |||||
| 2: '待收货', | |||||
| 3: '待评价', | |||||
| 4: '已完成', | |||||
| 5: '售后', | |||||
| } | |||||
| export default { | |||||
| props: { | |||||
| data: { | |||||
| type: Object, | |||||
| default() { | |||||
| return {} | |||||
| } | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| status() { | |||||
| const { orderStatus, afterSales } = this.data || {} | |||||
| if (afterSales) { | |||||
| return 5 | |||||
| } | |||||
| return orderStatus | |||||
| }, | |||||
| statusDesc() { | |||||
| return STATUS_AND_DESC_MAPPING[this.status] | |||||
| }, | |||||
| coverImg() { | |||||
| const { appletOrderProductList } = this.data || {} | |||||
| let arr = appletOrderProductList?.[0]?.image?.split?.(',') | |||||
| return arr?.[0] | |||||
| }, | |||||
| // detectProduct() { | |||||
| // const { appletOrderProductList } = this.data || {} | |||||
| // if (appletOrderProductList?.length == 1 && appletOrderProductList?.[0]?.type == 1) { // type: 产品类型(0营养剂,1预约,2课程) | |||||
| // return appletOrderProductList[0] | |||||
| // } | |||||
| // return null | |||||
| // }, | |||||
| }, | |||||
| methods: { | |||||
| onPay() { | |||||
| this.$emit('pay') | |||||
| }, | |||||
| async onConfirmReceipt() { | |||||
| try { | |||||
| await this.$fetch('confirmOrder', { id: this.data.id }) | |||||
| uni.showToast({ | |||||
| icon: 'success', | |||||
| title: '确认收货成功', | |||||
| }); | |||||
| this.$emit('statusChange') | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| onComment() { | |||||
| this.$utils.navigateTo(`/pages_order/comment/commentWrite?orderId=${this.data.id}`) | |||||
| }, | |||||
| onApplyService() { | |||||
| const { | |||||
| id, | |||||
| appletOrderProductList, | |||||
| } = this.data | |||||
| const obj = { | |||||
| id, | |||||
| appletOrderProductList: appletOrderProductList.map(item => ({ ...item, statusDesc: this.statusDesc })), | |||||
| } | |||||
| this.$emit('applyService', obj) | |||||
| }, | |||||
| onDetectModify() { | |||||
| // todo | |||||
| }, | |||||
| onDetectSendBack() { | |||||
| // todo | |||||
| }, | |||||
| onDetectBook() { | |||||
| // todo | |||||
| }, | |||||
| jumpToOrderDetail() { | |||||
| this.$utils.navigateTo(`/pages_order/order/orderDetail/index?id=${this.data.id}`) | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .card { | |||||
| width: 100%; | |||||
| padding: 32rpx; | |||||
| box-sizing: border-box; | |||||
| background: #FAFAFF; | |||||
| border: 2rpx solid #FFFFFF; | |||||
| } | |||||
| .top { | |||||
| justify-content: space-between; | |||||
| .title { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #252545; | |||||
| } | |||||
| .status { | |||||
| display: inline-flex; | |||||
| min-width: 120rpx; | |||||
| padding: 6rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #252545; | |||||
| background: #F3F3F3; | |||||
| border-radius: 12rpx; | |||||
| &-0 { | |||||
| color: #FF860E; | |||||
| background: #FFF4E9; | |||||
| } | |||||
| &-1 { | |||||
| color: #2799E0; | |||||
| background: #EEF7FD; | |||||
| } | |||||
| &-2 { | |||||
| color: #7D27E0; | |||||
| background: #F5EEFD; | |||||
| } | |||||
| &-5 { | |||||
| color: #E53C29; | |||||
| background: #FDE7E5; | |||||
| } | |||||
| } | |||||
| } | |||||
| .main { | |||||
| margin: 24rpx 0; | |||||
| column-gap: 24rpx; | |||||
| .img { | |||||
| flex: none; | |||||
| width: 120rpx; | |||||
| height: 120rpx; | |||||
| } | |||||
| .info { | |||||
| flex: 1; | |||||
| padding: 24rpx; | |||||
| background: #FFFFFF; | |||||
| border-radius: 32rpx; | |||||
| } | |||||
| .row { | |||||
| justify-content: flex-start; | |||||
| column-gap: 4rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 28rpx; | |||||
| line-height: 1.4; | |||||
| &-label { | |||||
| color: #8B8B8B; | |||||
| } | |||||
| &-content { | |||||
| color: #393939; | |||||
| } | |||||
| } | |||||
| .row + .row { | |||||
| margin-top: 16rpx; | |||||
| } | |||||
| } | |||||
| .bottom { | |||||
| justify-content: space-between; | |||||
| .price { | |||||
| column-gap: 8rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| line-height: 1.4; | |||||
| &-label { | |||||
| font-weight: 400; | |||||
| font-size: 26rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| &-unit { | |||||
| font-size: 24rpx; | |||||
| color: #7451DE; | |||||
| } | |||||
| &-value { | |||||
| font-size: 32rpx; | |||||
| color: #7451DE; | |||||
| } | |||||
| } | |||||
| .btns { | |||||
| flex: 1; | |||||
| justify-content: flex-end; | |||||
| column-gap: 16rpx; | |||||
| } | |||||
| .btn { | |||||
| padding: 10rpx 22rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 28rpx; | |||||
| line-height: 1.4; | |||||
| color: #393939; | |||||
| border: 2rpx solid #252545; | |||||
| border-radius: 32rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,115 @@ | |||||
| <template> | |||||
| <view class="list"> | |||||
| <view class="list-item" v-for="item in list" :key="item.id"> | |||||
| <view class="flex user"> | |||||
| <view class="avatar"> | |||||
| <image class="img" :src="item.avatar" mode="scaleToFill"></image> | |||||
| </view> | |||||
| <view class="name">{{ item.name }}</view> | |||||
| <view class="time">{{ item.createTime }}</view> | |||||
| </view> | |||||
| <view class="flex content"> | |||||
| <view class="content-text">{{ item.content }}</view> | |||||
| <view class="content-img"> | |||||
| <image class="img" v-if="getCoverImg(item)" :src="getCoverImg(item)" mode="aspectFill"></image> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| export default { | |||||
| props: { | |||||
| list: { | |||||
| type: Array, | |||||
| default() { | |||||
| return [] | |||||
| } | |||||
| } | |||||
| }, | |||||
| methods: { | |||||
| getCoverImg(obj) { | |||||
| const { image } = obj | |||||
| return image?.split?.(',')?.[0] | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .list { | |||||
| padding: 32rpx; | |||||
| background: #FAFAFA; | |||||
| border: 2rpx solid #FFFFFF; | |||||
| border-radius: 32rpx; | |||||
| &-item { | |||||
| & + & { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| .user { | |||||
| justify-content: flex-start; | |||||
| .avatar { | |||||
| width: 48rpx; | |||||
| height: 48rpx; | |||||
| border: 4rpx solid #FFFFFF; | |||||
| border-radius: 50%; | |||||
| overflow: hidden; | |||||
| .img { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| .name { | |||||
| margin-left: 8rpx; | |||||
| font-size: 36rpx; | |||||
| font-weight: 600; | |||||
| line-height: 1; | |||||
| color: #252545; | |||||
| } | |||||
| .time { | |||||
| margin-left: 16rpx; | |||||
| font-size: 24rpx; | |||||
| line-height: 1; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| } | |||||
| .content { | |||||
| margin-top: 24rpx; | |||||
| &-text { | |||||
| flex: 1; | |||||
| font-size: 32rpx; | |||||
| color: #181818; | |||||
| overflow: hidden; | |||||
| text-overflow: ellipsis; | |||||
| display:-webkit-box; //作为弹性伸缩盒子模型显示。 | |||||
| -webkit-box-orient:vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列 | |||||
| -webkit-line-clamp:4; //显示的行 | |||||
| } | |||||
| &-img { | |||||
| flex: none; | |||||
| width: 190rpx; | |||||
| height: 190rpx; | |||||
| border-radius: 16rpx; | |||||
| overflow: hidden; | |||||
| .img { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,507 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar title="活动详情" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="transparent" /> | |||||
| <view class="main"> | |||||
| <view class="swiper"> | |||||
| <uv-swiper | |||||
| :list="bannerList" | |||||
| keyName="image" | |||||
| indicator | |||||
| indicatorMode="dot" | |||||
| indicatorInactiveColor="rgba(255, 255, 255, 0.7)" | |||||
| height="680rpx" | |||||
| ></uv-swiper> | |||||
| </view> | |||||
| <view class="summary"> | |||||
| <view class="card info"> | |||||
| <view class="card-header">{{ detail.name }}</view> | |||||
| <view class="card-content"> | |||||
| <view class="desc">{{ detail.desc }}</view> | |||||
| <view class="flex tags" v-if="detail.tags"> | |||||
| <view class="tag" v-for="(tag, tIdx) in detail.tags" :key="tIdx"> | |||||
| {{ tag }} | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex data"> | |||||
| <view class="flex price"> | |||||
| <view class="price-val"> | |||||
| <text>¥</text> | |||||
| <text class="highlight">{{ priceInt }}</text> | |||||
| <text>{{ `${priceFrac}起` }}</text> | |||||
| </view> | |||||
| <view class="price-bef" v-if="detail.originalPrice">¥<text>{{ detail.originalPrice }}</text></view> | |||||
| </view> | |||||
| <view class="registered" v-if="detail.registered"> | |||||
| {{ `${detail.registered}人已报名` }} | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="card"> | |||||
| <view class="card-header">选择团期</view> | |||||
| <view class="card-content"> | |||||
| <timeOptionsSelect v-model="selectTime" :options="detail.timeOptions"></timeOptionsSelect> | |||||
| </view> | |||||
| </view> | |||||
| <view class="card comment"> | |||||
| <view class="flex card-header"> | |||||
| <view>评论</view> | |||||
| <button class="flex btn" @click="jumpToCommentRecords"> | |||||
| <view>查看全部</view> | |||||
| <image class="img" src="@/static/image/icon-arrow-right.png" mode="widthFix"></image> | |||||
| </button> | |||||
| </view> | |||||
| <view class="card-content"> | |||||
| <commentList :list="commentList"></commentList> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| <!-- <uv-sticky bgColor="#F3F3F3"> --> | |||||
| <view class="tabs"> | |||||
| <uv-tabs | |||||
| :list="tabs" | |||||
| :scrollable="false" | |||||
| lineColor="#00A9FF" | |||||
| lineWidth="48rpx" | |||||
| lineHeight="4rpx" | |||||
| :activeStyle="{ | |||||
| 'font-family': 'PingFang SC', | |||||
| 'font-weight': 500, | |||||
| 'font-size': '32rpx', | |||||
| 'line-height': 1.4, | |||||
| 'color': '#00A9FF', | |||||
| }" | |||||
| :inactiveStyle="{ | |||||
| 'font-family': 'PingFang SC', | |||||
| 'font-weight': 400, | |||||
| 'font-size': '32rpx', | |||||
| 'line-height': 1.4, | |||||
| 'color': '#191919', | |||||
| }" | |||||
| @click="clickTabs" | |||||
| ></uv-tabs> | |||||
| </view> | |||||
| <!-- </uv-sticky> --> | |||||
| <view class="detail" v-if="displayContent"> | |||||
| <uv-parse :content="displayContent"></uv-parse> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex bottom"> | |||||
| <button plain class="flex flex-column btn btn-simple" open-type="contact"> | |||||
| <image class="icon" src="@/pages_order/static/product/icon-service.png" mode="widthFix"></image> | |||||
| <view>客服</view> | |||||
| </button> | |||||
| <view class="flex operate"> | |||||
| <button class="flex btn btn-palin" @click="onCollect">收藏</button> | |||||
| <button class="flex btn btn-primary" @click="onBuy">立即购买</button> | |||||
| </view> | |||||
| </view> | |||||
| <orderInfoPopup ref="orderInfoPopup" :data="detail" @timeChange="selectTime = $event"></orderInfoPopup> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import timeOptionsSelect from '@/pages_order/order/orderConfirm/timeOptionsSelect.vue' | |||||
| import commentList from './commentList.vue' | |||||
| import orderInfoPopup from '@/pages_order/order/orderConfirm/infoPopup.vue' | |||||
| export default { | |||||
| components: { | |||||
| timeOptionsSelect, | |||||
| commentList, | |||||
| orderInfoPopup, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| id: null, | |||||
| detail: {}, | |||||
| next: 'createOrder', // createOrder | addCart | |||||
| commentList: [], | |||||
| tabs: [ | |||||
| { name: '行程亮点' }, | |||||
| { name: '课程目标' }, | |||||
| { name: '详细行程' }, | |||||
| ], | |||||
| current: 0, | |||||
| selectTime: null, | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| bannerList() { | |||||
| const { image } = this.detail | |||||
| if (!image) { | |||||
| return [] | |||||
| } | |||||
| return Array.isArray(image) ? image : image.split(',') | |||||
| }, | |||||
| priceInt() { | |||||
| return parseInt(this.detail.currentPrice) | |||||
| }, | |||||
| priceFrac() { | |||||
| return (this.detail.currentPrice % this.priceInt).toFixed(2).slice(1) | |||||
| }, | |||||
| displayContent() { | |||||
| const { | |||||
| itineraryHighlights, | |||||
| courseObjectives, | |||||
| itineraryDetail, | |||||
| } = this.detail | |||||
| if (this.current == 0) { | |||||
| return itineraryHighlights | |||||
| } else if (this.current == 1) { | |||||
| return courseObjectives | |||||
| } else if (this.current == 2) { | |||||
| return itineraryDetail | |||||
| } | |||||
| return '' | |||||
| } | |||||
| }, | |||||
| onLoad(arg) { | |||||
| const { id } = arg | |||||
| this.id = id | |||||
| this.fetchDetail(id) | |||||
| this.fetchComment(id) | |||||
| }, | |||||
| methods: { | |||||
| async fetchDetail(id) { | |||||
| this.detail = { | |||||
| id: '001', | |||||
| image: new Array(6).fill('/static/image/temp-20.png').join(','), | |||||
| name: '新疆天山行7/9日丨醉美伊犁&吐鲁番双套餐', | |||||
| desc: '每天车程4小时内,含一程高铁丨喀拉峻草原、夏塔古道、昭苏天马、赛里木湖、昭苏油菜花、伊犁薰衣草丨吐鲁番坎儿井&火焰山', | |||||
| tags: ['坝上草原', '自然探索', '户外探索', '亲子游玩'], | |||||
| currentPrice: 688.99, | |||||
| originalPrice: 1200, | |||||
| registered: 4168, | |||||
| timeOptions: [ | |||||
| { | |||||
| id: '0011', | |||||
| startDate: '08/25', | |||||
| endDate: '09/01', | |||||
| currentPrice: 1200.99, | |||||
| originalPrice: 2300, | |||||
| adultsPrice: 2400, | |||||
| teenagerPrice: 1800, | |||||
| childPrice: 1200.99, | |||||
| }, | |||||
| { | |||||
| id: '0012', | |||||
| startDate: '09/02', | |||||
| endDate: '09/11', | |||||
| currentPrice: 1200.99, | |||||
| originalPrice: 2300, | |||||
| adultsPrice: 2400, | |||||
| teenagerPrice: 1800, | |||||
| childPrice: 1200.99, | |||||
| }, | |||||
| { | |||||
| id: '0013', | |||||
| startDate: '09/12', | |||||
| endDate: '09/19', | |||||
| currentPrice: 1200.99, | |||||
| originalPrice: 2300, | |||||
| adultsPrice: 2400, | |||||
| teenagerPrice: 1800, | |||||
| childPrice: 1200.99, | |||||
| }, | |||||
| ], | |||||
| itineraryHighlights: ` | |||||
| <p> | |||||
| <img style="width: 100%;" src="/static/image/temp-31.png" mode="widthFix"/> | |||||
| <img style="width: 100%;" src="/static/image/temp-32.png" mode="widthFix"/> | |||||
| <img style="width: 100%;" src="/static/image/temp-33.png" mode="widthFix"/> | |||||
| <img style="width: 100%;" src="/static/image/temp-34.png" mode="widthFix"/> | |||||
| <img style="width: 100%;" src="/static/image/temp-35.png" mode="widthFix"/> | |||||
| </p> | |||||
| `, | |||||
| courseObjectives: ` | |||||
| <p style="font-size: 36rpx;"> | |||||
| 课程目标 | |||||
| <p> | |||||
| `, | |||||
| itineraryDetail: ` | |||||
| <p style="font-size: 36rpx;"> | |||||
| 详细行程 | |||||
| <p> | |||||
| `, | |||||
| } | |||||
| return | |||||
| try { | |||||
| const result = await this.$fetch('getProductDetail', { id }) | |||||
| const { specs } = result | |||||
| let arr = specs | |||||
| arr?.sort?.((a, b) => a.sortOrder - b.sortOrder) | |||||
| const spec = arr?.[0] | |||||
| this.detail = { | |||||
| ...result, | |||||
| specId: spec?.id || null, | |||||
| specName: spec?.specName || null, | |||||
| } | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| async fetchComment(id) { | |||||
| this.commentList = [ | |||||
| { | |||||
| avatar: '/static/image/temp-30.png', | |||||
| name: '战斗世界', | |||||
| createTime: '2025-07-12', | |||||
| content: '凌玉姐姐很温柔很耐心很负责我很喜欢她龙哥知识渊博很幽默给我们讲解很多内容行程很有趣我学到了很多东西最难忘的就是库木塔格沙漠我们爬到了很高的顶端看夕阳绝美还有我也很喜欢夏塔古道我们爬到了第四个卡拉房子的最远端看到了壮观的雪山下次还想参加活动去南疆', | |||||
| image: '/static/image/temp-36.png', | |||||
| }, | |||||
| { | |||||
| avatar: '/static/image/temp-30.png', | |||||
| name: '战斗世界', | |||||
| createTime: '2025-07-12', | |||||
| content: '凌玉姐姐很温柔很耐心很负责我很喜欢她龙哥知识渊博很幽默给我们讲解很多内容行程很有趣我学到了很多东西最难忘的就是库木塔格沙漠我们爬到了很高的顶端看夕阳绝美还有我也很喜欢夏塔古道我们爬到了第四个卡拉房子的最远端看到了壮观的雪山下次还想参加活动去南疆', | |||||
| image: '/static/image/temp-36.png', | |||||
| }, | |||||
| ] | |||||
| // todo: fetch | |||||
| }, | |||||
| onCollect() { | |||||
| this.$store.dispatch('collect', this.id) | |||||
| }, | |||||
| onBuy() { | |||||
| this.$refs.orderInfoPopup.open({ selectTime: this.selectTime }) | |||||
| }, | |||||
| jumpToCommentRecords() { | |||||
| // todo | |||||
| return | |||||
| this.$utils.navigateTo(`/pages_order/comment/commentRecordsOfProduct?productId=${this.id}`) | |||||
| }, | |||||
| //点击tab栏 | |||||
| clickTabs({ index }) { | |||||
| this.current = index | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background: linear-gradient(#DAF3FF, #F3F3F3 500rpx, #F3F3F3); | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| width: 100vw; | |||||
| padding: calc(var(--status-bar-height) + 120rpx) 0 198rpx 0; | |||||
| box-sizing: border-box; | |||||
| } | |||||
| .swiper { | |||||
| /deep/ .uv-swiper-indicator__wrapper__dot, | |||||
| /deep/ .uv-swiper-indicator__wrapper__dot--active { | |||||
| width: 30rpx; | |||||
| } | |||||
| /deep/ .uv-swiper-indicator__wrapper__dot--active { | |||||
| background: linear-gradient(to right, #21FEEC, #019AF9); | |||||
| } | |||||
| } | |||||
| .summary { | |||||
| width: 100%; | |||||
| padding: 40rpx 32rpx; | |||||
| box-sizing: border-box; | |||||
| } | |||||
| .card { | |||||
| width: 100%; | |||||
| padding: 32rpx; | |||||
| box-sizing: border-box; | |||||
| background: #FFFFFF; | |||||
| border-radius: 24rpx; | |||||
| & + & { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| &-header { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 32rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| } | |||||
| &-content { | |||||
| margin-top: 16rpx; | |||||
| } | |||||
| &.info { | |||||
| .desc { | |||||
| font-size: 26rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| .tags { | |||||
| margin-top: 16rpx; | |||||
| justify-content: flex-start; | |||||
| flex-wrap: wrap; | |||||
| gap: 16rpx; | |||||
| .tag { | |||||
| padding: 2rpx 14rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #00A9FF; | |||||
| background: #E9F8FF; | |||||
| border: 2rpx solid #00A9FF; | |||||
| border-radius: 8rpx; | |||||
| } | |||||
| } | |||||
| .data { | |||||
| margin-top: 16rpx; | |||||
| justify-content: space-between; | |||||
| } | |||||
| .price { | |||||
| justify-content: flex-start; | |||||
| align-items: baseline; | |||||
| column-gap: 6rpx; | |||||
| &-val { | |||||
| font-size: 24rpx; | |||||
| font-weight: 500; | |||||
| color: #FF4800; | |||||
| .highlight { | |||||
| font-size: 48rpx; | |||||
| } | |||||
| } | |||||
| &-bef { | |||||
| text-decoration: line-through; | |||||
| font-size: 24rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| } | |||||
| .registered { | |||||
| padding: 2rpx 10rpx; | |||||
| font-weight: 500; | |||||
| font-size: 30rpx; | |||||
| color: #FF4800; | |||||
| border: 2rpx solid #FF4800; | |||||
| border-radius: 4rpx; | |||||
| } | |||||
| } | |||||
| &.comment { | |||||
| .card-header { | |||||
| justify-content: space-between; | |||||
| } | |||||
| .btn { | |||||
| column-gap: 4rpx; | |||||
| font-size: 24rpx; | |||||
| color: #8B8B8B; | |||||
| .img { | |||||
| width: 32rpx; | |||||
| height: auto; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| .detail { | |||||
| font-size: 0; | |||||
| } | |||||
| .bottom { | |||||
| position: fixed; | |||||
| left: 0; | |||||
| bottom: 0; | |||||
| z-index: 999; | |||||
| justify-content: space-between; | |||||
| column-gap: 16rpx; | |||||
| width: 100vw; | |||||
| // height: 198rpx; | |||||
| padding: 24rpx 40rpx 0 40rpx; | |||||
| padding-bottom: calc(env(safe-area-inset-bottom) + 24rpx); | |||||
| background: #FFFFFF; | |||||
| box-sizing: border-box; | |||||
| .btn-simple { | |||||
| border: none; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 22rpx; | |||||
| line-height: 1.1; | |||||
| color: #999999; | |||||
| .icon { | |||||
| width: 52rpx; | |||||
| height: auto; | |||||
| margin-bottom: 4rpx; | |||||
| } | |||||
| } | |||||
| .operate { | |||||
| justify-content: flex-end; | |||||
| column-gap: 16rpx; | |||||
| .btn { | |||||
| font-size: 36rpx; | |||||
| font-weight: 500; | |||||
| border-radius: 41rpx; | |||||
| line-height: 1.4; | |||||
| &-palin { | |||||
| padding: 14rpx 46rpx; | |||||
| color: #252545; | |||||
| border: 2rpx solid #252545; | |||||
| } | |||||
| &-primary { | |||||
| padding: 14rpx 62rpx; | |||||
| color: #FFFFFF; | |||||
| background: linear-gradient(to right, #21FEEC, #019AF9); | |||||
| border: 2rpx solid #00A9FF; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,217 @@ | |||||
| <template> | |||||
| <view class="card"> | |||||
| <view class="flex top"> | |||||
| <view> | |||||
| <uv-checkbox-group | |||||
| v-model="selectCheckboxValue" | |||||
| @change="onSelectChange" | |||||
| > | |||||
| <uv-checkbox | |||||
| size="36rpx" | |||||
| icon-size="36rpx" | |||||
| activeColor="#00A9FF" | |||||
| :name="1" | |||||
| ></uv-checkbox> | |||||
| </uv-checkbox-group> | |||||
| </view> | |||||
| <view class="info"> | |||||
| <view class="flex name"> | |||||
| <view>{{ data.name }}</view> | |||||
| <view :class="['tag', `tag-${data.type}`]">{{ typeDesc }}</view> | |||||
| </view> | |||||
| <view class="id">{{ data.idNo }}</view> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex bottom"> | |||||
| <view class="flex col"> | |||||
| <view> | |||||
| <uv-checkbox-group | |||||
| v-model="defaultCheckboxValue" | |||||
| @change="onDefaultChange" | |||||
| > | |||||
| <uv-checkbox | |||||
| size="36rpx" | |||||
| icon-size="36rpx" | |||||
| activeColor="#00A9FF" | |||||
| :name="1" | |||||
| ></uv-checkbox> | |||||
| </uv-checkbox-group> | |||||
| </view> | |||||
| <view>默认地址</view> | |||||
| </view> | |||||
| <button class="flex col btn" @click="onEdit"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-edit.png" mode="scaleToFill"></image> | |||||
| <view>编辑</view> | |||||
| </button> | |||||
| <button class="flex col btn" @click="onDelete"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-delete.png" mode="scaleToFill"></image> | |||||
| <view>删除</view> | |||||
| </button> | |||||
| </view> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| const MEMBER_TYPE_AND_DESC_MAPPING = { | |||||
| 0: '成人', | |||||
| 1: '青少年', | |||||
| 2: '儿童', | |||||
| } | |||||
| export default { | |||||
| props: { | |||||
| data: { | |||||
| type: Object, | |||||
| default() { | |||||
| return {} | |||||
| } | |||||
| }, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| selectCheckboxValue: [], | |||||
| defaultCheckboxValue: [], | |||||
| } | |||||
| }, | |||||
| computed: { | |||||
| isSelected: { | |||||
| set(val) { | |||||
| this.selectCheckboxValue = val ? [1]: [] | |||||
| if (this.data.isSelected == val) { | |||||
| return | |||||
| } | |||||
| this.$emit('selectChange', val) | |||||
| }, | |||||
| get() { | |||||
| return this.selectCheckboxValue.length ? true : false | |||||
| } | |||||
| }, | |||||
| isDefault: { | |||||
| set(val) { | |||||
| this.defaultCheckboxValue = val ? [1]: [] | |||||
| if (this.data.isDefault == val) { | |||||
| return | |||||
| } | |||||
| this.$emit('defaultChange', val) | |||||
| }, | |||||
| get() { | |||||
| return this.defaultCheckboxValue.length ? true : false | |||||
| } | |||||
| }, | |||||
| typeDesc() { | |||||
| return MEMBER_TYPE_AND_DESC_MAPPING[this.data.type] | |||||
| }, | |||||
| }, | |||||
| watch: { | |||||
| data: { | |||||
| handler(val) { | |||||
| this.isSelected = val.isSelected | |||||
| this.isDefault = val.isDefault | |||||
| }, | |||||
| immediate: true, | |||||
| deep: true, | |||||
| } | |||||
| }, | |||||
| methods: { | |||||
| onSelectChange(val) { | |||||
| this.isSelected = val.length ? true : false | |||||
| }, | |||||
| onDefaultChange(val) { | |||||
| this.isDefault = val.length ? true : false | |||||
| }, | |||||
| onEdit() { | |||||
| // todo | |||||
| this.$emit('edit') | |||||
| }, | |||||
| onDelete() { | |||||
| uni.showModal({ | |||||
| title: '确认删除?', | |||||
| success : e => { | |||||
| if(e.confirm){ | |||||
| // todo | |||||
| this.$emit('delete') | |||||
| } | |||||
| } | |||||
| }) | |||||
| }, | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .card { | |||||
| padding: 24rpx 32rpx; | |||||
| background: #FFFFFF; | |||||
| border-radius: 24rpx; | |||||
| .top { | |||||
| padding-bottom: 24rpx; | |||||
| justify-content: flex-start; | |||||
| column-gap: 24rpx; | |||||
| .info { | |||||
| .name { | |||||
| justify-content: flex-start; | |||||
| column-gap: 24rpx; | |||||
| font-size: 32rpx; | |||||
| color: #181818; | |||||
| .tag { | |||||
| padding: 2rpx 14rpx; | |||||
| font-size: 24rpx; | |||||
| color: #00A9FF; | |||||
| background: #E9F8FF; | |||||
| border: 2rpx solid #00A9FF; | |||||
| border-radius: 8rpx; | |||||
| &-1 { | |||||
| color: #03C25C; | |||||
| background: #E9FFF5; | |||||
| border-color: #03C25C; | |||||
| } | |||||
| &-2 { | |||||
| color: #EE6E05; | |||||
| background: #FFEFE9; | |||||
| border-color: #EE6E05; | |||||
| } | |||||
| } | |||||
| } | |||||
| .id { | |||||
| margin-top: 8rpx; | |||||
| font-size: 28rpx; | |||||
| color: #9B9B9B; | |||||
| } | |||||
| } | |||||
| } | |||||
| .bottom { | |||||
| padding-top: 24rpx; | |||||
| column-gap: 24rpx; | |||||
| border-top: 2rpx dashed #DADADA; | |||||
| .col { | |||||
| flex: 1; | |||||
| column-gap: 8rpx; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 24rpx; | |||||
| line-height: 1.4; | |||||
| color: #9B9B9B; | |||||
| .icon { | |||||
| width: 36rpx; | |||||
| height: 36rpx; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,235 @@ | |||||
| <template> | |||||
| <view class="page__view"> | |||||
| <navbar title="选择出行人" leftClick @leftClick="$utils.navigateBack" color="#191919" bgColor="#FFFFFF" /> | |||||
| <view class="main"> | |||||
| <view class="card" v-for="item in list" :key="item.id"> | |||||
| <travelerCard | |||||
| :data="item" | |||||
| @defaultChange="onDefaultChange(item.id, $event)" | |||||
| @selectChange="onSelectChange(item.id, $event)" | |||||
| @edit="onEdit(item.id)" | |||||
| @delete="onDelete(item.id)" | |||||
| ></travelerCard> | |||||
| </view> | |||||
| </view> | |||||
| <view class="flex bottom"> | |||||
| <button class="btn" @click="onAdd">添加出行人</button> | |||||
| </view> | |||||
| <travelerPopup ref="travelerPopup" @submitted="getData"></travelerPopup> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import mixinsList from '@/mixins/list.js' | |||||
| import travelerCard from './travelerCard.vue' | |||||
| import travelerPopup from './travelerPopup.vue' | |||||
| export default { | |||||
| mixins: [mixinsList], | |||||
| components: { | |||||
| travelerCard, | |||||
| travelerPopup, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| // todo: check key | |||||
| mixinsListApi: '', | |||||
| queryParams: { | |||||
| pageNo: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| selectedIdList: [], | |||||
| } | |||||
| }, | |||||
| onLoad(arg) { | |||||
| const { selectIds } = arg | |||||
| this.selectedIdList = selectIds?.split?.(',') || [] | |||||
| this.getData() | |||||
| }, | |||||
| onUnload() { | |||||
| const list = this.selectedIdList.map(id => this.list.find(item => item.id === id)) | |||||
| this.$store.commit('setTravelerList', list) | |||||
| }, | |||||
| methods: { | |||||
| updateSelectIdList() { | |||||
| this.selectedIdList = this.list.filter(item => item.isSelected).map(item => item.id) | |||||
| }, | |||||
| // todo: delete | |||||
| getData() { | |||||
| let records = [ | |||||
| { | |||||
| id: '001', | |||||
| name: '李梓发', | |||||
| idNo: '430223********9999', | |||||
| type: 0, | |||||
| isSelected: false, | |||||
| isDefault: false, | |||||
| }, | |||||
| { | |||||
| id: '002', | |||||
| name: '吴彦谦', | |||||
| idNo: '430223********9999', | |||||
| type: 0, | |||||
| isSelected: false, | |||||
| isDefault: false, | |||||
| }, | |||||
| { | |||||
| id: '003', | |||||
| name: '冯云', | |||||
| idNo: '430223********9999', | |||||
| type: 1, | |||||
| isSelected: false, | |||||
| isDefault: false, | |||||
| }, | |||||
| { | |||||
| id: '004', | |||||
| name: '冯思钗', | |||||
| idNo: '430223********9999', | |||||
| type: 2, | |||||
| isSelected: false, | |||||
| isDefault: false, | |||||
| }, | |||||
| { | |||||
| id: '005', | |||||
| name: '李书萍', | |||||
| idNo: '430223********9999', | |||||
| type: 0, | |||||
| isSelected: false, | |||||
| isDefault: false, | |||||
| }, | |||||
| { | |||||
| id: '006', | |||||
| name: '冯艺莲', | |||||
| idNo: '430223********9999', | |||||
| type: 1, | |||||
| isSelected: false, | |||||
| isDefault: false, | |||||
| }, | |||||
| ] | |||||
| this.selectedIdList.forEach(id => { | |||||
| const target = records.find(item => item.id === id) | |||||
| target.isSelected = true | |||||
| }) | |||||
| this.list = records | |||||
| this.updateSelectIdList() | |||||
| }, | |||||
| async onDefaultChange(id, val) { | |||||
| try { | |||||
| // todo: fetch | |||||
| // await this.$fetch('setAddressDefault', { addressId }) | |||||
| const target = this.list.find(item => item.id === id) | |||||
| target.isDefault = val | |||||
| this.updateSelectIdList() | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| onSelectChange(id, val) { | |||||
| const target = this.list.find(item => item.id === id) | |||||
| target.isSelected = val | |||||
| this.updateSelectIdList() | |||||
| }, | |||||
| async onDelete(id) { | |||||
| uni.showToast({ | |||||
| icon: 'loading', | |||||
| title: '正在删除', | |||||
| }); | |||||
| try { | |||||
| // todo: check api & key | |||||
| // await this.$fetch('deleteAddress', { id }) | |||||
| uni.showToast({ | |||||
| icon: 'success', | |||||
| title: '删除成功', | |||||
| }); | |||||
| this.getData() | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| onEdit(id) { | |||||
| this.$refs.travelerPopup.open(id) | |||||
| }, | |||||
| onAdd() { | |||||
| this.$refs.travelerPopup.open() | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style scoped lang="scss"> | |||||
| .page__view { | |||||
| width: 100vw; | |||||
| min-height: 100vh; | |||||
| background-color: $uni-bg-color; | |||||
| position: relative; | |||||
| /deep/ .nav-bar__view { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| } | |||||
| } | |||||
| .main { | |||||
| padding: calc(var(--status-bar-height) + 160rpx) 40rpx 254rpx 40rpx; | |||||
| } | |||||
| .card { | |||||
| & + & { | |||||
| margin-top: 40rpx; | |||||
| } | |||||
| } | |||||
| .bottom { | |||||
| position: fixed; | |||||
| left: 0; | |||||
| bottom: 0; | |||||
| align-items: flex-start; | |||||
| width: 100vw; | |||||
| // height: 214rpx; | |||||
| padding: 32rpx 40rpx; | |||||
| padding-bottom: calc(env(safe-area-inset-bottom) + 32rpx); | |||||
| background: #FFFFFF; | |||||
| box-sizing: border-box; | |||||
| .btn { | |||||
| width: 100%; | |||||
| padding: 14rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #FFFFFF; | |||||
| background-image: linear-gradient(to right, #21FEEC, #019AF9); | |||||
| border: 2rpx solid #00A9FF; | |||||
| border-radius: 41rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,533 @@ | |||||
| <template> | |||||
| <view> | |||||
| <uv-popup ref="popup" mode="bottom" bgColor="none" > | |||||
| <view class="popup__view"> | |||||
| <view class="flex header"> | |||||
| <view class="title">{{ title }}</view> | |||||
| <button class="btn" @click="close">关闭</button> | |||||
| </view> | |||||
| <view class="form"> | |||||
| <uv-form | |||||
| ref="form" | |||||
| :model="form" | |||||
| :rules="rules" | |||||
| errorType="toast" | |||||
| > | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="name" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-require.png" mode="widthFix"></image> | |||||
| 姓名 | |||||
| </view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.name"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="idNo" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-require.png" mode="widthFix"></image> | |||||
| 身份证号 | |||||
| </view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.idNo"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="type" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-require.png" mode="widthFix"></image> | |||||
| 类型 | |||||
| </view> | |||||
| <view class="form-item-content"> | |||||
| <uv-radio-group v-model="form.type" | |||||
| iconColor="#00A9FF" | |||||
| iconSize="36rpx" | |||||
| size="36rpx" | |||||
| labelColor="#181818" | |||||
| labelSize="26rpx" | |||||
| > | |||||
| <uv-radio | |||||
| v-for="(item, index) in typeOptions" | |||||
| :key="index" | |||||
| :label="item.label" | |||||
| :name="item.value" | |||||
| :customStyle="{ flex: 1 }" | |||||
| ></uv-radio> | |||||
| </uv-radio-group> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="gender" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-require.png" mode="widthFix"></image> | |||||
| 性别 | |||||
| </view> | |||||
| <view class="form-item-content"> | |||||
| <uv-radio-group v-model="form.gender" | |||||
| iconColor="#00A9FF" | |||||
| iconSize="36rpx" | |||||
| size="36rpx" | |||||
| labelColor="#181818" | |||||
| labelSize="26rpx" | |||||
| > | |||||
| <uv-radio | |||||
| v-for="(item, index) in genderOptions" | |||||
| :key="index" | |||||
| :label="item.label" | |||||
| :name="item.value" | |||||
| :customStyle="{ flex: 1 }" | |||||
| ></uv-radio> | |||||
| </uv-radio-group> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="phone" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-require.png" mode="widthFix"></image> | |||||
| 手机号 | |||||
| </view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.phone"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="wx" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label">微信号</view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.wx"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="school" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label">学校</view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.school"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="grade" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label">年级</view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.grade"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="age" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label">年龄</view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.age"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="remark" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label">特殊需求(饮食/健康等)备注</view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.remark"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="emergencyContact" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-require.png" mode="widthFix"></image> | |||||
| 紧急联系人 | |||||
| </view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.emergencyContact"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| <view class="form-item"> | |||||
| <uv-form-item prop="guardiancontact" :customStyle="formItemStyle"> | |||||
| <view class="form-item-label"> | |||||
| <image class="icon" src="@/pages_order/static/traveler/icon-require.png" mode="widthFix"></image> | |||||
| 监护人联系方式 | |||||
| </view> | |||||
| <view class="form-item-content"> | |||||
| <formInput v-model="form.guardiancontact"></formInput> | |||||
| </view> | |||||
| </uv-form-item> | |||||
| </view> | |||||
| </uv-form> | |||||
| </view> | |||||
| <view class="footer"> | |||||
| <view class="agreement"> | |||||
| <view> | |||||
| <uv-checkbox-group | |||||
| v-model="checkboxValue" | |||||
| shape="circle" | |||||
| > | |||||
| <uv-checkbox | |||||
| size="40rpx" | |||||
| icon-size="40rpx" | |||||
| activeColor="#00A9FF" | |||||
| :name="1" | |||||
| ></uv-checkbox> | |||||
| </uv-checkbox-group> | |||||
| </view> | |||||
| <view class="desc"> | |||||
| 我已阅读并同意 | |||||
| <!-- todo: 替换配置项key --> | |||||
| <text class="highlight" @click="$refs.modal.open('config_agreement', '服务协议')">《服务协议》</text> | |||||
| 和 | |||||
| <!-- todo: 替换配置项key --> | |||||
| <text class="highlight" @click="$refs.modal.open('config_privacy', '隐私政策')">《隐私政策》</text> | |||||
| </view> | |||||
| </view> | |||||
| <view class="bar"> | |||||
| <button class="flex btn" @click="onSave">保存</button> | |||||
| </view> | |||||
| </view> | |||||
| </view> | |||||
| </uv-popup> | |||||
| <agreementModal ref="modal" @confirm="onConfirmAgreement"></agreementModal> | |||||
| </view> | |||||
| </template> | |||||
| <script> | |||||
| import formInput from '@/pages_order/components/formInput.vue' | |||||
| import agreementModal from '@/pages_order/components/agreementModal.vue' | |||||
| export default { | |||||
| components: { | |||||
| formInput, | |||||
| agreementModal, | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| id: null, | |||||
| title: null, | |||||
| form: { | |||||
| name: null, | |||||
| idNo: null, | |||||
| type: 0, | |||||
| gender: 0, | |||||
| phone: null, | |||||
| wx: null, | |||||
| school: null, | |||||
| grade: null, | |||||
| age: null, | |||||
| remark: null, | |||||
| emergencyContact: null, | |||||
| guardiancontact: null, | |||||
| }, | |||||
| rules: { | |||||
| 'name': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请输入姓名', | |||||
| }, | |||||
| 'idNo': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请输入身份证号', | |||||
| }, | |||||
| 'type': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请选择类型', | |||||
| }, | |||||
| 'gender': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请选择性别', | |||||
| }, | |||||
| 'phone': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请输入手机号', | |||||
| }, | |||||
| 'emergencyContact': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请输入紧急联系人联系方式', | |||||
| }, | |||||
| 'guardiancontact': { | |||||
| type: 'string', | |||||
| required: true, | |||||
| message: '请输入监护人联系方式', | |||||
| }, | |||||
| }, | |||||
| formItemStyle: { padding: 0 }, | |||||
| // todo: check | |||||
| typeOptions: [ | |||||
| { | |||||
| id: '001', | |||||
| label: '成人', | |||||
| value: '0', | |||||
| }, | |||||
| { | |||||
| id: '002', | |||||
| label: '青少年', | |||||
| value: '1', | |||||
| }, | |||||
| { | |||||
| id: '003', | |||||
| label: '儿童', | |||||
| value: '2', | |||||
| }, | |||||
| ], | |||||
| genderOptions: [ | |||||
| { | |||||
| id: '001', | |||||
| label: '男', | |||||
| value: '0', | |||||
| }, | |||||
| { | |||||
| id: '002', | |||||
| label: '女', | |||||
| value: '1', | |||||
| }, | |||||
| ], | |||||
| checkboxValue: [], | |||||
| } | |||||
| }, | |||||
| methods: { | |||||
| async fetchTravelerDetail(id) { | |||||
| try { | |||||
| // todo: fetch | |||||
| } catch (err) { | |||||
| } | |||||
| }, | |||||
| open(id) { | |||||
| if (id) { | |||||
| this.id = id | |||||
| this.title = '编辑出行人' | |||||
| this.fetchTravelerDetail(id) | |||||
| } else { | |||||
| this.id = null | |||||
| this.title = '添加出行人' | |||||
| this.form = { | |||||
| name: null, | |||||
| idNo: null, | |||||
| type: 0, | |||||
| gender: 0, | |||||
| phone: null, | |||||
| wx: null, | |||||
| school: null, | |||||
| grade: null, | |||||
| age: null, | |||||
| remark: null, | |||||
| emergencyContact: null, | |||||
| guardiancontact: null, | |||||
| } | |||||
| } | |||||
| this.$refs.popup.open() | |||||
| }, | |||||
| close() { | |||||
| this.$refs.popup.close() | |||||
| }, | |||||
| onConfirmAgreement(confirm) { | |||||
| if (confirm) { | |||||
| this.checkboxValue = [1] | |||||
| } else { | |||||
| this.checkboxValue = [] | |||||
| } | |||||
| }, | |||||
| async onSave() { | |||||
| if(!this.checkboxValue.length){ | |||||
| return uni.showToast({ | |||||
| title: '请先同意《服务协议》和《隐私政策》', | |||||
| icon:'none' | |||||
| }) | |||||
| } | |||||
| try { | |||||
| await this.$refs.form.validate() | |||||
| const { | |||||
| } = this.form | |||||
| const params = { | |||||
| } | |||||
| if (this.id) { | |||||
| params.id = this.id | |||||
| // todo: fetch | |||||
| // await this.$fetch('updateAddress', params) | |||||
| uni.showToast({ | |||||
| icon: 'success', | |||||
| title: '修改出行人成功', | |||||
| }); | |||||
| } else { | |||||
| // todo: fetch | |||||
| // await this.$fetch('addAddress', params) | |||||
| uni.showToast({ | |||||
| icon: 'success', | |||||
| title: '添加出行人成功', | |||||
| }); | |||||
| } | |||||
| this.$emit('submitted') | |||||
| this.close() | |||||
| } catch (err) { | |||||
| console.log('onSave err', err) | |||||
| } | |||||
| }, | |||||
| }, | |||||
| } | |||||
| </script> | |||||
| <style lang="scss" scoped> | |||||
| .popup__view { | |||||
| width: 100vw; | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| box-sizing: border-box; | |||||
| background: #FFFFFF; | |||||
| border-top-left-radius: 32rpx; | |||||
| border-top-right-radius: 32rpx; | |||||
| } | |||||
| .header { | |||||
| position: relative; | |||||
| width: 100%; | |||||
| padding: 24rpx 0; | |||||
| box-sizing: border-box; | |||||
| border-bottom: 2rpx solid #EEEEEE; | |||||
| .title { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 34rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| } | |||||
| .btn { | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 32rpx; | |||||
| line-height: 1.4; | |||||
| color: #8B8B8B; | |||||
| position: absolute; | |||||
| top: 26rpx; | |||||
| left: 40rpx; | |||||
| } | |||||
| } | |||||
| .form { | |||||
| max-height: 75vh; | |||||
| padding: 32rpx 40rpx; | |||||
| box-sizing: border-box; | |||||
| overflow-y: auto; | |||||
| &-item { | |||||
| padding: 8rpx 0 6rpx 0; | |||||
| & + & { | |||||
| padding-top: 24rpx; | |||||
| border-top: 2rpx solid #EEEEEE; | |||||
| } | |||||
| &-label { | |||||
| margin-bottom: 14rpx; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 400; | |||||
| font-size: 26rpx; | |||||
| line-height: 1.4; | |||||
| color: #181818; | |||||
| .icon { | |||||
| margin-right: 8rpx; | |||||
| width: 16rpx; | |||||
| height: auto; | |||||
| } | |||||
| } | |||||
| &-content { | |||||
| .placeholder { | |||||
| color: #C6C6C6; | |||||
| font-size: 32rpx; | |||||
| font-weight: 400; | |||||
| } | |||||
| .region { | |||||
| min-height: 44rpx; | |||||
| justify-content: flex-start; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| .footer { | |||||
| width: 100%; | |||||
| .agreement { | |||||
| display: flex; | |||||
| padding: 16rpx 40rpx; | |||||
| background: #E9F8FF; | |||||
| box-sizing: border-box; | |||||
| /deep/ .uv-checkbox-group { | |||||
| flex: none; | |||||
| } | |||||
| .desc { | |||||
| flex: 1; | |||||
| font-family: PingFang SC; | |||||
| font-size: 24rpx; | |||||
| font-weight: 400; | |||||
| line-height: 40rpx; | |||||
| color: #8B8B8B; | |||||
| } | |||||
| .highlight { | |||||
| color: $uni-color; | |||||
| } | |||||
| } | |||||
| .bar { | |||||
| width: 100%; | |||||
| padding: 32rpx 40rpx; | |||||
| box-sizing: border-box; | |||||
| border-top: 2rpx solid #F1F1F1; | |||||
| } | |||||
| .btn { | |||||
| width: 100%; | |||||
| padding: 14rpx 0; | |||||
| box-sizing: border-box; | |||||
| font-family: PingFang SC; | |||||
| font-weight: 500; | |||||
| font-size: 36rpx; | |||||
| line-height: 1.4; | |||||
| color: #FFFFFF; | |||||
| background-image: linear-gradient(to right, #21FEEC, #019AF9); | |||||
| border: 2rpx solid #00A9FF; | |||||
| border-radius: 41rpx; | |||||
| } | |||||
| } | |||||
| </style> | |||||