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.
yogaApp/pages/user/mycourseinfo.vue

1012 lines
24 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}} &nbsp;
<uni-rate :size="20" :readonly="true" :value="item.star" :max="item.star"/>
</view>
<view class="hright">
{{formatTime(item.startTime)+'-'+formatTime(item.endTime)}}
</view>
</view>
<view class="oscon">
<view class="osimg imgload">
<image class="osimg" v-if="item.pic" :src="getimgRemoteFile(item.pic)" mode="aspectFill"></image>
</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>
</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 class="txt">授课5000h</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">
<uni-icons type="staff" size="18" color="#00A99A"></uni-icons>
<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" v-if="item.claTimeStatus=='1'" @click="gotoorder">确认上课</button>
</view>
</view>
</view>
<!-- 信息 -->
<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">
<view class="ocell">
{{item.studentName}}
</view>
<view class="ocell">
{{item.sex==2?'男':(item.sex==1?'女':'未知')}}
</view>
<view class="ocell">
{{item.phone}}
</view>
<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>
</view>
</template>
<script>
import { myCache,getRemoteFile } from '../../utils/utils.js';
import openlogin from "../components/openlogin.vue";
export default {
components: {
openlogin
},
data() {
return {
openId:"",
phone:"",
userid:"",
id:"",
item:{
"courseTimeId": 1935590320322428931,
"claName": "普拉提A班",
"courseName": "普拉提",
"storeName": "慢瑜伽-三台子店",
"pic": null,
"claDate": "2025-10-01",
"weekDay": "星期二",
"startTime": "08:00",
"endTime": "09:30",
"staffName": "冉洪涛老师",
"studentCount": 1,
"roomName": null,
"claColor": "#409EFF",
"claTimeStatus": "1",
"bookAttendCnt": "1",
"atClassCnt": "5",
"lessCnt": "3",
},
loadStatus:"more",
studentlist:[
{
id:1,
name1:"金晓明",
name2:"精品小班课",
name3:"周卡",
name4:"990",
},
{
id:2,
name1:"金晓明",
name2:"精品小班课",
name3:"月卡",
name4:"1580",
},
{
id:3,
name1:"金晓明",
name2:"精品小班课",
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){
// this.item = JSON.parse(decodeURIComponent(options.data));
this.id = options.id;
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: {
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 '待上课 ';
}
else if(val=='3'){
return '已取消';
}
else if(val=='4'){
return '已完成';
}
else{
return '';
}
},
getimgRemoteFile(img){
if(img){
img=img.split(',')[0];
return getRemoteFile(img)
}
else{
return require("@/static/image/theme/p1.jpg")
}
},
timestampToTime(timestamp) {
const date = new Date(timestamp*1000);
const Y = date.getFullYear() + '-';
const M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-';
const D = (date.getDate() < 10 ? '0'+(date.getDate()) : date.getDate()) + ' ';
const h = (date.getHours() < 10 ? '0'+(date.getHours()) : date.getHours()) + ':';
const m = (date.getMinutes() < 10 ? '0'+(date.getMinutes()) : date.getMinutes()) + ':';
const s = (date.getSeconds() < 10 ? '0'+(date.getSeconds()) : date.getSeconds());
return Y+M+D+h+m+s;
},
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;
}
else{
return true;
}
},
goback(){
uni.navigateBack({
delta: 1
});
},
async getinfo() {
uni.showLoading({
title: '数据加载中...'
});
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;
// 再次预约
uni.showModal({
title: '提示',
content: '确定上课吗?',
cancelText: '取消',
confirmText: '确定',
success: ress => {
if (ress.confirm) {
that.orderdodo();
}
}
});
},
// 上课
async orderdodo(){
var param={
"courseTimeId": this.item.courseTimeId,
"startTime": this.item.startTime,
"endTime": this.item.endTime
};
uni.showToast({
title: '确定上课中...',
icon: 'success',
duration: 2000
});
const {data: res} = await uni.$http.post('/api/my/confirmClass', param);
console.log(res)
if(res.success=="true"){
uni.showToast({
title: '已确定上课!',
icon: 'success',
duration: 2000
});
// 刷新保存
myCache('courserefresh',1);
// 返回
uni.navigateBack({
delta: 1
});
}
else{
uni.showToast({
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>
<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;
}
.stitle{
width: 100%;
margin: 20rpx auto 20rpx auto;
text-align: center;
font-size: 30rpx;
color: #000;
font-weight: 600;
text-align: center;
}
.hcon{
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
padding-bottom: 10rpx;
margin-top: 10rpx;
border-top: 1rpx solid #e5e5e5;
border-bottom: 1rpx solid #e5e5e5;
}
.hcell{
width: 25%;
font-size: 26rpx;
font-weight: 600;
color: #000;
padding: 10rpx;
text-align: center;
}
.stcell{
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.oitem{
display: flex;
flex-direction: row;
background-color: #FFFFFF;
width: 100%;
border-bottom: 1rpx solid #e5e5e5;
align-items: center;
justify-content: center;
.ocell{
width: 25%;
font-size: 26rpx;
color: #333;
padding: 10rpx 10rpx 15rpx;
text-align: center;
word-wrap: break-word;
}
}
}
.oscon{
background-color: #f3fafa;
border-radius: 16rpx;
padding: 16rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.osimg{
width: 120rpx;
height: 120rpx;
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: 10rpx 10rpx 0 10rpx;
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{
width: 160rpx;
height: 260rpx;
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;
}
}
}
}
/* ===== 批量签到入口 ===== */
.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>