AnswerData.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div class="answer">
  4. <div class="title">{{ share_record_info.share_record_name }}</div>
  5. <div v-if="curStatisticsType === statisticsList[1].value" class="search">
  6. <span class="search-name">搜索</span>
  7. <el-input
  8. v-model="searchData.search_content"
  9. placeholder="全部"
  10. suffix-icon="el-icon-search"
  11. @keyup.enter.native="getPageList"
  12. />
  13. <span class="search-name">状态</span>
  14. <el-select v-model="searchData.status_list" multiple placeholder="全部" @change="getPageList">
  15. <el-option v-for="{ label, value } in statusType" :key="value" :label="label" :value="value" />
  16. </el-select>
  17. </div>
  18. <div class="sum-info">
  19. <div class="info-item">
  20. <span>收到邀请</span>
  21. <span class="info">
  22. <span>{{ sum_info.finish_person_count + sum_info.unfinish_person_count }}</span>
  23. <span class="light">人</span>
  24. </span>
  25. </div>
  26. <div class="info-item">
  27. <span>完成练习</span>
  28. <span class="info">
  29. <span>{{ sum_info.finish_person_count }}</span>
  30. <span class="light">人</span>
  31. </span>
  32. </div>
  33. <div class="info-item">
  34. <span>平均正确率</span>
  35. <span class="info">{{ sum_info.avg_right_percent }} <span class="light">%</span></span>
  36. </div>
  37. <div class="info-item">
  38. <span>平均耗时</span>
  39. <span class="info">
  40. <template v-if="avgTime.hour"> {{ avgTime.hour }} <span class="light">时</span> </template>
  41. <template v-if="avgTime.minute"> {{ avgTime.minute }} <span class="light">分</span> </template>
  42. {{ avgTime.second }} <span class="light">秒</span>
  43. </span>
  44. </div>
  45. </div>
  46. <ul class="statistics-type-list">
  47. <li
  48. v-for="{ label, value } in statisticsList"
  49. :key="value"
  50. :class="[{ active: value === curStatisticsType }]"
  51. @click="changeStatistics(value)"
  52. >
  53. {{ label }}
  54. </li>
  55. </ul>
  56. <!-- 按题统计 -->
  57. <template v-if="curStatisticsType === statisticsList[0].value">
  58. <el-table :data="question_user_answer_stat_list" height="100%" @sort-change="sortChange">
  59. <el-table-column prop="index" label="题号" width="70">
  60. <template slot-scope="{ row }">{{ row.question_number }}</template>
  61. </el-table-column>
  62. <el-table-column prop="name" label="题型" width="130" />
  63. <el-table-column label="主客观" width="90">
  64. <template slot-scope="{ row }">{{ row.is_objective === 'true' ? '客观题' : '主观题' }}</template>
  65. </el-table-column>
  66. <el-table-column label="题干" width="600">
  67. <div slot-scope="{ row }" class="rich-text" v-html="sanitizeHTML(row.stem)"></div>
  68. </el-table-column>
  69. <el-table-column label="回答正确" width="100">
  70. <template slot-scope="{ row }">
  71. <template v-if="row.answer_right_person_name_list.length > 0">
  72. <el-popover placement="bottom" width="270" trigger="hover">
  73. <ul class="name-list">
  74. <li v-for="(name, i) in row.answer_right_person_name_list" :key="i">{{ name }}</li>
  75. </ul>
  76. <span slot="reference">{{ row.is_objective === 'true' ? row.answer_right_person_count : '-' }}</span>
  77. </el-popover>
  78. </template>
  79. <span v-else>{{ row.is_objective === 'true' ? row.answer_right_person_count : '-' }}</span>
  80. </template>
  81. </el-table-column>
  82. <el-table-column label="回答错误" width="100">
  83. <template slot-scope="{ row }">
  84. <template v-if="row.answer_error_person_name_list.length > 0">
  85. <el-popover placement="bottom" width="270" trigger="hover">
  86. <ul class="name-list">
  87. <li v-for="(name, i) in row.answer_error_person_name_list" :key="i">{{ name }}</li>
  88. </ul>
  89. <span slot="reference">{{ row.is_objective === 'true' ? row.answer_error_person_count : '-' }}</span>
  90. </el-popover>
  91. </template>
  92. <span v-else>{{ row.is_objective === 'true' ? row.answer_error_person_count : '-' }} </span>
  93. </template>
  94. </el-table-column>
  95. <el-table-column
  96. label="正确率"
  97. sortable="custom"
  98. prop="right_percent"
  99. :sort-orders="['descending', 'ascending', null]"
  100. >
  101. <template slot-scope="{ row }">
  102. <template v-if="row.is_objective === 'true'">
  103. {{ row.right_percent }}% ({{ row.answer_right_person_count }}/{{ row.answer_person_count }})
  104. </template>
  105. <template v-else>-</template>
  106. </template>
  107. </el-table-column>
  108. <el-table-column label="操作" fixed="right" width="100">
  109. <template slot-scope="{ row }">
  110. <span class="link" @click="viewExerciseQuestion(row.question_id, row.exercise_id, row.question_type)">
  111. 查看
  112. </span>
  113. </template>
  114. </el-table-column>
  115. </el-table>
  116. </template>
  117. <!-- 按人统计 -->
  118. <template v-else-if="curStatisticsType === statisticsList[1].value">
  119. <el-table
  120. :data="answer_record_list"
  121. height="100%"
  122. :header-cell-class-name="handleHeaderClass"
  123. @sort-change="sortChangePerson"
  124. >
  125. <el-table-column prop="index" label="序号" width="70">
  126. <template slot-scope="{ $index }">{{ $index + 1 }}</template>
  127. </el-table-column>
  128. <el-table-column prop="user_real_name" label="用户" width="280" />
  129. <el-table-column
  130. prop="finish_time"
  131. label="完成时间"
  132. width="180"
  133. sortable="custom"
  134. :sort-orders="['ascending', 'descending', null]"
  135. />
  136. <el-table-column
  137. prop="answer_duration"
  138. label="耗时"
  139. width="180"
  140. sortable="custom"
  141. :sort-orders="['ascending', 'descending', null]"
  142. >
  143. <template slot-scope="{ row }">
  144. <span>{{ row.finish_time ? secondFormatConversion(row.answer_duration, 'chinese') : '' }}</span>
  145. </template>
  146. </el-table-column>
  147. <el-table-column prop="right_count" label="正确" width="160">
  148. <template slot-scope="{ row }">
  149. <span>{{ row.finish_time ? row.right_count : '' }}</span>
  150. </template>
  151. </el-table-column>
  152. <el-table-column prop="error_count" label="错误" width="160">
  153. <template slot-scope="{ row }">
  154. <span>{{ row.finish_time ? row.error_count : '' }}</span>
  155. </template>
  156. </el-table-column>
  157. <el-table-column
  158. label="正确率"
  159. sortable="custom"
  160. :sort-orders="['ascending', 'descending', null]"
  161. prop="right_percent"
  162. >
  163. <template slot-scope="{ row }">
  164. <span>{{ row.finish_time ? row.right_percent + '%' : '' }}</span>
  165. </template>
  166. </el-table-column>
  167. <el-table-column prop="operation" label="操作" fixed="right" width="200">
  168. <template v-if="row.finish_time" slot-scope="{ row }">
  169. <span class="link" @click="viewUserAnswerRecordLis(row.exercise_share_record_id, row.id)">查看</span>
  170. </template>
  171. </el-table-column>
  172. </el-table>
  173. <PaginationPage ref="pagination" :total="total" @getList="pageQueryExerciseUserAnswerRecordList" />
  174. </template>
  175. </div>
  176. </template>
  177. <script>
  178. import { PageQueryExerciseUserAnswerRecordList, GetExerciseQuestionUserAnswerStatList } from '@/api/exercise';
  179. import { secondFormatConversion } from '@/utils/transform';
  180. import DOMPurify from 'dompurify';
  181. import PaginationPage from '@/components/common/PaginationPage.vue';
  182. export default {
  183. name: 'AnswerData',
  184. components: {
  185. PaginationPage,
  186. },
  187. data() {
  188. const { query } = this.$route;
  189. return {
  190. total: 0,
  191. answer_record_list: [],
  192. share_record_info: { exercise_name: '', share_record_url: '' },
  193. sum_info: { finish_person_count: 0, unfinish_person_count: 0, avg_right_percent: 0, avg_answer_duration: 0 },
  194. searchData: {
  195. exercise_id: query.exercise_id,
  196. share_record_id: query.share_record_id,
  197. search_content: '',
  198. status_list: [],
  199. },
  200. statusType: [
  201. { label: '已完成', value: 0 },
  202. { label: '未完成', value: 1 },
  203. ],
  204. secondFormatConversion,
  205. curStatisticsType: 'question', // 当前统计类型
  206. // 统计类型列表
  207. statisticsList: [
  208. { label: '按题统计', value: 'question' },
  209. { label: '按人统计', value: 'person' },
  210. ],
  211. // 题目用户答题统计列表
  212. question_user_answer_stat_list: [],
  213. order_column_list: [], // 排序列
  214. };
  215. },
  216. computed: {
  217. // 平均耗时
  218. avgTime() {
  219. let duration = this.sum_info.avg_answer_duration;
  220. let hour = Math.floor(duration / 3600);
  221. let minute = Math.floor((duration - hour * 3600) / 60);
  222. let second = duration - hour * 3600 - minute * 60;
  223. return { hour, minute, second };
  224. },
  225. sort() {
  226. return this.order_column_list.map((item) => {
  227. const [prop, order] = item.split(':');
  228. return { prop, order: order === 'desc' ? 'descending' : 'ascending' };
  229. });
  230. },
  231. },
  232. created() {
  233. this.getExerciseQuestionUserAnswerStatList();
  234. },
  235. methods: {
  236. getPageList() {
  237. this.$refs.pagination.getList();
  238. },
  239. /**
  240. * 查看用户答题记录列表
  241. * @param {string} exercise_share_record_id 练习分享记录id
  242. * @param {string} answer_record_id 答题记录id
  243. */
  244. viewUserAnswerRecordLis(exercise_share_record_id, answer_record_id) {
  245. this.$router.push({
  246. path: '/answer_record_list',
  247. query: {
  248. exercise_share_record_id,
  249. answer_record_id,
  250. exercise_id: this.searchData.exercise_id,
  251. share_record_id: this.searchData.share_record_id,
  252. },
  253. });
  254. },
  255. /**
  256. * 查看练习题题目答题用户列表
  257. */
  258. viewExerciseQuestion(question_id, exercise_id, question_type) {
  259. this.$router.push({
  260. path: '/exercise_answer_user_list',
  261. query: {
  262. search_exercise_id: this.searchData.exercise_id,
  263. exercise_id,
  264. share_record_id: this.searchData.share_record_id,
  265. question_id,
  266. question_type,
  267. },
  268. });
  269. },
  270. pageQueryExerciseUserAnswerRecordList(data) {
  271. PageQueryExerciseUserAnswerRecordList({ ...data, ...this.searchData, order_column_list: this.order_column_list })
  272. .then(({ total_count, share_record_info, sum_info, answer_record_list }) => {
  273. this.answer_record_list = answer_record_list;
  274. this.total = total_count;
  275. this.share_record_info = share_record_info;
  276. this.sum_info = sum_info;
  277. })
  278. .catch(() => {});
  279. },
  280. /**
  281. * 按人统计表格排序修改事件
  282. * @param {string} prop 列属性
  283. * @param {string} order 排序方式
  284. */
  285. sortChangePerson({ prop, order }) {
  286. this.changeOrderColumnList(prop, order);
  287. this.getPageList();
  288. },
  289. /**
  290. * 切换统计类型
  291. * @param {string} value 统计类型
  292. */
  293. changeStatistics(value) {
  294. this.curStatisticsType = value;
  295. this.order_column_list = [];
  296. if (value === 'question') {
  297. this.getExerciseQuestionUserAnswerStatList();
  298. }
  299. },
  300. getExerciseQuestionUserAnswerStatList() {
  301. GetExerciseQuestionUserAnswerStatList({
  302. share_record_id: this.searchData.share_record_id,
  303. order_column_list: this.order_column_list,
  304. }).then(({ share_record_info, sum_info, question_user_answer_stat_list }) => {
  305. this.question_user_answer_stat_list = question_user_answer_stat_list;
  306. this.share_record_info = share_record_info;
  307. this.sum_info = sum_info;
  308. });
  309. },
  310. /**
  311. * 按题统计表格排序修改事件
  312. * @param {string} prop 列名称
  313. * @param {string} order 排序方式
  314. */
  315. sortChange({ prop, order }) {
  316. this.changeOrderColumnList(prop, order);
  317. this.getExerciseQuestionUserAnswerStatList();
  318. },
  319. /**
  320. * 修改 order_column_list 排序列
  321. * @param {string} prop 列名称
  322. * @param {string} order 排序方式
  323. */
  324. changeOrderColumnList(prop, order) {
  325. const findIndex = this.order_column_list.findIndex((item) => item.includes(prop));
  326. if (order) {
  327. const order_column = `${prop}${order === 'descending' ? ':desc' : ''}`;
  328. if (findIndex === -1) {
  329. this.order_column_list.push(order_column);
  330. } else {
  331. this.order_column_list.splice(findIndex, 1, order_column);
  332. }
  333. } else {
  334. this.order_column_list = this.order_column_list.filter((item) => !item.includes(prop));
  335. }
  336. },
  337. /**
  338. * 处理表头样式,为表头添加排序样式
  339. * @param {object} column 列对象
  340. */
  341. handleHeaderClass({ column }) {
  342. const find = this.order_column_list.find((item) => item.includes(column.property));
  343. if (!find) {
  344. column.order = null;
  345. return;
  346. }
  347. column.order = find.split(':')[1] === undefined ? 'ascending' : 'descending';
  348. },
  349. /**
  350. * 过滤 html,防止 xss 攻击
  351. * @param {string} html 需要过滤的html
  352. * @returns {string} 过滤后的html
  353. */
  354. sanitizeHTML(html) {
  355. return DOMPurify.sanitize(html);
  356. },
  357. },
  358. };
  359. </script>
  360. <style lang="scss" scoped>
  361. @use '@/styles/mixin.scss' as *;
  362. .answer {
  363. @include list;
  364. .title {
  365. font-size: 20px;
  366. font-weight: bold;
  367. }
  368. .search {
  369. display: grid;
  370. grid-template: 30px 32px / repeat(auto-fill, 210px);
  371. grid-auto-flow: column;
  372. column-gap: 24px;
  373. &-name {
  374. font-size: 14px;
  375. color: $font-light-color;
  376. }
  377. }
  378. .sum-info {
  379. display: flex;
  380. column-gap: 160px;
  381. align-items: center;
  382. justify-content: center;
  383. min-height: 90px;
  384. padding: 16px;
  385. background-color: $main-background-color;
  386. border-radius: 2px;
  387. .info-item {
  388. display: flex;
  389. flex-direction: column;
  390. :first-child {
  391. text-align: right;
  392. }
  393. .info {
  394. height: 33px;
  395. font-size: 22px;
  396. font-weight: bold;
  397. text-align: right;
  398. }
  399. .light {
  400. font-size: 12px;
  401. font-weight: normal;
  402. color: $font-light-color;
  403. }
  404. }
  405. }
  406. .statistics-type-list {
  407. display: flex;
  408. column-gap: 12px;
  409. font-size: 14px;
  410. li {
  411. padding: 5px 12px;
  412. color: $font-light-color;
  413. cursor: pointer;
  414. &.active {
  415. color: $main-color;
  416. background-color: $fill-color;
  417. border-radius: 40px;
  418. }
  419. }
  420. }
  421. .rich-text {
  422. @include rich-text(12pt);
  423. font-size: 14px !important;
  424. color: #1d2129 !important;
  425. text-decoration: none !important;
  426. background-color: transparent !important;
  427. :deep p {
  428. margin: 0;
  429. font-size: 14px !important;
  430. color: #1d2129 !important;
  431. text-decoration: none !important;
  432. background-color: transparent !important;
  433. }
  434. :deep span {
  435. font-size: 14px !important;
  436. color: #1d2129 !important;
  437. text-decoration: none !important;
  438. background-color: transparent !important;
  439. }
  440. :deep strong {
  441. font-weight: normal !important;
  442. color: #1d2129 !important;
  443. text-decoration: none !important;
  444. background-color: transparent !important;
  445. }
  446. :deep em {
  447. font-style: normal !important;
  448. color: #1d2129 !important;
  449. text-decoration: none !important;
  450. background-color: transparent !important;
  451. }
  452. }
  453. }
  454. </style>
  455. <style lang="scss">
  456. .el-popover {
  457. padding: 8px;
  458. .name-list {
  459. display: flex;
  460. flex-wrap: wrap;
  461. gap: 8px;
  462. }
  463. }
  464. </style>