detail-list.tsx 22 KB

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