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

8 months ago
<template>
<view class="page">
8 months ago
<view class="oinfo">
8 months ago
<view class="yycon scell" v-if="item">
<view class="oitem">
8 months ago
<view class="otop">
<view class="hleft">
{{item.courseName}} &nbsp;
<uni-rate :size="20" :readonly="true" :value="item.star" :max="item.star"/>
8 months ago
</view>
<view class="hright">
{{formatTime(item.startTime)+'-'+formatTime(item.endTime)}}
8 months ago
</view>
</view>
8 months ago
<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}}
1 month ago
<view v-if="item.courseTypeName=='团课'" class="txtbg">{{item.courseTypeName}}</view>
<view v-else-if="item.courseTypeName" class="txtbg1">{{item.courseTypeName}}</view>
8 months ago
</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>
8 months ago
</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>
8 months ago
</view>
</view>
<view class="rcpm" >
<button type="primary" class="orderbtn" v-if="item.claTimeStatus=='1'" @click="gotoorder">确认上课</button>
</view>
8 months ago
</view>
</view>
8 months ago
<!-- 信息 -->
<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>
8 months ago
<view class="hcon">
<view class="hcell">学员姓名</view>
<view class="hcell">性别</view>
<view class="hcell">电话</view>
<view class="hcell">预约状态</view>
<view class="hcell">是否签到</view>
8 months ago
</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?'女':'未知')}}
8 months ago
</view>
<view class="ocell">
{{item.phone}}
</view>
<view class="ocell">
{{ retstr(item.bookStatus)}}
</view>
<view class="ocell">
{{ item.checkIn=='1'?'已签到':'未签到'}}
</view>
8 months ago
</view>
</view>
<uni-load-more v-if="studentlist.length<1" iconType="circle" :status="loadStatus" />
</view>
8 months ago
</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>
8 months ago
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
8 months ago
</view>
</template>
<script>
1 month ago
import { myCache,getRemoteFile } from '../../utils/utils.js';
8 months ago
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 // 防重复提交
8 months ago
};
},
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;
}
},
8 months ago
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"
},
8 months ago
retstr(val){
// 描述:0->预约中;1->教练确认;2->店长确认/预约成功/待上课 3->已取消;4->已完成/待评价;
if(val=='0'){
return '预约中';
}
else if (val == '1')
{
return '教练确认';
}
8 months ago
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(); // ← 关键
}
8 months ago
},
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,
8 months ago
"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"){
8 months ago
uni.showToast({
title: '已确定上课!',
icon: 'success',
duration: 2000
});
// 刷新保存
myCache('courserefresh',1);
// 返回
uni.navigateBack({
delta: 1
});
}
else{
uni.showToast({
title: res.message? res.message:'确定上课失败!',
icon: 'none',
8 months ago
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;
}
}
}
8 months ago
}
</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;
7 months ago
word-wrap: break-word;
8 months ago
}
}
}
8 months ago
.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;
}
}
}
8 months ago
.oinfo{
padding:20rpx;
position: relative;
margin-bottom: 20rpx;
.yycon{
display: flex;
flex-direction: column;
justify-content: center;
background-color: #FFFFFF;
padding: 20rpx 20rpx;
}
}
8 months ago
.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;
}
}
}
8 months ago
.red{
color:#ff3d0e;
}
.green{
color:#00cd00;
}
8 months ago
::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;
}
8 months ago
.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;
}
8 months ago
.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;
}
8 months ago
</style>