detail-list.tsx 21 KB

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