修改预约相关功能

main
@xiao-xiao0105 12 hours ago
parent 96f0f9464c
commit 0067a6a3c1

@ -51,8 +51,8 @@
</view>
</view>
<view class="rcpm">
<!-- <button type="primary" class="orderbtn" @click="openCardSelect(item)">预约</button>-->
<button type="primary" class="orderbtn" @click="gotoorder">预约</button>
<button type="primary" class="orderbtn" @click="openCardSelect(item)">预约</button>
<!-- <button type="primary" class="orderbtn" @click="gotoorder">预约</button>-->
</view>
</view>
</view>
@ -73,46 +73,53 @@
</view>
</view>
<view class="course-info">
<view class="course-row">
<text class="course-name">{{ currentCourse.courseName }}</text>
<text class="deduct">
需扣 {{ currentCourse.deductType === 'count'
? currentCourse.deductCount + '次'
: '¥' + currentCourse.deductAmount }}
</text>
<!-- 卡片为空提示 -->
<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>
<!-- ✅ 本次使用卡片 -->
<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>
<!-- 卡片横向滑动 -->
<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>
<!-- 卡片横向滑动 -->
<member-card-scroll
ref="cardScroll"
:cards="memberCards"
size="small"
:selectable="true"
:center="false"
:auto-select="false"
:default-card-id="defaultCardId"
:deduct-type="currentCourse.deductType"
:deduct-count="currentCourse.deductCount"
:deduct-amount="currentCourse.deductAmount"
@select="onCardSelect"
/>
<view class="popup-footer">
<button class="confirm-btn" @click="confirmSelect">确认选择</button>
</view>
</view>
</uni-popup>
<u-toast ref="uToast" />
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
</view>
@ -135,31 +142,27 @@ export default {
userid: "",
id: "",
item: {
courseTimeId: 1935590320322428931,
claName: "普拉提A班",
courseName: "普拉提",
storeName: "慢瑜伽-三台子店",
courseTimeId: null,
claName: "",
courseName: "",
storeName: "",
pic: null,
claDate: "2025-10-01",
weekDay: "星期二",
startTime: "08:00",
endTime: "09:30",
staffName: "冉洪涛老师",
studentCount: 1,
claDate: "",
weekDay: "",
startTime: "",
endTime: "",
staffName: "",
studentCount: null,
roomName: null,
claColor: "#409EFF",
claTimeStatus: "1",
bookAttendCnt: "1",
atClassCnt: "5",
lessCnt: "3"
claColor: "",
claTimeStatus: "",
bookAttendCnt: "",
atClassCnt: "",
lessCnt: ""
},
remark: null,
memberCards: [],
currentCourse: {
courseName: '普拉提A班',
deductType: 'COUNT',
deductCount: 1,
deductAmount: 0
},
defaultCardId: null,
// ✅ 用户手动选中的卡(父组件持有)
@ -210,9 +213,9 @@ export default {
isCardUsable(card) {
if (!card) return false;
if (card.status && card.status !== 'ACTIVE') return false;
if (card.expireDate) {
if (card.expiryDate) {
const now = new Date();
const expire = new Date(card.expireDate.replace(/-/g, '/'));
const expire = new Date(card.expiryDate.replace(/-/g, '/'));
if (now > expire) return false;
}
const type = this.currentCourse.deductType;
@ -235,15 +238,19 @@ export default {
// ✅ 打开弹窗:重置手动选择,用默认卡
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",
{visitStore:user.visitStore,
courseId:item.courseId
});
console.log("memberCards",memberCards)
item);
//如果为空 则没有适合的卡片
this.memberCards = memberCards.data
this.selectedCard = null;
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();
},
@ -252,37 +259,53 @@ export default {
},
async confirmSelect() {
// ✅ 直接从父组件 computed 拿有效卡
const card = this.effectiveCard;
if (!card) {
uni.showToast({ title: '无可用会员卡', icon: 'none' });
return;
}
const { data: res } = await uni.$http.post('/api/course/bookCourse', {
courseTimeId: this.item.courseTimeId,
cardId: card.id
// 开启loading
uni.showLoading({
title: '预约中...',
mask: true // 遮罩,防止重复点击
});
if (res && res.success) {
myCache('courserefresh', 1);
uni.showModal({
title: '提示',
content: '课程已预约!',
cancelText: '返回',
confirmText: '确定',
success: ress => {
uni.navigateBack({ delta: 1 });
}
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
});
} else {
uni.showModal({
title: '提示',
content: res.message ? res.message : '课程预约失败!',
cancelText: '取消',
confirmText: '确定'
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);
},
@ -819,5 +842,11 @@ export default {
border: none;
&::after { border: none; }
}
.empty-tip {
padding: 60rpx 20rpx;
text-align: center;
font-size: 28rpx;
color: #999;
}
</style>

@ -23,19 +23,24 @@
</view>
<view class="oscell">
预约状态:
<view v-if="info.bookStatus==1" class="ztip">教练确认</view>
<view v-if="info.bookStatus==1" class="ztip">预约中</view>
<view v-else-if="info.bookStatus==0" class="jtip">预约中</view>
<view v-else-if="info.bookStatus==2" class="htip">待上课</view>
<view v-else-if="info.bookStatus==2" class="htip">预约成功,待上课</view>
<view v-else-if="info.bookStatus==4" class="wtip">已完成</view>
<view v-else-if="info.bookStatus==3" class="ctip">已取消</view>
</view>
<view class="oscell">
签到状态:
<view v-if="info.checkIn==1" class="ztip">已签到</view>
<view v-else-if="info.checkIn==0" class="jtip">未签到</view>
</view>
<view class="oscell">
授课教练:
<text class="txt">{{info.teacherName }}</text>
</view>
<view class="oscell">
上课教室:
<text class="txt">{{info.roomName }}</text>
<text class="txt">{{info.roomName?info.roomName:'待定'}}</text>
</view>
<view class="oscell">
上课时间:

@ -19,11 +19,13 @@
<view class="hright">
<!-- 1:待上课,2:预约中,3:待评价 ,4:已完成 ,5:已取消 -->
<!-- 描述:0->预约中;1->教练确认;2->店长确认/预约成功/待上课 3->已取消;4->已完成/待评价; -->
<view v-if="item.bookStatus==1" class="ztip">教练确认</view>
<view v-if="item.bookStatus==1" class="ztip">预约中</view>
<view v-else-if="item.bookStatus==0" class="jtip">预约中</view>
<view v-else-if="item.bookStatus==2" class="htip">待上课</view>
<view v-else-if="item.bookStatus==2" class="htip">预约成功,待上课</view>
<view v-else-if="item.bookStatus==4" class="wtip">已完成</view>
<view v-else-if="item.bookStatus==3" class="ctip">已取消</view>
<view v-else-if="item.bookStatus==5" class="ctip">预约失败</view>
</view>
</view>
@ -35,7 +37,8 @@
<text class="ntxt">{{item.courseName}} {{item.claName}} </text>
<text class="txt"> 教练:{{item.teacherName || '-'}}</text>
<text class="txt"> 上课时间:{{ item.claDate+' '+item.startTime+'-'+item.endTime}}</text>
<text class="txt"> 教室:{{item.roomName}}</text>
<text class="txt"> 教室:{{item.roomName?item.roomName:'待定'}}</text>
<text class="txt"> 使用会员卡号:{{item.cardNo}}</text>
</view>
</view>
@ -49,7 +52,7 @@
<!-- 描述:0->预约中;1->教练确认;2->店长确认/预约成功/待上课 3->已取消;4->已完成/待评价; -->
<button class="conbtn" @click="gototeacher(item)">联系教练</button>
<!-- <button v-if="item.bookStatus==2||item.bookStatus==1" type="primary" class="cancelbtn" :data-id="item.courseTimeId" @click="cancelkcdo">取消课程</button>-->
<button v-if="'0,1,2'.includes(item.bookStatus)" class="cancelbtn" :data-id="item.courseTimeId" @click="cancelyydo">取消预约</button>
<button v-if="'0,1,2'.includes(item.bookStatus) && item.checkIn!=1" class="cancelbtn" :data-id="item.courseTimeId" @click="cancelyydo(item)">取消预约</button>
<!-- <button v-if="item.bookStatus==0" type="primary" class="paybtn" :data-id="item.courseTimeId" @click="checkdo($event,item)">确认上课</button>-->
<button v-if="item.bookStatus==4" class="viewbtn" :data-id="item.courseTimeId" @click="orderdo($event,item)">再次预约</button>
<!-- <button v-if="item.bookStatus==4" type="primary" class="gzbtn" :data-id="item.courseTimeId" @click="gotoevaluate($event,item)">评价</button>-->
@ -677,7 +680,7 @@ import { mapState, mapGetters, mapActions } from 'vuex';
// 取消预约
cancelyydo(e){
console.log(e);
var id= e.currentTarget.dataset.id;
// var id= e.currentTarget.dataset.id;
var that = this;
//取消预约
uni.showModal({
@ -687,16 +690,14 @@ import { mapState, mapGetters, mapActions } from 'vuex';
confirmText: '确定',
success: ress => {
if (ress.confirm) {
that.delyydo(id);
that.delyydo(e);
}
}
});
},
// 取消预约
async delyydo(id){
var param={
"courseTimeId":id
};
async delyydo(e){
var param=e;
uni.showToast({
title: '取消中...',
icon: 'success',

@ -12,10 +12,12 @@
<text style="margin-left: 30rpx;">性别:{{item.sex==2?'男':(item.sex==1?'女':'未知')}}</text>
</view>
<text>电话:{{item.phone?item.phone:'-'}}</text>
<text>会员卡号:{{item.cardNo?item.cardNo:'-'}}</text>
</view>
<view class="hright">
<!-- 1:待上课,2:预约中,3:待评价 ,4:已完成 ,5:已取消 -->
<!-- 描述:0->预约中;1->教练确认;2->店长确认/预约成功/待上课 3->已取消;4->已完成/待评价; -->
<view v-if="item.bookStatus==1" class="ztip">教练确认</view>
<view v-else-if="item.bookStatus==0" class="jtip">预约中</view>
<view v-else-if="item.bookStatus==2" class="htip">待上课</view>
@ -139,6 +141,7 @@
// 全部
this.loadStatus="noMore";
this.list=res.data;
console.log('this.list',this.list)
this.$forceUpdate();
}
else{

@ -8,16 +8,6 @@
<view class="uid">ID:{{ userInfo.id || userInfo.userId || '-' }}</view>
</view>
<!-- 黑名单开关行 仿微信 -->
<!-- <view class="black-row">-->
<!-- <text class="black-label">加入黑名单</text>-->
<!-- <switch-->
<!-- :checked="isBlacked"-->
<!-- active-color="#00a89b"-->
<!-- @change="onBlackSwitchChange"-->
<!-- />-->
<!-- </view>-->
<view class="black-row">
<text class="black-label">加入黑名单</text>
<view class="custom-switch" :class="{ on: isBlacked }" @tap="handleSwitchTap">
@ -84,45 +74,7 @@ export default {
console.error('查询黑名单状态失败', e)
}
},
/**
* switch点击,不直接改状态,弹出确认弹窗
*/
onBlackSwitchChange(e) {
// e.detail.value 是用户想要切换到的目标状态
const targetStatus = e.detail.value
const title = targetStatus ? '加入黑名单' : '取消黑名单'
const content = targetStatus
? '加入黑名单后,对方将无法给你发送消息,确定?'
: '确定将该用户移出黑名单?'
uni.showModal({
title,
content,
success: async (res) => {
if (!res.confirm) {
// 用户点取消,不做任何操作,switch会自动回弹回旧状态,无需手动赋值
return
}
try {
const url = targetStatus ? '/api/blacklist/add?targetUid=' : '/api/blacklist/cancel?targetUid='
await uni.$http.post(`${url}${this.userId}`)
// 接口成功才更新本地状态
this.isBlacked = targetStatus
uni.showToast({
title: this.isBlacked ? '已加入黑名单' : '已取消黑名单',
icon: 'none'
})
} catch (err) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
})
},
onSendMessage() {
// if (this.isBlacked) {
// uni.showToast({ title: '你已将对方加入黑名单', icon: 'none' })
// return
// }
let chat = {
type: 'PRIVATE',
targetId: this.userId,
@ -190,7 +142,7 @@ export default {
onBackPress(e) {
// 来源是左上角返回 / 安卓返回键
if(e.from === 'backbutton'){
if(this.isBlacked){
if(this.isBlacked && this.chatIndex){
// 拉黑成功,自定义跳转chat
this.goBackChat()
return true // return true 阻止原生默认返回

@ -1,7 +1,7 @@
<template>
<view class="member-card-scroll">
<scroll-view
class="card-scroll"
class="card-scroll-wrap"
scroll-x="true"
scroll-with-animation
:show-scrollbar="false"
@ -22,7 +22,7 @@
/>
<!-- v-if="selectable && !isCardUsable(card)"-->
<view class="unusable-tip">
<text v-if=" card.status == 'ACTIVE'" style="color:#00a89b;" >正常</text>
<text v-if=" isCardUsable(card)" style="color:#00a89b;" >正常</text>
<text v-else style="color: #ff6b6b;">{{ getUnusableReason(card) }}</text>
</view>
</view>
@ -99,15 +99,20 @@ export default {
},
// 动态计算首尾 padding
innerStyle() {
if (!this.center) {
// ✅ 只要卡片总数=1,不管外部center参数传啥,自动触发居中计算
const isSingleCard = this.cards && this.cards.length === 1;
if (!this.center && !isSingleCard) {
// 多卡正常:保留原来的20rpx边距
return { paddingLeft: '20rpx', paddingRight: '20rpx' };
}
// 居中计算:左右padding = (屏幕宽度750rpx - 卡片宽度) / 2,刚好把卡片推到正中间
const padding = Math.max(0, (this.screenWidthRpx - this.cardWidthRpx) / 2);
return {
paddingLeft: padding + 'rpx',
paddingLeft: padding-30 + 'rpx',
paddingRight: padding + 'rpx'
};
}
},
watch: {
cards: {
@ -183,9 +188,9 @@ export default {
if (!card) return false;
// ✅ 只有 ACTIVE 才可用
if (card.status && card.status !== 'ACTIVE') return false;
if (card.expireDate) {
if (card.expiryDate) {
const now = new Date();
const expire = new Date(card.expireDate.replace(/-/g, '/'));
const expire = new Date(card.expiryDate.replace(/-/g, '/'));
if (now > expire) return false;
}
if (card.type === 'count') {
@ -214,9 +219,9 @@ export default {
if (card.status && card.status == 'DEPLETED') {
return '卡片额度已用完';
}
if (card.expireDate) {
if (card.expiryDate) {
const now = new Date();
const expire = new Date(card.expireDate.replace(/-/g, '/'));
const expire = new Date(card.expiryDate.replace(/-/g, '/'));
if (now > expire) return '卡片已过期';
}
if (card.type === 'count') {
@ -299,4 +304,17 @@ export default {
background: #00a89b;
}
}
.card-scroll-wrap {
width: 100%;
overflow-x: auto;
padding: 20rpx 0;
/* 多卡时默认左对齐横向滚动 */
display: flex;
gap: 20rpx;
}
</style>

@ -21,22 +21,28 @@
<view class="card-middle">
<view class="balance-box">
<text class="balance-label">
{{ card.chargeType === 'count' ? '剩余次数' : card.chargeType ==='days'?'剩余天数':'卡内余额' }}
{{ card.chargeType === 'count' ? '剩余次数' : card.chargeType ==='days'?'剩余天数':'可用余额' }}
</text>
<view class="balance-value-box">
<text v-if="card.chargeType == 'total_fee'" class="unit">¥</text>
<text class="balance-value">
{{ card.chargeType === 'count' ? (card.remainingCount || 0) : card.chargeType ==='days'?card.remainingDays: formatAmount(card.remainingTotalFee) }}
{{
card.chargeType === 'count'
? Math.max((card.remainingCount || 0) - (card.freezeCount || 0), 0)
: card.chargeType === 'days'
? (card.remainingDays || 0)
: formatAmount( Math.max( (Number(card.remainingTotalFee)||0) - (Number(card.freezeAmount)||0), 0 ) )
}}
</text>
<text v-if="card.chargeType === 'count'" class="unit">次</text>
<text v-if="card.chargeType === 'days'" class="unit">天</text>
</view>
</view>
<!-- 冻结信息(可选) -->
<view v-if="card.chargeType !== 'days' || card.frozenCount > 0 || card.frozenAmount > 0" class="frozen-box">
<view v-if="card.chargeType !== 'days' || card.freezeCount > 0 || card.freezeAmount > 0" class="frozen-box">
<text class="frozen-label">预约占用</text>
<text class="frozen-value">
{{ card.type === 'count' ? card.frozenCount + '次' : '¥' + formatAmount(card.frozenAmount) }}
{{ card.type === 'count' ? card.freezeCount + '次' : '¥' + formatAmount(card.freezeAmount) }}
</text>
</view>
</view>
@ -45,7 +51,7 @@
<view class="card-bottom">
<text class="card-no">NO. {{ card.cardNo || '--' }}</text>
<text class="card-expire">
{{ card.expireDate ? '有效期至 ' + card.expireDate : '长期有效' }}
{{ card.expiryDate ? '有效期至 ' + card.expiryDate : '长期有效' }}
</text>
</view>
</view>

@ -74,7 +74,7 @@
</view>
<view class="info-row">
<text class="info-label">有效期至</text>
<text class="info-value">{{ card.expireDate || '长期有效' }}</text>
<text class="info-value">{{ card.expiryDate || '长期有效' }}</text>
</view>
<view class="info-row">
<text class="info-label">状态</text>

@ -56,11 +56,12 @@
<!-- <view v-if="item.claName=='团课'" class="txtbg">{{item.claName}}</view>
<view v-else class="txtbg1">{{item.claName}}</view> -->
<!-- <view v-if="item.claName" class="txtbg1">{{item.claName}}</view>-->
<uni-rate :size="15" :readonly="true" :value="item.star?item.star:0" :max="5"/>
</view>
<view class="oscell" v-if="item.storeName">
<uni-icons type="paperclip" size="18" color="#00A99A"></uni-icons>
<view class="ntip">{{item.courseName}}</view>
<view class="ntip" style="margin-right: 10rpx;">{{item.courseName}}</view>
<uni-rate :size="15" :readonly="true" :value="item.star?item.star:0" :max="5"/>
</view>
<view class="oscell">
<uni-icons type="calendar" size="18" color="#00A99A"></uni-icons>
@ -74,7 +75,7 @@
</view>
<view class="oscell">
<uni-icons type="map-pin-ellipse" size="18" color="#00A99A"></uni-icons>
<view class="ntxt">{{item.roomName}}</view>
<view class="ntxt">{{item.roomName?item.roomName:'教室待定'}}</view>
</view>
<view class="oscell">
<uni-icons type="checkbox" size="18" color="#00A99A"></uni-icons>

@ -8,10 +8,11 @@
<view class="otop">
<view class="hleft">
{{item.courseName}}
{{item.courseName}} &nbsp;
<uni-rate :size="20" :readonly="true" :value="item.star" :max="item.star"/>
</view>
<view class="hright">
{{item.startTime+'-'+item.endTime}}
{{formatTime(item.startTime)+'-'+formatTime(item.endTime)}}
</view>
</view>
@ -21,9 +22,10 @@
</view>
<view class="osname">
<view class="oscell">
{{item.courseTypeName}}
<view v-if="item.courseTypeName=='团课'" class="txtbg">{{item.courseTypeName}}</view>
<view v-else-if="item.courseTypeName" class="txtbg1">{{item.courseTypeName}}</view>
<uni-rate :size="20" :readonly="true" :value="item.star" :max="item.star"/>
</view>
<view class="oscell" v-if="item.storeName">
<uni-icons type="shop" size="18" color="#00A99A"></uni-icons>
@ -49,12 +51,17 @@
</view>
<view class="oscell" v-if="item.lessCnt">
<uni-icons type="staff" size="18" color="#00A99A"></uni-icons>
<view class="ntxt" >预约人数{{item.lessCnt}}人</view>
<view class="ntxt" >满{{item.lessCnt}}人开课</view>
</view>
<view class="oscell" v-if="item.claTimeStatus">
<uni-icons type="wallet" size="18" color="#00A99A"></uni-icons>
<view class="ntxt" v-if="item.claTimeStatus=='1'"> 未上课 </view>
<view class="ntxt" v-else>已确认上课 </view>
</view>
</view>
</view>
<view class="rcpm">
<button type="primary" class="orderbtn" @click="gotoorder">确认上课</button>
<view class="rcpm" >
<button type="primary" class="orderbtn" v-if="item.claTimeStatus=='1'" @click="gotoorder">确认上课</button>
</view>
</view>
</view>
@ -62,11 +69,16 @@
<!-- 信息 -->
<view class="yycon tcon">
<view class="stitle">学员名单</view>
<view class="batch-bar" v-if="checkableList.length > 0">
<view class="batch-tip">可签到 {{ checkableList.length }} 人</view>
<button class="batch-btn" @click="openBatch">批量签到</button>
</view>
<view class="hcon">
<view class="hcell">学员姓名</view>
<view class="hcell">性别</view>
<view class="hcell">电话</view>
<view class="hcell">预约状态</view>
<view class="hcell">是否签到</view>
</view>
<view class="stcell" v-if="studentlist.length>0">
<view v-for="(item, index) in studentlist" :key="index" class="oitem">
@ -74,7 +86,7 @@
{{item.studentName}}
</view>
<view class="ocell">
{{item.sex=1?'女':(item.sex=2?'男':'未知')}}
{{item.sex==2?'男':(item.sex==1?'女':'未知')}}
</view>
<view class="ocell">
{{item.phone}}
@ -82,12 +94,65 @@
<view class="ocell">
{{ retstr(item.bookStatus)}}
</view>
<view class="ocell">
{{ item.checkIn=='1'?'已签到':'未签到'}}
</view>
</view>
</view>
<uni-load-more v-if="studentlist.length<1" iconType="circle" :status="loadStatus" />
</view>
</view>
<!-- 批量签到弹窗 -->
<view class="batch-mask" v-if="batchVisible" @click="closeBatch">
<view class="batch-dialog" @click.stop>
<view class="bd-title">批量签到</view>
<view class="bd-sub">已选 {{ selectedIds.length }} / {{ checkableList.length }} 人</view>
<!-- 全选 -->
<view class="bd-all" @click="toggleAll">
<view class="cb" :class="{ checked: isAllSelected }">
<text v-if="isAllSelected" class="cb-icon">✓</text>
</view>
<view class="bd-info">
<view class="bd-name">全选</view>
</view>
</view>
<!-- 可签到名单 -->
<scroll-view scroll-y class="bd-list">
<view
v-for="stu in checkableList"
:key="getBookId(stu)"
class="bd-item"
@click="toggleOne(stu)"
>
<view class="cb" :class="{ checked: isChecked(stu) }">
<text v-if="isChecked(stu)" class="cb-icon">✓</text>
</view>
<view class="bd-info">
<view class="bd-name">{{ stu.studentName }}</view>
<view class="bd-meta">{{ stu.phone }} · {{ retstr(stu.bookStatus) }}</view>
</view>
</view>
<view v-if="checkableList.length === 0" class="bd-empty">暂无可签到学员</view>
</scroll-view>
<view class="bd-btns">
<button class="bd-cancel" @click="closeBatch">取消</button>
<button
class="bd-confirm"
:disabled="selectedIds.length === 0 || submitting"
@click="doBatchCheckIn"
>
{{ submitting ? '提交中...' : '确认签到(' + selectedIds.length + ')' }}
</button>
</view>
</view>
</view>
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
@ -150,9 +215,24 @@
name3:"年卡",
name4:"3390",
},
]
],
batchVisible: false, // 弹窗开关
selectedIds: [], // 已勾选的 bookId
submitting: false // 防重复提交
};
},
computed: {
// 可签到数据:bookStatus ∈ [0,1,2] 且 checkIn === 0
checkableList() {
return (this.studentlist || []).filter(stu => this.isCheckable(stu));
},
// 是否全选
isAllSelected() {
return this.checkableList.length > 0 &&
this.selectedIds.length === this.checkableList.length;
}
},
onLoad(options) {
// 课程详情
if(options.id){
@ -173,11 +253,19 @@
}
},
methods: {
formatTime(timeStr) {
// 假设 timeStr 格式为 "HH:MM:SS"
return timeStr.slice(0, 5); // 取前5位 "HH:MM"
},
retstr(val){
// 描述:0->预约中;1->教练确认;2->店长确认/预约成功/待上课 3->已取消;4->已完成/待评价;
if(val=='0'){
return '预约中';
}
else if (val == '1')
{
return '教练确认';
}
else if(val=='2'){
return '待上课 ';
}
@ -242,15 +330,18 @@
uni.showLoading({
title: '数据加载中...'
});
const {data: res} = await uni.$http.post('/api/my/courseTimeDetail', {courseTimeId:this.id});
if(res&&res.data){
var data=res.data;
// 信息
this.item=data.courseDetail;
this.studentlist=data.studentList;
this.loadStatus='noMore';
this.$forceUpdate();
}
try {
const { data: res } = await uni.$http.post('/api/my/courseTimeDetail', { courseTimeId: this.id });
if (res && res.data) {
var data = res.data;
this.item = data.courseDetail;
this.studentlist = data.studentList;
this.loadStatus = 'noMore';
this.$forceUpdate();
}
} finally {
uni.hideLoading(); // ← 关键
}
},
gotoorder(){
var that = this;
@ -280,7 +371,8 @@
duration: 2000
});
const {data: res} = await uni.$http.post('/api/my/confirmClass', param);
if(res.msg=="successed"){
console.log(res)
if(res.success=="true"){
uni.showToast({
title: '已确定上课!',
icon: 'success',
@ -295,14 +387,115 @@
}
else{
uni.showToast({
title: res.msg? res.msg:'确定上课失败!',
icon: 'error',
title: res.message? res.message:'确定上课失败!',
icon: 'none',
duration: 2000
});
}
},
}
// 取预约记录主键(兼容 bookId / id 两种字段名)
getBookId(stu) {
return stu.bookId || stu.id;
},
// 是否可签到
isCheckable(stu) {
if (!stu) return false;
const rawStatus = stu.bookStatus;
if (rawStatus === null || rawStatus === undefined || rawStatus === '') return false;
const status = Number(rawStatus);
const checkIn = Number(stu.checkIn === null || stu.checkIn === undefined || stu.checkIn === '' ? 0 : stu.checkIn);
return [0, 1, 2].includes(status) && checkIn === 0;
},
// 是否已勾选
isChecked(stu) {
return this.selectedIds.includes(this.getBookId(stu));
},
// 打开弹窗(默认全选)
openBatch() {
if (this.checkableList.length === 0) {
uni.showToast({ title: '暂无可签到学员', icon: 'none' });
return;
}
this.selectedIds = this.checkableList.map(stu => this.getBookId(stu));
this.batchVisible = true;
},
// 关闭弹窗
closeBatch() {
if (this.submitting) return;
this.batchVisible = false;
},
// 全选/取消全选
toggleAll() {
if (this.isAllSelected) {
this.selectedIds = [];
} else {
this.selectedIds = this.checkableList.map(stu => this.getBookId(stu));
}
},
// 单选切换
toggleOne(stu) {
const id = this.getBookId(stu);
const idx = this.selectedIds.indexOf(id);
if (idx > -1) {
this.selectedIds.splice(idx, 1);
} else {
this.selectedIds.push(id);
}
},
// 提交批量签到
async doBatchCheckIn() {
if (this.submitting) return;
if (this.selectedIds.length === 0) {
uni.showToast({ title: '请至少勾选一名学员', icon: 'none' });
return;
}
const submitCount = this.selectedIds.length;
const bookIds = this.selectedIds.slice();
this.submitting = true;
uni.showLoading({ title: '提交中...', mask: true });
try {
const { data: res } = await uni.$http.post('/api/my/batchCheckIn', {
bookIds: bookIds
});
if (res && (res.success === true || res.success === 'true')) {
this.batchVisible = false;
this.selectedIds = [];
myCache('courserefresh', 1);
uni.showToast({
title: `已签到 ${submitCount} 人`.trim() || '签到成功',
icon: 'success',
duration: 2000
});
// 刷新名单
await this.getinfo();
} else {
uni.showToast({
title: (res && res.msg) ? res.msg : '批量签到失败!',
icon: 'none',
duration: 2500
});
}
} catch (e) {
uni.showToast({ title: '签到请求异常,请稍后重试', icon: 'none', duration: 2500 });
} finally {
uni.hideLoading();
this.submitting = false;
}
}
}
}
</script>
@ -670,6 +863,149 @@
}
}
}
/* ===== 批量签到入口 ===== */
.batch-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rpx 16rpx;
}
.batch-tip {
font-size: 24rpx;
color: #999;
}
.batch-btn {
margin: 0;
height: 64rpx;
line-height: 64rpx;
padding: 0 30rpx;
font-size: 26rpx;
color: #fff;
background-color: #00A99A !important;
border-radius: 32rpx;
border: none;
}
.batch-btn::after {
border: none !important;
}
/* ===== 批量签到弹窗 ===== */
.batch-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
}
.batch-dialog {
width: 640rpx;
max-height: 72vh;
background: #fff;
border-radius: 20rpx;
display: flex;
flex-direction: column;
overflow: hidden;
}
.bd-title {
text-align: center;
font-size: 32rpx;
font-weight: 600;
color: #000;
padding: 30rpx 0 8rpx;
}
.bd-sub {
text-align: center;
font-size: 24rpx;
color: #999;
padding-bottom: 16rpx;
}
.bd-all,
.bd-item {
display: flex;
align-items: center;
padding: 22rpx 30rpx;
border-top: 1rpx solid #f2f2f2;
}
.cb {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #c8c8c8;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
box-sizing: border-box;
}
.cb.checked {
background: #00A99A;
border-color: #00A99A;
}
.cb-icon {
color: #fff;
font-size: 22rpx;
line-height: 1;
}
.bd-list {
max-height: 460rpx;
}
.bd-info {
flex: 1;
}
.bd-name {
font-size: 28rpx;
color: #333;
}
.bd-meta {
font-size: 24rpx;
color: #999;
margin-top: 6rpx;
}
.bd-empty {
text-align: center;
font-size: 26rpx;
color: #999;
padding: 80rpx 0;
}
.bd-btns {
display: flex;
padding: 20rpx 30rpx 30rpx;
}
.bd-cancel {
flex: 1;
margin: 0 20rpx 0 0;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #666;
background: #f5f5f5 !important;
border-radius: 40rpx;
border: none;
}
.bd-confirm {
flex: 1;
margin: 0;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #fff;
background: #00A99A !important;
border-radius: 40rpx;
border: none;
}
.bd-cancel::after,
.bd-confirm::after {
border: none !important;
}
.bd-confirm[disabled] {
opacity: 0.5;
}
</style>

@ -13,20 +13,27 @@
</view>
</view>
<!-- <view class="s-list" >-->
<!-- <view class="s-header">-->
<!-- <view class="tit-box">-->
<!-- <text class="tit">我的会员卡</text>-->
<!-- </view>-->
<!-- </view>-->
<!-- &lt;!&ndash; ✅ 会员卡横向滑动(不可选,纯展示) &ndash;&gt;-->
<!-- <member-card-scroll-->
<!-- :cards="memberCards"-->
<!-- size="small"-->
<!-- :selectable="false"-->
<!-- @card-click="onCardClick"-->
<!-- />-->
<!-- </view>-->
<view class="s-list" >
<view class="s-header">
<view class="tit-box">
<text class="tit">我的会员卡</text>
</view>
</view>
<!-- 卡片为空提示 -->
<view v-if="!memberCards || memberCards.length === 0" class="empty-tip">
暂无会员卡
</view>
<view v-else>
<!-- ✅ 会员卡横向滑动(不可选,纯展示) -->
<member-card-scroll
:cards="memberCards"
size="small"
:selectable="false"
@card-click="onCardClick"
/>
</view>
</view>
<!-- 菜单1 -->
<!-- <view class="s-list">
<view class="s-header">
@ -115,7 +122,7 @@
<view class="title">{{servers[13].name}}</view>
</view>
<!-- 学员签到 教练/顾问 -->
<view v-if="ifview('103,104')" class="product-item" @click="gotoServe(14)">
<view v-if="ifview('103,104,105')" class="product-item" @click="gotoServe(14)">
<view class="image-wrapper">
<image class="imgicon" :src="servers[14].img" mode="aspectFill" lazy-load></image>
</view>
@ -219,77 +226,7 @@
avatar:"../../static/image/logo.png",
roleId:[],
memberCards: [
// 1. 次卡:正常,余额充足
{
id: 1001,
cardNo: '88880001',
chargeType: 'count',//收费模式 count:课时 days:时间 total_fee:储值
cardName: '次卡',
remainingCount: 8,
frozenCount: 1,
remainingTotalFee: 0,
frozenAmount: 0,
expireDate: '2027-12-31',
status: 'ACTIVE'
},
// 2. 次卡:剩余次数不足(只剩1次)
{
id: 1002,
type: 'COUNT',
cardName: '次卡',
name: '体验次卡',
cardNo: '88880002',
remainingCount: 1,
frozenCount: 0,
remainAmount: 0,
frozenAmount: 0,
expireDate: '2026-12-31',
status: 'NORMAL'
},
// 3. 储值卡:余额充足
{
id: 1003,
type: 'AMOUNT',
cardName: '储值卡',
name: '瑜伽储值卡',
cardNo: '88880003',
remainingCount: 0,
frozenCount: 0,
remainAmount: 1280.50,
frozenAmount: 200.00,
expireDate: '2028-06-30',
status: 'NORMAL'
},
// 4. 储值卡:余额不足(只剩50元)
{
id: 1004,
type: 'AMOUNT',
cardName: '储值卡',
name: '小额储值卡',
cardNo: '88880004',
remainingCount: 0,
frozenCount: 0,
remainAmount: 50.00,
frozenAmount: 0,
expireDate: '2027-01-01',
status: 'NORMAL'
},
// 5. 已过期的次卡
{
id: 1005,
type: 'COUNT',
cardName: '次卡',
name: '过期次卡',
cardNo: '88880005',
remainingCount: 5,
frozenCount: 0,
remainAmount: 0,
frozenAmount: 0,
expireDate: '2025-01-01', // 已过期
status: 'NORMAL'
}
],
favoritesnum:0,//我的收藏数量
menus: [
@ -425,7 +362,7 @@
this.signature = user.signature?user.signature:this.signature;
this.roleId=user.roleId?user.roleId:[];
this.getmember();
this.getMemberCards();
// if(myCache("ifuserEdit"))
// {
// myCache("ifuserEdit",false)
@ -1238,5 +1175,10 @@
align-items: center;
}
}
.empty-tip {
padding: 60rpx 20rpx;
text-align: center;
font-size: 28rpx;
color: #999;
}
</style>

Loading…
Cancel
Save