修改:拉黑成功 删除对话框

main
15004070936 1 week ago
parent 97819e4d8d
commit 96f0f9464c

@ -7,8 +7,8 @@
{{ $date.toTimeText(msgInfo.sendTime) }}
</view>
<view class="message-normal" v-else-if="isNormal" :class="{ 'message-mine': msgInfo.selfSend }">
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
:name="showName" size="small" :selfSend="msgInfo.selfSend" ></head-image>
<head-image class="avatar" @longpress.prevent="$emit('longPressHead')" :id="msgInfo.sendId" :url="headImage"
:name="showName" size="small" :selfSend="msgInfo.selfSend" :chatIndex="chatIndex"></head-image>
<view class="content">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="top">
<text>{{ showName }}</text>
@ -119,6 +119,10 @@ export default {
type: Object,
required: true
},
chatIndex: {
type: Number,
required: true
},
groupMembers: {
type: Array
}

@ -42,12 +42,16 @@ export default {
type: Boolean,
default: false
},
chatIndex: {
type: Number,
required: true
},
},
methods: {
showUserInfo(e) {
if (!this.selfSend && this.id && this.id > 0) {
uni.navigateTo({
url: "/pages/chatbox/im-user-info?id=" + this.id
url: `/pages/chatbox/im-user-info?id=${this.id}&chatIndex=${this.chatIndex}`
})
}
}

@ -1,291 +1,403 @@
<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 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 class="oscell">
<!-- <view v-if="item.claName=='团课'" class="txtbg">{{item.claName}}</view>
<view v-else class="txtbg1">{{item.claName?item.claName:'--'}}</view> -->
<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 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">
<view class="txt" style="margin-left: 46rpx;">满{{item.lessCnt}}人开课</view>
</view>
</view>
</view>
<view class="rcpm">
<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 class="olist">
<view class="ltxt">会籍类型:</view>
<view class="rtxt">精品团课</view>
</view>
<view class="olist">
<view class="ltxt">内容功效:</view>
<view class="rtxt">体育锻炼可以提高心肺的功能,提高心肌功能和血管弹性,还可以消耗体内多余的脂肪,促进肌肉的发育,增加韧带的强度,缓解精神紧张和压力</view>
</view>
<view class="olist">
<view class="ltxt">适合人群:</view>
<view class="rtxt">所有人</view>
</view>
<view class="olist">
<view class="ltxt">适合人群:</view>
<view class="rtxt">
● 可预约3天内课程,今天07:00后可约第3天的课<br>
● 预约课程请提前35分钟 <br>
● 取消预约请提前40分钟 <br>
● 爽约惩罚:在30天内爽约累计3次,则禁止约课3天<br>
● 签到说明:在课程开始前30分钟内至课程结束后30分钟内可以签到,其余时间不可签到<br>
● 请至少提前10分钟进教室(保持手机静音)<br>
● 课程中如有特殊情况请及时联系老师
</view>
</view> -->
</view>
</view>
<!-- 是否登录 -->
<openlogin ref="loginId" @getPhoneNumber="getPhoneNumber"></openlogin>
</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 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>
<!-- ✅ 本次使用卡片 -->
<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
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>
<!-- 是否登录 -->
<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",
},
remark:null
};
},
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: {
formatTime(timeStr) {
// 假设 timeStr 格式为 "HH:MM:SS"
return timeStr.slice(0, 5); // 取前5位 "HH:MM"
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: 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"
},
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;
}
else{
return true;
}
},
goback(){
uni.navigateBack({
delta: 1
});
},
async getinfo() {
// 课程详情
uni.showLoading({
title: '数据加载中...'
});
// const {data: res} = await uni.$http.post('/api/my/courseTimeDetail', {"courseTimeId":this.id}); // this.id
const {data: res} = await uni.$http.post('/api/course/getCourseDetail', {"courseTimeId":this.id}); // this.id
if(res&&res.data){
// 订单信息
this.item=res.data;
console.log(this.item)
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
});
}
}
});
remark: null,
memberCards: [],
currentCourse: {
courseName: '普拉提A班',
deductType: 'COUNT',
deductCount: 1,
deductAmount: 0
},
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.expireDate) {
const now = new Date();
const expire = new Date(card.expireDate.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;
},
}
else{
uni.showModal({
title: '提示',
content: res.message? res.message:'课程预约失败!',
cancelText: '取消',
confirmText: '确定',
success: ress => {
if (ress.confirm) {
}
}
});
}
},
// ✅ 打开弹窗:重置手动选择,用默认卡
async openCardSelect(item) {
var user = myCache('userInfo');
const { data: memberCards } =
await uni.$http.post("/api/memberCard/getMemberCardListByCourseTimeId",
{visitStore:user.visitStore,
courseId:item.courseId
});
console.log("memberCards",memberCards)
//如果为空 则没有适合的卡片
this.memberCards = memberCards.data
this.selectedCard = null;
this.$refs.cardPopup.open();
},
}
}
closeCardSelect() {
this.$refs.cardPopup.close();
},
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
});
if (res && res.success) {
myCache('courserefresh', 1);
uni.showModal({
title: '提示',
content: '课程已预约!',
cancelText: '返回',
confirmText: '确定',
success: ress => {
uni.navigateBack({ delta: 1 });
}
});
} else {
uni.showModal({
title: '提示',
content: res.message ? res.message : '课程预约失败!',
cancelText: '取消',
confirmText: '确定'
});
}
},
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>
@ -472,6 +584,7 @@
justify-content: flex-end;
align-items: center;
font-size: 28rpx;
color: #000;
font-weight: 600;
}
@ -614,6 +727,97 @@
}
}
}
.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; }
}
</style>

@ -32,7 +32,9 @@
@audioStateChange="onAudioStateChange"
:id="'chat-item-' + idx"
:msgInfo="msgInfo"
:groupMembers="groupMembers">
:groupMembers="groupMembers"
:chatIndex="chatIdx"
>
</chat-message-item>
</view>
<view v-else="!selectionMode" >
@ -48,7 +50,9 @@
@audioStateChange="onAudioStateChange"
:id="'chat-item-' + idx"
:msgInfo="msgInfo"
:groupMembers="groupMembers">
:groupMembers="groupMembers"
:chatIndex="chatIdx"
>
</chat-message-item>
</view>
</view>
@ -161,6 +165,9 @@ export default {
}
return title;
},
chatIdx(){
return this.findChatIdx(this.chat);
},
// 消息发送接口 URL
messageAction() {
return `/api/message/${this.chat.type.toLowerCase()}/send`;
@ -1077,13 +1084,6 @@ export default {
this.isInBottom = true;
this.newMessageSize = 0;
},
onShowMore() {
if (this.chat.type === 'GROUP') {
uni.navigateTo({ url: `/pages/group/group-info?id=${this.group.id}` });
} else {
uni.navigateTo({ url: `/pages/common/user-info?id=${this.userInfo.id}` });
}
},
onTextInput(e) {
this.isEmpty = e.detail.html === '<p><br></p>';
},

@ -41,7 +41,8 @@ export default {
userId: '',
userInfo: {},
isBlacked: false,
defaultAvatar: '/static/image/kff.png'
defaultAvatar: '/static/image/kff.png',
chatIndex:null
}
},
computed: {
@ -53,13 +54,15 @@ export default {
},
onLoad(options) {
this.userId = options.id || ''
this.chatIndex = options.chatIndex || ''
console.log('chatIndex...',this.chatIndex)
if (this.userId) {
this.loadFriend()
this.checkBlack()
}
},
methods: {
...mapActions('chatStore', ['openChat']),
...mapActions('chatStore', ['removeChat','openChat']),
async loadFriend() {
try {
const { data: res } = await uni.$http.get('/api/friend/find/' + this.userId)
@ -130,6 +133,15 @@ export default {
let chatIdx = this.findChatIdx(chat)
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` })
},
findChatIdxByUser(){
let chat = {
type: 'PRIVATE',
targetId: this.userId,
showName: this.displayName,
headImage: this.userInfo.headImage || ''
}
return this.findChatIdx(chat)
},
handleSwitchTap() {
// 用户想要切换到的目标状态
const targetStatus = !this.isBlacked
@ -154,12 +166,41 @@ export default {
title: this.isBlacked ? '已加入黑名单' : '已取消黑名单',
icon: 'none'
})
//聊天框 - 删除
if (this.isBlacked && this.chatIndex){
this.removeChat(Number(this.chatIndex)) // 调用 Vuex action
}
const privateChatIdx=this.findChatIdxByUser();
if (this.isBlacked && privateChatIdx>-1){
this.removeChat(Number(privateChatIdx))
}
} catch (err) {
uni.showToast({ title: '操作失败', icon: 'none' })
}
}
})
},
goBackChat() {
uni.reLaunch({
url: `/pages/chatbox/chat`
});
}
},
// 拦截返回
onBackPress(e) {
// 来源是左上角返回 / 安卓返回键
if(e.from === 'backbutton'){
if(this.isBlacked){
// 拉黑成功,自定义跳转chat
this.goBackChat()
return true // return true 阻止原生默认返回
}else{
// 未拉黑,放行,走原来的返回(回到chat-box)
return false
}
}
return false
}
}
</script>

@ -0,0 +1,302 @@
<template>
<view class="member-card-scroll">
<scroll-view
class="card-scroll"
scroll-x="true"
scroll-with-animation
:show-scrollbar="false"
@scroll="onScroll"
>
<view class="card-scroll-inner" :style="innerStyle">
<view
v-for="(card, index) in cards"
:key="card.id || index"
class="card-item"
@click="onCardClick(card, index)"
>
<member-card
:card="card"
:size="size"
:selectable="selectable"
:selected="selectable && highlightIndex === index"
/>
<!-- v-if="selectable && !isCardUsable(card)"-->
<view class="unusable-tip">
<text v-if=" card.status == 'ACTIVE'" style="color:#00a89b;" >正常</text>
<text v-else style="color: #ff6b6b;">{{ getUnusableReason(card) }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 指示器 -->
<view v-if="cards && cards.length > 1" class="indicator">
<view
v-for="(card, index) in cards"
:key="index"
class="dot"
:class="{ active: index === currentIndex }"
></view>
</view>
</view>
</template>
<script>
import MemberCard from './member-card.vue';
export default {
name: 'MemberCardScroll',
components: { MemberCard },
props: {
cards: { type: Array, default: () => [] },
size: { type: String, default: 'mini' },
center: { type: Boolean, default: false },
selectable: { type: Boolean, default: false },
deductType: { type: String, default: 'COUNT' },
deductCount: { type: Number, default: 1 },
deductAmount: { type: Number, default: 0 },
// 默认卡片ID(后端可标记某张卡为默认卡)
defaultCardId: { type: [String, Number], default: null },
// 是否自动选中第一张可用卡(弹窗场景设为 false)
autoSelect: { type: Boolean, default: true }
},
data() {
return {
selectedIndex: -1,
currentIndex: 0,
screenWidthRpx: 750
};
},
computed: {
highlightIndex() {
if (this.selectedIndex >= 0) {
return this.selectedIndex;
}
if (this.selectable && this.defaultCard) {
return this.cards.findIndex(c => c.id === this.defaultCard.id);
}
return -1;
},
selectedCard() {
return this.selectedIndex >= 0 ? this.cards[this.selectedIndex] : null;
},
// 默认卡:后端指定的 defaultCardId,或第一张可用卡
defaultCard() {
if (this.defaultCardId != null) {
const card = this.cards.find(c => c.id === this.defaultCardId);
if (card && this.isCardUsable(card)) return card;
}
return this.cards.find(card => this.isCardUsable(card)) || null;
},
// 有效卡:用户选中的卡,没选就用默认卡
effectiveCard() {
return this.selectedCard || this.defaultCard;
},
// 卡片宽度映射(与 member-card.vue 保持一致)
cardWidthRpx() {
const map = { large: 690, small: 440, mini: 380 };
return map[this.size] || 380;
},
// 动态计算首尾 padding
innerStyle() {
if (!this.center) {
return { paddingLeft: '20rpx', paddingRight: '20rpx' };
}
const padding = Math.max(0, (this.screenWidthRpx - this.cardWidthRpx) / 2);
return {
paddingLeft: padding + 'rpx',
paddingRight: padding + 'rpx'
};
}
},
watch: {
cards: {
immediate: true,
handler() {
this.initSelected();
}
},
defaultCardId() {
this.initSelected();
}
},
created() {
this.screenWidthRpx = 750;
},
methods: {
onScroll(e) {
const scrollLeft = e.detail.scrollLeft;
const sysInfo = uni.getSystemInfoSync();
const screenWidth = sysInfo.windowWidth;
const rpxRatio = screenWidth / 750;
const cardWidthPx = this.cardWidthRpx * rpxRatio;
const gapPx = 20 * rpxRatio;
const paddingRpx = this.center
? Math.max(0, (750 - this.cardWidthRpx) / 2)
: 20;
const paddingPx = paddingRpx * rpxRatio;
const centerX = scrollLeft + screenWidth / 2;
let index = Math.round(
(centerX - paddingPx - cardWidthPx / 2) / (cardWidthPx + gapPx)
);
index = Math.max(0, Math.min(index, this.cards.length - 1));
// ✅ 只更新 currentIndex,用于指示器显示当前位置
// ✅ 不再修改 selectedIndex,选中卡只由点击/默认卡决定
this.currentIndex = index;
},
initSelected() {
if (!this.selectable) return;
if (!this.autoSelect) {
this.selectedIndex = -1;
return;
}
if (this.defaultCardId != null) {
const idx = this.cards.findIndex(c => c.id === this.defaultCardId);
if (idx >= 0 && this.isCardUsable(this.cards[idx])) {
this.selectedIndex = idx;
return;
}
}
const idx = this.cards.findIndex(card => this.isCardUsable(card));
this.selectedIndex = idx;
},
onCardClick(card, index) {
this.currentIndex = index;
if (this.selectable) {
if (!this.isCardUsable(card)) {
uni.showToast({ title: this.getUnusableReason(card), icon: 'none' });
return;
}
this.selectedIndex = index;
this.$emit('select', { card, index });
} else {
this.$emit('card-click', { card, index });
}
},
isCardUsable(card) {
if (!card) return false;
// ✅ 只有 ACTIVE 才可用
if (card.status && card.status !== 'ACTIVE') return false;
if (card.expireDate) {
const now = new Date();
const expire = new Date(card.expireDate.replace(/-/g, '/'));
if (now > expire) return false;
}
if (card.type === 'count') {
const available = (card.remainingCount || 0) - (card.frozenCount || 0);
if (available < this.deductCount) return false;
} else if (card.type === 'total_fee') {
const available = (card.remainingTotalFee || 0) - (card.frozenAmount || 0);
if (available < this.deductAmount) return false;
}
return true;
},
getUnusableReason(card) {
if (card.status && card.status == 'ACTIVE') {
return '卡片已激活';
}
if (card.status && card.status == 'INACTIVE') {
return '卡片未激活';
}
if (card.status && card.status == 'EXPIRED') {
return '卡片已过期';
}
if (card.status && card.status == 'SUSPENDED') {
return '卡片已暂停';
}
if (card.status && card.status == 'DEPLETED') {
return '卡片额度已用完';
}
if (card.expireDate) {
const now = new Date();
const expire = new Date(card.expireDate.replace(/-/g, '/'));
if (now > expire) return '卡片已过期';
}
if (card.type === 'count') {
const available = (card.remainingCount || 0) - (card.frozenCount || 0);
if (available < this.deductCount) return '剩余次数不足';
}
else if (card.type === 'total_fee') {
const available = (card.remainingTotalFee || 0) - (card.frozenAmount || 0);
if (available < this.deductAmount) return '余额不足';
}
return '不可用';
},
// 对外方法:获取有效卡(用于提交预约)
getEffectiveCard() {
return this.effectiveCard;
},
isManuallySelected() {
return this.selectedIndex >= 0;
},
getSelected() {
return { card: this.selectedCard, index: this.selectedIndex };
},
isSelectedValid() {
return this.selectedIndex >= 0 && this.isCardUsable(this.selectedCard);
}
}
};
</script>
<style lang="scss" scoped>
.member-card-scroll {
width: 100%;
}
.card-scroll {
width: 100%;
white-space: nowrap;
}
.card-scroll-inner {
display: inline-block;
white-space: nowrap;
font-size: 0;
}
.card-item {
display: inline-block;
vertical-align: top;
margin-right: 20rpx;
position: relative;
font-size: 28rpx;
}
.card-item:last-child {
margin-right: 0;
}
.unusable-tip {
margin-top: 8rpx;
font-size: 22rpx;
text-align: center;
}
.indicator {
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
}
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #ddd;
margin: 0 6rpx;
transition: all 0.3s;
&.active {
width: 24rpx;
border-radius: 6rpx;
background: #00a89b;
}
}
</style>

@ -0,0 +1,280 @@
<template>
<view
class="member-card"
:class="[`size-${size}`, { 'is-select': selectable && selected }]"
:style="{ background: cardGradient }"
@click="onClick"
>
<!-- 背景装饰 -->
<view class="decor-circle decor-1"></view>
<view class="decor-circle decor-2"></view>
<!-- 卡片内容 -->
<view class="card-content">
<!-- 顶部:卡类型标签 + 卡名 -->
<view class="card-top">
<text class="card-type">{{ card.chargeType === 'count' ? '次卡' : card.chargeType ==='days'?'周期卡':'储值卡' }}</text>
<text class="card-name">{{ card.cardName || '会员卡' }}</text>
</view>
<!-- 中部:余额/次数 -->
<view class="card-middle">
<view class="balance-box">
<text class="balance-label">
{{ 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) }}
</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">
<text class="frozen-label">预约占用</text>
<text class="frozen-value">
{{ card.type === 'count' ? card.frozenCount + '次' : '¥' + formatAmount(card.frozenAmount) }}
</text>
</view>
</view>
<!-- 底部:卡号 + 有效期 -->
<view class="card-bottom">
<text class="card-no">NO. {{ card.cardNo || '--' }}</text>
<text class="card-expire">
{{ card.expireDate ? '有效期至 ' + card.expireDate : '长期有效' }}
</text>
</view>
</view>
<!-- 选中标记 -->
<view v-if="selectable && selected" class="selected-icon">
<uni-icons type="checkmarkempty" size="22" color="#fff"></uni-icons>
</view>
</view>
</template>
<script>
export default {
name: 'MemberCard',
props: {
// 会员卡数据
card: {
type: Object,
required: true,
default: () => ({})
},
// 尺寸:small | large
size: {
type: String,
default: 'small'
},
// 是否可选(用于选择弹窗)
selectable: {
type: Boolean,
default: false
},
// 是否选中
selected: {
type: Boolean,
default: false
}
},
computed: {
// 根据卡号生成渐变色(保证每张卡颜色稳定)
cardGradient() {
const themes = [
'linear-gradient(135deg, #00A99A 0%, #1abcb3 100%)', // 青绿
'linear-gradient(135deg, #FF6B6B 0%, #FF8E8E 100%)', // 珊瑚红
'linear-gradient(135deg, #4E7AF5 0%, #6C9EFF 100%)', // 宝蓝
'linear-gradient(135deg, #9B59B6 0%, #B07CC6 100%)', // 紫罗兰
'linear-gradient(135deg, #F39C12 0%, #F7B955 100%)', // 琥珀橙
'linear-gradient(135deg, #16A085 0%, #1ABC9C 100%)', // 翡翠绿
];
// 优先用后端返回的主题色,其次按卡号/ID取模
if (this.card.themeColor) {
return this.card.themeColor;
}
const key = this.card.id || this.card.cardNo || '0';
const idx = Number(String(key).replace(/\D/g, '').slice(-2) || 0) % themes.length;
return themes[idx];
}
},
methods: {
onClick() {
this.$emit('click', this.card);
},
// 金额千分位
formatAmount(val) {
if (val === null || val === undefined) return '0.00';
return Number(val).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
}
};
</script>
<style lang="scss" scoped>
.member-card {
position: relative;
border-radius: 16rpx; /* 20 → 16,圆角同步缩小 */
overflow: hidden;
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(0, 169, 154, 0.25);
transition: transform 0.2s;
flex-shrink: 0;
&:active {
transform: scale(0.98);
}
/* ====== 尺寸变体(缩小后) ====== */
&.size-large {
width: 690rpx;
height: 360rpx;
}
&.size-small {
width: 440rpx;
height: 250rpx;
}
&.size-mini {
width: 380rpx;
height: 220rpx;
}
/* ====== 选中态 ====== */
&.is-select {
border: 4rpx solid #fff;
box-shadow: 0 0 0 4rpx #00A99A, 0 6rpx 18rpx rgba(0, 169, 154, 0.4);
}
}
/* 装饰圆:同步缩小 */
.decor-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.decor-1 {
width: 200rpx;
height: 200rpx;
right: -60rpx;
top: -60rpx;
}
.decor-2 {
width: 130rpx;
height: 130rpx;
right: 50rpx;
bottom: -40rpx;
background: rgba(255, 255, 255, 0.05);
}
.card-content {
position: relative;
z-index: 2;
padding: 24rpx 28rpx; /* 32/36 → 24/28 */
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* 顶部 */
.card-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-type {
font-size: 20rpx; /* 22 → 20 */
padding: 2rpx 12rpx;
border-radius: 16rpx;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10rpx);
}
.card-name {
font-size: 24rpx; /* 28 → 24 */
font-weight: 600;
max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 中部 */
.card-middle {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin: 8rpx 0;
}
.balance-label {
font-size: 20rpx; /* 22 → 20 */
opacity: 0.85;
display: block;
margin-bottom: 4rpx;
}
.balance-value-box {
display: flex;
align-items: baseline;
}
.unit {
font-size: 22rpx; /* 26 → 22 */
font-weight: 500;
margin: 0 4rpx;
}
.balance-value {
font-size: 42rpx; /* 56 → 42 */
font-weight: 700;
letter-spacing: 1rpx;
font-family: 'DIN Alternate', 'PingFang SC', sans-serif;
}
.frozen-box {
display: flex;
flex-direction: column;
align-items: flex-end;
font-size: 20rpx;
opacity: 0.85;
}
.frozen-label {
margin-bottom: 2rpx;
}
.frozen-value {
font-weight: 600;
}
/* 底部 */
.card-bottom {
display: flex;
flex-direction: column;
font-size: 20rpx;
opacity: 0.85;
}
.card-no {
align-self: flex-start;
}
.card-expire {
align-self: flex-end; /* 有效期靠右 */
margin-top: 4rpx;
}
/* 选中图标:同步缩小 */
.selected-icon {
position: absolute;
top: 16rpx;
right: 16rpx;
width: 36rpx; /* 44 → 36 */
height: 36rpx;
border-radius: 50%;
background: #00A99A;
display: flex;
align-items: center;
justify-content: center;
z-index: 3;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
</style>

@ -216,10 +216,11 @@ import { mapState, mapActions, mapGetters } from 'vuex'
},
// 跳转聊天框
gotoGroup(item){
var timestamp = new Date().getTime();
if(this.tabCurrentIndex==4){
// 群聊
// 跳转逻辑修改:私聊跳用户详情页;群聊不变直接进群聊聊天页
gotoGroup(item){
var timestamp = new Date().getTime();
if(this.tabCurrentIndex==4){
// ========== 群聊:保持原有逻辑不变,直接进入群聊聊天室 ==========
let chat = {
type: 'GROUP',
targetId: item.id,
@ -229,105 +230,16 @@ import { mapState, mapActions, mapGetters } from 'vuex'
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
/**
this.getGroupInfo(item.id);
var chatid="groupchat-" + item.id;
var info={
chatId: chatid,
groupId: item.id,
chatName: item.name,
chatAvatar: item.headImage?item.headImage:'/static/image/kfr.png',
chatTime: timestamp,
friendId: item.customerService,
teacherId: item.instructor,
minId: "",
sort:"groupchat", // privatechat 私聊 groupchat 群聊
from:"message", // yh 用户咨询进入聊天框,message 从消息进入聊天框
notice: item.notice,
remarkNickName: item.remarkNickName,
remarkGroupName: item.remarkGroupName,
showNickName: item.showNickName,
showGroupName: item.showGroupName,
reason: item.reason,
customerService: item.customerService,
instructor: item.instructor,
ownerId: item.ownerId,
productId: item.productId,
productName: item.productName
}
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/groupchat?data=${data}`
});
**/
}
else{
let chat = {
type: 'PRIVATE',
targetId: item.id,
showName: item.nickName,
headImage: item.headImage,
};
this.openChat(chat);
let chatIdx = this.findChatIdx(chat);
console.log("chatIdx",chatIdx)
uni.navigateTo({ url: `/pages/chatbox/chat-box?chatIdx=${chatIdx}` });
// 直接跳转到聊天框
/**
var info={
chatId: "privatechat-" + this.userid +"-"+ item.id,
chatType: (this.tabCurrentIndex==0?"coach":(this.tabCurrentIndex==1?"customerService":(this.tabCurrentIndex==2?"adviser":
(this.tabCurrentIndex==3?"storeManager":(this.tabCurrentIndex==4?"groupchat":(this.tabCurrentIndex==5?"consumer":"")))))),
chatName: item.nickName,
chatAvatar: item.headImage,
chatTime: timestamp,
userid: this.userid,
friendId: item.id, // 会话对象
minId: null, // 已读消息的最大id
sort: this.tabCurrentIndex==4?"groupchat":"privatechat", // privatechat 私聊 groupchat 群聊
from: "message" // yh 用户咨询进入聊天框,message 从消息进入聊天框
}
console.log(info);
if(this.tabCurrentIndex==0){
// 教练
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
}
else if(this.tabCurrentIndex==1){
// 客服
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
}
else if(this.tabCurrentIndex==2){
// 顾问
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
}
else if(this.tabCurrentIndex==3){
// 店长
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
}
else if(this.tabCurrentIndex==5){
// 学员
var data=encodeURIComponent(JSON.stringify(info));
uni.navigateTo({
url: `/pages/chat/chat?data=${data}`
});
}
**/
}
}
else{
// ========== 私聊联系人:跳转到 用户详情页 user‑info,把用户id传过去 ==========
uni.navigateTo({
url: `/pages/chatbox/im-user-info?id=${item.id}`
})
}
},
},
loadData(){
loadData(){
// 底部数据加载
if(this.list[this.tabCurrentIndex].loadStatus=="loadmore") {
this.list[this.tabCurrentIndex].loadStatus="loading";
@ -361,6 +273,7 @@ import { mapState, mapActions, mapGetters } from 'vuex'
// 教练列表
if(res.data&&res.data.coach&&res.data.coach.length>0){
this.list[0].grouplist=res.data.coach;
console.log('res.data',res.data)
this.$forceUpdate();
}
// 客服列表

@ -0,0 +1,723 @@
<template>
<view class="page">
<!-- 顶部卡片展示 -->
<view class="card-header">
<member-card
v-if="card"
:card="card"
size="large"
:selectable="false"
/>
<!-- 默认卡标记 -->
<view v-if="card && card.isDefault" class="default-badge">
<uni-icons type="star-filled" size="16" color="#FFB800"></uni-icons>
<text>默认卡</text>
</view>
</view>
<!-- 额度概览 -->
<view class="overview-card">
<view class="overview-title">额度概览</view>
<view class="overview-grid">
<view class="overview-item">
<text class="ov-label">总额度</text>
<text class="ov-value">
{{ card.type === 'COUNT'
? (card.totalCount || 0) + '次'
: '¥' + formatAmount(card.totalAmount) }}
</text>
</view>
<view class="overview-item">
<text class="ov-label">已使用</text>
<text class="ov-value">
{{ card.type === 'COUNT'
? (card.usedCount || 0) + '次'
: '¥' + formatAmount(card.usedAmount) }}
</text>
</view>
<view class="overview-item">
<text class="ov-label">冻结中</text>
<text class="ov-value frozen">
{{ card.type === 'COUNT'
? (card.frozenCount || 0) + '次'
: '¥' + formatAmount(card.frozenAmount) }}
</text>
</view>
<view class="overview-item">
<text class="ov-label">可用</text>
<text class="ov-value available">
{{ card.type === 'COUNT'
? (card.remainCount || 0) + '次'
: '¥' + formatAmount(card.remainAmount) }}
</text>
</view>
</view>
</view>
<!-- 卡片基本信息 -->
<view class="info-card">
<view class="info-row">
<text class="info-label">卡类型</text>
<text class="info-value">{{ card.cardTypeName || card.typeName || '--' }}</text>
</view>
<view class="info-row">
<text class="info-label">卡名称</text>
<text class="info-value">{{ card.name || '--' }}</text>
</view>
<view class="info-row">
<text class="info-label">卡号</text>
<text class="info-value">{{ card.cardNo || '--' }}</text>
</view>
<view class="info-row">
<text class="info-label">激活日期</text>
<text class="info-value">{{ card.activationDate || '未激活' }}</text>
</view>
<view class="info-row">
<text class="info-label">有效期至</text>
<text class="info-value">{{ card.expireDate || '长期有效' }}</text>
</view>
<view class="info-row">
<text class="info-label">状态</text>
<text class="info-value" :class="statusClass">{{ statusText }}</text>
</view>
</view>
<!-- 交易记录 / 冻结记录 Tab -->
<view class="tab-section">
<view class="tab-header">
<view
class="tab-item"
:class="{ active: activeTab === 'transaction' }"
@click="switchTab('transaction')"
>
消费记录
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'freeze' }"
@click="switchTab('freeze')"
>
冻结记录
</view>
</view>
<!-- 消费记录 -->
<scroll-view
v-if="activeTab === 'transaction'"
class="record-list"
scroll-y="true"
style="max-height: 800rpx;"
@scrolltolower="loadMoreTransactions"
>
<view v-if="transactions.length === 0" class="empty-tip">
<text>暂无消费记录</text>
</view>
<view
v-for="(item, index) in transactions"
:key="index"
class="record-item"
>
<view class="record-left">
<view class="record-icon" :class="getTransactionIconClass(item.type)">
<uni-icons :type="getTransactionIcon(item.type)" size="20" :color="getTransactionColor(item.type)"></uni-icons>
</view>
<view class="record-info">
<text class="record-name">{{ getTransactionName(item.type) }}</text>
<text class="record-time">{{ formatDateTime(item.createTime) }}</text>
<text v-if="item.remark" class="record-remark">{{ item.remark }}</text>
</view>
</view>
<view class="record-right">
<text
class="record-amount"
:class="item.direction > 0 ? 'income' : 'expense'"
>
{{ item.direction > 0 ? '+' : '-' }}{{ formatAmount(item.amount) }}
</text>
<text class="record-balance">
余额:{{ item.cardType === 'COUNT'
? (item.balanceAfter || 0) + '次'
: '¥' + formatAmount(item.balanceAfter) }}
</text>
</view>
</view>
<uni-load-more v-if="transactions.length > 0" :status="transactionLoadStatus" />
</scroll-view>
<!-- 冻结记录 -->
<scroll-view
v-if="activeTab === 'freeze'"
class="record-list"
scroll-y="true"
style="max-height: 800rpx;"
>
<view v-if="freezes.length === 0" class="empty-tip">
<text>暂无冻结记录</text>
</view>
<view
v-for="(item, index) in freezes"
:key="index"
class="record-item"
>
<view class="record-left">
<view class="record-icon freeze">
<uni-icons type="locked" size="20" color="#FF6B6B"></uni-icons>
</view>
<view class="record-info">
<text class="record-name">
{{ item.freezeType === 'COUNT'
? '预约冻结 ' + item.freezeCount + '次'
: '预约冻结 ¥' + formatAmount(item.freezeAmount) }}
</text>
<text class="record-time">{{ formatDateTime(item.createTime) }}</text>
<text class="record-remark">
关联预约:{{ item.appointmentTitle || ('#' + item.appointmentId) }}
</text>
</view>
</view>
<view class="record-right">
<text class="freeze-status" :class="'status-' + item.status">
{{ getFreezeStatusText(item.status) }}
</text>
<text v-if="item.releaseTime" class="record-time">
{{ formatDateTime(item.releaseTime) }}
</text>
</view>
</view>
</scroll-view>
</view>
<!-- 底部操作 -->
<view class="footer-actions">
<button class="action-btn secondary" @click="gotoBook">去预约</button>
<button
v-if="!card.isDefault && card.status === 'ACTIVE'"
class="action-btn primary"
@click="setAsDefault"
>
设为默认卡
</button>
</view>
<!-- Toast -->
<u-toast ref="uToast" />
</view>
</template>
<script>
import MemberCard from '../components/member-card.vue';
import { myCache } from '../../utils/utils.js';
export default {
components: { MemberCard },
data() {
return {
cardId: null,
card: null,
activeTab: 'transaction',
transactions: [],
freezes: [],
transactionPage: 1,
transactionPageSize: 20,
transactionLoadStatus: 'more', // more / loading / noMore
userId: ''
};
},
computed: {
statusText() {
if (!this.card) return '';
const map = {
ACTIVE: '已激活',
INACTIVE: '未激活',
SUSPENDED: '已暂停',
DEPLETED: '已用完',
EXPIRED: '已过期'
};
return map[this.card.status] || this.card.status;
},
statusClass() {
if (!this.card) return '';
const map = {
ACTIVE: 'status-active',
INACTIVE: 'status-inactive',
SUSPENDED: 'status-suspended',
DEPLETED: 'status-depleted',
EXPIRED: 'status-expired'
};
return map[this.card.status] || '';
}
},
onLoad(options) {
this.cardId = options.cardId;
this.userId = myCache('user')?.userid || '';
if (!this.cardId) {
uni.showToast({ title: '参数错误', icon: 'none' });
setTimeout(() => uni.navigateBack(), 1500);
return;
}
this.loadCardDetail();
this.loadTransactions(true);
},
methods: {
// ========== 加载卡片详情 ==========
async loadCardDetail() {
uni.showLoading({ title: '加载中...' });
try {
const { data: res } = await uni.$http.get('/api/memberCard/detail', {
cardId: this.cardId
});
uni.hideLoading();
if (res && res.code === 200 && res.data) {
this.card = res.data;
} else {
uni.showToast({ title: res.msg || '加载失败', icon: 'none' });
}
} catch (e) {
uni.hideLoading();
uni.showToast({ title: '网络异常', icon: 'none' });
}
},
// ========== 加载交易记录 ==========
async loadTransactions(reset = false) {
if (this.transactionLoadStatus === 'loading') return;
if (!reset && this.transactionLoadStatus === 'noMore') return;
if (reset) {
this.transactionPage = 1;
this.transactions = [];
this.transactionLoadStatus = 'more';
}
this.transactionLoadStatus = 'loading';
try {
const { data: res } = await uni.$http.get('/api/member/card/transactions', {
cardId: this.cardId,
pageNum: this.transactionPage,
pageSize: this.transactionPageSize
});
if (res && res.code === 200) {
const list = res.data?.list || res.data || [];
if (list.length < this.transactionPageSize) {
this.transactionLoadStatus = 'noMore';
} else {
this.transactionLoadStatus = 'more';
}
this.transactions = this.transactions.concat(list);
this.transactionPage++;
} else {
this.transactionLoadStatus = 'noMore';
}
} catch (e) {
this.transactionLoadStatus = 'noMore';
console.error('加载交易记录失败', e);
}
},
loadMoreTransactions() {
this.loadTransactions();
},
// ========== 加载冻结记录 ==========
async loadFreezes() {
try {
const { data: res } = await uni.$http.get('/api/member/card/freezes', {
cardId: this.cardId
});
if (res && res.code === 200) {
this.freezes = res.data || [];
}
} catch (e) {
console.error('加载冻结记录失败', e);
}
},
// ========== Tab 切换 ==========
switchTab(tab) {
this.activeTab = tab;
if (tab === 'freeze' && this.freezes.length === 0) {
this.loadFreezes();
}
},
// ========== 设为默认卡 ==========
async setAsDefault() {
uni.showModal({
title: '提示',
content: '确定将这张卡设为默认卡吗?',
success: async (res) => {
if (!res.confirm) return;
try {
const { data: r } = await uni.$http.post('/api/member/card/setDefault', {
cardId: this.cardId
});
if (r && r.code === 200) {
this.card.isDefault = true;
uni.showToast({ title: '设置成功', icon: 'success' });
} else {
uni.showToast({ title: r.msg || '设置失败', icon: 'none' });
}
} catch (e) {
uni.showToast({ title: '网络异常', icon: 'none' });
}
}
});
},
// ========== 跳转预约 ==========
gotoBook() {
uni.switchTab({ url: '/pages/book/booklist' });
},
// ========== 交易记录辅助方法 ==========
getTransactionName(type) {
const map = {
RECHARGE: '会员充值',
COURSE_DEDUCT: '课程核销',
SHOP_PURCHASE: '商城消费',
CASH_OUT: '销卡取现',
TRANSFER_IN: '转卡转入',
TRANSFER_OUT: '转卡转出',
ADMIN_ADD: '后台调增',
ADMIN_SUB: '后台调减',
FREEZE: '预约冻结',
UNFREEZE: '取消解冻',
REFUND: '退款',
REVERSE: '冲正'
};
return map[type] || type;
},
getTransactionIcon(type) {
const map = {
RECHARGE: 'plus-circle',
COURSE_DEDUCT: 'checkbox',
SHOP_PURCHASE: 'cart',
CASH_OUT: 'wallet',
TRANSFER_IN: 'arrow-down',
TRANSFER_OUT: 'arrow-up',
ADMIN_ADD: 'plus-circle',
ADMIN_SUB: 'minus-circle',
FREEZE: 'locked',
UNFREEZE: 'unlock',
REFUND: 'undo',
REVERSE: 'refresh'
};
return map[type] || 'info';
},
getTransactionColor(type) {
const income = ['RECHARGE', 'TRANSFER_IN', 'ADMIN_ADD', 'REFUND'];
const expense = ['COURSE_DEDUCT', 'SHOP_PURCHASE', 'CASH_OUT', 'TRANSFER_OUT', 'ADMIN_SUB'];
if (income.includes(type)) return '#00A89B';
if (expense.includes(type)) return '#FF6B6B';
return '#999';
},
getTransactionIconClass(type) {
const income = ['RECHARGE', 'TRANSFER_IN', 'ADMIN_ADD', 'REFUND'];
const expense = ['COURSE_DEDUCT', 'SHOP_PURCHASE', 'CASH_OUT', 'TRANSFER_OUT', 'ADMIN_SUB'];
if (income.includes(type)) return 'income';
if (expense.includes(type)) return 'expense';
return '';
},
getFreezeStatusText(status) {
const map = {
1: '冻结中',
2: '已核销',
3: '已释放'
};
return map[status] || '--';
},
// ========== 通用工具 ==========
formatAmount(val) {
if (val === null || val === undefined) return '0.00';
return Number(val).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
formatDateTime(time) {
if (!time) return '--';
const d = new Date(time.replace(/-/g, '/'));
if (isNaN(d.getTime())) return time;
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
return `${d.getFullYear()}-${m}-${day} ${h}:${min}`;
}
}
};
</script>
<style lang="scss" scoped>
.page {
padding: 20rpx;
min-height: 100vh;
background-color: #f5f5f5;
}
/* ========== 顶部卡片 ========== */
.card-header {
position: relative;
display: flex;
justify-content: center;
margin-bottom: 20rpx;
}
.default-badge {
position: absolute;
top: -10rpx;
right: 30rpx;
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 16rpx;
background: #FFF8E1;
border-radius: 20rpx;
font-size: 22rpx;
color: #FFB800;
box-shadow: 0 2rpx 8rpx rgba(255, 184, 0, 0.2);
}
/* ========== 额度概览 ========== */
.overview-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.overview-title {
font-size: 28rpx;
font-weight: 600;
color: #000;
margin-bottom: 24rpx;
padding-left: 10rpx;
border-left: 6rpx solid #00A89B;
}
.overview-grid {
display: flex;
justify-content: space-between;
}
.overview-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 10rpx 0;
}
.ov-label {
font-size: 22rpx;
color: #999;
margin-bottom: 10rpx;
}
.ov-value {
font-size: 30rpx;
font-weight: 600;
color: #000;
&.frozen {
color: #FF6B6B;
}
&.available {
color: #00A89B;
}
}
/* ========== 卡片信息 ========== */
.info-card {
background: #fff;
border-radius: 20rpx;
padding: 10rpx 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 22rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
}
.info-label {
font-size: 26rpx;
color: #999;
}
.info-value {
font-size: 26rpx;
color: #000;
font-weight: 500;
&.status-active { color: #00A89B; }
&.status-inactive { color: #FFB800; }
&.status-suspended { color: #FF6B6B; }
&.status-depleted { color: #999; }
&.status-expired { color: #ccc; }
}
/* ========== Tab 区域 ========== */
.tab-section {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 140rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.tab-header {
display: flex;
border-bottom: 1rpx solid #f0f0f0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 28rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
&.active {
color: #00A89B;
font-weight: 600;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background: #00A89B;
border-radius: 2rpx;
}
}
}
.record-list {
padding: 10rpx 0;
min-height: 300rpx;
}
.empty-tip {
text-align: center;
padding: 80rpx 0;
color: #999;
font-size: 26rpx;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f8f8f8;
&:last-child {
border-bottom: none;
}
}
.record-left {
display: flex;
flex: 1;
align-items: flex-start;
overflow: hidden;
}
.record-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: #f5f5f5;
&.income {
background: rgba(0, 168, 155, 0.1);
}
&.expense {
background: rgba(255, 107, 107, 0.1);
}
&.freeze {
background: rgba(255, 107, 107, 0.1);
}
}
.record-info {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 18rpx;
overflow: hidden;
}
.record-name {
font-size: 28rpx;
color: #000;
font-weight: 500;
margin-bottom: 6rpx;
}
.record-time {
font-size: 22rpx;
color: #999;
margin-bottom: 4rpx;
}
.record-remark {
font-size: 22rpx;
color: #bbb;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.record-right {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 10rpx;
flex-shrink: 0;
}
.record-amount {
font-size: 30rpx;
font-weight: 600;
margin-bottom: 6rpx;
&.income { color: #00A89B; }
&.expense { color: #FF6B6B; }
}
.record-balance {
font-size: 22rpx;
color: #999;
}
.freeze-status {
font-size: 26rpx;
font-weight: 600;
&.status-1 { color: #FF6B6B; }
&.status-2 { color: #00A89B; }
&.status-3 { color: #999; }
}
/* ========== 底部操作 ========== */
.footer-actions {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
z-index: 10;
}
.action-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
font-size: 30rpx;
border-radius: 44rpx;
border: none;
margin: 0 10rpx;
&::after { border: none; }
&.primary {
background: #00A89B;
color: #fff;
}
&.secondary {
background: #f5f5f5;
color: #333;
}
}
</style>

@ -13,6 +13,20 @@
</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>-->
<!-- 菜单1 -->
<!-- <view class="s-list">
<view class="s-header">
@ -190,9 +204,10 @@
import top from "../components/top.vue";
import service from "../components/service.vue";
import MemberCardScroll from '../components/member-card-scroll.vue';
export default {
components: {
openlogin,top,service
openlogin,top,service,MemberCardScroll
},
data() {
return {
@ -203,6 +218,79 @@
signature:"",
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: [
{
@ -405,6 +493,18 @@
},500);
},
methods: {
onCardClick({ card }) {
// uni.navigateTo({ url: `/pages/user/cardDetail?cardId=${card.id}` });
},
async getMemberCards(){
var user = myCache('userInfo');
if (user&&user.visitStore){
const { data: memberCards } = await uni.$http.post("/api/memberCard/getMemberCardList",{visitStore:user.visitStore});
if (memberCards&&memberCards.data) {
this.memberCards=memberCards.data
}
}
},
ifview(value){
// 描述:角色ID;顾问:103 ;教练:104 ;店长:105 ;普通用户:107;
var str=false;
@ -731,7 +831,7 @@
uni.showModal({
title: '提示',
content: '账号注销失败!请稍后再试!',
cancelText: '取消',
cancelText: '取消 ',
confirmText: '确定',
success: ress => {
if (ress.confirm) {

Loading…
Cancel
Save