| @ -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> | |||