dedsudiyu 1 week ago
parent
commit
42c029eaec

+ 139 - 0
pages_safetyEducationExamination/component/richTextPdf.vue

@@ -0,0 +1,139 @@
+<!-- 富文本PDF预览与下载组件 (小程序版本) -->
+<template>
+	<view class="rich-text-pdf-wrapper">
+		<scroll-view scroll-y class="content-scroll">
+			<view class="rich-content">
+				<rich-text :nodes="content"></rich-text>
+			</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'RichTextPdf',
+		props: {
+			content: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+				loading: false
+			};
+		},
+		methods: {
+			// 微信小程序版本:使用 Canvas 生成图片后保存
+			async downloadPDF() {
+				if (!this.content) {
+					uni.showToast({
+						title: '没有可下载的内容',
+						icon: 'none'
+					});
+					return;
+				}
+
+				this.loading = true;
+				uni.showLoading({ title: '生成中...' });
+
+				try {
+					// 由于小程序限制,这里使用简化方案:
+					// 1. 将富文本内容通过截图保存为图片
+					// 2. 或者直接保存HTML内容供后续查看
+
+					// 方案1: 使用 Canvas 截图(需要配合后端或使用第三方服务)
+					// 由于小程序无法直接将 rich-text 渲染到 canvas,
+					// 这里采用保存为图片的简化方案
+
+					await this._saveAsImage();
+
+				} catch (error) {
+					console.error('PDF 生成失败:', error);
+					uni.showToast({
+						title: 'PDF下载失败,请重试',
+						icon: 'none'
+					});
+				} finally {
+					this.loading = false;
+					uni.hideLoading();
+				}
+			},
+
+			// 简化方案:保存为图片
+			async _saveAsImage() {
+				// 微信小程序中,rich-text 无法直接转为 canvas
+				// 需要使用以下方案之一:
+				// 1. 后端生成PDF接口
+				// 2. 使用 web-view 加载HTML后截图
+				// 3. 提示用户长按保存或分享
+
+				uni.showModal({
+					title: '提示',
+					content: '微信小程序暂不支持直接生成PDF,建议在电脑端下载或长按保存内容',
+					showCancel: false
+				});
+
+				// 如果有后端PDF生成接口,可以这样调用:
+				// const result = await this._callBackendPdfApi();
+				// if (result.pdfUrl) {
+				//   uni.downloadFile({
+				//     url: result.pdfUrl,
+				//     success: (res) => {
+				//       uni.saveFile({
+				//         tempFilePath: res.tempFilePath,
+				//         success: () => {
+				//           uni.showToast({ title: '保存成功' });
+				//         }
+				//       });
+				//     }
+				//   });
+				// }
+			},
+
+			// 调用后端PDF生成接口(示例)
+			async _callBackendPdfApi() {
+				return new Promise((resolve, reject) => {
+					uni.request({
+						url: '/api/generate-pdf', // 替换为实际接口
+						method: 'POST',
+						data: {
+							htmlContent: this.content
+						},
+						success: (res) => {
+							if (res.data.code === 200) {
+								resolve(res.data.data);
+							} else {
+								reject(new Error(res.data.message));
+							}
+						},
+						fail: reject
+					});
+				});
+			}
+		}
+	};
+</script>
+
+<style lang="stylus" scoped>
+	.rich-text-pdf-wrapper {
+		width: 100%;
+		height: 100%;
+		display: flex;
+		flex-direction: column;
+		background: #fff;
+	}
+
+	.content-scroll {
+		flex: 1;
+		overflow: hidden;
+	}
+
+	.rich-content {
+		padding: 40rpx;
+		font-size: 28rpx;
+		line-height: 1.8;
+		color: #333;
+		word-wrap: break-word;
+	}
+</style>

+ 165 - 13
pages_safetyEducationExamination/views/home/component/examTask.vue

@@ -21,17 +21,47 @@
 				<view v-if="!onlineExamList[0]" class="empty-tip">暂无考试任务</view>
 			</view>
 		</view>
+
+		<!-- 考试承诺书弹窗 -->
+		<uni-popup ref="preExamDialog" type="center" :mask-click="false">
+			<view class="pre-exam-dialog">
+				<view class="dialog-header">
+					<text class="dialog-title">考试承诺书</text>
+				</view>
+				<view class="dialog-body">
+					<scroll-view scroll-y class="content-scroll">
+						<richTextPdf ref="richTextPdf" :content="richTextHtml" />
+					</scroll-view>
+				</view>
+				<view class="dialog-footer">
+					<view v-if="preButtonType" class="btn btn-border" @click="downloadCommitment">下载</view>
+					<view class="btn btn-primary" @click="confirmCommitment">确认</view>
+				</view>
+			</view>
+		</uni-popup>
 	</view>
 </template>
 
 <script>
-	import { 
-		examUserExamStudyTaskList, 
+	import {
+		examUserExamStudyTaskList,
+		examUserExamStart,
+		examUserExamCommitmentConfirm,
 	} from '@/pages_safetyEducationExamination/api/index.js'
+	import richTextPdf from '@/pages_safetyEducationExamination/component/richTextPdf.vue'
+
 	export default {
+		components: {
+			richTextPdf
+		},
 		data() {
 			return {
-				onlineExamList:[],
+				onlineExamList: [],
+				// 承诺书相关
+				preExamDialogType: false,
+				preButtonType: false,
+				richTextHtml: '',
+				preData: null,
 			}
 		},
 		created() {
@@ -59,10 +89,10 @@
 							//等待后续逻辑
 						}else{
 							//学时要求
-							if(item.knowledgePointIds&&item.completedStudyHours&&item.studyHoursRequirement){
+							if(item.knowledgePointIds){
 								//学时知识点
 								uni.navigateTo({
-									url: '/pages_safetyEducationExamination/views/studyHourTask/index',
+									url: `/pages_safetyEducationExamination/views/studyHourTask/index?knowledgePointId=${item.knowledgePointIds}`,
 								});
 							}else{
 								//无知识点要求
@@ -111,10 +141,10 @@
 			setRichExamData(item){
 				if(item.commitmentEnabled == 1 && !item.commitmentConfirmed){
 					//富文本弹窗考试承诺书
-					this.$set(this,'preButtonType',item.commitmentPrintEnabled == 1?true:false);
-					this.$set(this,'richTextHtml',item.commitmentContent);
-					this.$set(this,'preData',item);
-					this.$set(this,'preExamDialogType',true);
+					this.preButtonType = item.commitmentPrintEnabled == 1;
+					this.richTextHtml = item.commitmentContent;
+					this.preData = item;
+					this.$refs.preExamDialog.open();
 				}else{
 					if(item.attemptId){
 						//上次考试未完成继续考试
@@ -126,25 +156,91 @@
 						//等待后续逻辑
 					}else{
 						//开始新考试
-						this.examUserExamStart(item);
+						this.startNewExam(item);
+					}
+				}
+			},
+			// 下载承诺书
+			downloadCommitment() {
+				uni.showToast({
+					title: '正在下载,请耐心等候',
+					icon: 'none'
+				});
+				this.$refs.richTextPdf.downloadPDF();
+			},
+			// 确认承诺书
+			async confirmCommitment() {
+				if (this.preData.attemptId) {
+					await this.confirmAndStart(this.preData.attemptId);
+				} else {
+					try {
+						const { data } = await examUserExamStart({
+							examId: this.preData.examId,
+							examScene: 1
+						});
+						if (data.code === 200 && data.data.attemptId) {
+							await this.confirmAndStart(data.data.attemptId);
+						} else {
+							uni.showToast({
+								title: data.message || '开始考试失败',
+								icon: 'none'
+							});
+						}
+					} catch (e) {
+						console.error('开始考试失败', e);
+						uni.showToast({
+							title: '开始考试失败',
+							icon: 'none'
+						});
 					}
 				}
 			},
+			// 确认承诺并开始考试
+			async confirmAndStart(attemptId) {
+				try {
+					const { data } = await examUserExamCommitmentConfirm({ attemptId });
+					if (data.code === 200) {
+						this.$refs.preExamDialog.close();
+						// 跳转到考试页面
+						let obj = {
+							examId: this.preData.examId,
+							attemptId: attemptId,
+							type: '1',
+							examKind: this.preData.examKind,
+						};
+						// 等待后续逻辑:跳转到答题页面
+						uni.showToast({
+							title: '已确认承诺书',
+							icon: 'success'
+						});
+					}
+				} catch (e) {
+					console.error('确认承诺书失败', e);
+					uni.showToast({
+						title: '确认失败',
+						icon: 'none'
+					});
+				}
+			},
 			//开始考试
 			async examUserExamStart(item) {
 				try {
 					let obj = {examId:item.examId,examScene:1};
-					const { data } = await examUserExamStudyTaskList(obj);
+					const { data } = await examUserExamStart(obj);
 					if (data.code === 200) {
 						let obj = {
 							examId: item.examId,
-							attemptId: response.data.attemptId,
+							attemptId: data.data.attemptId,
 							type: '1',
 							examKind:item.examKind,
 						}
 						//等待后续逻辑
 					}
-				} catch (e) { console.error('获取考试任务失败', e); }
+				} catch (e) { console.error('开始考试失败', e); }
+			},
+			// 开始新考试
+			async startNewExam(item) {
+				await this.examUserExamStart(item);
 			},
 			//获取任务列表
 			async examUserExamStudyTaskList() {
@@ -231,4 +327,60 @@
 			}
 		}
 	}
+
+	// 承诺书弹窗样式
+	.pre-exam-dialog {
+		width: 600rpx;
+		max-height: 80vh;
+		background: #fff;
+		border-radius: 20rpx;
+		overflow: hidden;
+	}
+
+	.dialog-header {
+		padding: 32rpx;
+		border-bottom: 1rpx solid #eee;
+
+		.dialog-title {
+			font-size: 32rpx;
+			font-weight: 600;
+			color: #333;
+		}
+	}
+
+	.dialog-body {
+		height: 800rpx;
+		overflow: hidden;
+
+		.content-scroll {
+			height: 100%;
+		}
+	}
+
+	.dialog-footer {
+		display: flex;
+		gap: 20rpx;
+		padding: 24rpx 32rpx;
+		border-top: 1rpx solid #eee;
+
+		.btn {
+			flex: 1;
+			height: 72rpx;
+			line-height: 72rpx;
+			text-align: center;
+			font-size: 28rpx;
+			border-radius: 12rpx;
+		}
+
+		.btn-border {
+			background: #fff;
+			color: #1857d4;
+			border: 1rpx solid #1857d4;
+		}
+
+		.btn-primary {
+			background: #1857d4;
+			color: #fff;
+		}
+	}
 </style>

+ 107 - 30
pages_safetyEducationExamination/views/studyHourTask/index.vue

@@ -2,7 +2,7 @@
 <template>
 	<view class="safetyEducationExamination-studyHourTask">
 		<!-- 顶部类型标签 -->
-		<scroll-view scroll-x class="tag-scroll">
+		<scroll-view scroll-x class="tag-scroll" enable-flex>
 			<view class="tag-scroll-inner">
 				<text
 					v-for="(tag, index) in typeTags"
@@ -15,7 +15,7 @@
 		</scroll-view>
 
 		<!-- 课程列表 -->
-		<scroll-view scroll-y class="scroll-content">
+		<scroll-view scroll-y class="scroll-content" @scrolltolower="onScrollToLower">
 			<view class="panel">
 				<view
 					v-for="(item, index) in courseList"
@@ -24,18 +24,20 @@
 					@click="goCourseDetail(item)"
 				>
 					<view class="course-cover" :style="'background:' + item.coverGradient">
-						<text class="cover-icon">{{ item.coverIcon || '📖' }}</text>
+						<image v-if="item.materialType == 1 && item.exampleImg" :src="baseUrl + item.exampleImg" class="cover-image" mode="aspectFill" />
+						<text v-else class="cover-icon">{{ item.coverIcon || '📖' }}</text>
 					</view>
 					<view class="course-info">
 						<view class="course-info-top">
-							<text class="course-name">{{ item.courseName }}</text>
+							<text class="course-name">{{ item.coursewareName }}</text>
 							<text class="status-tag" :class="item.statusClass">{{ item.statusName }}</text>
 						</view>
-						<text class="course-meta">{{ item.courseDuration }}分钟 | {{ item.studyHours }}学时 | {{ item.points }}积分</text>
+						<text class="course-meta">{{ formatDuration(item.minLearningDuration) }}分钟 | {{ item.creditHours }}学时 | {{ item.points }}积分</text>
 					</view>
 				</view>
 				<view v-if="!courseList[0] && !loading" class="empty-tip">暂无学时任务</view>
 				<view v-if="loading" class="loading-tip">加载中...</view>
+				<view v-if="noMore && courseList[0]" class="loading-tip">没有更多了</view>
 			</view>
 		</scroll-view>
 	</view>
@@ -43,8 +45,11 @@
 
 <script>
 	import {
-		examUserCourseLearningList,
-		examUserExamTypeSelectExamType,
+		config
+	} from '@/api/request/config.js'
+	import {
+		examUserCoursewareLearningList,
+		examElKnowledgePointTreeList,
 	} from '@/pages_safetyEducationExamination/api/index.js'
 
 	// 封面渐变色池
@@ -72,61 +77,122 @@
 	export default {
 		data() {
 			return {
-				typeTags: [{ label: '全部', value: null }],
+				baseUrl: config.base_url,
+				typeTags: [],
 				activeTagIndex: 0,
 				courseList: [],
 				loading: false,
+				page: 1,
+				pageSize: 10,
+				total: 0,
+				noMore: false,
+				knowledgePointIds: null, // 从上个页面传来的知识点ID数组
+			}
+		},
+		onLoad(options) {
+			console.log('options',options);
+			// 接收知识点ID参数
+			if (options.knowledgePointId) {
+				this.knowledgePointIds = options.knowledgePointId;
 			}
 		},
 		created() {
-			this.loadTypeTags();
+
 		},
-		onShow() {
-			this.loadCourseList();
+		async onShow() {
+			await this.loadTypeTags();
+			this.resetList();
 		},
 		methods: {
-			// 加载考试类型标签
+			// 递归扁平化树形结构,提取所有节点
+			flattenTree(nodes, result = []) {
+				if (!nodes || !Array.isArray(nodes)) return result;
+
+				nodes.forEach(node => {
+					result.push(node);
+					if (node.children && node.children.length > 0) {
+						this.flattenTree(node.children, result);
+					}
+				});
+
+				return result;
+			},
+
+			// 加载知识点标签(根据传入的knowledgePointIds过滤)
 			async loadTypeTags() {
 				try {
-					const { data } = await examUserExamTypeSelectExamType({});
+					const { data } = await examElKnowledgePointTreeList({ dataScope: 1 });
 					if (data.code === 200 && data.data) {
-						const tags = (data.data || []).map(item => ({
-							label: item.examTypeName,
-							value: item.examTypeId,
+						// 扁平化树形结构,获取所有节点
+						const allNodes = this.flattenTree(data.data);
+
+						let tags = allNodes;
+
+						// 如果有传入的知识点ID,则过滤标签
+						if (this.knowledgePointIds) {
+							const idsArray = this.knowledgePointIds.split(',').map(id => id.trim());
+							tags = allNodes.filter(item => idsArray.includes(String(item.id)));
+						}
+
+						const typeTags = tags.map(item => ({
+							label: item.knowledgePointName,
+							value: item.id,
 						}));
-						this.$set(this, 'typeTags', [{ label: '全部', value: null }, ...tags]);
+
+						this.$set(this, 'typeTags', typeTags);
+
+						// 默认选中第一个
+						if (typeTags.length > 0) {
+							this.activeTagIndex = 0;
+						}
 					}
 				} catch (e) {
-					console.error('获取考试类型失败', e);
+					console.error('获取知识点类型失败', e);
 				}
 			},
 			onTagChange(index) {
-				this.activeTagIndex = index;
+				if (this.activeTagIndex !== index) {
+					this.activeTagIndex = index;
+					this.resetList();
+				}
+			},
+			resetList() {
+				this.page = 1;
+				this.noMore = false;
+				this.courseList = [];
 				this.loadCourseList();
 			},
-			// 学时任务列表(courseType=1)
+			// 学时任务列表(materialType为空,查询全部类型
 			async loadCourseList() {
-				if (this.loading) return;
+				if (this.loading || this.noMore) return;
 				this.loading = true;
 				try {
 					const tag = this.typeTags[this.activeTagIndex];
-					const { data } = await examUserCourseLearningList({
-						page: 1,
-						pageSize: 50,
-						courseType: 1,
-						examTypeId: tag.value || undefined,
+					const { data } = await examUserCoursewareLearningList({
+						page: this.page,
+						pageSize: this.pageSize,
+						materialType: '', // 空字符串,查询全部类型
+						knowledgePointId: tag ? tag.value : undefined,
 					});
 					if (data.code === 200) {
-						const list = (data.data.records || []).map((item, i) => {
+						const records = (data.data.records || []).map((item, i) => {
 							const s = getStatusInfo(item.learnStatus);
+							const colorIndex = (this.courseList.length + i) % COVER_GRADIENTS.length;
 							return {
 								...item,
-								coverGradient: COVER_GRADIENTS[i % COVER_GRADIENTS.length],
+								coverGradient: COVER_GRADIENTS[colorIndex],
 								statusName: s.name,
 								statusClass: s.cls,
 							};
 						});
-						this.$set(this, 'courseList', list);
+						const newList = this.page === 1 ? records : [...this.courseList, ...records];
+						this.$set(this, 'courseList', newList);
+						this.total = data.data.total;
+						if (newList.length >= this.total) {
+							this.noMore = true;
+						} else {
+							this.page++;
+						}
 					}
 				} catch (e) {
 					console.error('获取学时任务失败', e);
@@ -134,8 +200,15 @@
 					this.loading = false;
 				}
 			},
+			onScrollToLower() {
+				this.loadCourseList();
+			},
+			// 秒数转分钟
+			formatDuration(seconds) {
+				return seconds ? Math.ceil(seconds / 60) : 0;
+			},
 			goCourseDetail(item) {
-				// 跳转课程详情,传递 courseId
+				// 跳转课程详情,传递 coursewareId
 			},
 		},
 	}
@@ -204,6 +277,10 @@
 	.cover-icon
 		font-size 64rpx
 
+	.cover-image
+		width 100%
+		height 100%
+
 	.course-info
 		padding 20rpx 24rpx