610 lines
14 KiB
Vue
610 lines
14 KiB
Vue
<template>
|
||
<view class="container">
|
||
<scroll-view scroll-y class="content-scroll">
|
||
<!-- 服务信息 -->
|
||
<view class="service-card">
|
||
<image :src="serviceInfo.coverImage" mode="aspectFill"></image>
|
||
<view class="service-info">
|
||
<text class="service-name">{{ serviceInfo.serviceName }}</text>
|
||
<text class="service-price">¥{{ serviceInfo.price }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 学生选择 -->
|
||
<view class="section">
|
||
<view class="section-title">选择学生</view>
|
||
<view class="student-select" @click="showStudentPicker = true">
|
||
<view class="student-info" v-if="selectedStudent">
|
||
<text class="student-name">{{ selectedStudent.studentName }}</text>
|
||
<text class="student-grade">{{ selectedStudent.grade }}</text>
|
||
</view>
|
||
<text class="placeholder" v-else>请选择学生</text>
|
||
<uni-icons class="iconfont" type="arrow-right" size="16" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 教师选择 -->
|
||
<view class="section">
|
||
<view class="section-title">选择教师(可选)</view>
|
||
<view class="teacher-select" @click="goSelectTeacher">
|
||
<view class="teacher-info" v-if="selectedTeacher">
|
||
<image :src="selectedTeacher.avatar" mode="aspectFill"></image>
|
||
<view class="teacher-detail">
|
||
<text class="teacher-name">{{ selectedTeacher.teacherName }}</text>
|
||
<text class="teacher-subjects">{{ selectedTeacher.subjects }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="placeholder" v-else>系统将为您推荐合适的教师</text>
|
||
<uni-icons class="iconfont" type="arrow-right" size="16" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 服务时间 -->
|
||
<view class="section">
|
||
<view class="section-title">服务时间</view>
|
||
<view class="time-select">
|
||
<view class="date-select" @click="showDatePicker = true">
|
||
<text>{{ selectedDate || '选择日期' }}</text>
|
||
<uni-icons class="iconfont" type="calendar" size="16" color="#999"></uni-icons>
|
||
</view>
|
||
<view class="time-slot-select" @click="showTimeSlotPicker = true">
|
||
<text>{{ selectedTimeSlot || '选择时间段' }}</text>
|
||
<uni-icons class="iconfont" type="clock" size="16" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 服务地址 -->
|
||
<view class="section">
|
||
<view class="section-title">服务地址</view>
|
||
<textarea class="address-input" v-model="serviceAddress" placeholder="请输入详细地址" maxlength="200"></textarea>
|
||
</view>
|
||
|
||
<!-- 优惠券 -->
|
||
<view class="section">
|
||
<view class="section-row" @click="goSelectCoupon">
|
||
<text>优惠券</text>
|
||
<view class="section-right">
|
||
<text class="coupon-text" v-if="selectedCoupon">-¥{{ selectedCoupon.discountValue }}</text>
|
||
<text class="placeholder" v-else>选择优惠券</text>
|
||
<uni-icons class="iconfont" type="arrow-right" size="16" color="#999"></uni-icons>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 备注 -->
|
||
<view class="section">
|
||
<view class="section-title">备注信息</view>
|
||
<textarea class="remark-input" v-model="userRemark" placeholder="请输入备注信息(选填)" maxlength="200"></textarea>
|
||
</view>
|
||
|
||
<!-- 价格明细 -->
|
||
<view class="price-detail">
|
||
<view class="price-row">
|
||
<text>服务价格</text>
|
||
<text>¥{{ serviceInfo.price }}</text>
|
||
</view>
|
||
<view class="price-row" v-if="selectedCoupon">
|
||
<text>优惠券</text>
|
||
<text class="discount">-¥{{ selectedCoupon.discountValue }}</text>
|
||
</view>
|
||
<view class="price-row total">
|
||
<text>实付金额</text>
|
||
<text class="total-price">¥{{ totalPrice }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部提交 -->
|
||
<view class="bottom-bar">
|
||
<view class="price-info">
|
||
<text class="label">实付</text>
|
||
<text class="price">¥{{ totalPrice }}</text>
|
||
</view>
|
||
<button class="btn-submit" @click="submitOrder">提交订单</button>
|
||
</view>
|
||
|
||
<!-- 学生选择弹窗 -->
|
||
<view class="student-popup" v-if="showStudentPicker" @click="closeStudentPicker">
|
||
<view class="student-picker" @click.stop>
|
||
<view class="picker-header">
|
||
<text @click="closeStudentPicker">取消</text>
|
||
<text class="title">选择学生</text>
|
||
<text @click="goAddStudent">新增</text>
|
||
</view>
|
||
<view class="student-list">
|
||
<view class="student-item" v-for="item in studentList" :key="item.id" @click="selectStudent(item)">
|
||
<text class="student-name">{{ item.studentName }}</text>
|
||
<text class="student-grade">{{ item.grade }}</text>
|
||
<uni-icons v-if="selectedStudent && selectedStudent.id === item.id" class="iconfont" type="checkbox-filled" size="18" color="#4a9b9f"></uni-icons>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { serviceApi, studentApi, orderApi } from '@/api/index.js'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
serviceId: '',
|
||
serviceInfo: {},
|
||
studentList: [],
|
||
selectedStudent: null,
|
||
selectedTeacher: null,
|
||
selectedDate: '',
|
||
selectedTimeSlot: '',
|
||
serviceAddress: '',
|
||
selectedCoupon: null,
|
||
userRemark: '',
|
||
showStudentPicker: false,
|
||
showDatePicker: false,
|
||
showTimeSlotPicker: false
|
||
}
|
||
},
|
||
computed: {
|
||
// 计算总价
|
||
totalPrice() {
|
||
let price = this.serviceInfo.price || 0
|
||
if (this.selectedCoupon) {
|
||
price -= this.selectedCoupon.discountValue
|
||
}
|
||
return price.toFixed(2)
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.serviceId = options.serviceId
|
||
if (options.teacherId) {
|
||
// 从教师详情页跳转过来
|
||
this.selectedTeacher = JSON.parse(decodeURIComponent(options.teacher))
|
||
}
|
||
this.loadServiceDetail()
|
||
this.loadStudentList()
|
||
},
|
||
methods: {
|
||
// 加载服务详情
|
||
async loadServiceDetail() {
|
||
uni.showLoading({ title: '加载中...' })
|
||
try {
|
||
const res = await serviceApi.getServiceDetail(this.serviceId)
|
||
this.serviceInfo = res
|
||
} catch (error) {
|
||
console.error('加载服务详情失败:', error)
|
||
uni.showToast({ title: '加载服务详情失败', icon: 'none' })
|
||
// 降级处理
|
||
this.serviceInfo = {
|
||
serviceName: '服务',
|
||
price: 0,
|
||
coverImage: '/static/images/default-service.png'
|
||
}
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
},
|
||
|
||
// 加载学生列表
|
||
async loadStudentList() {
|
||
try {
|
||
const res = await studentApi.getStudentList()
|
||
this.studentList = res.list || res || []
|
||
// 默认选择第一个学生
|
||
if (this.studentList.length > 0) {
|
||
this.selectedStudent = this.studentList.find(item => item.isDefault) || this.studentList[0]
|
||
}
|
||
} catch (error) {
|
||
console.error('加载学生列表失败:', error)
|
||
this.studentList = []
|
||
}
|
||
},
|
||
|
||
// 选择学生
|
||
selectStudent(student) {
|
||
this.selectedStudent = student
|
||
this.showStudentPicker = false
|
||
},
|
||
|
||
// 关闭学生选择器
|
||
closeStudentPicker() {
|
||
this.showStudentPicker = false
|
||
},
|
||
|
||
// 新增学生
|
||
goAddStudent() {
|
||
uni.navigateTo({
|
||
url: '/pages/student/add'
|
||
})
|
||
},
|
||
|
||
// 选择教师
|
||
goSelectTeacher() {
|
||
uni.navigateTo({
|
||
url: `/pages/teacher/list?serviceId=${this.serviceId}&select=true`
|
||
})
|
||
},
|
||
|
||
// 选择优惠券
|
||
goSelectCoupon() {
|
||
uni.navigateTo({
|
||
url: `/pages/coupon/select?amount=${this.serviceInfo.price}`
|
||
})
|
||
},
|
||
|
||
// 提交订单
|
||
async submitOrder() {
|
||
// 验证必填项
|
||
if (!this.selectedStudent) {
|
||
uni.showToast({ title: '请选择学生', icon: 'none' })
|
||
return
|
||
}
|
||
if (!this.selectedDate) {
|
||
uni.showToast({ title: '请选择服务日期', icon: 'none' })
|
||
return
|
||
}
|
||
if (!this.selectedTimeSlot) {
|
||
uni.showToast({ title: '请选择服务时间段', icon: 'none' })
|
||
return
|
||
}
|
||
if (!this.serviceAddress) {
|
||
uni.showToast({ title: '请输入服务地址', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
try {
|
||
uni.showLoading({ title: '提交中...' })
|
||
|
||
const params = {
|
||
serviceId: this.serviceId,
|
||
studentId: this.selectedStudent.id,
|
||
teacherId: this.selectedTeacher ? this.selectedTeacher.id : null,
|
||
serviceDate: this.selectedDate,
|
||
timeSlot: this.selectedTimeSlot,
|
||
serviceAddress: this.serviceAddress,
|
||
couponId: this.selectedCoupon ? this.selectedCoupon.id : null,
|
||
userRemark: this.userRemark
|
||
}
|
||
|
||
const res = await orderApi.createOrder(params)
|
||
|
||
uni.hideLoading()
|
||
|
||
// 跳转支付页面
|
||
uni.redirectTo({
|
||
url: `/pages/payment/index?orderNo=${res.orderNo}`
|
||
})
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: error.message || '提交失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.container {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #f8f8f8;
|
||
}
|
||
|
||
.content-scroll {
|
||
flex: 1;
|
||
padding-bottom: 120rpx;
|
||
}
|
||
|
||
.service-card {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
display: flex;
|
||
margin-bottom: 20rpx;
|
||
|
||
image {
|
||
width: 160rpx;
|
||
height: 120rpx;
|
||
border-radius: 12rpx;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.service-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
|
||
.service-name {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.service-price {
|
||
font-size: 32rpx;
|
||
color: #ff6b6b;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
}
|
||
|
||
.section {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.section-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 28rpx;
|
||
|
||
.section-right {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.coupon-text {
|
||
color: #ff6b6b;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.placeholder {
|
||
color: #999;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.iconfont {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.student-select,
|
||
.teacher-select {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 24rpx;
|
||
background: #f8f8f8;
|
||
border-radius: 12rpx;
|
||
|
||
.student-info {
|
||
flex: 1;
|
||
|
||
.student-name {
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.student-grade {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.teacher-info {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
image {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 40rpx;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.teacher-detail {
|
||
flex: 1;
|
||
|
||
.teacher-name {
|
||
display: block;
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.teacher-subjects {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
.placeholder {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.iconfont {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.time-select {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
|
||
.date-select,
|
||
.time-slot-select {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 24rpx;
|
||
background: #f8f8f8;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
|
||
.iconfont {
|
||
font-size: 32rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.address-input,
|
||
.remark-input {
|
||
width: 100%;
|
||
min-height: 160rpx;
|
||
padding: 20rpx;
|
||
background: #f8f8f8;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.price-detail {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
|
||
.price-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
margin-bottom: 20rpx;
|
||
|
||
&.total {
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
font-weight: bold;
|
||
padding-top: 20rpx;
|
||
border-top: 1rpx solid #eee;
|
||
|
||
.total-price {
|
||
font-size: 36rpx;
|
||
color: #ff6b6b;
|
||
}
|
||
}
|
||
|
||
.discount {
|
||
color: #ff6b6b;
|
||
}
|
||
}
|
||
}
|
||
|
||
.bottom-bar {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 120rpx;
|
||
background: #fff;
|
||
border-top: 1rpx solid #eee;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 30rpx;
|
||
|
||
.price-info {
|
||
flex: 1;
|
||
|
||
.label {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.price {
|
||
font-size: 36rpx;
|
||
color: #ff6b6b;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.btn-submit {
|
||
width: 240rpx;
|
||
height: 80rpx;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #fff;
|
||
border-radius: 40rpx;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.student-popup {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 999;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.student-picker {
|
||
width: 100%;
|
||
max-height: 80vh;
|
||
background: #fff;
|
||
border-radius: 24rpx 24rpx 0 0;
|
||
|
||
.picker-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 30rpx;
|
||
border-bottom: 1rpx solid #eee;
|
||
|
||
text {
|
||
font-size: 28rpx;
|
||
color: #4a9b9f;
|
||
|
||
&.title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.student-list {
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
|
||
.student-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 30rpx;
|
||
border-bottom: 1rpx solid #f5f5f5;
|
||
|
||
.student-name {
|
||
flex: 1;
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.student-grade {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.iconfont {
|
||
font-size: 32rpx;
|
||
color: #4a9b9f;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|