detail-list.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import {
  2. Button,
  3. Cell,
  4. CellGroup,
  5. Checkbox,
  6. CheckboxGroup,
  7. Field,
  8. Icon,
  9. Image,
  10. List,
  11. Radio,
  12. RadioGroup,
  13. Tag,
  14. showToast
  15. } from 'vant';
  16. import { defineComponent, onMounted, reactive, ref, watch } from 'vue';
  17. import styles from './detail.module.less';
  18. import iconTimer from '@/common/images/icon-timer.png';
  19. import iconTeacher from '@/common/images/icon-teacher-default.png';
  20. import iconEdit from '@/common/images/icon-edit.png';
  21. import iconFace1 from './images/icon-face-1.png';
  22. import iconFace2 from './images/icon-face-2.png';
  23. import iconFace3 from './images/icon-face-3.png';
  24. import iconFace4 from './images/icon-face-4.png';
  25. import iconUploadImg from './images/icon-upload-img.png';
  26. import iconUploadVideo from './images/icon-upload-video.png';
  27. import iconTips from './images/icon-tips.png';
  28. import SkeletionDetailModal from './skeletion-detail.modal';
  29. import MUploader from '@/components/m-uploader';
  30. import MUploaderInside from '@/components/m-uploader/inside';
  31. import MFullRefresh from '@/components/m-full-refresh';
  32. import { coursesStatus, evaluateStatus, problemType } from '@/helpers/constant';
  33. import MEmpty from '@/components/m-empty';
  34. import request from '@/helpers/request';
  35. import dayjs from 'dayjs';
  36. import { useRoute } from 'vue-router';
  37. import MImagePreview from '@/components/m-image-preview';
  38. import { checkFile } from '@/helpers/toolsValidate';
  39. import deepClone from '@/helpers/deep-clone';
  40. import iconVideoDefault from '@common/images/icon-video-c.png';
  41. export default defineComponent({
  42. name: 'detail-list',
  43. props: {
  44. type: {
  45. type: String,
  46. default: ''
  47. },
  48. evaluateStatus: {
  49. type: String,
  50. default: ''
  51. },
  52. problemType: {
  53. type: String,
  54. default: ''
  55. },
  56. courseType: {
  57. type: String,
  58. default: ''
  59. },
  60. status: {
  61. type: String,
  62. default: ''
  63. },
  64. activeTab: {
  65. type: String,
  66. default: 'NotEvaluated'
  67. }
  68. },
  69. setup(props) {
  70. const route = useRoute();
  71. const forms = reactive({
  72. isClick: false,
  73. imageShow: false,
  74. startPosition: 0,
  75. imagePreview: [] as string[],
  76. listState: {
  77. dataShow: true, // 判断是否有数据
  78. loading: true,
  79. finished: false,
  80. refreshing: false
  81. },
  82. params: {
  83. evaluateFlag: props.type === 'Evaluated' ? true : false,
  84. evaluateStatus: '',
  85. problemType: '',
  86. courseType: '',
  87. status: '',
  88. startTime: route.query.date || '',
  89. endTime: route.query.date || '',
  90. page: 1,
  91. rows: 20
  92. },
  93. changeType: null,
  94. questionType: null,
  95. evaluateList: [] as any,
  96. problemTypeList: [] as any,
  97. list: [],
  98. btnLoading: false
  99. });
  100. const list = ref([] as any[]);
  101. const onRefresh = () => {
  102. forms.params.page = 1;
  103. forms.listState.refreshing = true;
  104. getList();
  105. };
  106. const getList = async () => {
  107. try {
  108. if (forms.isClick) return;
  109. forms.isClick = true;
  110. const { data } = await request.post(
  111. '/api-web/coursePatrolEvaluation/page',
  112. {
  113. data: forms.params
  114. }
  115. );
  116. const result = data || {};
  117. // 格式化数据
  118. const rows = result.rows || [];
  119. rows.forEach((row: any) => {
  120. const attachmentUrlList = row.attachmentUrl
  121. ? row.attachmentUrl.split(',')
  122. : [];
  123. const problemTypeList = row.problemType
  124. ? row.problemType.split(',')
  125. : [];
  126. row.problemTypeList = problemTypeList;
  127. // 级别
  128. row.submitEvaluateStatus = row.evaluateStatus || '';
  129. // 问题类型
  130. row.submitProblemType = problemTypeList || [];
  131. row.submitProblemDesc = row.problemDesc || '';
  132. // 图片和视屏
  133. row.submitVideoList = [];
  134. row.submitImgList = [];
  135. attachmentUrlList.forEach((url: any) => {
  136. // 判断是否是图片
  137. if (checkFile(url, 'image')) {
  138. row.submitImgList.push(url);
  139. } else {
  140. row.submitVideoList.push(url);
  141. }
  142. // 判断是否是视频
  143. });
  144. row.attachmentUrlList = attachmentUrlList || [];
  145. // 判断是否评价,如果有评价则直接显示评价内容
  146. if (!row.evaluateFlag) {
  147. row.isEdit = true;
  148. }
  149. // 判断课程时间是否在当前时间之前;
  150. if (
  151. dayjs(dayjs().format('YYYY-MM-DD')).isAfter(
  152. dayjs(row.courseDate).format('YYYY-MM-DD')
  153. ) &&
  154. !row.evaluateFlag
  155. ) {
  156. row.isEdit = false;
  157. row.isDisabled = true;
  158. }
  159. });
  160. // 判断是否有数据
  161. if (forms.listState.refreshing) {
  162. list.value = rows || [];
  163. } else {
  164. list.value = list.value.concat(rows || []);
  165. }
  166. forms.listState.finished = result.pageNo >= result.totalPage;
  167. forms.params.page = result.pageNo + 1;
  168. } catch {
  169. forms.listState.finished = true;
  170. } finally {
  171. setTimeout(() => {
  172. forms.listState.dataShow = list.value.length > 0;
  173. forms.listState.refreshing = false;
  174. forms.listState.loading = false;
  175. forms.isClick = false;
  176. }, 300);
  177. }
  178. };
  179. // 提交评价
  180. const onSubmit = async (item: any) => {
  181. try {
  182. const url = [...item.submitImgList, ...item.submitVideoList];
  183. //
  184. if (!item.submitEvaluateStatus) {
  185. showToast('请选择评价');
  186. return;
  187. }
  188. // 当选择“不合格”的时候,问题类型、问题描述、上传附件为必填项
  189. if (item.submitEvaluateStatus === 'UNQUALIFIED') {
  190. if (
  191. !item.submitProblemType ||
  192. (item.submitProblemType && item.submitProblemType.length <= 0)
  193. ) {
  194. showToast('请选择问题类型');
  195. return;
  196. }
  197. if (!item.submitProblemDesc) {
  198. showToast('请输入问题描述');
  199. return;
  200. }
  201. if (item.submitProblemDesc.length < 3) {
  202. showToast('问题描述最少输入3个字');
  203. return;
  204. }
  205. // 最大支持3-50汉字
  206. if (
  207. item.submitProblemDesc.length < 3 ||
  208. item.submitProblemDesc.length <= 50
  209. )
  210. if (url.length <= 0) {
  211. showToast('请上传附件');
  212. return;
  213. }
  214. }
  215. const params = {
  216. id: item.id,
  217. evaluateStatus: item.submitEvaluateStatus,
  218. problemType: item.submitProblemType.join(','),
  219. problemDesc: item.submitProblemDesc,
  220. attachmentUrl: url.join(',')
  221. };
  222. forms.btnLoading = true;
  223. if (item.evaluateFlag) {
  224. // 修改
  225. await request.post('/api-web/coursePatrolEvaluation/update', {
  226. // hideLoading: false,
  227. data: params
  228. });
  229. } else {
  230. // 添加
  231. await request.post('/api-web/coursePatrolEvaluation/save', {
  232. // hideLoading: false,
  233. data: {
  234. ...params,
  235. courseScheduleId: item.courseScheduleId
  236. }
  237. });
  238. }
  239. forms.btnLoading = false;
  240. list.value = [];
  241. onRefresh();
  242. } catch {
  243. //
  244. forms.btnLoading = false;
  245. }
  246. };
  247. const onShowPreView = (attachmentUrlList: string[], index: number) => {
  248. forms.imagePreview = deepClone(attachmentUrlList);
  249. forms.imageShow = true;
  250. forms.startPosition = index;
  251. };
  252. const formatFace = (type: string) => {
  253. if (type === 'EXCELLENT') {
  254. return iconFace1;
  255. } else if (type === 'GOOD') {
  256. return iconFace2;
  257. } else if (type === 'QUALIFIED') {
  258. return iconFace3;
  259. } else if (type === 'UNQUALIFIED') {
  260. return iconFace4;
  261. } else {
  262. return iconFace1;
  263. }
  264. };
  265. onMounted(() => {
  266. for (const key in evaluateStatus) {
  267. if (Object.prototype.hasOwnProperty.call(evaluateStatus, key)) {
  268. forms.evaluateList.push({
  269. text: evaluateStatus[key],
  270. value: key
  271. });
  272. }
  273. }
  274. for (const key in problemType) {
  275. if (Object.prototype.hasOwnProperty.call(problemType, key)) {
  276. forms.problemTypeList.push({
  277. text: problemType[key],
  278. value: key
  279. });
  280. }
  281. }
  282. getList();
  283. });
  284. // 监听
  285. watch(
  286. () => [
  287. props.evaluateStatus,
  288. props.problemType,
  289. props.courseType,
  290. props.status
  291. ],
  292. () => {
  293. forms.params.evaluateStatus = props.evaluateStatus;
  294. forms.params.problemType = props.problemType;
  295. forms.params.courseType = props.courseType;
  296. forms.params.status = props.status;
  297. list.value = [];
  298. onRefresh();
  299. }
  300. );
  301. watch(
  302. () => props.activeTab,
  303. () => {
  304. if (props.type === props.activeTab) {
  305. forms.params.evaluateStatus = props.evaluateStatus;
  306. forms.params.problemType = props.problemType;
  307. forms.params.courseType = props.courseType;
  308. forms.params.status = props.status;
  309. list.value = [];
  310. onRefresh();
  311. }
  312. }
  313. );
  314. return () => (
  315. <div>
  316. <SkeletionDetailModal v-model:show={forms.listState.loading}>
  317. <MFullRefresh
  318. v-model:modelValue={forms.listState.refreshing}
  319. onRefresh={() => onRefresh()}
  320. style={{
  321. minHeight: `calc(100vh - var(--header-height) - var(--van-tabs-line-height))`
  322. }}>
  323. <List
  324. finished={forms.listState.finished}
  325. finishedText=" "
  326. style={{ overflow: 'hidden', marginBottom: '18px' }}
  327. onLoad={getList}
  328. offset={100}
  329. immediateCheck={false}>
  330. {forms.listState.dataShow ? (
  331. list.value.map((item: any) => {
  332. return (
  333. <CellGroup inset class={styles.cellGroup}>
  334. <Cell center class={styles.timerCell} border={false}>
  335. {{
  336. icon: () => (
  337. <Icon name={iconTimer} class={styles.iconTimer} />
  338. ),
  339. title: () => (
  340. <div class={styles.timer}>
  341. {dayjs(item.startClassTime).format(
  342. 'YYYY-MM-DD HH:mm'
  343. )}
  344. ~{dayjs(item.endClassTime).format('HH:mm')}
  345. </div>
  346. ),
  347. value: () => (
  348. <div
  349. class={styles.eStatus}
  350. onClick={() => {
  351. if (item.isDisabled) return;
  352. item.isEdit = true;
  353. }}>
  354. {/* 判断是否评价 */}
  355. {item.evaluateFlag ? (
  356. <>
  357. <div class={styles.evaluateResult}>
  358. <Icon
  359. name={formatFace(item.evaluateStatus)}
  360. class={styles.iconFace}
  361. />
  362. <span
  363. class={[
  364. styles.sLevel,
  365. item.evaluateStatus === 'UNQUALIFIED'
  366. ? styles.error
  367. : styles.success
  368. ]}>
  369. {evaluateStatus[item.evaluateStatus]}
  370. </span>
  371. </div>
  372. <Icon
  373. name={iconEdit}
  374. class={styles.iconEdit}
  375. />
  376. </>
  377. ) : (
  378. <span
  379. class={[
  380. styles.sLevel,
  381. item.courseStatus === 'UNDERWAY'
  382. ? styles.success
  383. : '',
  384. item.courseStatus == 'OVER'
  385. ? styles.over
  386. : ''
  387. ]}>
  388. {coursesStatus[item.courseStatus]}
  389. </span>
  390. )}
  391. </div>
  392. )
  393. }}
  394. </Cell>
  395. <Cell center class={styles.usernameCell}>
  396. {{
  397. icon: () => (
  398. <Image
  399. src={item.teacherAvatar || iconTeacher}
  400. class={styles.iconTeacher}
  401. fit="cover"
  402. />
  403. ),
  404. title: () => (
  405. <div>
  406. <div class={styles.classname}>
  407. {item.courseName}
  408. </div>
  409. <div class={styles.name}>{item.teacherName}</div>
  410. </div>
  411. ),
  412. value: () => (
  413. <div class={styles.photoList}>
  414. {item.attachmentUrlList.map(
  415. (file: string, index: number) =>
  416. index < 3 && (
  417. <div
  418. class={styles.photo}
  419. onClick={(e: MouseEvent) => {
  420. e.stopPropagation();
  421. e.preventDefault();
  422. onShowPreView(
  423. item.attachmentUrlList,
  424. index
  425. );
  426. }}>
  427. {checkFile(file, 'image') ? (
  428. <Image
  429. src={
  430. file
  431. }
  432. fit="cover"
  433. />
  434. ) : (
  435. <video
  436. poster={iconVideoDefault}
  437. src={file + '#t=1,4'}
  438. controls={false}></video>
  439. )}
  440. {/* 判断是否大于三个 */}
  441. {item.attachmentUrlList.length > 3 &&
  442. index === 2 ? (
  443. <div class={styles.photoMore}>
  444. +{item.attachmentUrlList.length - 3}
  445. </div>
  446. ) : (
  447. ''
  448. )}
  449. </div>
  450. )
  451. )}
  452. </div>
  453. )
  454. }}
  455. </Cell>
  456. {/* 展示结果 */}
  457. {(item.submitProblemType.length > 0 ||
  458. item.problemDesc) &&
  459. !item.isEdit ? (
  460. <Cell center class={styles.resultCell}>
  461. {item.problemTypeList.length > 0 ? (
  462. <div class={styles.typeGroup}>
  463. {item.problemTypeList.map((type: string) => (
  464. <Tag type="primary" plain>
  465. {problemType[type]}
  466. </Tag>
  467. ))}
  468. </div>
  469. ) : (
  470. ''
  471. )}
  472. {item.problemDesc ? (
  473. <div class={styles.result}>{item.problemDesc}</div>
  474. ) : (
  475. ''
  476. )}
  477. </Cell>
  478. ) : (
  479. ''
  480. )}
  481. {/* 未开始的不能进行评价 */}
  482. {item.isEdit && item.courseStatus != 'NOT_START' ? (
  483. <Cell center class={styles.operationCell}>
  484. <RadioGroup
  485. class={styles.typeGroup}
  486. v-model={item.submitEvaluateStatus}>
  487. {forms.evaluateList.map((child: any) => (
  488. <Tag
  489. type={
  490. item.submitEvaluateStatus === child.value
  491. ? 'primary'
  492. : 'default'
  493. }
  494. plain>
  495. <Radio name={child.value} />
  496. {child.text}
  497. </Tag>
  498. ))}
  499. </RadioGroup>
  500. {/* 当选择“不合格”的时候,问题类型、问题描述、上传附件为必填项 当选择除不合格的其他选项时,问题类型、问题描述不展示,桑川附件为选填 */}
  501. {item.submitEvaluateStatus === 'UNQUALIFIED' ? (
  502. <>
  503. <div class={styles.operationTitle}>问题类型</div>
  504. <CheckboxGroup
  505. class={styles.typeGroup}
  506. v-model={item.submitProblemType}>
  507. {forms.problemTypeList.map((child: any) => (
  508. <Tag
  509. type={
  510. item.submitProblemType.includes(
  511. child.value
  512. )
  513. ? 'primary'
  514. : 'default'
  515. }
  516. plain>
  517. <Checkbox name={child.value} />
  518. {child.text}
  519. </Tag>
  520. ))}
  521. </CheckboxGroup>
  522. <div class={styles.operationTitle}>问题描述</div>
  523. <Field
  524. type="textarea"
  525. rows={2}
  526. v-model={item.submitProblemDesc}
  527. maxlength={50}
  528. class={styles.questionContent}
  529. placeholder="请输入问题描述..."
  530. border={false}
  531. />
  532. </>
  533. ) : (
  534. ''
  535. )}
  536. <div class={styles.operationTitle}>上传附件</div>
  537. <div class={styles.uploadGroup}>
  538. <MUploader
  539. uploadIcon={iconUploadImg}
  540. maxCount={5}
  541. native
  542. v-model:modelValue={item.submitImgList}>
  543. <MUploaderInside
  544. uploadIcon={iconUploadVideo}
  545. uploadType="VIDEO"
  546. accept=".mp4"
  547. uploadSize={50}
  548. native
  549. maxCount={3}
  550. v-model:modelValue={item.submitVideoList}
  551. />
  552. </MUploader>
  553. </div>
  554. <div
  555. class={[
  556. styles.btnGroup,
  557. !item.evaluateFlag ? styles.singleBtn : ''
  558. ]}>
  559. {/* 评价之后才能取消 */}
  560. {item.evaluateFlag ? (
  561. <Button
  562. type="default"
  563. round
  564. block
  565. onClick={() => (item.isEdit = false)}>
  566. 取消
  567. </Button>
  568. ) : (
  569. ''
  570. )}
  571. <Button
  572. type="primary"
  573. round
  574. block
  575. disabled={forms.btnLoading}
  576. loading={forms.btnLoading}
  577. onClick={() => onSubmit(item)}>
  578. 确认
  579. </Button>
  580. </div>
  581. </Cell>
  582. ) : (
  583. ''
  584. )}
  585. {/* 课程是否已经结束 - 未评价 */}
  586. {item.isDisabled ? (
  587. <div class={styles.tips}>
  588. <Icon name={iconTips} class={styles.iconTips} />
  589. 昨天及昨天之前课程不可评价
  590. </div>
  591. ) : (
  592. ''
  593. )}
  594. </CellGroup>
  595. );
  596. })
  597. ) : (
  598. <MEmpty
  599. style={{
  600. minHeight: `calc(100vh - var(--header-height))`
  601. }}
  602. description="暂无数据"
  603. />
  604. )}
  605. </List>
  606. </MFullRefresh>
  607. </SkeletionDetailModal>
  608. <MImagePreview
  609. teleport="body"
  610. v-model:show={forms.imageShow}
  611. images={forms.imagePreview}
  612. startPosition={forms.startPosition}
  613. />
  614. </div>
  615. );
  616. }
  617. });