You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

822 lines
24 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="survey-fill-page">
<image class="cbg" :src="base.imgHost('common_bg.png')"></image>
<!-- 进度条 -->
<view class="progress-bar-container">
<view class="progress-info">
<view class="progress-info-progress">
<u-line-progress :height="10" :percent="progressPercent" :show-percent="false"
active-color="#2018a0"></u-line-progress>
</view>
<text class="progress-text">{{ answeredCount }}/{{ questionList.length }}</text>
</view>
</view>
<view class="survey-container">
<view class="survey-header">
<h1 class="survey-title">{{survey.title||''}}
</h1>
<p class="survey-description">
{{survey.desc||''}}
</p>
</view>
<!-- 问卷头部 -->
<!-- <view class="survey-header">
<view class="survey-header-item">
<view>
<span>课程名称</span>
<span>{{survey.name || ''}}</span>
</view>
<view v-if="!course_id">
<span>课程主题</span>
<span>{{survey.theme || ''}}</span>
</view>
<view v-if="!course_id">
<span>授课老师</span>
<span>{{survey.teacher?survey.teacher.name:''}}</span>
</view>
<view>
<span>授课时间</span>
<span v-if="course_id">{{survey.start_date || ''}} 至 {{survey.end_date || ''}}</span>
<span v-else>{{survey.date}} {{survey.start_time?survey.start_time:''}}-{{survey.end_time?survey.end_time:''}}</span>
</view>
</view>
</view> -->
<!-- 问卷表单 -->
<view class="survey-form" :class="{ 'survey-form--last-input-focused': lastInputFocused }">
<view v-for="(q, index) in questionList" :key="q.id" :id="'question-'+q.id" class="question-item">
<view class="question-header">
<view class="question-number">{{ index + 1 }}</view>
<view class="question-title">
<view class="question-title-item">
<text>{{ q.name }}</text>
<text v-if="q.rule && q.rule.includes('required')" class="required-mark">*</text>
</view>
<view v-if="q.course_content" class="question-title-theme">
<text>{{ q.course_content.theme }}{{ q.course_content.teacher?' -- '+q.course_content.teacher.name:'' }}</text>
</view>
<!-- <text>{{ q.name }}</text>
<text v-if="q.rule && q.rule.includes('required')" class="required-mark">*</text> -->
</view>
</view>
<view class="question-content">
<!-- 单选题 -->
<view v-if="q.edit_input === 'radio'">
<u-radio-group wrap v-model="q.value" @change="onValueChange">
<u-radio active-color="#2018a0" v-for="(opt, i) in q.select_item" :key="i"
:name="opt.value">{{ opt.value }}</u-radio>
</u-radio-group>
<u-input v-if="q.allow_input" v-model="q.allow_input_text" type="text"
:placeholder="'请输入'" :adjust-position="false" :cursor-spacing="80"
@focus="onInputFocus(q)" @blur="onInputBlur" />
</view>
<!-- 多选题 -->
<view v-if="q.edit_input === 'checkbox'">
<!-- @change="(e)=>{onValueChange(e,q)}" -->
<!-- <u-checkbox-group >
<u-checkbox @change="(e)=>{changeCheck(e,q.select_item)}" v-model="opt.checked" v-for="(opt, i) in q.select_item" :key="i"
:name="opt.value">{{ opt.value }}</u-checkbox>
</u-checkbox-group> -->
<checkbox-group style="display: flex;flex-wrap: wrap;"
@change="(e)=>{onValueChange(e,q)}">
<label style="flex-basis: 100%;margin-bottom:10rpx" v-for="(checkboxitem, checkboxindex) in q.select_item">
<checkbox activeBackgroundColor="#2018a0" :value="checkboxitem.value" :checked="checkboxitem.checked" />
{{checkboxitem.value}}
</label>
</checkbox-group>
<u-input v-if="q.allow_input" v-model="q.allow_input_text" type="text"
:placeholder="'请输入'" :adjust-position="false" :cursor-spacing="80"
@focus="onInputFocus(q)" @blur="onInputBlur" />
</view>
<!-- 多维度题目 -->
<view v-if="q.edit_input === 'multi_dimension'" class="multi-dimension-container">
<view v-for="(dimension, dimIndex) in q.dimensions" :key="dimIndex" class="dimension-item">
<view class="dimension-title">
<text v-if="dimension.need_fill" class="required-mark">*</text>
<text class="dimension-name">{{ dimension.name }}</text>
<text v-if="dimension.help" class="dimension-help">{{ dimension.help }}</text>
</view>
<u-radio-group wrap :value="q.value && q.value[dimension.field] ? q.value[dimension.field] : ''" @change="(val) => onDimensionValueChange(q, dimension.field, val)">
<u-radio active-color="#2018a0" v-for="(opt, optIndex) in dimension.select_item" :key="optIndex"
:name="opt.value">{{ opt.value }}</u-radio>
</u-radio-group>
</view>
</view>
<!-- 文本题 -->
<u-input v-if="q.edit_input === 'text'" v-model="q.value" type="text"
:placeholder="q.help||'请输入'" :adjust-position="false" :cursor-spacing="80"
@input="onValueChange" @focus="onInputFocus(q)" @blur="onInputBlur" />
<!-- 文本域 -->
<u-input v-if="q.edit_input === 'textarea'" v-model="q.value" type="textarea"
:placeholder="q.help||'请输入'" :maxlength="9999" :adjust-position="false" :cursor-spacing="80"
@input="onValueChange" @focus="onInputFocus(q)" @blur="onInputBlur" />
<!-- 评分题 -->
<!-- <u-rate v-if="q.edit_input === 'rating'" v-model="answers[index]" :count="q.select_item.maxRating"
@change="onValueChange"></u-rate> -->
<!-- 量表题 -->
<!-- <view v-if="q.edit_input === 'scale'" class="scale-container">
<text>{{ q.select_item.min }}</text>
<u-slider v-model="answers[index]" :min="q.select_item.min" :max="q.select_item.max"
@end="onValueChange"></u-slider>
<text>{{ answers[index] || q.select_item.min }}</text>
</view> -->
<!-- 日期题 -->
<u-input v-if="q.edit_input === 'date'" v-model="q.value" type="text"
:placeholder="q.help||'请选择日期'" @click="openDatePicker(index)" />
<u-input v-if="q.edit_input === 'datetime'" v-model="q.value" type="text"
:placeholder="q.help||'请选择日期时间'" @click="openTimePicker(index)" />
</view>
</view>
</view>
<!-- 提交区域 -->
<view class="submit-section">
<!-- <u-button type="primary" shape="circle" @click="submitSurvey">
<u-icon name="send" class="btn-icon"></u-icon>
提交问卷
</u-button> -->
<view @click="submitSurvey" type="primary">提交</view>
</view>
</view>
<!-- 日期选择器 -->
<u-picker v-model="showDatePicker" mode="time" @confirm="confirmDate"></u-picker>
<!-- 日期选择器 -->
<u-picker v-model="showTimePicker" mode="time" @confirm="confirmTime" :params="params"></u-picker>
</view>
</template>
<script>
export default {
data() {
return {
id: '',
survey: {},
questionList: [],
answers: [],
isSubmit:false,
showDatePicker: false,
showTimePicker: false,
currentDateIndex: -1,
answeredCount: 0,
course_id:false,
lastInputFocused: false,
params: {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true
},
};
},
computed: {
progressPercent() {
if (!this.questionList.length) return 0;
return (this.answeredCount / this.questionList.length) * 100;
}
},
onLoad(options) {
// 获取页面参数
this.id = options.id
this.course_id = options.course_id
console.log("页面参数:", { id: this.id, course_id: this.course_id })
this.getDetail(this.id)
this.updateProgress();
},
onShow() {
// 真机上 blur 可能不触发,改用键盘高度变化:键盘收起时去掉底部留白
this._keyboardListener = (res) => {
if (res && res.height === 0) {
this.lastInputFocused = false
}
}
uni.onKeyboardHeightChange(this._keyboardListener)
},
onUnload() {
if (this._keyboardListener) {
uni.offKeyboardHeightChange(this._keyboardListener)
}
},
onShareAppMessage() {
let path = '/packages/surveyFill/index?id=' + this.id
if (this.course_id) path += '&course_id=' + this.course_id
return {
path,
title: this.survey.title || '课程问卷',
imageUrl: '/static/share.jpg'
}
},
onShareTimeline() {
let path = '/packages/surveyFill/index?id=' + this.id
if (this.course_id) path += '&course_id=' + this.course_id
return {
path,
title: this.survey.title || '课程问卷',
imageUrl: '/static/share.jpg'
}
},
methods: {
// 获取
async getDetail(id) {
// 检查传入的参数类型
// if (this.course_id) {
// // 如果参数是course_id使用courseDetail接口
// const res = await this.$u.api.courseDetail({
// course_id: this.course_id
// })
// console.log("courseDetail res", res)
// this.survey = res
// } else {
// // 如果不是course_id按之前的方式使用courseContentDetail接口
// const res = await this.$u.api.courseContentDetail({
// course_content_id: id
// })
// console.log("courseContentDetail res", res)
// this.survey = res
// }
const res = await this.$u.api.courseEvaluationDetail({
course_content_evaluation_id:id
})
console.log("courseEvaluationDetail res", res)
this.survey = res
// 检查是否有问卷调查数据
console.log("courseEvaluationDetail res", res.course_content_evaluation_asks)
if (res && res.course_content_evaluation_asks) {
this.questionList = res.course_content_evaluation_asks
} else {
this.questionList = []
console.warn("未找到问卷调查数据")
return
}
// 初始化答案数组
this.questionList.map(q => {
// 多维度题目初始化
if (q.edit_input === 'multi_dimension') {
// 解析 dimensions可能是字符串 JSON
if (q.dimensions && typeof q.dimensions === 'string') {
try {
q.dimensions = JSON.parse(q.dimensions)
} catch (e) {
console.error('解析 dimensions 失败:', e)
q.dimensions = []
}
}
// 解析 value如果后端返回的是 JSON 字符串,需要解析为对象)
if (q.value && typeof q.value === 'string') {
try {
const parsedValue = JSON.parse(q.value)
if (parsedValue && typeof parsedValue === 'object' && !Array.isArray(parsedValue)) {
q.value = parsedValue
} else {
q.value = {}
}
} catch (e) {
// 如果不是 JSON 字符串,初始化为空对象
q.value = {}
}
}
// 确保 value 是对象
if (!q.value || typeof q.value !== 'object' || Array.isArray(q.value)) {
q.value = {}
}
// 为每个维度初始化字段,使用 $set 确保响应式
if (q.dimensions && Array.isArray(q.dimensions)) {
// 找到 questionList 中的索引
const index = this.questionList.findIndex(item => item.id === q.id);
if (index !== -1) {
const questionItem = this.questionList[index];
q.dimensions.forEach(dim => {
if (dim.field) {
// 如果字段不存在,初始化为空字符串
if (!questionItem.value.hasOwnProperty(dim.field)) {
this.$set(questionItem.value, dim.field, '');
}
}
});
}
}
} else {
q.value = q.value?q.value:''
}
if (q.edit_input === 'checkbox') {
q.select_item.map(item=>{
item.checked = false
})
console.log("res", q)
};
if (q.allow_input) return q.allow_input_text = q.allow_input_text?q.allow_input_text:'';
});
},
updateProgress() {
let count = 0;
for (let i = 0; i < this.questionList.length; i++) {
const q = this.questionList[i];
// 多维度题目的完成状态检查
if (q.edit_input === 'multi_dimension') {
if (q.dimensions && Array.isArray(q.dimensions)) {
// 获取所有必填维度
const requiredDims = q.dimensions.filter(dim => dim.need_fill);
if (requiredDims.length === 0) {
// 如果没有必填维度,则至少有一个维度有值就算完成
const answer = q.value;
if (answer && typeof answer === 'object' && !Array.isArray(answer)) {
const hasAny = Object.values(answer).some(val => {
return val !== '' && val !== null && val !== undefined && val !== 'undefined' && val !== 'null';
});
if (hasAny) {
count++;
}
}
} else {
// 所有必填维度都必须有值才算完成
const answer = q.value;
if (answer && typeof answer === 'object' && !Array.isArray(answer)) {
const allFilled = requiredDims.every(dim => {
const dimValue = answer[dim.field];
return dimValue !== '' && dimValue !== null && dimValue !== undefined && dimValue !== 'undefined' && dimValue !== 'null';
});
if (allFilled) {
count++;
}
}
}
}
} else {
const answer = q.value;
if (Array.isArray(answer) && answer.length > 0) {
count++;
} else if (!Array.isArray(answer) && answer !== '' && answer !== null && answer !== undefined && answer !== 'undefined' && answer !== 'null') {
count++;
}
}
}
this.answeredCount = count;
},
onInputFocus(q) {
// 输入框获得焦点时,将题目滚动到可见区域,避免键盘弹起时输入框错位
if (!q || !q.id) return
const isLastInputQuestion = this.isLastInputQuestion(q)
if (isLastInputQuestion) {
this.lastInputFocused = true
}
const query = uni.createSelectorQuery().in(this)
query.select('#question-' + q.id).boundingClientRect((rect) => {
if (rect) {
uni.createSelectorQuery().in(this).selectViewport().scrollOffset((scroll) => {
if (scroll) {
// 最后一题输入框时多滚动使输入框出现在键盘上方键盘约300px高
const offsetFromTop = isLastInputQuestion ? 80 : 120
const targetScrollTop = scroll.scrollTop + rect.top - offsetFromTop
uni.pageScrollTo({
scrollTop: Math.max(0, targetScrollTop),
duration: 200
})
}
}).exec()
}
}).exec()
},
onInputBlur() {
this.lastInputFocused = false
},
isLastInputQuestion(q) {
// 判断是否为最后一个包含输入框的题目
const hasInput = (item) => item.edit_input === 'text' || item.edit_input === 'textarea' || item.allow_input
const inputQuestions = this.questionList.filter(hasInput)
return inputQuestions.length > 0 && inputQuestions[inputQuestions.length - 1].id === q.id
},
onValueChange(e, item) {
console.log("value", e,item)
if(item){
if (e.detail.value.length > 0) {
item.value = e.detail.value.join(",")
}
}
// 在下一个事件循环更新进度确保v-model生效
this.$nextTick(() => {
this.updateProgress();
});
},
onDimensionValueChange(q, field, value) {
console.log('onDimensionValueChange 触发:', q.id, field, value, '当前q.value:', q.value);
// 找到 questionList 中的索引
const index = this.questionList.findIndex(item => item.id === q.id);
if (index === -1) {
console.error('找不到题目索引:', q.id);
return;
}
const questionItem = this.questionList[index];
// 确保 value 是对象
if (!questionItem.value || typeof questionItem.value !== 'object' || Array.isArray(questionItem.value)) {
this.$set(questionItem, 'value', {});
}
// 创建新对象,确保响应式更新
const currentValue = questionItem.value || {};
const newValue = { ...currentValue };
newValue[field] = value;
console.log('更新前:', questionItem.value, '更新后:', newValue);
// 使用 $set 更新整个 value 对象,确保响应式
this.$set(questionItem, 'value', newValue);
// 触发进度更新
this.$nextTick(() => {
this.updateProgress();
});
},
openDatePicker(index) {
this.currentDateIndex = index;
this.showDatePicker = true;
},
confirmDate(e) {
console.log("e", e)
if (e) {
if (this.currentDateIndex !== -1) {
this.questionList[this.currentDateIndex].value = e.year + '-' + e.month + '-' + e.day
this.onValueChange();
}
}
},
openTimePicker(index) {
this.currentDateIndex = index;
this.showTimePicker = true;
},
confirmTime(e) {
console.log("e", e)
if (e) {
if (this.currentDateIndex !== -1) {
this.questionList[this.currentDateIndex].value = e.year + '-' + e.month + '-' + e.day + ' ' + e
.hour + ':' + e.minute + ':' + e.second
this.onValueChange();
}
}
},
validateForm() {
console.log("this.questionList[i]", this.questionList)
let errorCount = 0
for (let i = 0; i < this.questionList.length; i++) {
const item = this.questionList[i];
// 多维度题目的验证
if (item.edit_input === 'multi_dimension') {
if (item.dimensions && Array.isArray(item.dimensions)) {
for (let j = 0; j < item.dimensions.length; j++) {
const dim = item.dimensions[j];
if (dim.need_fill) {
const dimValue = item.value && typeof item.value === 'object' ? item.value[dim.field] : '';
if (!dimValue || dimValue === '' || dimValue === null || dimValue === undefined) {
this.base.toast(`${dim.name}不能为空`)
errorCount++
break; // 跳出内层循环
}
}
}
if (errorCount > 0) {
break; // 如果有错误,跳出外层循环
}
}
} else if (item.rule) {
// 不能为空
if (item.rule.indexOf('required') !== -1) {
if (this.base.isNull(item.value)) {
this.base.toast(`${item.name}不能为空`)
errorCount++
break;
}
}
// 手机号
if (!this.base.isNull(item.value) && item.rule.indexOf('mobile') !== -1) {
if (!this.base.isMobile(item.value)) {
this.base.toast(`${item.name}不正确`)
errorCount++
break;
}
}
// 身份证
if (!this.base.isNull(item.value) && item.rule.indexOf('idcard') !== -1) {
if (!this.$u.test.idCard(item.value)) {
this.base.toast(`${item.name}不正确`)
errorCount++
break;
}
}
// 邮箱
if (item.rule.indexOf('email') !== -1) {
if (!this.base.isNull(item.value) && !this.base.isMail(item.value)) {
this.base.toast(`${item.name}不正确`)
errorCount++
break;
}
}
// 整数
if (item.rule.indexOf('integer') !== -1) {
if (!this.base.isNull(item.value) && !this.base.isInteger(item.value)) {
this.base.toast(`${item.name}必须为整数`)
errorCount++
break;
}
}
// 数字
if (item.rule.indexOf('numeric') !== -1) {
if (!this.base.isNull(item.value) && !this.base.isNumber(item.value)) {
this.base.toast(`${item.name}必须为数字`)
errorCount++
break;
}
}
}
}
if (errorCount > 0) {
return false;
}
return true;
},
submitSurvey() {
if (!this.validateForm()) {
return;
}
let that = this
if(this.isSubmit){
return
}
this.isSubmit = true
uni.showModal({
title: '提交确认',
content: '确定要提交问卷吗?',
success: (res) => {
if (res.confirm) {
// 准备提交数据,将多维度题目的 value 对象序列化为 JSON 字符串
const submitData = that.questionList.map(q => {
const item = { ...q };
// 多维度题目的 value 对象需要序列化为 JSON 字符串
if (item.edit_input === 'multi_dimension' && item.value && typeof item.value === 'object' && !Array.isArray(item.value)) {
item.value = JSON.stringify(item.value);
}
return item;
});
that.$u.api.courseContentForm({
course_content_evaluation_id:that.id,
data:submitData
}).then(res=>{
that.base.toast('提交成功',1500,function(){
that.isSubmit = false
uni.navigateBack(-1)
})
}).catch(err=>{
that.isSubmit = false
})
} else {
that.isSubmit = false
}
}
});
}
}
};
</script>
<style scoped lang="scss">
.survey-fill-page {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
// background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
overflow: scroll;
}
.cbg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
}
.survey-container {
margin: 0 auto;
padding: 30rpx;
padding-top: 100rpx;
position: relative;
}
.survey-header {
background: white;
border-radius: 24rpx;
padding: 40rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
font-size: 28rpx;
padding-bottom: 20rpx;
&-item {
color: #666;
&>view {
display: flex;
margin-bottom: 40rpx;
&>span:first-child {
color: #000;
margin-right: 20rpx;
}
}
}
}
.survey-title {
font-size: 44rpx;
font-weight: 700;
margin-bottom: 24rpx;
text-align: center;
}
.survey-description {
font-size: 28rpx;
color: #6c757d;
line-height: 1.6;
margin-bottom: 30rpx;
}
.survey-meta {
display: flex;
justify-content: center;
gap: 30rpx;
flex-wrap: wrap;
font-size: 24rpx;
color: #6c757d;
}
.meta-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.progress-bar-container {
background: white;
padding: 20rpx 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
position: fixed;
width: 100%;
top: 0;
left: 0;
z-index: 99;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24rpx;
color: #2018a0;
&-progress {
width: calc(100% - 50rpx)
}
}
.survey-form {}
.survey-form--last-input-focused { padding-bottom: 450rpx; }
.question-item {
// padding: 30rpx 0;
// border-bottom: 1rpx solid #e9ecef;
background: white;
border-radius: 24rpx;
padding: 40rpx 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
font-size: 28rpx;
}
.question-item:last-child {
border-bottom: none;
}
.question-header {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 24rpx;
}
.question-number {
background: #bf976e;
color: white;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 600;
flex-shrink: 0;
}
.question-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
line-height: 1.4;
flex: 1;
}
.question-title-theme{
font-size: 24rpx;
color: #6c757d;
line-height: 1.6;
// margin-bottom: 30rpx;
}
.required-mark {
color: #e74c3c;
margin-left: 8rpx;
}
.question-content {
// padding-left: 64rpx;
}
/* 多维度题目样式 */
.multi-dimension-container {
width: 100%;
}
.dimension-item {
margin-bottom: 30rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 16rpx;
border: 1rpx solid #e9ecef;
}
.dimension-title {
display: flex;
align-items: center;
margin-bottom: 20rpx;
font-size: 28rpx;
font-weight: 600;
color: #2c3e50;
}
.dimension-name {
margin-right: 16rpx;
}
.dimension-help {
font-size: 24rpx;
font-weight: normal;
color: #6c757d;
margin-left: 16rpx;
}
.scale-container {
display: flex;
align-items: center;
gap: 20rpx;
color: #666;
}
.submit-section {
width: 100%;
position: relative;
padding: 60rpx 0;
&>view {
width: 70%;
text-align: center;
margin: 0 auto;
color: #fff;
background: linear-gradient(to right, #5e5fbc, #0d0398);
border-radius: 30rpx;
padding: 20rpx;
}
}
.btn-icon {
margin-right: 10rpx;
}
</style>