|
@@ -0,0 +1,306 @@
|
|
|
+<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/index2.vue';
|
|
|
+export default {
|
|
|
+ components: { QuestionItem },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ title: '',
|
|
|
+ teamQuestionnaireId: '',
|
|
|
+ list: [],
|
|
|
+ questionnaire: null,
|
|
|
+ isLoading: true,
|
|
|
+ isSubmitting: false,
|
|
|
+ scrollTop: 0, // scroll-view 的滚动位置
|
|
|
+ oldScrollTop: 0, // 辅助值,确保即使滚动到相同位置也能触发
|
|
|
+ userAnswerTemp: []
|
|
|
+ };
|
|
|
+ },
|
|
|
+ onLoad(option) {
|
|
|
+ this.title = option.title;
|
|
|
+ this.teamQuestionnaireId = option.teamQuestionnaireId;
|
|
|
+ this.getList();
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ answerNum() {
|
|
|
+ return this.list.filter(l => l?.answer.filter(a => a?.value).length == this.userAnswerTemp.length).length || 0;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ handleAnswerChange(e) {
|
|
|
+ const item = this.list[e.index];
|
|
|
+ if (item) {
|
|
|
+ let answer = JSON.parse(JSON.stringify(item.answer));
|
|
|
+ answer.forEach(a=>{
|
|
|
+ a.value = a.assessmentMethod==e.assessmentMethod?e.value:a.value
|
|
|
+ })
|
|
|
+ this.$set(item, 'answer', answer);
|
|
|
+ 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.userAnswerTemp = res.data.data[0].userAnswer.map(u=>{
|
|
|
+ return {
|
|
|
+ assessmentMethod:u.assessmentMethod,
|
|
|
+ value:''
|
|
|
+ }
|
|
|
+ })
|
|
|
+ let uaTemp = JSON.parse(JSON.stringify(this.userAnswerTemp))
|
|
|
+ this.list = res.data.data.map(item => {
|
|
|
+ let _a = answerMap.get(item.id);
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ answer:_a?uaTemp.map(u=>{
|
|
|
+ return {
|
|
|
+ assessmentMethod:u.assessmentMethod,
|
|
|
+ value:_a.find(a=>a.assessmentMethod==u.assessmentMethod)?.value||''
|
|
|
+ }
|
|
|
+ }):this.userAnswerTemp,
|
|
|
+ warn: false
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.list.forEach(l=>{
|
|
|
+ let _a = answerMap.get(l.id);
|
|
|
+ l.userAnswer.forEach(u=>{
|
|
|
+ u.answer = _a?(_a.find(a=>a.assessmentMethod==u.assessmentMethod)?.value||''):''
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .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;
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ submitWj() {
|
|
|
+ if (this.isSubmitting) return;
|
|
|
+
|
|
|
+ let firstUnansweredIndex = -1;
|
|
|
+ this.list.forEach((l, i) => {
|
|
|
+ const isAnswered = l.answer.filter(a => a?.value).length==this.userAnswerTemp.length?true:false;
|
|
|
+ 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; // 终止提交
|
|
|
+ }
|
|
|
+
|
|
|
+ const submitList = this.list.map((l) => {
|
|
|
+ let {answer,userAnswer,warn,...other} = JSON.parse(JSON.stringify(l));
|
|
|
+ userAnswer.forEach(u=>{
|
|
|
+ u.questionOption.forEach(q=>{
|
|
|
+ q.userAnswer=answer.find(a=>a.assessmentMethod==u.assessmentMethod)?.value==q.questionOption;
|
|
|
+ })
|
|
|
+ })
|
|
|
+ return {
|
|
|
+ ...other,
|
|
|
+ isAnswer: '1',
|
|
|
+ userAnswer
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ 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.filter(a => a?.value).length>0).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>
|