Files
property-resident-side/property-uniapp-project/pageSubPack/service-list/questionnaireFillout.vue
zhouyanli 56641cbc56 修改bug
2026-08-26 18:00:43 +08:00

476 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="contentStyle">
<!-- <view class="status-bar"></view>
<uni-nav-bar title="问卷详情" backgroundColor="transparent" :border="false" left-icon="left" @clickLeft="goBack">
</uni-nav-bar> -->
<!-- 表单区域 -->
<scroll-view class="page" scroll-y>
<view class="content-wrap">
<!-- 问卷标题 -->
<view class="survey-title">{{ detailItem.title || '' }}</view>
<!-- 问卷说明 -->
<!-- <view class="survey-desc">{{ detailItem.description || '' }}</view> -->
<view class="survey-desc" v-if="questionList.length >0">
为了给您提供更好的服务希望您能抽出几分钟时间将最近的感受和建议告诉我们
我们十分重视每位用户的宝贵意见期待您的参与
</view>
<!-- 空状态 -->
<view v-if="questionList.length === 0" class="empty-tip">
<text>暂无数据</text>
</view>
<!-- 动态问题列表 -->
<view v-for="(q, idx) in questionList" :key="q.cid" class="question-item">
<view class="question-title">
<text v-if="q.required" class="required">*</text>
<text>{{ idx + 1 }}.{{ q.title || '未命名问题' }}</text>
</view>
<!-- 单选 -->
<view v-if="q.type === 'radio'" class="radio-group">
<view
v-for="(opt, oIdx) in q.options"
:key="oIdx"
class="radio-item"
:class="{ checked: answers[q.cid] === opt.label }"
@click="!isEndedAndUnjoined && selectRadio(q.cid, opt.label)"
>
<view class="radio-circle" :class="{ active: answers[q.cid] === opt.label }">
<text v-if="answers[q.cid] === opt.label" class="check-icon"></text>
</view>
<text class="radio-label">{{ opt.label }}</text>
</view>
</view>
<!-- 多选 -->
<view v-if="q.type === 'checkbox'" class="checkbox-group">
<view
v-for="(opt, oIdx) in q.options"
:key="oIdx"
class="checkbox-item"
:class="{ checked: isChecked(q.cid, opt.label) }"
@click="!isEndedAndUnjoined && toggleCheckbox(q.cid, opt.label)"
>
<view class="checkbox-box" :class="{ active: isChecked(q.cid, opt.label) }">
<text v-if="isChecked(q.cid, opt.label)" class="check-icon"></text>
</view>
<text class="checkbox-label">{{ opt.label }}</text>
</view>
</view>
<!-- 单行输入 -->
<view v-if="q.type === 'input'">
<input
class="text-input"
:class="{ 'input-disabled': isEndedAndUnjoined }"
v-model="answers[q.cid]"
placeholder="请输入"
:disabled="isEndedAndUnjoined"
/>
</view>
<!-- 多行输入 -->
<view v-if="q.type === 'textarea'">
<textarea
class="textarea-input"
:class="{ 'input-disabled': isEndedAndUnjoined }"
v-model="answers[q.cid]"
placeholder="请输入"
maxlength="500"
:disabled="isEndedAndUnjoined"
/>
</view>
</view>
</view>
</scroll-view>
<!-- 固定底部提交按钮 -->
<view class="submit-btn-wrap">
<button
v-if="isEndedAndUnjoined"
class="submit-btn disabled-btn"
disabled
>已结束</button>
<button v-else class="submit-btn" @click="handleSubmit">提交</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { getQuestionnaireDetail, submitQuestionnair } from '../api/apiSub.js'
const detailItem = ref({})
const questionnaireId = ref('') // 保存问卷ID用于 onShow 重新获取
const questionList = computed(() => {
const content = detailItem.value?.content
if (!content) return []
try {
const list = JSON.parse(content)
return Array.isArray(list) ? list : []
} catch (e) {
return []
}
})
// 是否已结束且未参与
const isEndedAndUnjoined = computed(() => {
const endTime = detailItem.value?.endTime
const participated = detailItem.value?.participated
// participated 不存在null/undefined说明接口没返回参与状态按未参与处理
const isNotParticipated = !participated
// endTime 存在且已过期
const isEnded = endTime && new Date(endTime.replace(/-/g, '/')).getTime() < Date.now()
return isEnded && isNotParticipated
})
const answers = ref({})
let hasLoaded = false
// 获取问卷调查
const getDetailData = async (id) => {
try {
let params = {
villageId:uni.getStorageSync('currentVillageId')
}
const res = await getQuestionnaireDetail(id,params)
// console.log('接口返回:', JSON.stringify(res))
if (res.code === 200) {
const data = Array.isArray(res.data) ? res.data[0] : res.data
detailItem.value = data || {}
} else {
uni.showToast({ title: res.msg || '获取失败', icon: 'none' })
}
} catch (err) {
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
}
}
onLoad((option) => {
if (option.id) {
questionnaireId.value = option.id
getDetailData(option.id)
} else {
uni.showToast({ title: '缺少问卷ID', icon: 'none' })
}
})
// 切换小区后返回页面时,重新获取数据
onShow(() => {
if (!hasLoaded) {
hasLoaded = true
return
}
if (questionnaireId.value) {
getDetailData(questionnaireId.value)
}
})
const selectRadio = (cid, label) => {
// 再次点击已选中的选项时取消选择
if (answers.value[cid] === label) {
delete answers.value[cid]
} else {
answers.value[cid] = label
}
}
const isChecked = (cid, label) => {
const arr = answers.value[cid]
if (!Array.isArray(arr)) return false
return arr.includes(label)
}
const toggleCheckbox = (cid, label) => {
if (!Array.isArray(answers.value[cid])) {
answers.value[cid] = []
}
const arr = answers.value[cid]
const pos = arr.indexOf(label)
if (pos > -1) {
arr.splice(pos, 1)
} else {
arr.push(label)
}
}
const handleSubmit = async () => {
for (const q of questionList.value) {
if (q.required) {
const ans = answers.value[q.cid]
if (ans === undefined || ans === null || ans === '' || (Array.isArray(ans) && ans.length === 0)) {
uni.showToast({ title: `请填写第${questionList.value.indexOf(q) + 1}`, icon: 'none' })
return
}
}
}
const answerList = []
for (const q of questionList.value) {
const val = answers.value[q.cid]
if (val !== undefined && val !== null && val !== '' && !(Array.isArray(val) && val.length === 0)) {
answerList.push({ cid: q.cid, value: val })
}
}
uni.showLoading({ title: "提交中...", mask: true })
try {
let params = {
answerList,
villageId: uni.getStorageSync("currentVillageId")
}
const res = await submitQuestionnair(detailItem.value.id,params)
uni.hideLoading()
if (res.code === 200) {
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => { goBack() }, 1500)
} else {
uni.showToast({ title: res.msg || '提交失败', icon: 'none' })
}
} catch (err) {
console.log('errrr',err)
uni.hideLoading()
uni.showToast({ title: err.msg || '网络异常', icon: 'none' })
}
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style lang="scss">
page {
background: #f2f1f6;
}
</style>
<style scoped lang="scss">
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
overflow-y: auto;
padding: 30rpx 40rpx 200rpx;
box-sizing: border-box;
}
.content-wrap {
padding-bottom: 40rpx;
}
.survey-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin: 0px 0 20rpx 0;
white-space: pre-line;
}
.survey-desc {
font-size: 24rpx;
color: #4A4A4A;
line-height: 1.6;
margin-bottom: 20rpx;
text-align: justify;
white-space: pre-line;
word-break: break-all;
word-wrap: break-word;
overflow-wrap: break-word;
}
.empty-tip {
text-align: center;
padding: 100rpx 0;
font-size: 28rpx;
color: #999;
}
.question-item {
margin-bottom: 30rpx;
.question-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
line-height: 1.4;
.required {
color: #ff3b30;
font-size: 36rpx;
margin-right: 8rpx;
}
}
}
/* 单选 */
.radio-group {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-left: 10rpx;
}
.radio-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 0;
.radio-circle {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
&.active {
background-color: #007aff;
border-color: #007aff;
}
}
.check-icon {
color: #fff;
font-size: 22rpx;
font-weight: bold;
}
.radio-label {
font-size: 32rpx;
color: #333;
}
}
/* 多选 */
.checkbox-group {
display: flex;
flex-direction: column;
gap: 20rpx;
padding-left: 10rpx;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 12rpx 0;
.checkbox-box {
width: 40rpx;
height: 40rpx;
border-radius: 8rpx;
border: 2rpx solid #ccc;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
&.active {
background-color: #007aff;
border-color: #007aff;
}
}
.check-icon {
color: #fff;
font-size: 22rpx;
font-weight: bold;
}
.checkbox-label {
font-size: 32rpx;
color: #333;
}
}
/* 文本输入 */
.text-input {
width: calc(100% - 30rpx);
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
font-size: 32rpx;
color: #333;
box-sizing: border-box;
margin-left: 30rpx;
}
.textarea-input {
width: calc(100% - 30rpx);
min-height: 240rpx;
padding: 20rpx;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
background-color: #fff;
font-size: 32rpx;
color: #333;
box-sizing: border-box;
margin-left: 30rpx;
}
.input-disabled {
background-color: #f5f5f5;
color: #999;
}
/* 提交按钮 */
.submit-btn-wrap {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #ffffff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
.submit-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background-color: #007aff;
color: #fff;
font-size: 36rpx;
font-weight: 500;
border-radius: 12rpx;
border: none;
padding: 0;
margin: 0;
&::after {
border: none;
}
&.disabled-btn {
background-color: #ccc;
color: #999;
}
}
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>