backgroundSurvey.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <template>
  2. <view class="survey-page" :style="{'min-height':h+'px', 'padding-top':mt+'px'}">
  3. <cus-header :title="pageTitle"></cus-header>
  4. <view v-if="loading" class="survey-loading adfacjc">
  5. <u-loading-icon size="42"></u-loading-icon>
  6. <text>背景调研加载中...</text>
  7. </view>
  8. <template v-else-if="currentQuestion">
  9. <view class="survey-progress">
  10. <view class="survey-progress-row adfacjb">
  11. <text>答题进度</text>
  12. <text><span>{{currentIndex + 1}}</span>/{{survey.questionCount}}</text>
  13. </view>
  14. <view class="survey-progress-track">
  15. <view class="survey-progress-current" :style="{width:progressPercent+'%'}"></view>
  16. </view>
  17. </view>
  18. <scroll-view class="survey-content" scroll-y>
  19. <view class="question-card">
  20. <view class="question-sequence">第 {{currentIndex + 1}} 题</view>
  21. <view class="question-cn">{{currentQuestion.question}}</view>
  22. <view class="question-en" v-if="currentQuestion.questionEnglish">{{currentQuestion.questionEnglish}}</view>
  23. <view class="option-list">
  24. <view
  25. v-for="option in currentQuestion.options"
  26. :key="option.code"
  27. class="option-item adf"
  28. :class="{active:selectedCode===option.code}"
  29. @click="selectOption(option)"
  30. >
  31. <view class="option-radio adfacjc">
  32. <view v-if="selectedCode===option.code"></view>
  33. </view>
  34. <text class="option-text">{{option.text}}</text>
  35. </view>
  36. </view>
  37. <textarea
  38. v-if="selectedOption && selectedOption.customAllowed"
  39. v-model="customText"
  40. class="custom-input"
  41. maxlength="500"
  42. placeholder="请填写您的理解"
  43. placeholder-style="color:#B3BFC8"
  44. ></textarea>
  45. </view>
  46. </scroll-view>
  47. <view class="survey-actions adfacjb">
  48. <view class="survey-btn secondary" :class="{disabled:currentIndex===0}" @click="previous">上一题</view>
  49. <view class="survey-btn primary" :class="{disabled:saving}" @click="next">
  50. {{currentIndex === survey.questionCount - 1 ? completeText : '下一题'}}
  51. </view>
  52. </view>
  53. </template>
  54. <view v-else class="survey-empty adfacjc">背景调研题库暂无题目</view>
  55. </view>
  56. </template>
  57. <script>
  58. import CusHeader from '@/components/CusHeader/index.vue'
  59. import backgroundSurveyRules from '@/utils/backgroundSurvey.js'
  60. const {
  61. selectedOption,
  62. validateBackgroundAnswer,
  63. createBackgroundAnswerPayload,
  64. perillIntroUrl
  65. } = backgroundSurveyRules
  66. export default {
  67. components:{ CusHeader },
  68. data(){
  69. return {
  70. mode:'coach',
  71. teamId:'',
  72. teamQuestionnaireId:'',
  73. surveyId:'',
  74. title:'',
  75. type:'',
  76. turnType:'',
  77. survey:null,
  78. currentIndex:0,
  79. selectedCode:'',
  80. customText:'',
  81. loading:true,
  82. saving:false,
  83. allowBack:false,
  84. completed:false
  85. }
  86. },
  87. computed:{
  88. pageTitle(){
  89. return this.mode === 'coach' ? '团队背景调研' : '团队背景调研'
  90. },
  91. completeText(){
  92. return this.mode === 'coach' ? '完成调研' : '下一阶段'
  93. },
  94. currentQuestion(){
  95. return this.survey && this.survey.questions
  96. ? this.survey.questions[this.currentIndex]
  97. : null
  98. },
  99. selectedOption(){
  100. return selectedOption(this.currentQuestion, this.selectedCode)
  101. },
  102. progressPercent(){
  103. if(!this.survey || !this.survey.questionCount) return 0
  104. return ((this.currentIndex + 1) / this.survey.questionCount) * 100
  105. }
  106. },
  107. onLoad(options){
  108. this.mode = options.mode || 'coach'
  109. this.teamId = options.teamId || ''
  110. this.teamQuestionnaireId = options.teamQuestionnaireId || ''
  111. this.surveyId = options.surveyId || ''
  112. this.title = options.title || ''
  113. this.type = options.type || ''
  114. this.turnType = options.turnType || ''
  115. this.loadSurvey()
  116. },
  117. onBackPress(){
  118. if(this.allowBack || this.completed || !this.survey || this.survey.status === 'COMPLETED') return false
  119. uni.showModal({
  120. title:'背景调研尚未完成',
  121. content:'当前答案已保存为草稿,仍要退出吗?',
  122. confirmText:'仍然退出',
  123. confirmColor:'#FD4F66',
  124. success: result => {
  125. if(result.confirm){
  126. this.allowBack = true
  127. uni.navigateBack()
  128. }
  129. }
  130. })
  131. return true
  132. },
  133. methods:{
  134. async loadSurvey(){
  135. this.loading = true
  136. try {
  137. let response
  138. if(this.surveyId){
  139. response = await this.$api.get(`/core/background/survey/${this.surveyId}`)
  140. }else if(this.mode === 'coach' && this.teamQuestionnaireId){
  141. response = await this.$api.get(`/core/background/survey/coach/team-questionnaire/${this.teamQuestionnaireId}`)
  142. }else if(this.mode === 'coach'){
  143. response = await this.$api.post('/core/background/survey/coach/draft',{teamId:this.teamId})
  144. }else{
  145. response = await this.$api.get(`/core/background/survey/member/team-questionnaire/${this.teamQuestionnaireId}`)
  146. }
  147. const res = response.data
  148. if(res.code!==0) throw new Error(res.msg || '背景调研加载失败')
  149. this.applySurvey(res.data)
  150. const eventChannel = this.getOpenerEventChannel()
  151. if(eventChannel) eventChannel.emit('backgroundSurveyReady',{
  152. surveyId:this.survey.id,
  153. status:this.survey.status
  154. })
  155. } catch(error) {
  156. this.$showToast(error && (error.message || error.msg) || '背景调研加载失败')
  157. } finally {
  158. this.loading = false
  159. }
  160. },
  161. applySurvey(survey){
  162. this.survey = survey || { questions:[], questionCount:0 }
  163. this.surveyId = this.survey.id || this.surveyId
  164. if(this.currentIndex >= this.survey.questionCount) this.currentIndex = 0
  165. this.loadCurrentAnswer()
  166. },
  167. loadCurrentAnswer(){
  168. const question = this.currentQuestion
  169. this.selectedCode = question && question.optionCode || ''
  170. this.customText = question && question.customText || ''
  171. },
  172. selectOption(option){
  173. this.selectedCode = option.code
  174. if(!option.customAllowed) this.customText = ''
  175. },
  176. previous(){
  177. if(this.currentIndex===0 || this.saving) return
  178. this.currentIndex--
  179. this.loadCurrentAnswer()
  180. },
  181. async saveCurrent(){
  182. const error = validateBackgroundAnswer(
  183. this.currentQuestion, this.selectedCode, this.customText)
  184. if(error) throw new Error(error)
  185. const response = await this.$api.put(
  186. `/core/background/survey/${this.survey.id}/answer`,
  187. createBackgroundAnswerPayload(
  188. this.currentQuestion, this.selectedCode, this.customText))
  189. const res = response.data
  190. if(res.code!==0) throw new Error(res.msg || '答案保存失败')
  191. this.applySurvey(res.data)
  192. },
  193. async next(){
  194. if(this.saving) return
  195. this.saving = true
  196. try {
  197. await this.saveCurrent()
  198. if(this.currentIndex < this.survey.questionCount - 1){
  199. this.currentIndex++
  200. this.loadCurrentAnswer()
  201. return
  202. }
  203. const response = await this.$api.post(
  204. `/core/background/survey/${this.survey.id}/complete`,{})
  205. const res = response.data
  206. if(res.code!==0) throw new Error(res.msg || '背景调研提交失败')
  207. this.applySurvey(res.data)
  208. this.completed = true
  209. if(this.mode === 'coach'){
  210. const eventChannel = this.getOpenerEventChannel()
  211. if(eventChannel) eventChannel.emit('backgroundSurveyCompleted',{
  212. surveyId:this.survey.id,
  213. status:this.survey.status
  214. })
  215. this.$showToast('团队背景调研已完成')
  216. setTimeout(()=>{
  217. this.allowBack = true
  218. uni.navigateBack()
  219. },500)
  220. return
  221. }
  222. uni.redirectTo({
  223. url:perillIntroUrl({
  224. teamQuestionnaireId:this.teamQuestionnaireId,
  225. teamId:this.teamId,
  226. type:this.type,
  227. turnType:this.turnType,
  228. title:this.title
  229. })
  230. })
  231. } catch(error) {
  232. this.$showToast(error && (error.message || error.msg) || '背景调研处理失败')
  233. } finally {
  234. this.saving = false
  235. }
  236. }
  237. }
  238. }
  239. </script>
  240. <style scoped lang="scss">
  241. .survey-page{
  242. background:#F7F7F7;
  243. box-sizing:border-box;
  244. padding-bottom:170rpx;
  245. }
  246. .survey-loading,.survey-empty{
  247. height:60vh;
  248. color:#667E90;
  249. font-size:30rpx;
  250. text{margin-left:12rpx;}
  251. }
  252. .survey-progress{
  253. background:#FFFFFF;
  254. padding:28rpx 32rpx;
  255. &-row{
  256. font-size:28rpx;
  257. color:#667E90;
  258. span{font-size:36rpx;color:#199C9C;}
  259. }
  260. &-track{
  261. height:14rpx;
  262. margin-top:20rpx;
  263. border-radius:8rpx;
  264. background:#F0F2F8;
  265. overflow:hidden;
  266. }
  267. &-current{
  268. height:100%;
  269. background:#FFD750;
  270. border-radius:8rpx;
  271. transition:width .2s;
  272. }
  273. }
  274. .survey-content{
  275. height:calc(100vh - 360rpx);
  276. padding:24rpx;
  277. box-sizing:border-box;
  278. }
  279. .question-card{
  280. background:#FFFFFF;
  281. border-radius:24rpx;
  282. padding:32rpx 24rpx;
  283. }
  284. .question-sequence{font-size:26rpx;color:#199C9C;margin-bottom:20rpx;}
  285. .question-cn{font-size:34rpx;color:#002846;line-height:52rpx;font-weight:500;}
  286. .question-en{font-size:26rpx;color:#667E90;line-height:40rpx;margin-top:16rpx;}
  287. .option-list{margin-top:30rpx;}
  288. .option-item{
  289. padding:24rpx 20rpx;
  290. background:#F7F8FA;
  291. border:2rpx solid transparent;
  292. border-radius:18rpx;
  293. margin-top:18rpx;
  294. align-items:flex-start;
  295. &.active{background:#F0FAFA;border-color:#33A7A7;}
  296. }
  297. .option-radio{
  298. width:34rpx;
  299. height:34rpx;
  300. border:2rpx solid #B3BFC8;
  301. border-radius:50%;
  302. margin:4rpx 18rpx 0 0;
  303. flex:none;
  304. view{width:20rpx;height:20rpx;border-radius:50%;background:#199C9C;}
  305. }
  306. .option-item.active .option-radio{border-color:#199C9C;}
  307. .option-text{font-size:28rpx;color:#335368;line-height:42rpx;white-space:pre-line;}
  308. .custom-input{
  309. width:100%;
  310. height:180rpx;
  311. background:#F7F8FA;
  312. border-radius:18rpx;
  313. padding:24rpx;
  314. box-sizing:border-box;
  315. font-size:28rpx;
  316. line-height:42rpx;
  317. color:#335368;
  318. margin-top:22rpx;
  319. }
  320. .survey-actions{
  321. position:fixed;
  322. left:0;
  323. right:0;
  324. bottom:0;
  325. padding:24rpx 32rpx 44rpx;
  326. background:#FFFFFF;
  327. box-shadow:0 -2rpx 10rpx rgba(0,0,0,.06);
  328. z-index:10;
  329. }
  330. .survey-btn{
  331. width:calc(50% - 12rpx);
  332. padding:26rpx 0;
  333. border-radius:48rpx;
  334. text-align:center;
  335. font-size:30rpx;
  336. &.secondary{background:#F0F2F8;color:#667E90;}
  337. &.primary{background:linear-gradient(90deg,#33A7A7,#64BBBB);color:#FFFFFF;}
  338. &.disabled{opacity:.5;}
  339. }
  340. </style>