You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

853 lines
21 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page">
<view class="oinfo">
<view class="yycon scell" v-if="item">
<view class="oitem">
<view class="otop">
<view class="hleft">{{ item.courseName }}</view>
<view class="hright">
{{ formatTime(item.startTime) + ' - ' + formatTime(item.endTime) }}
</view>
</view>
<view class="oscon">
<view class="osname">
<view class="osimg imgload">
<image v-if="item.pic" :src="getimgRemoteFile(item.pic)" mode="aspectFill"></image>
</view>
<view class="oscell">
<view v-if="item.claName" class="txtbg1" :style="{ color: item.claColor ? item.claColor : '#333' }">
{{ item.claName }}
</view>
<view v-if="item.courseTypeName" style="color: #888; font-weight: 500; font-size: 28rpx;">
{{ item.courseTypeName }}
</view>
<uni-rate :size="20" :readonly="true" :value="item.star ? item.star : 5" :max="5" />
</view>
<view class="oscell" v-if="item.storeName">
<uni-icons type="shop" size="18" color="#00A99A"></uni-icons>
<view class="ntip">{{ item.storeName }}</view>
</view>
<view class="oscell">
<uni-icons type="calendar" size="18" color="#00A99A"></uni-icons>
<view class="ntip">{{ item.claDate }}</view>
<view class="txt">{{ item.weekDay }}</view>
</view>
<view class="oscell">
<uni-icons type="person" size="18" color="#666"></uni-icons>
<view class="ntxt">{{ item.staffName }}</view>
</view>
<view class="oscell" v-if="item.roomName">
<uni-icons type="map-pin-ellipse" size="18" color="#00A99A"></uni-icons>
<view class="ntxt">{{ item.roomName }}</view>
</view>
<view class="oscell" v-if="item.atClassCnt">
<uni-icons type="checkbox" size="18" color="#00A99A"></uni-icons>
<view class="ntxt">已预约{{ item.bookAttendCnt }}人/限{{ item.atClassCnt }}人</view>
</view>
<view class="oscell" v-if="item.lessCnt">
<view class="txt" style="margin-left: 46rpx;">满{{ item.lessCnt }}人开课</view>
</view>
</view>
</view>
<view class="rcpm">
<button type="primary" class="orderbtn" @click="openCardSelect(item)">预约</button>
<!-- <button type="primary" class="orderbtn" @click="gotoorder">预约</button>-->
</view>
</view>
</view>
<!-- 信息 -->
<view class="yycon tcon">
<mp-html v-if="remark" :content="remark" :markdown="true" :lazy-load="true" />
</view>
</view>
<!-- ✅ 选卡弹窗 -->
<uni-popup ref="cardPopup" type="center" :mask-click="false">
<view class="select-popup">
<view class="popup-header">
<text class="popup-title">选择会员卡</text>
<view class="popup-close" @click="closeCardSelect">
<uni-icons type="closeempty" size="22" color="#999"></uni-icons>
</view>
</view>
<!-- 卡片为空提示 -->
<view v-if="!memberCards || memberCards.length === 0" class="empty-tip">
无对应会员卡可使用
</view>
<view v-else>
<view class="course-info">
<view class="course-row">
<text class="course-name">{{ item.courseName}}</text>
<text class="deduct" v-if="selectedCard && selectedCard.memberChargeType !='days'">
需扣 {{ selectedCard.memberChargeType === 'total_fee'
? '¥' + selectedCard.chargeFee
: selectedCard.chargeCnt + '次'}}
</text>
</view>
<!-- ✅ 本次使用卡片 -->
<view class="use-card-row">
<text class="use-label">本次使用卡片:</text>
<text class="use-name" :class="{ 'is-default': !isManuallySelected }">
{{ effectiveCardName }}
</text>
<text v-if="!isManuallySelected && effectiveCard" class="default-tag">默认</text>
</view>
</view>
<!-- 卡片横向滑动 -->
<member-card-scroll
v-if="memberCards && memberCards.length > 0"
ref="cardScroll"
:cards="memberCards"
size="small"
:selectable="true"
:center="false"
:auto-select="false"
:default-card-id="defaultCardId"
@select="onCardSelect"
/>
<view v-if="memberCards && memberCards.length > 0" class="popup-footer">
<button class="confirm-btn" @click="confirmSelect">确认选择</button>
</view>
</view>
</view>
</uni-popup>
<u-toast ref="uToast" />
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
</view>
</template>
<script>
import { myCache, getRemoteFile } from '../../utils/utils.js';
import openlogin from "../components/openlogin.vue";
import MemberCardScroll from '../components/member-card-scroll.vue';
export default {
components: {
openlogin,
MemberCardScroll
},
data() {
return {
openId: "",
phone: "",
userid: "",
id: "",
item: {
courseTimeId: null,
claName: "",
courseName: "",
storeName: "",
pic: null,
claDate: "",
weekDay: "",
startTime: "",
endTime: "",
staffName: "",
studentCount: null,
roomName: null,
claColor: "",
claTimeStatus: "",
bookAttendCnt: "",
atClassCnt: "",
lessCnt: ""
},
remark: null,
memberCards: [],
currentCourse: {
},
defaultCardId: null,
// ✅ 用户手动选中的卡(父组件持有)
selectedCard: null
};
},
computed: {
// ✅ 默认卡:后端指定的,或第一张可用卡
defaultCard() {
if (this.defaultCardId != null) {
const card = this.memberCards.find(c => c.id === this.defaultCardId);
if (card && this.isCardUsable(card)) return card;
}
return this.memberCards.find(card => this.isCardUsable(card)) || null;
},
// ✅ 有效卡:用户选中的,没选就用默认卡
effectiveCard() {
return this.selectedCard || this.defaultCard;
},
// ✅ 有效卡名称(这就是弹窗里显示的内容)
effectiveCardName() {
const card = this.effectiveCard;
return card ? card.cardName : '无可用卡';
},
// ✅ 是否手动选择了
isManuallySelected() {
return !!this.selectedCard;
}
},
onLoad(options) {
if (options.data) {
this.item = JSON.parse(decodeURIComponent(options.data));
this.id = this.item.courseTimeId;
this.getinfo();
}
},
onShow() {
this.openId = myCache('openId');
var user = myCache('user');
this.userid = user.userid ? user.userid : '';
this.phone = user.userphone;
if (this.userid == "" || this.userid == "0") {
uni.navigateTo({ url: `/pages/login/login` });
}
},
methods: {
// ✅ 判断卡片是否可用(父组件也需要)
isCardUsable(card) {
if (!card) return false;
if (card.status && card.status !== 'ACTIVE') return false;
if (card.expiryDate) {
const now = new Date();
const expire = new Date(card.expiryDate.replace(/-/g, '/'));
if (now > expire) return false;
}
const type = this.currentCourse.deductType;
const count = this.currentCourse.deductCount || 1;
const amount = this.currentCourse.deductAmount || 0;
if (type === 'count') {
const available = (card.remainCount || 0) - (card.frozenCount || 0);
return available >= count;
} else if (type === 'total_fee') {
const available = (card.remainAmount || 0) - (card.frozenAmount || 0);
return available >= amount;
}
},
// ✅ 子组件选中回调:更新父组件的 selectedCard
onCardSelect({ card }) {
this.selectedCard = card;
},
// ✅ 打开弹窗:重置手动选择,用默认卡
async openCardSelect(item) {
var user = myCache('userInfo');
item.visitStore=user.visitStore;
console.log('item',item)
const { data: memberCards } =
await uni.$http.post("/api/memberCard/getMemberCardListByCourseTimeId",
item);
//如果为空 则没有适合的卡片
this.memberCards = memberCards.data
console.log('this.memberCards',this.memberCards)
if(this.memberCards&&this.memberCards.length > 0){
this.selectedCard = this.memberCards[0];
}else{
this.selectedCard = null;
}
this.$refs.cardPopup.open();
},
closeCardSelect() {
this.$refs.cardPopup.close();
},
async confirmSelect() {
const card = this.effectiveCard;
if (!card) {
uni.showToast({ title: '无可用会员卡', icon: 'none' });
return;
}
// 开启loading
uni.showLoading({
title: '预约中...',
mask: true // 遮罩,防止重复点击
});
try {
const { data: res } = await uni.$http.post('/api/course/bookCourse', {
courseTimeId: this.item.courseTimeId,
memberCardId: card.memberCardId,
memberChargeType: this.selectedCard.memberChargeType,
chargeCnt: this.selectedCard.chargeCnt,
chargeFee: this.selectedCard.chargeFee
});
if (res && res.success) {
myCache('courserefresh', 1);
this.closeCardSelect();
this.$refs.uToast.show({
title: '课程预约成功,请到 我的预约-预约中 查看',
type: 'success',
duration: 2000
});
} else {
this.$refs.uToast.show({
title: res.message ? res.message : '课程预约失败!',
type: 'error',
duration: 2000
});
}
} catch (err) {
// 网络异常捕获
this.$refs.uToast.show({
title: '预约请求异常,请稍后重试',
type: 'error',
duration: 2000
});
} finally {
// 不管成功失败、报错,都关闭loading
uni.hideLoading();
}
},
formatTime(timeStr) {
return timeStr.slice(0, 5);
},
getimgRemoteFile(img) {
if (img) {
img = img.split(',')[0];
return getRemoteFile(img);
} else {
return require("@/static/image/theme/p1.jpg");
}
},
getPhoneNumber(e) {
if (e.phone) this.phone = e.phone;
if (e.userid) this.userid = e.userid;
},
iflogin() {
this.openId = myCache('openId');
var user = myCache('user');
this.userid = user.userid ? user.userid : '';
this.phone = user.userphone;
if (this.userid == "" || this.userid == "0" || this.phone == "") {
uni.navigateTo({ url: `/pages/login/login` });
return false;
}
return true;
},
goback() {
uni.navigateBack({ delta: 1 });
},
async getinfo() {
uni.showLoading({ title: '数据加载中...' });
const { data: res } = await uni.$http.post('/api/course/getCourseDetail', { courseTimeId: this.id });
if (res && res.data) {
this.item = res.data;
this.remark = res.data.remark;
this.$forceUpdate();
}
},
gotoorder(){
var that = this;
// 再次预约
uni.showModal({
title: '提示',
content: '确定预约吗?',
cancelText: '取消',
confirmText: '确定',
success: ress => {
if (ress.confirm) {
that.orderdodo();
}
}
});
},
// 预约
async orderdodo(){
var param={
"courseTimeId": this.item.courseTimeId
};
uni.showLoading({
title: '课程预约中...'
});
const {data: res} = await uni.$http.post('/api/course/bookCourse', param);
console.log(res)
if(res.success){
// uni.showToast({
// title: '课程已预约!',
// icon: 'success',
// duration: 2000
// });
// 刷新保存
myCache('courserefresh',1);
uni.showModal({
title: '提示',
content: '课程已预约!',
cancelText: '返回',
confirmText: '确定',
success: ress => {
if (ress.confirm) {
// 返回
uni.navigateBack({
delta: 1
});
}
else{
// 返回
uni.navigateBack({
delta: 1
});
}
}
});
}
else{
uni.showModal({
title: '提示',
content: res.message? res.message:'课程预约失败!',
cancelText: '取消',
confirmText: '确定',
success: ress => {
if (ress.confirm) {
}
}
});
}
},
}
};
</script>
<style lang="scss" scoped>
::v-deep .uni-collapse-item__title-box{
padding: 0 !important;
}
::v-deep .uni-collapse-item__title-text{
font-weight: 600;
}
.page{
padding: 0;
position: relative;
background-image: url('@/static/image/bg.jpg');
background-attachment: fixed;
background-size: cover;
background-position: center center;
min-height: calc(100vh - var(--window-top) - var(--window-bottom));
}
.ret{
position: fixed;
top:20rpx;
left:20rpx;
background: rgba(0, 0, 0, .3);
height: 60rpx;
width: 60rpx;
border-radius: 50% 50%;
text-align: center;
line-height: 60rpx;
z-index: 9999;
}
.oscon{
background-color: #f3fafa;
border-radius: 16rpx;
padding: 16rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.osimg{
margin-left: 24%;
width: 300rpx;
height: 300rpx;
margin-right: 10rpx;
>image {
width: 300rpx;
height: 300rpx;
border-radius: 10rpx;
}
}
.osrcon{
display: flex;
flex-direction: column;
flex: 1;
justify-content: flex-start;
margin-left: 20rpx;
}
.osname{
line-height: 48rpx;
font-size:32rpx;
font-weight: 600;
color:#009999;
padding-bottom: 15rpx;
}
.oscell{
display: flex;
flex-direction: row;
align-items: center;
font-size: 28rpx;
color:#333;
padding: 10rpx 0;
border-bottom: 1rpx dotted #74defb;
.txt{
display: flex; flex: 1;
}
}
}
.oinfo{
padding:20rpx;
position: relative;
margin-bottom: 20rpx;
.yycon{
display: flex;
flex-direction: column;
justify-content: center;
background-color: #FFFFFF;
padding: 20rpx 20rpx;
}
}
.tcon{
display: flex;
flex-direction: column;
justify-content: center;
background-color: #FFFFFF;
margin: 20rpx 10rpx 10rpx 10rpx;
.olist{
display: flex;
flex-direction: row;
padding: 10rpx;
width: 100%;
.ltxt{
font-size: 26rpx;
color:#747474;
width: 150rpx;
}
.rtxt{
display: flex;
flex: 1;
font-size: 26rpx;
color:#747474;
display: flex;
flex-direction: row;
}
}
}
.red{
color:#ff3d0e;
}
.green{
color:#00cd00;
}
::v-deep.uni-radio-input-checked{
background-color: #00A99A !important;
border-color: #00A99A !important;
background-clip: content-box!important;
box-sizing: border-box;
}
::v-deep.uni-radio-input-checked::before{
display: none!important;
}
.scell{
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
margin: 10rpx;
.oitem{
display: flex;
flex-direction: column;
background-color: #FFFFFF;
border-radius: 6rpx;
padding: 20rpx 20rpx 0 20rpx;
width: 100%;
margin-bottom: 20rpx;
.rcpm{
display: flex;
flex-direction: column;
width: 100%;
margin: 20rpx 0 0;
}
.orderbtn{
width: 100%;
background-color: #00A99A !important;
height: 80rpx;
line-height: 80rpx;
padding: 0 20rpx;
border:1rpx solid #00A99A;
}
.orderbtn::after{
border:0 !important;
}
.otop{
display: flex;
flex-direction: row;
padding: 10rpx 0 20rpx;
width: 100%;
border-bottom: 1rpx solid #f0f0f0;
.hleft{
display: flex;
flex:1;
align-items: center;
font-size: 30rpx;
font-weight: 600;
line-height: 32rpx;
color: #000;
}
.hright{
display: flex;
justify-content: flex-end;
align-items: center;
font-size: 28rpx;
color: #000;
font-weight: 600;
}
}
.oscon{
margin:20rpx 0 10rpx;
border-radius: 16rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.osimg{
margin-left: 24%;
width: 300rpx;
height: 300rpx;
margin-right: 10rpx;
>image {
width: 300rpx;
height: 300rpx;
border-radius: 10rpx;
}
}
.osname{
display: flex;
flex: 1;
display: flex;
flex-direction: column;
margin-left: 26rpx;
.oscell{
display: flex;
flex-direction: row;
}
.txtbg{
padding: 2rpx 15rpx;
border-radius: 30rpx;
background-color: #f3fbfb;
border: 1rpx solid #7cced6;
font-size: 26rpx;
color: #333;
margin-right: 10rpx;
}
.txtbg1{
padding: 2rpx 15rpx;
border-radius: 30rpx;
background-color: #fcf2f3;
border: 1rpx solid #f09fb6;
font-size: 26rpx;
color: #333;
margin-right: 10rpx;
}
.txtbg2{
padding: 2rpx 15rpx;
border-radius: 30rpx;
background-color: #fcf3ef;
border: 1rpx solid #e99b12;
font-size: 26rpx;
color: #333;
margin-right: 10rpx;
}
.txtbg3{
padding: 2rpx 15rpx;
border-radius: 30rpx;
background-color: #f1f8f0;
border: 1rpx solid #7dbd71;
font-size: 26rpx;
color: #333;
margin-right: 10rpx;
}
.ntip{
font-size: 26rpx;
color:#1abbc3;
line-height: 32rpx;
margin-left: 10rpx;
}
.ntxt{
font-size: 26rpx;
color:#000;
line-height: 32rpx;
margin-left: 10rpx;
}
.txt{
font-size: 24rpx;
color:#666;
margin-left: 10rpx;
}
}
}
}
.sitem {
display: flex;
flex-direction: column;
flex: 1;
background-color: #FFFFFF;
/*设置最小宽度,才会让元素排不下,导致换行排列*/
min-width: calc((100% - 20rpx) / 2);
max-width: calc((100% - 20rpx) / 2);
margin-right:5rpx;
margin-left:5rpx;
margin-bottom: 10rpx;
border-radius: 16rpx;
.simg{
width: 100%;
height: 360rpx;
.img{
width: 100%;
height: 100%;
border-radius: 16rpx 16rpx 0 0;
}
}
.sname{
font-size: 30rpx;
font-weight: 600;
color: rgb(29, 29, 29);
padding: 16rpx;
}
.sinfo{
font-size: 26rpx;
color:rgb(116, 116, 116);
padding: 0 16rpx;
height: 60rpx;
line-height: 30rpx;
overflow: hidden;
position: relative;
text-overflow: ellipsis;
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:2;
}
.sprices{
padding: 0 16rpx 20rpx 16rpx;
font-size: 36rpx;
color:rgb(252, 105, 0);
border-radius: 0 0 16rpx 16rpx;
.cx{
font-size: 28rpx;
color:rgb(116, 116, 116);
text-decoration: line-through;
margin-left: 20rpx;
}
}
}
}
.select-popup {
width: 660rpx;
max-height: 80vh;
background: #fff;
border-radius: 24rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.popup-header {
position: relative;
padding: 32rpx 0;
text-align: center;
border-bottom: 1rpx solid #eee;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #000;
}
.popup-close {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
padding: 10rpx;
}
.course-info {
padding: 20rpx 32rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.course-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.course-name {
font-size: 28rpx;
font-weight: 600;
color: #000;
}
.deduct {
color: #ff6b6b;
font-weight: 600;
font-size: 24rpx;
}
/* ✅ 本次使用卡片 */
.use-card-row {
display: flex;
align-items: center;
margin-top: 12rpx;
font-size: 24rpx;
}
.use-label {
color: #999;
}
.use-name {
color: #00a89b;
font-weight: 600;
&.is-default {
color: #666; /* 默认卡用灰色,区别于手动选中 */
}
}
.default-tag {
margin-left: 10rpx;
font-size: 20rpx;
color: #ff6b6b;
background: #ffeeee;
padding: 2rpx 10rpx;
border-radius: 10rpx;
}
.popup-footer {
padding: 20rpx 32rpx 32rpx;
background: #fff;
border-top: 1rpx solid #eee;
}
.confirm-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #00a89b;
color: #fff;
font-size: 30rpx;
border-radius: 44rpx;
border: none;
&::after { border: none; }
}
.empty-tip {
padding: 60rpx 20rpx;
text-align: center;
font-size: 28rpx;
color: #999;
}
</style>