WebQn.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. <template>
  2. <div>
  3. <!-- page title -->
  4. <div v-if="stage == 0">
  5. <mt-header type="default" title="问卷调查-问候语" fixed></mt-header>
  6. </div>
  7. <div v-else-if="stage == 1">
  8. <mt-header title="问卷调查-问卷题目" fixed></mt-header>
  9. </div>
  10. <div v-else-if="stage == 2">
  11. <mt-header title="问卷调查-结束语" fixed></mt-header>
  12. </div>
  13. <!-- 问候语 -->
  14. <div v-if="stage == 0">
  15. <p style="margin: 80px 0;">{{questionaire.info.hello}}</p>
  16. <mt-button type="primary" @click="start" class="button-bottom">继续</mt-button>
  17. </div>
  18. <!-- 问卷题目 -->
  19. <div rows="auto * auto" v-else-if="stage==1">
  20. <!-- 题目问题 -->
  21. <div style="margin:20px 0 40px 0">
  22. <!-- <Progress :value="questionHEAD" :maxValue="questionaire.questions.length" /> -->
  23. <mt-progress :value="percent" :bar-height="5" style="margin-top: -30px;"></mt-progress>
  24. <div>
  25. <span class="h3">{{questionHEAD + 1}}、</span>
  26. <span class="h3" style="margin-left: 5;">[</span>
  27. <span class="h3">{{questionType}}</span>
  28. <span class="h3" style="margin-right: 5;">]</span>
  29. </div>
  30. <p class="h3">{{questionBody}}</p>
  31. <span class="h3">{{questionRemark}}</span>
  32. </div>
  33. <mt-radio v-if="questionType == '单选题'" v-model="selectedSingle" :options="questionOptions"
  34. @change="onOptionSelected">
  35. </mt-radio>
  36. <mt-checklist v-else-if="questionType == '多选题'" v-model="selectedMultiple" :options="questionOptions"
  37. @change="onOptionSelected">
  38. </mt-checklist>
  39. <textarea v-model="answerValue" placeholder="请在此输入答案" v-else-if="questionType == '开放题'"
  40. style="width: 96%;height: 160px;" />
  41. <mt-button type="primary" @click="onQuestionConfirm" class="button-bottom" v-show="nextBtnVisible">继续
  42. </mt-button>
  43. </div>
  44. <!-- <div rows="* auto" v-else class="margin">
  45. <Label class="body" :text="questionaire.info.bye" textWrap="true" verticalAlignment="top" />
  46. <Button text="继续" @tap="onFinishTap" row="1" />
  47. </div> -->
  48. <div v-if="stage == 2">
  49. <p style="margin: 80px 0;">{{questionaire.info.bye}}</p>
  50. </div>
  51. </div>
  52. </template>
  53. <script>
  54. import axios from 'axios';
  55. import dateFormat from '../dateFormat.js'
  56. import indicatorBiz from '../indicatorBiz.js'
  57. import Vue from 'vue'
  58. import { MessageBox } from 'mint-ui';
  59. // CREATED(0, "已创建"),
  60. // CLICKED(1, "已点击"),
  61. // STARTED(2, "已开始"),
  62. // COMPLETE(3, "已完成");
  63. export default {
  64. name: 'WebQn',
  65. data: function () {
  66. return {
  67. }
  68. },
  69. data: function () {
  70. return {
  71. webQuestionaire: {},
  72. questionaire: { info: {} },
  73. questionHEAD: 0,
  74. answerSheet: {
  75. // questionaireId: this.questionaire.info.id,
  76. questionaireId: 0,
  77. sheetName: '',
  78. status: 0,
  79. startTime: null,
  80. endTime: null,
  81. locationLong: this.long,
  82. locationLat: this.lat,
  83. },
  84. answers: {
  85. },
  86. answerValue: '',
  87. answerNote: {},
  88. selectedSingle: '',
  89. selectedMultiple: [],
  90. recorder: null,
  91. isRecording: false,
  92. indicators: {},
  93. historyStack: [],
  94. nextBtnVisible: true,
  95. webQnUrl: '',
  96. lastCheckList: []
  97. }
  98. },
  99. mounted: function () {
  100. let id = this.getParam('id');
  101. if (!id) {
  102. alert('无效的问卷地址');
  103. return;
  104. }
  105. this.webQnUrl = this.$baseUrl + '/web-qn/' + id;
  106. console.log('webQnUrl', this.webQnUrl);
  107. axios.get(this.webQnUrl).then(webQnResponse => {
  108. console.log(webQnResponse.data);
  109. let qnId = webQnResponse.data.qnId;
  110. let qnDataUrl = this.webQnUrl + '/complete-info';
  111. console.log('qnDataUrl', qnDataUrl);
  112. axios.get(qnDataUrl).then(r => {
  113. this.questionaire = r.data;
  114. this.webQuestionaire = webQnResponse.data;
  115. // this.questionaire.info = r.data.info;
  116. console.log(this.questionaire);
  117. })
  118. // 获取最新的配额
  119. // let $this = this;
  120. // axios.get(`${this.$baseUrl}/questionaire/${this.questionaire.info.id}/indicator`).then(function (response) {
  121. // // $this.indicators = response.data
  122. // for (let i = 0; i < response.data.length; i++) {
  123. // $this.indicators[response.data[i].id] = response.data[i];
  124. // }
  125. // });
  126. // // 将本次运行写入数据库
  127. // if (this.questionaire.info.status >= 3) {
  128. // this.insertAnswerSheet();
  129. // }
  130. // this.onQuestionLoaded();
  131. })
  132. let time = new Date();
  133. this.answerSheet.startTime = dateFormat.formatDate('yyyy-MM-dd HH:mm:ss', time);
  134. this.answerSheet.sheetName = this.generateSheetName(time);
  135. },
  136. computed: {
  137. stage() {
  138. if (this.webQuestionaire.status == 3) {
  139. // 显示结束语
  140. return 2;
  141. } else if (this.webQuestionaire.status == 2) {
  142. // 显示题目
  143. return 1;
  144. } else {
  145. // 显示问候语
  146. return 0;
  147. }
  148. },
  149. // pageTitle() {
  150. // console.log('compute page title');
  151. // let title = '';
  152. // switch (this.stage) {
  153. // case 0:
  154. // title = "问候语";
  155. // break;
  156. // case 1: title = "问卷内容";
  157. // break;
  158. // case 2: title = "问卷已结束";
  159. // break;
  160. // }
  161. // if (this.isRecording) {
  162. // return `${title}(录音中...)`
  163. // } else {
  164. // return title;
  165. // }
  166. // },
  167. currentQuestion() {
  168. return this.questionaire.questions[this.questionHEAD];
  169. },
  170. percent(){
  171. let perc = (this.questionHEAD + 1) / this.questionaire.questions.length * 100;
  172. console.log('percent', perc)
  173. return perc;
  174. },
  175. questionType() {
  176. let type = this.currentQuestion.type;
  177. if (type == 1) {
  178. if (this.currentQuestion.minOptions == 1 && this.currentQuestion.maxOptions == 1) {
  179. return '单选题';
  180. } else {
  181. return '多选题';
  182. }
  183. } else if (type == 3) {
  184. return '排序题';
  185. } else if (type == 4) {
  186. return '开放题';
  187. }
  188. },
  189. questionBody() {
  190. // 处理${ID}的情况
  191. return this.replaceDollarId(this.currentQuestion.body);
  192. },
  193. questionRemark() {
  194. // 处理${ID}的情况
  195. return this.replaceDollarId(this.currentQuestion.remark);
  196. },
  197. upperDatasourceQuestion() {
  198. console.log('call upperDatasourceQuestion');
  199. // 获取上一级的数据源所在的题目id(同一个id,但列数小1的题目)
  200. if (!this.currentQuestion.datasourceId) {
  201. return 0;
  202. } else {
  203. let dsId = this.currentQuestion.datasourceId
  204. let dsColumn = this.currentQuestion.datasourceColumn;
  205. for (let i = 0; i < this.questionHEAD; i++) {
  206. if (this.questionaire.questions[i].datasourceId == dsId
  207. && this.questionaire.questions[i].datasourceColumn == dsColumn - 1) {
  208. return this.questionaire.questions[i].id;
  209. }
  210. }
  211. return 0;
  212. }
  213. },
  214. questionOptions() {
  215. console.log('call questionOptions');
  216. let opts = [];
  217. if (this.upperDatasourceQuestion == 0) {
  218. opts = this.currentQuestion.options;
  219. } else {
  220. let dsId = this.currentQuestion.datasourceId
  221. let dsColumn = this.currentQuestion.datasourceColumn;
  222. let targetOptionId = this.answers[this.upperDatasourceQuestion].answer;
  223. let dsFilter = this.findOptionContent(this.upperDatasourceQuestion, targetOptionId);
  224. let subDataColumn = [];
  225. let datasource = null;
  226. for (let i in this.questionaire.datasource) {
  227. if (this.questionaire.datasource[i].id == dsId) {
  228. datasource = this.questionaire.datasource[i];
  229. break;
  230. }
  231. }
  232. if (!datasource) {
  233. return this.currentQuestion.options;
  234. }
  235. for (let i in datasource.contentList) {
  236. if (datasource.contentList[i]['c' + (dsColumn - 1)] == dsFilter) {
  237. subDataColumn.push(datasource.contentList[i]['c' + dsColumn])
  238. }
  239. }
  240. let subOptions = [];
  241. for (let i in this.currentQuestion.options) {
  242. if (this.currentQuestion.options[i].datasourceId == 0 ||
  243. subDataColumn.indexOf(this.currentQuestion.options[i].optionContent) >= 0) {
  244. subOptions.push(this.currentQuestion.options[i]);
  245. }
  246. }
  247. opts = subOptions;
  248. }
  249. // 组装成 mt-radio 或 mt-checklist 组件需要的格式
  250. for (let i = 0; i < opts.length; i++) {
  251. // let label = `${i + 1}、${opts[i].optionContent}`;
  252. let label = `${opts[i].optionContent}`;
  253. let value = i.toString();
  254. Vue.set(opts[i], 'label', label);
  255. Vue.set(opts[i], 'value', value);
  256. }
  257. return opts;
  258. }
  259. },
  260. methods: {
  261. getParam: function (name) {
  262. if (name = (new RegExp('[?&]' + encodeURIComponent(name) + '=([^&]*)')).exec(location.search))
  263. return decodeURIComponent(name[1]);
  264. },
  265. // backEventHandler(args) {
  266. // console.log('activityBackPressedEvent......')
  267. // debugger;
  268. // if (this.stage < 2) {
  269. // args.cancel = true;
  270. // this.onQuitTap();
  271. // } else if (this.stage == 2) {
  272. // args.cancel = true;
  273. // this.onFinishTap();
  274. // }
  275. // },
  276. start() {
  277. this.webQuestionaire.status = 2;
  278. axios.put(this.webQnUrl, { status: 2 }).then(() => console.log('update status success'));
  279. },
  280. getUserName() {
  281. return this.webQuestionaire.phoneNumber;
  282. },
  283. getOrgCode() {
  284. let orgCode = 'webqn';
  285. return orgCode
  286. },
  287. generateSheetName(time) {
  288. let orgCode = this.getOrgCode();
  289. let userName = this.getUserName();
  290. return `${orgCode}-${dateFormat.formatDate('yyyyMMddHHmmss', time)}-${userName}`
  291. },
  292. replaceDollarId(body) {
  293. // 处理${ID}的情况
  294. let regex = /\$\{(\d+)\}/m;
  295. let match = regex.exec(body);
  296. while (match && match[0]) {
  297. let targetQuestionId = match[1];
  298. let targetOptionId = this.answers[targetQuestionId].answer;
  299. let targetOptionContent = this.findOptionContent(targetQuestionId, targetOptionId);
  300. body = body.replace(match[0], targetOptionContent)
  301. match = regex.exec(body);
  302. }
  303. return body;
  304. },
  305. onQuitTap() {
  306. confirm({
  307. title: "请确认",
  308. message: "确定要结束问卷吗?",
  309. okButtonText: "确定",
  310. cancelButtonText: "取消"
  311. }).then(result => {
  312. if (result) {
  313. // 中途拒访
  314. this.finishQuestionaire(2);
  315. }
  316. })
  317. },
  318. onPreTap() {
  319. let lastIndex = this.historyStack.pop();
  320. this.questionHEAD = lastIndex;
  321. this.onQuestionLoaded(true)
  322. },
  323. onListItemTap(event) {
  324. let selectedOpt = event.item;
  325. selectedOpt.isSelected = !selectedOpt.isSelected;
  326. console.log('onListItemTap', `select option: ${selectedOpt.optionContent}, selected? ${selectedOpt.isSelected}`);
  327. this.onOptionChange(selectedOpt);
  328. },
  329. onOptionSelected(index) {
  330. console.log('option changed : ', typeof index);
  331. let _type = typeof index;
  332. let selectedOpt = null;
  333. if (_type == 'object') {
  334. // 多选
  335. let isUnselect = this.lastCheckList.length > index.length
  336. if (!isUnselect) {
  337. let lastIndex = index[index.length - 1];
  338. selectedOpt = this.questionOptions[lastIndex]
  339. }
  340. // 将本次选项写入this.lastCheckList
  341. this.lastCheckList.length = 0;
  342. for (let i = 0; i < index.length; i++) {
  343. this.lastCheckList.push(index[i]);
  344. }
  345. if (isUnselect) {
  346. return;
  347. }
  348. } else if (_type == 'string') {
  349. selectedOpt = this.questionOptions[index]
  350. }
  351. // 处理开放选项
  352. if (selectedOpt.open) {
  353. MessageBox.prompt(selectedOpt.openOptionTips, '请填写')
  354. .then(({ value }) => {
  355. console.log(`Dialog value: ${value}, `)
  356. this.answerNote[selectedOpt.id] = value;
  357. });
  358. }
  359. },
  360. onQuestionLoaded(isBack) {
  361. console.log('call onQuestionLoaded')
  362. if (isBack) {
  363. let lastQuestionId = this.questionaire.questions[this.questionHEAD].id;
  364. let lastAnswer = this.answers[lastQuestionId];
  365. this.answerValue = lastAnswer.answer;
  366. if (this.currentQuestion.type == 1 && lastAnswer.answerNote) {
  367. this.answerNote[lastAnswer.answer] = lastAnswer.answerNote;
  368. }
  369. delete this.answers[lastQuestionId];
  370. } else {
  371. this.answerValue = '';
  372. this.answerNote = {};
  373. this.selectedSingle = '';
  374. this.selectedMultiple = [];
  375. }
  376. // 根据题目类型,动态展示对应的答题方式
  377. // ---- 已在页面绑定中体现了
  378. // 处理${ID}的情况
  379. // ---- 已在computed questionBody()中处理
  380. // 处理数据源绑定(如果绑定了数据源并且上一列已经选中,则根据上一列的结果筛选选项)
  381. // ---- 已在computed questionOptions()中处理
  382. // 如果某选项会跳转到结束问卷,则提示出来
  383. // ---- 已在页面绑定中体现了
  384. },
  385. findOptionContent(targetQuestionId, targetOptionId) {
  386. for (let i in this.questionaire.questions) {
  387. if (this.questionaire.questions[i].id == targetQuestionId) {
  388. console.log('target qeustion', this.questionaire.questions[i].body);
  389. for (let j in this.questionaire.questions[i].options) {
  390. if (this.questionaire.questions[i].options[j].id == targetOptionId) {
  391. console.log('target option', this.questionaire.questions[i].options[j].optionContent);
  392. return this.questionaire.questions[i].options[j].optionContent;
  393. }
  394. }
  395. }
  396. }
  397. },
  398. onQuestionConfirm() {
  399. console.log('onQuestionConfirm...')
  400. // let selections = [];
  401. let selectionIds = []
  402. let questionId = this.questionaire.questions[this.questionHEAD].id;
  403. if (this.currentQuestion.type == 1) {
  404. if (this.questionType == '单选题') {
  405. if (this.selectedSingle) {
  406. selectionIds.push(this.questionOptions[this.selectedSingle].id);
  407. }
  408. } else {
  409. for (let i = 0; i < this.selectedMultiple.length; i++) {
  410. selectionIds.push(this.questionOptions[this.selectedMultiple[i]].id);
  411. }
  412. }
  413. console.log('selectionIds', selectionIds);
  414. if (this.currentQuestion.maxOptions && this.currentQuestion.maxOptions < selectionIds.length) {
  415. alert(`最多可选中${this.currentQuestion.maxOptions}项`);
  416. return;
  417. }
  418. if (this.currentQuestion.minOptions && this.currentQuestion.minOptions > selectionIds.length) {
  419. alert(`最少需选中${this.currentQuestion.minOptions}项`);
  420. return;
  421. }
  422. // 确定答案
  423. // this.answers[questionId] = selectionIds.join(',');
  424. this.answers[questionId] = {
  425. questionId: questionId,
  426. answer: selectionIds.join(',')
  427. };
  428. if (this.currentQuestion.maxOptions == 1 && this.answerNote[selectionIds[0]]) {
  429. this.answers[questionId].answerNote = this.answerNote[selectionIds[0]]
  430. } else if (this.currentQuestion.maxOptions > 1) {
  431. let answerNoteObj = {}
  432. for (let index in selectionIds) {
  433. let selectedId = selectionIds[index];
  434. if (this.answerNote[selectedId]) {
  435. answerNoteObj[selectedId] = this.answerNote[selectedId];
  436. }
  437. }
  438. this.answers[questionId].answerNote = JSON.stringify(answerNoteObj);
  439. }
  440. // 判断是否配额不足
  441. let isIndicartorOut = indicatorBiz.isIndicartorOut(this.currentQuestion, this.indicators, this.answers);
  442. if (isIndicartorOut) {
  443. // 配额不足,跳转至结束
  444. this.finishQuestionaire(4);
  445. return;
  446. }
  447. // 判断是否需要跳题以及是否需要结束问卷
  448. let hopToQuestionId = 0;
  449. if (this.currentQuestion.type == 1 && this.currentQuestion.maxOptions == 1) {
  450. for (let i in this.questionOptions) {
  451. let opt = this.questionOptions[i];
  452. if (opt.id == selectionIds[0] && opt.redirectTo != 0) {
  453. hopToQuestionId = opt.redirectTo;
  454. break;
  455. }
  456. }
  457. }
  458. console.log('hop to question', hopToQuestionId);
  459. if (hopToQuestionId == -2) {
  460. // 甄别不过,跳转至结束
  461. this.finishQuestionaire(3);
  462. return;
  463. } else if (hopToQuestionId == -1) {
  464. // 正常结束
  465. this.finishQuestionaire(1);
  466. return;
  467. } else if (hopToQuestionId > 0) {
  468. // 跳转到某道题
  469. // for (let i in this.questionaire.questions) {
  470. for (let i = 0; i < this.questionaire.questions.length; i++) {
  471. if (this.questionaire.questions[i].id == hopToQuestionId) {
  472. this.questionHEAD = i;
  473. this.onQuestionLoaded();
  474. return;
  475. }
  476. }
  477. }
  478. } else if (this.currentQuestion.type == 3) {
  479. // 排序题
  480. } else if (this.currentQuestion.type == 4) {
  481. // 开放题
  482. if (this.currentQuestion.required && !this.answerValue) {
  483. alert('此项必填');
  484. return;
  485. }
  486. this.answers[questionId] = {
  487. questionId: questionId,
  488. answer: this.answerValue
  489. };
  490. }
  491. if (this.questionHEAD == this.questionaire.questions.length - 1) {
  492. // 所有题目都回答完了,问卷正常结束
  493. this.finishQuestionaire(1);
  494. return;
  495. } else {
  496. // 进入下一道题
  497. this.historyStack.push(this.questionHEAD);
  498. this.questionHEAD++;
  499. }
  500. this.onQuestionLoaded();
  501. },
  502. finishQuestionaire(status) {
  503. if (this.questionaire.info.status >= 3) {
  504. this.answerSheet.status = status;
  505. this.answerSheet.endTime = dateFormat.formatDate('yyyy-MM-dd HH:mm:ss', new Date());
  506. let arr = [];
  507. for (let i in this.answers) {
  508. arr.push(this.answers[i])
  509. }
  510. // 持久化answerSheet数据
  511. // this.updateAnswerSheetData(JSON.stringify(arr));
  512. axios.post(this.webQnUrl, {
  513. sheet: this.answerSheet,
  514. answers: arr
  515. }).then(() => {
  516. // this.updateAnswerSheetDataUploaded(true);
  517. // alert('问卷结果已提交');
  518. this.webQuestionaire.status = 3;
  519. })
  520. }
  521. },
  522. onFinishTap() {
  523. if (app.android) {
  524. app.android.off(app.AndroidApplication.activityBackPressedEvent, this.backEventHandler);
  525. console.log('backEventHandler off')
  526. }
  527. if (this.isRecord) {
  528. this.recorder.stop().catch(ex => {
  529. console.log('recorder stop failed', ex);
  530. this.isRecording = false;
  531. });
  532. }
  533. this.$modal.close();
  534. },
  535. insertAnswerSheet() {
  536. this.$db.execSQL('insert into answer_sheet values(?,?,?,?,?,?,?,?,?,?,?,?)',
  537. [
  538. this.answerSheet.sheetName,
  539. this.answerSheet.questionaireId,
  540. this.answerSheet.status,
  541. this.answerSheet.startTime,
  542. null,
  543. this.answerSheet.locationLong,
  544. this.answerSheet.locationLat,
  545. null,
  546. 0,
  547. this.isRecord ? 1 : 0,
  548. 0,
  549. this.getUserName()
  550. ], function (err) {
  551. if (err) {
  552. console.error('insertAnswerSheet error', err)
  553. }
  554. })
  555. },
  556. // updateAnswerSheetData(answerJson) {
  557. // this.$db.execSQL('update answer_sheet set endTime = ?, answerJson = ?, status = ? where sheetName = ?',
  558. // [
  559. // this.answerSheet.endTime,
  560. // answerJson,
  561. // this.answerSheet.status,
  562. // this.answerSheet.sheetName,
  563. // ], function (err) {
  564. // if (err) {
  565. // console.error('updateAnswerSheetData error', err)
  566. // }
  567. // })
  568. // },
  569. // updateAnswerSheetDataUploaded(isDataUploaded) {
  570. // this.$db.execSQL('update answer_sheet set dataUploaded = ? where sheetName = ?',
  571. // [
  572. // isDataUploaded ? 1 : 0,
  573. // this.answerSheet.sheetName,
  574. // ], function (err) {
  575. // if (err) {
  576. // console.error('updateAnswerSheetDataUploaded error', err)
  577. // }
  578. // })
  579. // }
  580. },
  581. }
  582. </script>
  583. <style scoped>
  584. .button-bottom {
  585. width: 96%;
  586. /* position: absolute; */
  587. /* left: 2%; */
  588. /* bottom: 2%; */
  589. margin-top: 40px;
  590. margin-left: 2%;
  591. }
  592. </style>