【北大开源实践】- 问卷断点续答 - 前端 (#282)

* feat:增加断点续答功能

* feat:增加断点续答功能

* fix: 同步代码并且解决冲突

---------

Co-authored-by: dayou <853094838@qq.com>
This commit is contained in:
shiyiting763 2024-07-05 10:37:04 +08:00 committed by GitHub
parent ef5d775276
commit 9047e6a344
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 351 additions and 29 deletions

52
web/components.d.ts vendored Normal file
View File

@ -0,0 +1,52 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElInput: typeof import('element-plus/es')['ElInput']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
IEpBottom: typeof import('~icons/ep/bottom')['default']
IEpCheck: typeof import('~icons/ep/check')['default']
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
IEpClose: typeof import('~icons/ep/close')['default']
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
IEpDelete: typeof import('~icons/ep/delete')['default']
IEpIphone: typeof import('~icons/ep/iphone')['default']
IEpLoading: typeof import('~icons/ep/loading')['default']
IEpMinus: typeof import('~icons/ep/minus')['default']
IEpMonitor: typeof import('~icons/ep/monitor')['default']
IEpMore: typeof import('~icons/ep/more')['default']
IEpSearch: typeof import('~icons/ep/search')['default']
IEpSort: typeof import('~icons/ep/sort')['default']
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
IEpTop: typeof import('~icons/ep/top')['default']
IEpView: typeof import('~icons/ep/view')['default']
IEpWarningFilled: typeof import('~icons/ep/warning-filled')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -67,13 +67,13 @@ const setterList = computed(() => {
}
} else {
formValue = _get(store.state.edit.schema, formKey, formItem.value)
console.log("formVaue:", formValue)
dataConfig[formKey] = formValue
}
formItem.value = formValue
}
form.dataConfig = dataConfig
return form
})
})

View File

@ -7,6 +7,6 @@ export default [
{
title: '提交限制',
key: 'limitConfig',
formList: ['limit_tLimit']
formList: ['limit_tLimit', 'limit_breakAnswer', 'limit_backAnswer']
}
]

View File

@ -21,5 +21,19 @@ export default {
tip: '问卷仅在指定时间段内可填写',
type: 'QuestionTimeHour',
placement: 'top'
},
limit_breakAnswer: {
key: 'baseConf.breakAnswer',
label: '允许断点续答',
tip: '回填前一次作答中的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
type: 'ELSwitch',
value: false
},
limit_backAnswer: {
key: 'baseConf.backAnswer',
label: '自动填充上次填写内容',
tip: '回填前一次提交的内容(注:更换设备/浏览器/清除缓存/更改内容重新发布则此功能失效)',
type: 'ELSwitch',
value: false
}
}

View File

@ -42,7 +42,9 @@ export default {
tLimit: 0,
answerBegTime: '',
answerEndTime: '',
answerLimitTime: 0
answerLimitTime: 0,
breakAnswer: false,
backAnswer: false
},
submitConf: {
submitTitle: '',

View File

@ -0,0 +1,38 @@
<template>
<el-switch
v-model="modelValue"
class="ml-2"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="是"
inactive-text="否"
@change="handleInputChange"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
interface Props {
formConfig: any
moduleConfig: any
}
interface Emit {
(ev: typeof FORM_CHANGE_EVENT_KEY, arg: { key: string; value: boolean }): void
}
const emit = defineEmits<Emit>()
const props = defineProps<Props>()
const modelValue = ref(props.formConfig.value)
const handleInputChange = (value: boolean) => {
const key = props.formConfig.key
modelValue.value = value
emit(FORM_CHANGE_EVENT_KEY, { key, value })
}
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="mask" v-show="visible">
<div class="box">
<div class="title">{{ title }}</div>
<div class="btn-box">
<div class="btn cancel" @click="handleCancel">{{ cancelBtnText }}</div>
<div class="btn confirm" @click="handleConfirm">{{ confirmBtnText }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
visible?: boolean
cancelBtnText?: string
confirmBtnText?: string
title?: string
}
interface Emit {
(ev: 'confirm', callback: () => void): void
(ev: 'cancel', callback: () => void): void
(ev: 'close'): void
}
const emit = defineEmits<Emit>()
withDefaults(defineProps<Props>(), {
visible: false,
cancelBtnText: '取消',
confirmBtnText: '确定',
title: ''
})
const handleConfirm = () => {
emit('confirm', () => {
emit('close')
})
}
const handleCancel = () => {
emit('cancel', () => {
emit('close')
})
}
</script>
<style lang="scss" scoped>
@import url('../styles/dialog.scss');
.btn-box {
padding: 20px 0 0;
display: flex;
align-items: center;
justify-content: space-between;
.btn {
width: 48%;
font-size: 0.28rem;
border-radius: 0.04rem;
text-align: center;
padding: 0.16rem 0;
line-height: 0.4rem;
cursor: pointer;
&.cancel {
background: #fff;
color: #92949d;
border: 1px solid #e3e4e8;
}
&.confirm {
background-color: #4a4c5b;
border: 1px solid #4a4c5b;
color: #fff;
}
}
}
</style>

View File

@ -81,6 +81,17 @@ const normalizationRequestBody = () => {
clientTime: Date.now()
}
//
localStorage.removeItem(surveyPath + "_questionData")
localStorage.removeItem("isSubmit")
//
var formData = Object.assign({}, store.state.formValues)
for(const key in formData){
formData[key] = encodeURIComponent(formData[key])
}
localStorage.setItem(surveyPath + "_questionData", JSON.stringify(formData))
localStorage.setItem('isSubmit', JSON.stringify(true))
if (encryptInfo?.encryptType) {
result.encryptType = encryptInfo?.encryptType
result.data = encrypt[result.encryptType as 'rsa']({

View File

@ -6,6 +6,9 @@ moment.locale('zh-cn')
import adapter from '../adapter'
import { queryVote, getEncryptInfo } from '@/render/api/survey'
import { RuleMatch } from '@/common/logicEngine/RulesMatch'
import state from './state'
import useCommandComponent from '../hooks/useCommandComponent'
import BackAnswerDialog from '../components/BackAnswerDialog.vue'
/**
* CODE_MAP不从management引入在dev阶段会导致B端 router被加载进而导致C端路由被添加 baseUrl: /management
*/
@ -16,6 +19,9 @@ const CODE_MAP = {
}
const VOTE_INFO_KEY = 'voteinfo'
import router from '../router'
const confirm = useCommandComponent(BackAnswerDialog)
export default {
// 初始化
init(
@ -23,7 +29,7 @@ export default {
{ bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }
) {
commit('setEnterTime')
const { begTime, endTime, answerBegTime, answerEndTime } = baseConf
const { begTime, endTime, answerBegTime, answerEndTime, breakAnswer, backAnswer} = baseConf
const { msgContent } = submitConf
const now = Date.now()
if (now < new Date(begTime).getTime()) {
@ -57,31 +63,72 @@ export default {
}
}
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf
})
//回填,断点续填
const localData = JSON.parse(localStorage.getItem(state.surveyPath + "_questionData"))
// 将数据设置到state上
commit('assignState', {
questionData,
questionSeq,
rules,
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
formValues
})
// 获取已投票数据
dispatch('initVoteData')
//数据解密
for(const key in localData){
localData[key] = decodeURIComponent(localData[key])
}
const isSubmit = JSON.parse(localStorage.getItem('isSubmit'))
if(localData) {
if(isSubmit){
if(!backAnswer) {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} else {
confirm({
title: "您之前已提交过问卷,是否要回填?",
onConfirm: async () => {
try {
loadFormData({ commit, dispatch }, {bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }, localData)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
},
onCancel: async() => {
try {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
}
})
}
} else{
if(!breakAnswer) {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} else {
confirm({
title: "您之前已填写部分内容, 是否要继续填写?",
onConfirm: async () => {
try {
loadFormData({ commit, dispatch }, {bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }, localData)
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
},
onCancel: async() => {
try {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
} catch (error) {
console.log(error)
} finally {
confirm.close()
}
}
})
}
}
} else {
clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf })
}
},
// 用户输入或者选择后,更新表单数据
changeData({ commit }, data) {
@ -177,3 +224,71 @@ export default {
commit('setRuleEgine', ruleEngine)
}
}
// 加载上次填写过的数据到问卷页
function loadFormData({ commit, dispatch }, {bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }, formData) {
commit('setRouter', 'indexPage')
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf
})
console.log("formdata", formData)
for(const key in formData){
formValues[key] = formData[key]
console.log("formValues",formValues)
}
// 将数据设置到state上
commit('assignState', {
questionData,
questionSeq,
rules,
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
formValues
})
// 获取已投票数据
dispatch('initVoteData')
}
// 加载空白页面
function clearFormData({ commit, dispatch }, { bannerConf, baseConf, bottomConf, dataConf, skinConf, submitConf }) {
commit('setRouter', 'indexPage')
// 根据初始的schema生成questionData, questionSeq, rules, formValues, 这四个字段
const { questionData, questionSeq, rules, formValues } = adapter.generateData({
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf
})
// 将数据设置到state上
commit('assignState', {
questionData,
questionSeq,
rules,
bannerConf,
baseConf,
bottomConf,
dataConf,
skinConf,
submitConf,
formValues
})
// 获取已投票数据
dispatch('initVoteData')
}

View File

@ -17,8 +17,19 @@ export default {
},
changeFormData(state, data) {
let { key, value } = data
// console.log('formValues', key, value)
set(state, `formValues.${key}`, value)
//数据加密
var formData = Object.assign({}, state.formValues);
for(const key in formData){
formData[key] = encodeURIComponent(formData[key])
}
//浏览器存储
localStorage.removeItem(state.surveyPath + "_questionData")
localStorage.setItem(state.surveyPath + "_questionData", JSON.stringify(formData))
localStorage.setItem('isSubmit', JSON.stringify(false))
},
changeSelectMoreData(state, data) {
const { key, value, field } = data