소스 검색

优化:提交是自动滚动到未作答项位置

htc 1 일 전
부모
커밋
3702c25367
2개의 변경된 파일285개의 추가작업 그리고 224개의 파일을 삭제
  1. 1 0
      components/QuestionItem/index.vue
  2. 284 224
      pages/questionnaireFill.vue

+ 1 - 0
components/QuestionItem/index.vue

@@ -56,6 +56,7 @@
 		box-sizing: border-box;
 		&.active{
 			border: 2rpx dotted #FD4F66;
+			padding: 30rpx 32rpx;
 		}
 		.lb_title{
 			font-family: PingFang-SC, PingFang-SC;

+ 284 - 224
pages/questionnaireFill.vue

@@ -1,228 +1,288 @@
-<template>
-	<view class="page" :style="{'min-height':h+'px', 'padding-top':mt+'px'}">
-		<cus-header title='问卷填写' :backAlert="true"></cus-header>
-		<div class="top adffcacjc">
-			<p>{{title}}</p>
-			<p class="tip">共 <span>{{list.length}}</span> 题,已作答 <span style="font-weight: bold;">{{answerNum}}</span> 题,请耐心选择!</p>
-		</div>
-		<div class="list" :style="{'height':(h-180-mt)+'px'}">
-			<div v-if="isLoading" class="loading-container adfacjc">
-				<div class="adfac">
-					<u-loading-icon size="42"></u-loading-icon>
-					<text style="margin-left: 10rpx;font-size: 34rpx;color: #666666;">问卷加载中...</text>
-				</div>
-			</div>
-			<template v-else>
-				<div class="l_item" v-for="(item,index) in list" :key="item.id">
-					<QuestionItem :item="item" :index="index" @change="handleAnswerChange"></QuestionItem>
-				</div>
-			</template>
-		</div>
+<template>
+	<view class="page" :style="{ 'min-height': h + 'px', 'padding-top': mt + 'px' }">
+		<cus-header title="问卷填写" :backAlert="true"></cus-header>
+		<div class="top adffcacjc">
+			<p>{{ title }}</p>
+			<p class="tip">
+				共
+				<span>{{ list.length }}</span>
+				题,已作答
+				<span style="font-weight: bold">{{ answerNum }}</span>
+				题,请耐心选择!
+			</p>
+		</div>
+		<scroll-view class="list" scroll-y="true" :scroll-top="scrollTop" :style="{ height: h - 180 - mt + 'px' }">
+			<div v-if="isLoading" class="loading-container adfacjc">
+				<div class="adfac">
+					<u-loading-icon size="42"></u-loading-icon>
+					<text style="margin-left: 10rpx; font-size: 34rpx; color: #666666">问卷加载中...</text>
+				</div>
+			</div>
+			<template v-else>
+				<view>
+					<div class="l_item" v-for="(item, index) in list" :key="item.id" :id="'question-' + index">
+						<QuestionItem :item="item" :index="index" @change="handleAnswerChange"></QuestionItem>
+					</div>
+				</view>
+			</template>
+		</scroll-view>
 		<div class="bottom">
-			<view class="zt_btn" @tap="submitWj">{{isSubmitting ? '提交中...' : '提交问卷'}}</view>
-		</div>
-	</view>
-</template>
-
-<script>
-	import QuestionItem from '@/components/QuestionItem/index.vue'
-	export default {
-		components:{QuestionItem},
-		data(){
-			return {
-				title:'',
-				teamQuestionnaireId:'',
-				list:[],
-				questionnaire:null,
-				isLoading: true,
-				isSubmitting: false
-			}
-		},
-		onLoad(option) {
-			this.title = option.title;
-			this.teamQuestionnaireId = option.teamQuestionnaireId;
-			this.getList();
+			<view class="zt_btn" @tap="submitWj">{{ isSubmitting ? '提交中...' : '提交问卷' }}</view>
+		</div>
+	</view>
+</template>
+
+<script>
+import QuestionItem from '@/components/QuestionItem/index.vue';
+export default {
+	components: { QuestionItem },
+	data() {
+		return {
+			title: '',
+			teamQuestionnaireId: '',
+			list: [],
+			questionnaire: null,
+			isLoading: true,
+			isSubmitting: false,
+			h: 0, // 屏幕可用高度
+			mt: 0, // 页面顶部内边距 (用于适配自定义导航栏)
+			scrollTop: 0, // scroll-view 的滚动位置
+			oldScrollTop: 0 // 辅助值,确保即使滚动到相同位置也能触发
+		};
+	},
+	onLoad(option) {
+		const sysInfo = uni.getSystemInfoSync();
+		const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
+		this.h = sysInfo.windowHeight;
+		// 适配自定义导航栏的顶部安全距离 = 状态栏高度 + 胶囊高度 + 上下边距
+		this.mt = menuButtonInfo.top + menuButtonInfo.height + (menuButtonInfo.top - sysInfo.statusBarHeight);
+
+		this.title = option.title;
+		this.teamQuestionnaireId = option.teamQuestionnaireId;
+		this.getList();
+	},
+	computed: {
+		answerNum() {
+			return this.list.filter((l) => !!l.answer).length || 0;
+		}
+	},
+	methods: {
+		handleAnswerChange(e) {
+			const item = this.list[e.index];
+			if (item) {
+				this.$set(item, 'answer', e.value);
+				if (item.warn) {
+					this.$set(item, 'warn', false);
+				}
+			}
+			this.setQuestionnaireCache();
+		},
+		getList() {
+			let questionnaire = null;
+			try {
+				const cacheStr = uni.getStorageSync('questionnaire');
+				if (cacheStr) {
+					questionnaire = JSON.parse(cacheStr);
+				}
+			} catch (e) {
+				console.error('解析问卷缓存失败:', e);
+				uni.removeStorageSync('questionnaire');
+			}
+
+			this.isLoading = true;
+			this.$api
+				.get('/core/team/member/answer/listByUser/' + this.teamQuestionnaireId)
+				.then((res) => {
+					if (res.data.code !== 0) return this.$showToast(res.data.msg);
+					const answerMap = new Map();
+					if (questionnaire && this.teamQuestionnaireId == questionnaire.key) {
+						questionnaire.list.forEach((q) => answerMap.set(q.id, q.answer));
+					}
+
+					this.list = res.data.data.map((item) => ({
+						...item,
+						warn: false,
+						answer: answerMap.get(item.id) || ''
+					}));
+				})
+				.catch(() => {
+					this.isLoading = false;
+					this.$showToast('网络异常,请稍后重试');
+				})
+				.finally(() => {
+					this.isLoading = false;
+				});
+		},
+		scrollToQuestion(index) {
+			const query = uni.createSelectorQuery().in(this);
+			const targetId = `#question-${index}`;
+			query.select(targetId).boundingClientRect();
+			query.select('.list').scrollOffset(); // 获取 scroll-view 的滚动信息
+			query.select('.list').boundingClientRect(); // 获取 scroll-view 自身的位置信息
+			query.exec((res) => {
+				// 增加安全校验
+				if (!res || !res[0] || !res[1] || !res[2]) {
+					return;
+				}
+
+				// res[0]: 目标元素 (#question-N) 的位置信息
+				const itemRect = res[0];
+				// res[1]: 滚动容器 (.list) 的滚动信息
+				const scrollViewScroll = res[1];
+				// res[2]: 滚动容器 (.list) 自身的位置信息
+				const scrollViewRect = res[2];
+
+				// 计算目标滚动位置 = 当前滚动距离 + (目标元素顶部 - 滚动容器顶部) - 预留间距
+				const targetPosition = scrollViewScroll.scrollTop + itemRect.top - scrollViewRect.top - 10; // 减10px作为顶部留白
+
+				// 通过先设置为旧值,再在 nextTick 中设置为新值的方式,确保滚动能够触发
+				this.scrollTop = this.oldScrollTop;
+				this.$nextTick(() => {
+					this.scrollTop = targetPosition;
+					this.oldScrollTop = targetPosition;
+				});
+			});
 		},
-		computed: {
-			answerNum() {
-				return this.list.filter(l => !!l.answer).length || 0;
+		submitWj() {
+			if (this.isSubmitting) return;
+
+			let firstUnansweredIndex = -1;
+			this.list.forEach((l, i) => {
+				const isAnswered = !!l.answer;
+				this.$set(l, 'warn', !isAnswered);
+				if (!isAnswered && firstUnansweredIndex === -1) {
+					firstUnansweredIndex = i;
+				}
+			});
+
+			if (firstUnansweredIndex > -1) {
+				uni.showModal({
+					title: '提示',
+					content: `第 ${firstUnansweredIndex + 1} 项未选择答案,请选择。`,
+					showCancel: false,
+					success: (res) => {
+						if (res.confirm) {
+							// 调用新的滚动方法
+							this.scrollToQuestion(firstUnansweredIndex);
+						}
+					}
+				});
+				return; // 终止提交
 			}
-		},
-		methods:{
-			handleAnswerChange(e) {
-				const item = this.list[e.index];
-				if (item) {
-					this.$set(item, 'answer', e.value);
-					if (item.warn) {
-						this.$set(item, 'warn', false);
+
+			const submitList = this.list.map((question) => {
+				const formattedUserAnswer = question.userAnswer.map((option) => ({
+					...option,
+					isSelected: option.questionOption === question.answer
+				}));
+				const { answer, warn, ...restOfQuestion } = question;
+				return {
+					...restOfQuestion,
+					isAnswer: '1',
+					userAnswer: formattedUserAnswer
+				};
+			});
+
+			this.isSubmitting = true;
+			this.$api
+				.post('/core/team/member/answer/submit', submitList)
+				.then((res) => {
+					if (res.data.code !== 0) {
+						this.isSubmitting = false;
+						return this.$showToast(res.data.msg);
 					}
-				}
-				
-				this.setQuestionnaireCache();
-			},
-			getList(){
-				let questionnaire = null;
-				try {
-					const cacheStr = uni.getStorageSync('questionnaire');
-					if (cacheStr) {
-						questionnaire = JSON.parse(cacheStr);
-					}
-				} catch (e) {
-					console.error("解析问卷缓存失败:", e);
-					uni.removeStorageSync('questionnaire'); // 缓存有问题,直接清除
-				}
-				
-				this.isLoading = true;
-				this.$api.get('/core/team/member/answer/listByUser/' + this.teamQuestionnaireId).then(res => {
-					if (res.data.code !== 0) return this.$showToast(res.data.msg);
-					const answerMap = new Map();
-					if (questionnaire && this.teamQuestionnaireId == questionnaire.key) {
-						questionnaire.list.forEach(q => answerMap.set(q.id, q.answer));
-					}
-					
-					this.list = res.data.data.map(item => ({
-						...item,
-						warn: false,
-						answer: answerMap.get(item.id) || '' // 直接从 Map 中获取答案
-					}));
-				}).catch(()=>{
-					this.isLoading = false;
-					this.$showToast('网络异常,请稍后重试');
-				}).finally(() => {
-					this.isLoading = false;
-				});
-			},
-			submitWj() {
-			    if (this.isSubmitting) return;
-			
-			    let firstUnansweredIndex = -1;
-			    this.list.forEach((l, i) => {
-			        const isAnswered = !!l.answer;
-			        this.$set(l, 'warn', !isAnswered);
-			        if (!isAnswered && firstUnansweredIndex === -1) {
-			            firstUnansweredIndex = i;
-			        }
-			    });
-			
-			    if (firstUnansweredIndex > -1) {
-			        return uni.showModal({
-			            title: '提示',
-			            content: `第 ${firstUnansweredIndex + 1} 项未选择答案,请选择。`,
-			            showCancel: false
-			        });
-			    }
-			
-			    const submitList = this.list.map(question => {
-			        const formattedUserAnswer = question.userAnswer.map(option => ({
-			            ...option,
-			            isSelected: option.questionOption === question.answer
-			        }));
-			
-			        const { answer, warn, ...restOfQuestion } = question;
-			
-			        return {
-			            ...restOfQuestion,
-			            isAnswer: '1',
-			            userAnswer: formattedUserAnswer
-			        };
-			    });
-			
-			    this.isSubmitting = true;
-			    this.$api.post('/core/team/member/answer/submit', submitList).then(res => {
-			        if (res.data.code !== 0) {
-			            this.isSubmitting = false; 
-			            return this.$showToast(res.data.msg);
-			        }
-			        uni.removeStorageSync('questionnaire');
-			        uni.redirectTo({
-			            url: '/pages/questionnaireResult'
-			        });
-			    }).catch(err => {
-			        this.$showToast('网络异常,请稍后重试');
-			    })
-			    .finally(() => {
-			        this.isSubmitting = false;
-			    });
-			},
-			setQuestionnaireCache(){
-				const answeredList = this.list
-					.filter(l => l.answer)
-					.map(l => ({ id: l.id, answer: l.answer }));
-				
-				if (answeredList.length === 0) {
-					uni.removeStorageSync('questionnaire');
-					return;
-				}
-				
-				const qinfo = {
-					key: this.teamQuestionnaireId,
-					list: answeredList
-				};
-				uni.setStorageSync('questionnaire', JSON.stringify(qinfo));
-			}
-		},
-		// onUnload() {
-		// 	this.setQuestionnaireCache();
-		// },
-		// onBackPress() {
-		// 	this.setQuestionnaireCache();
-		// 	return false; 
-		// }
-	}
-</script>
-
-<style scoped lang="less">
-	.loading-container {
-		width: 100%;
-		height: 100%;
-	}
-	.page{
-		background: #F7F2F6;
-		box-sizing: border-box;
-		
-		.top{
-			p{
-				font-family: PingFang-SC, PingFang-SC;
-				font-weight: bold;
-				font-size: 42rpx;
-				color: #252525;
-				line-height: 51rpx;
-				text-align: center;
-				margin-top: 48rpx;
-			}
-			.tip{
-				font-family: PingFangSC, PingFang SC;
-				font-weight: 400;
-				font-size: 26rpx;
-				color: #646464;
-				line-height: 26rpx;
-				text-align: center;
-				margin-top: 36rpx;
-				span{
-					margin: 0 10rpx;
-				}
-			}
-		}
-	
-		.list{
-			width: 100%;
-			overflow-y: auto;
-			margin-top: 28rpx;
-			.l_item{
-				margin-top: 20rpx;
-				width: 100%;
-				background: #FFFFFF;
-				padding: 6rpx;
-				box-sizing: border-box;
-			}
-		}
-	
-		.bottom{
-			width: calc(100% - 80rpx);
-			height: 88rpx;
-			position: fixed;
-			left: 40rpx;
-			bottom: 40rpx;
-		}
-	}
-</style>
+					uni.removeStorageSync('questionnaire');
+					uni.redirectTo({
+						url: '/pages/questionnaireResult'
+					});
+				})
+				.catch((err) => {
+					this.$showToast('网络异常,请稍后重试');
+				})
+				.finally(() => {
+					this.isSubmitting = false;
+				});
+		},
+		setQuestionnaireCache() {
+			const answeredList = this.list.filter((l) => l.answer).map((l) => ({ id: l.id, answer: l.answer }));
+
+			if (answeredList.length === 0) {
+				uni.removeStorageSync('questionnaire');
+				return;
+			}
+
+			const qinfo = {
+				key: this.teamQuestionnaireId,
+				list: answeredList
+			};
+			uni.setStorageSync('questionnaire', JSON.stringify(qinfo));
+		}
+	}
+};
+</script>
+
+<style scoped lang="less">
+.loading-container {
+	width: 100%;
+	height: 100%;
+}
+.page {
+	background: #f7f2f6;
+	box-sizing: border-box;
+	/* 【重要】让页面本身不可滚动,所有滚动都交给scroll-view处理 */
+	height: 100vh;
+	overflow: hidden;
+	display: flex;
+	flex-direction: column;
+
+	.top {
+		p {
+			font-family: PingFang-SC, PingFang-SC;
+			font-weight: bold;
+			font-size: 42rpx;
+			color: #252525;
+			line-height: 51rpx;
+			text-align: center;
+			margin-top: 48rpx;
+		}
+		.tip {
+			font-family: PingFangSC, PingFang SC;
+			font-weight: 400;
+			font-size: 26rpx;
+			color: #646464;
+			line-height: 26rpx;
+			text-align: center;
+			margin-top: 36rpx;
+			span {
+				margin: 0 10rpx;
+			}
+		}
+	}
+
+	.list {
+		width: 100%;
+		/* flex: 1;  如果使用flex布局,可以这样自适应高度 */
+		/* overflow-y: auto;  scroll-view自带滚动,无需此属性 */
+		margin-top: 28rpx;
+		.l_item {
+			margin-top: 20rpx;
+			width: 100%;
+			background: #ffffff;
+			padding: 6rpx;
+			box-sizing: border-box;
+
+			&:first-child {
+				margin-top: 0;
+			}
+		}
+	}
+
+	.bottom {
+		width: calc(100% - 80rpx);
+		height: 88rpx;
+		position: fixed;
+		left: 40rpx;
+		bottom: 40rpx;
+	}
+}
+</style>