Browse Source

直播间答题功能

yuhongqi 9 hours ago
parent
commit
a3dfe0f05b
2 changed files with 247 additions and 2 deletions
  1. 5 0
      api/living.js
  2. 242 2
      pages_course/living.vue

+ 5 - 0
api/living.js

@@ -244,3 +244,8 @@ export function getIsAddKf(data) {
 export function completionInfo(data) {
 	return request('/app/live/completion/info', data, 'GET', 'application/json;charset=UTF-8');
 }
+
+/** 直播答题提交(占位接口,后端逻辑待补充) */
+export function submitLiveQuiz(data) {
+	return request('/app/live/liveQuiz/submit', data, 'POST', 'application/json;charset=UTF-8');
+}

+ 242 - 2
pages_course/living.vue

@@ -203,6 +203,15 @@
 
 				</view>
 
+				<!-- 直播答题入口(socket liveQuizStart 显示,liveQuizClose 隐藏) -->
+				<view class="slide-group live-quiz-entry-wrap" v-if="!isFocus && !isFullscreen && liveQuizShowEntry">
+					<view class="action-button-group end">
+						<view :class="[liveItem.showType === 1 ? 'horizontal' : 'vertical','ml20']" @tap.stop="openLiveQuizSheet">
+							<view class="live-quiz-entry-btn">参与答题</view>
+						</view>
+					</view>
+				</view>
+
 				<!-- 底部聊天区域 -->
 				<view class="chat-area-container" :class="{
 								    'chat-area-container2': liveItem.showType == 1,
@@ -387,6 +396,47 @@
 				</view>
 			</u-popup>
 
+			<!-- 课堂答题弹窗(与抽奖弹窗同结构,u-popup) -->
+			<u-popup
+				:show="liveQuizSheetVisible"
+				mode="center"
+				round="40rpx"
+				zIndex="10080"
+				@close="closeLiveQuizSheet"
+			>
+				<view class="lottery-popup live-quiz-lottery-popup">
+					<view class="lottery-content">
+						<view class="lottery-close-section">
+							<view class="close-button-wrapper">
+								<view class="close-button" @click="closeLiveQuizSheet">
+									<u-icon class="close-icon" name="close" color="#fff" size="20"></u-icon>
+								</view>
+							</view>
+						</view>
+						<view class="lottery-main-content">
+							<view class="live-quiz-popup-question">{{ liveQuiz.title || '课堂答题' }}</view>
+							<view v-if="Number(liveQuiz.type) === 2" class="live-quiz-popup-type-tip">可多选</view>
+							<view class="live-quiz-popup-options">
+								<view
+									v-for="(quizOpt, idx) in liveQuizOptionList"
+									:key="idx"
+									:class="isQuizOptionSelected(quizOpt) ? 'live-quiz-popup-opt live-quiz-popup-opt-active' : 'live-quiz-popup-opt'"
+									@click="selectQuizOption(quizOpt)"
+								>
+									<view class="quiz-opt-key">{{ numberToLetter(idx) }}.</view>
+									<view class="quiz-opt-label">{{ quizOpt.name }}</view>
+								</view>
+							</view>
+							<view
+								class="lottery-action-button"
+								:class="{ 'live-quiz-popup-submit-disabled': !liveQuizCanSubmit || !hasQuizSelection }"
+								@click="submitLiveQuizAnswer"
+							>参与答题</view>
+						</view>
+					</view>
+				</view>
+			</u-popup>
+
 			<!-- 积分弹窗(完课) -->
 			<!-- showPoints -->
 			<!-- :show="!!integral.status" -->
@@ -731,7 +781,8 @@
 		loginByMp,
 		getUserInfo,
 		getIsAddKf,
-		liveWatchUser
+		liveWatchUser,
+		submitLiveQuiz
 	} from '@/api/living.js';
 	import {
 		editUser
@@ -997,6 +1048,20 @@
 				diffReplayGenerationSeconds: 0, //经历回放生成的秒速
 				completionTime: 0, //领取积分所需时间
 				hasLiveEnd: false, //是否经历过首播结束
+
+				// 直播答题(WebSocket: liveQuizStart / liveQuizClose)
+				liveQuizShowEntry: false,
+				liveQuizSheetVisible: false,
+				liveQuizCanSubmit: true,
+				liveQuiz: {
+					title: '',
+					type: 1,
+					options: [],
+					relId: null,
+					questionBankId: null
+				},
+				liveQuizSelectedKey: null,
+				liveQuizSelectedKeys: [],
 				isPageLoadFirst: true, //是否首次加载直播间
 				pointsRetryTimer: null, // 积分领取重试定时器
 				completionPointsEnabled: false //是否开启积分
@@ -1347,6 +1412,18 @@
 			formattedLikeCount() {
 				return this.formatNumber(this.liveViewData.like || 0);
 			},
+			hasQuizSelection() {
+				if (this.liveQuiz && Number(this.liveQuiz.type) === 2) {
+					return Array.isArray(this.liveQuizSelectedKeys) && this.liveQuizSelectedKeys.length > 0;
+				}
+				return !!this.liveQuizSelectedKey;
+			},
+			// 答题选项列表(模板 v-for 只用稳定数组,避免部分端解析异常)
+			liveQuizOptionList() {
+				const q = this.liveQuiz;
+				if (!q || !Array.isArray(q.options)) return [];
+				return q.options;
+			},
 			filteredViewers() {
 				// 获取3个随机假头像,而不是显示真实观众
 				const avatarCount = 3; // 需要显示的假头像数量
@@ -4960,6 +5037,67 @@
 				// 	url: '/pages/home/newindex'
 				// });
 			},
+			openLiveQuizSheet() {
+				if (!this.liveQuizCanSubmit) {
+					return;
+				}
+				this.liveQuizSheetVisible = true;
+			},
+			closeLiveQuizSheet() {
+				this.liveQuizSheetVisible = false;
+			},
+			numberToLetter(num) {
+				return String.fromCharCode(Number(num) + 65);
+			},
+			isQuizOptionSelected(opt) {
+				if (!opt || opt.key == null) return false;
+				if (this.liveQuiz && Number(this.liveQuiz.type) === 2) {
+					return this.liveQuizSelectedKeys.indexOf(opt.key) !== -1;
+				}
+				return this.liveQuizSelectedKey === opt.key;
+			},
+			selectQuizOption(opt) {
+				if (!this.liveQuizCanSubmit || !opt) return;
+				if (Number(this.liveQuiz.type) === 2) {
+					const k = opt.key;
+					const i = this.liveQuizSelectedKeys.indexOf(k);
+					if (i >= 0) {
+						this.liveQuizSelectedKeys.splice(i, 1);
+					} else {
+						this.liveQuizSelectedKeys.push(k);
+					}
+				} else {
+					this.liveQuizSelectedKey = opt.key;
+				}
+			},
+			async submitLiveQuizAnswer() {
+				if (!this.liveQuizCanSubmit || !this.hasQuizSelection) return;
+				const keys = Number(this.liveQuiz.type) === 2
+					? [...this.liveQuizSelectedKeys]
+					: [this.liveQuizSelectedKey];
+				try {
+					const res = await submitLiveQuiz({
+						liveId: this.liveId,
+						relId: this.liveQuiz.relId,
+						questionBankId: this.liveQuiz.questionBankId,
+						type: this.liveQuiz.type,
+						answerKeys: keys
+					});
+					if (res && res.code === 200) {
+						if (res.correct === false) {
+							uni.showToast({ title: res.msg || '回答错误', icon: 'none' });
+						} else {
+							uni.showToast({ title: res.msg || '已提交', icon: 'success' });
+							this.liveQuizSheetVisible = false;
+						}
+					} else {
+						uni.showToast({ title: (res && res.msg) || '提交失败', icon: 'none' });
+					}
+				} catch (e) {
+					console.error('submitLiveQuizAnswer', e);
+					uni.showToast({ title: '网络异常', icon: 'none' });
+				}
+			},
 			// 点赞
 			async onLike() {
 				if (!this.liveId) return;
@@ -5250,7 +5388,7 @@
 				).toString(CryptoJS.enc.Hex);
 
 				try {
-					const baseWsUrl = 'wss://ws.klbycp.com/ws/app/webSocket';
+					const baseWsUrl = 'ws://localhost:7114/ws/app/webSocket';
 					// const baseWsUrl = 'wss://api.fhhx.runtzh.com/ws/app/webSocket';
 					// const baseWsUrl = 'ws://d6998672.natappfree.cc/ws/app/webSocket';
 					// const baseWsUrl = 'ws://nd967d83.natappfree.cc/ws/app/webSocket';
@@ -5758,6 +5896,28 @@
 									});
 								}
 							});
+						} else if (socketMessage.cmd == 'liveQuizStart') {
+							try {
+								const rawStr = socketMessage.data;
+								const raw = rawStr ? (typeof rawStr === 'string' ? JSON.parse(rawStr) : rawStr) : {};
+								this.liveQuiz = {
+									title: raw.title || '',
+									type: raw.type != null ? Number(raw.type) : 1,
+									options: Array.isArray(raw.options) ? raw.options : [],
+									relId: raw.relId != null ? raw.relId : null,
+									questionBankId: raw.questionBankId != null ? raw.questionBankId : null
+								};
+								this.liveQuizSelectedKey = null;
+								this.liveQuizSelectedKeys = [];
+								this.liveQuizShowEntry = true;
+								this.liveQuizCanSubmit = true;
+							} catch (err) {
+								console.error('liveQuizStart 解析失败:', err);
+							}
+						} else if (socketMessage.cmd == 'liveQuizClose') {
+							this.liveQuizShowEntry = false;
+							this.liveQuizSheetVisible = false;
+							this.liveQuizCanSubmit = false;
 						}
 					} else {
 						uni.showToast({
@@ -6619,6 +6779,86 @@
 				}
 			}
 
+			.live-quiz-entry-wrap {
+				top: calc(60% + 240rpx);
+			}
+
+			.live-quiz-entry-btn {
+				padding: 20rpx 28rpx;
+				background: linear-gradient(270deg, #02B176, #04d68f);
+				color: #fff;
+				font-size: 26rpx;
+				border-radius: 40rpx;
+				font-weight: 500;
+				box-shadow: 0 8rpx 24rpx rgba(2, 177, 118, 0.35);
+				white-space: nowrap;
+			}
+
+			.live-quiz-lottery-popup {
+				.lottery-title-image {
+					margin-bottom: 8rpx;
+				}
+
+				.live-quiz-popup-question {
+					font-size: 28rpx;
+					color: #fff;
+					text-align: center;
+					line-height: 1.45;
+					padding: 0 28rpx 12rpx;
+					max-height: 160rpx;
+					overflow-y: auto;
+				}
+
+				.live-quiz-popup-type-tip {
+					font-size: 24rpx;
+					color: rgba(255, 255, 255, 0.88);
+					margin-bottom: 16rpx;
+				}
+
+				.live-quiz-popup-options {
+					max-height: 340rpx;
+					width: 100%;
+					padding: 0 28rpx;
+					box-sizing: border-box;
+				}
+
+				.live-quiz-popup-opt {
+					display: flex;
+					align-items: center;
+					padding: 20rpx 24rpx;
+					margin-bottom: 14rpx;
+					border-radius: 16rpx;
+					background: rgba(255, 255, 255, 0.96);
+					border: 2rpx solid transparent;
+					box-sizing: border-box;
+
+					&:last-child {
+						margin-bottom: 0;
+					}
+
+					&.live-quiz-popup-opt-active {
+						border-color: #ffeb66;
+						background: #fffef0;
+					}
+
+					.quiz-opt-key {
+						width: 48rpx;
+						font-weight: 600;
+						color: #f4410b;
+					}
+
+					.quiz-opt-label {
+						flex: 1;
+						font-size: 28rpx;
+						color: #333;
+					}
+				}
+
+				.lottery-action-button.live-quiz-popup-submit-disabled {
+					opacity: 0.45;
+				}
+			}
+
 			// 聊天区域
 			.chat-area-container {
 				background: var(--chat-bg);