Browse Source

feat: page-productDetailℴ

pull/2/head
Fox-33 3 months ago
parent
commit
006adbdce5
38 changed files with 4833 additions and 9 deletions
  1. +17
    -0
      common.scss
  2. +1
    -1
      components/base/navbar.vue
  3. +11
    -4
      components/product/productCard.vue
  4. +1
    -1
      manifest.json
  5. +21
    -0
      pages.json
  6. +4
    -0
      pages/index/index.vue
  7. +203
    -0
      pages_order/comment/commentCard.vue
  8. +130
    -0
      pages_order/comment/commentRecords.vue
  9. +81
    -0
      pages_order/comment/commentRecordsOfProduct.vue
  10. +270
    -0
      pages_order/comment/commentWrite.vue
  11. +6
    -2
      pages_order/components/formInput.vue
  12. +371
    -0
      pages_order/order/orderConfirm/index.vue
  13. +374
    -0
      pages_order/order/orderConfirm/infoPopup.vue
  14. +193
    -0
      pages_order/order/orderConfirm/peopleNumberInput.vue
  15. +145
    -0
      pages_order/order/orderConfirm/productCard.vue
  16. +91
    -0
      pages_order/order/orderConfirm/timeCalendarSelect.vue
  17. +150
    -0
      pages_order/order/orderConfirm/timeOptionsSelect.vue
  18. +652
    -0
      pages_order/order/orderDetail/index.vue
  19. +156
    -0
      pages_order/order/orderList/index.vue
  20. +320
    -0
      pages_order/order/orderList/orderCard.vue
  21. +115
    -0
      pages_order/product/commentList.vue
  22. +507
    -0
      pages_order/product/productDetail.vue
  23. BIN
      pages_order/static/address/icon.png
  24. BIN
      pages_order/static/address/selectIcon.png
  25. BIN
      pages_order/static/product/icon-service.png
  26. BIN
      pages_order/static/traveler/icon-delete.png
  27. BIN
      pages_order/static/traveler/icon-edit.png
  28. BIN
      pages_order/static/traveler/icon-require.png
  29. +217
    -0
      pages_order/traveler/travelerCard.vue
  30. +235
    -0
      pages_order/traveler/travelerList.vue
  31. +533
    -0
      pages_order/traveler/travelerPopup.vue
  32. BIN
      static/image/temp-31.png
  33. BIN
      static/image/temp-32.png
  34. BIN
      static/image/temp-33.png
  35. BIN
      static/image/temp-34.png
  36. BIN
      static/image/temp-35.png
  37. BIN
      static/image/temp-36.png
  38. +29
    -1
      store/store.js

+ 17
- 0
common.scss View File

@ -76,4 +76,21 @@
/deep/ .uv-modal__content {
padding: 0 !important;
}
/deep/ .uv-calendar-month__days__day {
// height: 60px !important;
}
/deep/ .uv-calendar-month__days__day__select__top-info,
/deep/ .uv-calendar-month__days__day__select__buttom-info {
white-space: nowrap;
}
/deep/ .uv-number-box__minus--disabled {
background: transparent !important;
}
/deep/ .uv-number-box__input {
font-size: 40rpx;
}

+ 1
- 1
components/base/navbar.vue View File

@ -1,7 +1,7 @@
<template>
<!-- <view class="navbar"
:style="{backgroundColor : bgColor}"> -->
<view class="title"
<view class="title nav-bar__view"
:style="{backgroundColor : bgColor,color}">
<view class="left">


+ 11
- 4
components/product/productCard.vue View File

@ -1,5 +1,9 @@
<template>
<view class="product" @touchstart="onTouchstart" @touchmove="onTouchmove" @touchend="onTouchend">
<view class="product"
@touchstart="onTouchstart"
@touchmove="onTouchmove"
@touchend="onTouchend"
>
<image class="product-img" :src="data.image" mode="aspectFill"></image>
<view class="flex flex-column product-info">
<view class="product-info-top">
@ -22,7 +26,7 @@
{{ `${data.registered}人已报名` }}
</view>
</view>
<button class="btn">报名</button>
<button class="btn" @click="onRegistrate">报名</button>
</view>
</view>
@ -112,7 +116,10 @@
title: '已收藏',
});
this.hiddenCollectBtn()
}
},
onRegistrate() {
this.$utils.navigateTo(`/pages_order/product/productDetail?id=${this.data.id}`)
},
},
}
</script>
@ -168,7 +175,7 @@
&-price {
justify-content: flex-start;
align-items: baseline;
column-gap: 6rpx;
column-gap: 12rpx;
&-val {
font-size: 24rpx;


+ 1
- 1
manifest.json View File

@ -52,7 +52,7 @@
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx489ca73503f461be",
"appid" : "wxee64675d48680dd4",
"setting" : {
"urlCheck" : false
},


+ 21
- 0
pages.json View File

@ -42,6 +42,27 @@
},
{
"path": "product/search"
},
{
"path": "product/productDetail"
},
{
"path": "traveler/travelerList"
},
{
"path": "order/orderConfirm/index"
},
{
"path": "order/orderList/index",
"style": {
"enablePullDownRefresh": true
}
},
{
"path": "order/orderDetail/index",
"style": {
"enablePullDownRefresh": true
}
}
]
}],


+ 4
- 0
pages/index/index.vue View File

@ -93,6 +93,10 @@
}
},
onLoad() {
uni.navigateTo({
url: `/pages_order/product/productDetail`
})
// this.$utils.navigateTo('/pages_order/traveler/travelerList')
// let id = '1948353988875821058'
// uni.navigateTo({


+ 203
- 0
pages_order/comment/commentCard.vue View File

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

+ 130
- 0
pages_order/comment/commentRecords.vue View File

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

+ 81
- 0
pages_order/comment/commentRecordsOfProduct.vue View File

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

+ 270
- 0
pages_order/comment/commentWrite.vue View File

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

+ 6
- 2
pages_order/components/formInput.vue View File

@ -10,11 +10,11 @@
backgroundColor: 'transparent',
padding: '0',
boxSizing: 'border-box',
fontSize: '32rpx',
fontSize: fontSize,
border: 'none',
transform: 'translateX(-4px)'
}"
fontSize="32rpx"
:fontSize="fontSize"
></uv-input>
</template>
@ -37,6 +37,10 @@
type: String,
default: 'left'
},
fontSize: {
type: String,
default: '32rpx'
},
},
data() {
return {


+ 371
- 0
pages_order/order/orderConfirm/index.vue View File

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

+ 374
- 0
pages_order/order/orderConfirm/infoPopup.vue View File

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

+ 193
- 0
pages_order/order/orderConfirm/peopleNumberInput.vue View File

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

+ 145
- 0
pages_order/order/orderConfirm/productCard.vue View File

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

+ 91
- 0
pages_order/order/orderConfirm/timeCalendarSelect.vue View File

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

+ 150
- 0
pages_order/order/orderConfirm/timeOptionsSelect.vue View File

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

+ 652
- 0
pages_order/order/orderDetail/index.vue View File

@ -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: '售后',
}
// 012
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: 012
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>

+ 156
- 0
pages_order/order/orderList/index.vue View File

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

+ 320
- 0
pages_order/order/orderList/orderCard.vue View File

@ -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: 012
// 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>

+ 115
- 0
pages_order/product/commentList.vue View File

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

+ 507
- 0
pages_order/product/productDetail.vue View File

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

BIN
pages_order/static/address/icon.png View File

Before After
Width: 96  |  Height: 96  |  Size: 4.5 KiB

BIN
pages_order/static/address/selectIcon.png View File

Before After
Width: 28  |  Height: 36  |  Size: 1.3 KiB

BIN
pages_order/static/product/icon-service.png View File

Before After
Width: 78  |  Height: 79  |  Size: 1.9 KiB

BIN
pages_order/static/traveler/icon-delete.png View File

Before After
Width: 73  |  Height: 72  |  Size: 907 B

BIN
pages_order/static/traveler/icon-edit.png View File

Before After
Width: 73  |  Height: 72  |  Size: 942 B

BIN
pages_order/static/traveler/icon-require.png View File

Before After
Width: 24  |  Height: 24  |  Size: 657 B

+ 217
- 0
pages_order/traveler/travelerCard.vue View File

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

+ 235
- 0
pages_order/traveler/travelerList.vue View File

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

+ 533
- 0
pages_order/traveler/travelerPopup.vue View File

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

BIN
static/image/temp-31.png View File

Before After
Width: 667  |  Height: 4096  |  Size: 2.7 MiB

BIN
static/image/temp-32.png View File

Before After
Width: 667  |  Height: 4096  |  Size: 3.0 MiB

BIN
static/image/temp-33.png View File

Before After
Width: 667  |  Height: 4096  |  Size: 4.7 MiB

BIN
static/image/temp-34.png View File

Before After
Width: 667  |  Height: 4096  |  Size: 3.9 MiB

BIN
static/image/temp-35.png View File

Before After
Width: 667  |  Height: 4096  |  Size: 2.7 MiB

BIN
static/image/temp-36.png View File

Before After
Width: 495  |  Height: 660  |  Size: 356 KiB

+ 29
- 1
store/store.js View File

@ -4,6 +4,7 @@ import Vuex from 'vuex'
Vue.use(Vuex); //vue的插件机制
import api from '@/api/api.js'
import fetch from '@/api/fetch.js'
//Vuex.Store 构造器选项
const store = new Vuex.Store({
@ -11,6 +12,8 @@ const store = new Vuex.Store({
configList: {}, //配置列表
shop : false,//身份判断如果不需要,可以删除
userInfo : {}, //用户信息
travelerList: null,
orderInfo: null,
},
getters: {
// 角色 true为水洗店 false为酒店 : 身份判断如果不需要,可以删除
@ -103,8 +106,33 @@ const store = new Vuex.Store({
}
})
},
setTravelerList(state, data) {
state.travelerList = data
},
setOrderInfo(state, data) {
state.orderInfo = data
},
},
actions: {
async collect(state, id) {
console.log('collect', id)
try {
// todo: fetch
// await fetch('collect', { id })
uni.showToast({
icon: 'success',
title: '已收藏',
});
return true
} catch (err) {
return false
}
},
},
actions: {},
})
export default store

Loading…
Cancel
Save