index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. import { Tag, Image, Button } from 'vant'
  2. import { computed, defineComponent, nextTick, onMounted, PropType, reactive, ref } from 'vue'
  3. import styles from './index.module.less'
  4. import { useRect } from '@vant/use'
  5. import { AnswerType, QuestionType } from '../../unit'
  6. import AnserTitle from '../anser-title'
  7. import AnswerAnalysis from '../answer-analysis'
  8. // 单选和多选题
  9. export default defineComponent({
  10. name: 'keep-look-question',
  11. props: {
  12. value: {
  13. type: Array,
  14. default: () => []
  15. },
  16. index: {
  17. // 题目是第几道
  18. type: Number,
  19. default: 1
  20. },
  21. data: {
  22. type: Object,
  23. default: () => ({})
  24. },
  25. /* 只读 */
  26. readOnly: {
  27. type: Boolean,
  28. default: false
  29. },
  30. showRate: {
  31. type: Boolean,
  32. default: false
  33. },
  34. showAnalysis: {
  35. // 是否显示解析
  36. type: Boolean,
  37. default: false
  38. },
  39. analysis: {
  40. type: Object,
  41. default: () => ({
  42. message: '',
  43. topic: false, // 是否显示结果
  44. userResult: true // 用户答题对错
  45. })
  46. }
  47. },
  48. emits: ['update:value'],
  49. setup(props, { emit }) {
  50. const canvasRef = ref()
  51. const state = reactive({
  52. answerDomId: 'answer' + +new Date(),
  53. answerRect: {} as any,
  54. sortable: null as any,
  55. list: [] as any,
  56. options: [] as any,
  57. drawLineList: [] as any,
  58. selectItem: [] as any
  59. })
  60. /*
  61. drawLineList 下的对象
  62. {
  63. startPoint: { x: 0, y: 0 },
  64. endPoint: { x: 0, y: 0},
  65. leftIndex: 1,
  66. rightIndex: 2
  67. }
  68. */
  69. const onLeftClick = (e: any, item: any) => {
  70. // 是否只读
  71. if (props.readOnly) return
  72. state.options.forEach((option: any) => {
  73. if (!option.leftLocked && item.index !== option.index) {
  74. option.left = false
  75. }
  76. })
  77. const selectd = isDrawLine(item.index, 'left')
  78. // 判断当前元素是否已经连线了
  79. if (selectd.status) {
  80. state.options.forEach((option: any) => {
  81. if (!option.rightLocked) {
  82. option.right = false
  83. }
  84. })
  85. state.selectItem = []
  86. // 如果已经连线了则去掉
  87. state.drawLineList.splice(selectd.selectIndex, 1)
  88. // 重新绘制
  89. renderDrawLine(canvasRef.value)
  90. state.options.forEach((option: any) => {
  91. if (selectd.selectOption.leftIndex === option.index) {
  92. option.left = false
  93. option.leftLocked = false
  94. }
  95. if (selectd.selectOption.rightIndex === option.index) {
  96. option.right = false
  97. option.rightLocked = false
  98. }
  99. })
  100. } else {
  101. item.left = !item.left
  102. if (item.left) {
  103. // 为true时添加定位
  104. state.selectItem[0] = {
  105. index: item.index
  106. }
  107. } else {
  108. state.selectItem[0] = null
  109. }
  110. // 判断是否有二个的关联
  111. if (state.selectItem[0] && state.selectItem[1]) {
  112. const postion = calcPoint()
  113. state.drawLineList.push(postion)
  114. state.selectItem = []
  115. renderDrawLine(canvasRef.value)
  116. }
  117. }
  118. onSelect()
  119. }
  120. const onRightClick = (e: any, item: any) => {
  121. // 是否只读
  122. if (props.readOnly) return
  123. // 去掉
  124. state.options.forEach((option: any) => {
  125. if (!option.rightLocked && item.index !== option.index) {
  126. option.right = false
  127. }
  128. })
  129. const selectd = isDrawLine(item.index, 'right')
  130. // 判断当前元素是否已经连线了
  131. if (selectd.status) {
  132. state.options.forEach((option: any) => {
  133. if (!option.leftLocked) {
  134. option.left = false
  135. }
  136. })
  137. state.selectItem = []
  138. // console.log(
  139. // selectd,
  140. // 'selected',
  141. // JSON.stringify(state.drawLineList),
  142. // state.drawLineList,
  143. // state.drawLineList.length
  144. // )
  145. // 如果已经连线了则去掉
  146. state.drawLineList.splice(selectd.selectIndex, 1)
  147. // 重新绘制
  148. renderDrawLine(canvasRef.value)
  149. state.options.forEach((option: any) => {
  150. if (selectd.selectOption.leftIndex === option.index) {
  151. option.left = false
  152. option.leftLocked = false
  153. }
  154. if (selectd.selectOption.rightIndex === option.index) {
  155. option.right = false
  156. option.rightLocked = false
  157. }
  158. })
  159. } else {
  160. item.right = !item.right
  161. if (item.right) {
  162. // 为true时添加定位
  163. state.selectItem[1] = {
  164. index: item.index
  165. }
  166. } else {
  167. state.selectItem[1] = null
  168. }
  169. // 判断是否有二个的关联
  170. if (state.selectItem[0] && state.selectItem[1]) {
  171. const postion = calcPoint()
  172. state.drawLineList.push(postion)
  173. state.selectItem = []
  174. renderDrawLine(canvasRef.value)
  175. }
  176. }
  177. onSelect()
  178. }
  179. /**
  180. * @description 判断是否已连接
  181. * @param key 选中的选项标识
  182. * @returns status 状态, selectIndex 索引, selectOption 选中选项
  183. */
  184. const isDrawLine = (key: number, type = 'left') => {
  185. const drawLineList = state.drawLineList || []
  186. let status = false // true 连,false 没连
  187. let selectIndex = 0 // 连了所在的索引
  188. let selectOption: any = {}
  189. drawLineList.forEach((item: any, index: number) => {
  190. if (item.leftIndex === key && type === 'left') {
  191. selectOption = item
  192. status = true
  193. selectIndex = index
  194. } else if (item.rightIndex === key && type === 'right') {
  195. selectOption = item
  196. status = true
  197. selectIndex = index
  198. }
  199. })
  200. return {
  201. status,
  202. selectIndex,
  203. selectOption
  204. }
  205. }
  206. /**
  207. * @description 计算连线坐标位置及左右关联编号,每次计算坐标的时候都取元素最新位置定位
  208. * @returns 连线的坐标
  209. */
  210. const calcPoint = () => {
  211. const canvasPostion = useRect(canvasRef.value)
  212. const leftSectionItem = state.selectItem[0]
  213. const firstPostion: any = useRect(
  214. document.getElementById(leftSectionItem.index + '-left') as any
  215. )
  216. firstPostion.index = leftSectionItem.index
  217. // console.log('🚀 ~ calcPoint ~ leftObj', leftObj)
  218. const rightSectionItem = state.selectItem[1]
  219. const secondPostion: any = useRect(
  220. document.getElementById(rightSectionItem.index + '-right') as any
  221. )
  222. secondPostion.index = rightSectionItem.index
  223. console.log(state.selectItem, firstPostion, secondPostion)
  224. const startPoint = {
  225. x: firstPostion.width,
  226. y: firstPostion.top + firstPostion.height / 2 - canvasPostion.top
  227. }
  228. const endPoint = {
  229. x: secondPostion.left - canvasPostion.left,
  230. y: secondPostion.top + secondPostion.height / 2 - canvasPostion.top
  231. }
  232. state.options.forEach((item: any) => {
  233. if (item.index === firstPostion.index) {
  234. item.leftLocked = true
  235. }
  236. if (item.index === secondPostion.index) {
  237. item.rightLocked = true
  238. }
  239. })
  240. return {
  241. startPoint,
  242. endPoint,
  243. leftIndex: firstPostion.index,
  244. rightIndex: secondPostion.index
  245. }
  246. }
  247. /**
  248. * @description 绘制连线
  249. * @param canvasRef 对象
  250. */
  251. const renderDrawLine = (canvasRef: any) => {
  252. // 重新画线
  253. if (canvasRef.getContext) {
  254. const ctx = canvasRef.getContext('2d')
  255. ctx.clearRect(0, 0, state.answerRect.width, state.answerRect.height)
  256. state.drawLineList.forEach((item: any) => {
  257. drawLine(ctx, item.startPoint, item.endPoint)
  258. })
  259. }
  260. }
  261. /**
  262. * @description 连线
  263. * @param ctx canvas 对象
  264. * @param startPoint 开始坐标
  265. * @param endPoint 结束坐标
  266. * @returns void(0)
  267. */
  268. const drawLine = (ctx: any, startPoint: any, endPoint: any) => {
  269. ctx.beginPath()
  270. ctx.moveTo(startPoint.x, startPoint.y)
  271. ctx.lineTo(endPoint.x, endPoint.y)
  272. ctx.lineWidth = 2
  273. ctx.strokeStyle = '#FF8057'
  274. ctx.stroke()
  275. }
  276. // 返回选中的结果
  277. const onSelect = () => {
  278. const options = state.options || []
  279. const drawLineList = state.drawLineList || []
  280. const result: any = []
  281. drawLineList.forEach((item: any) => {
  282. const leftOption = options.find((child: any) => child.index === item.leftIndex)
  283. const rightOption = options.find((child: any) => child.index === item.rightIndex)
  284. result.push({
  285. answerId: leftOption.index,
  286. answer: leftOption.leftValue,
  287. answerExtra: rightOption.rightValue
  288. })
  289. })
  290. emit('update:value', result)
  291. }
  292. const initOptions = () => {
  293. const answers = props.data.answers || []
  294. const userAnswer = props.data.userAnswer || [] // 用户填写的答案
  295. // console.log(answers, '111')
  296. answers.forEach((answer: any) => {
  297. const tmp = {
  298. index: answer.examinationQuestionAnswerId, // 左边的值
  299. leftValue: answer.questionAnswer, // 左边的值
  300. rightValue: answer.questionExtra, // 右边的值
  301. leftType: answer.questionAnswerTypeCode || 'TXT', // 左边类型
  302. rightType: answer.questionExtraTypeCode || 'TXT', // 右边类型
  303. left: false, // 左边是否选中
  304. right: false, // 右边是否选中
  305. leftLocked: false, // 是否已经连线
  306. rightLocked: false // 是否已经连线
  307. }
  308. state.options.push(tmp)
  309. })
  310. // 反显答案-初始化数据
  311. // console.log(userAnswer)
  312. userAnswer.forEach((user: any) => {
  313. const temps: any = {
  314. startPoint: { x: 0, y: 0 },
  315. endPoint: { x: 0, y: 0 },
  316. leftIndex: 0,
  317. rightIndex: 0
  318. }
  319. state.options.forEach((option: any) => {
  320. // 左边状态
  321. if (option.index === user.answerId) {
  322. option.left = true
  323. option.leftLocked = true
  324. temps.leftIndex = option.index
  325. }
  326. // 右边状态
  327. // console.log(option, user, '----')
  328. if (option.rightValue === user.answerExtra) {
  329. option.right = true
  330. option.leftLocked = true
  331. temps.rightIndex = option.index
  332. }
  333. })
  334. state.drawLineList.push(temps)
  335. })
  336. // console.log(state.drawLineList, state.options)
  337. // 反显答案-连线
  338. nextTick(() => {
  339. state.drawLineList.forEach((draw: any) => {
  340. state.selectItem = []
  341. const leftObj: any = useRect(document.getElementById(draw.leftIndex + '-left') as any)
  342. leftObj.index = draw.leftIndex
  343. state.selectItem[0] = leftObj
  344. const rightObj: any = useRect(document.getElementById(draw.rightIndex + '-right') as any)
  345. rightObj.index = draw.rightIndex
  346. state.selectItem[1] = rightObj
  347. const postion = calcPoint()
  348. draw.endPoint = postion.endPoint
  349. draw.startPoint = postion.startPoint
  350. state.selectItem = []
  351. })
  352. setTimeout(() => {
  353. renderDrawLine(canvasRef.value)
  354. }, 100)
  355. })
  356. }
  357. onMounted(() => {
  358. initOptions()
  359. // 获取canvas 对象
  360. nextTick(() => {
  361. // 获取canvas元素定位
  362. const answer: any = document.getElementById(state.answerDomId)
  363. const answerRect = useRect(answer)
  364. state.answerRect = answerRect
  365. })
  366. })
  367. return () => (
  368. <>
  369. <div class={styles.unitSubject}>
  370. {/* 标题 */}
  371. <AnserTitle
  372. index={props.index}
  373. name={props.data.name}
  374. score={props.data.totalScore}
  375. showRate={props.showRate}
  376. answerType={QuestionType.LINK}
  377. extra={{
  378. questionDetail: props.data.questionDetail,
  379. mediaUrls: props.data.mediaUrls,
  380. rightRate: props.data.rightRate
  381. }}
  382. />
  383. <div class={[styles.unitAnswers]} id={state.answerDomId}>
  384. <canvas
  385. ref={canvasRef}
  386. class={styles.canvasSection}
  387. width={state.answerRect.width || 0}
  388. height={state.answerRect.height || 0}
  389. ></canvas>
  390. {state.options.map((item: any) => (
  391. <div class={styles.answerItem}>
  392. <div
  393. class={[styles.unitItem, item.left && styles.active]}
  394. id={item.index + '-left'}
  395. onClick={(e: any) => onLeftClick(e, item)}
  396. >
  397. {item.leftType === AnswerType.TXT && item.leftValue}
  398. {item.leftType === AnswerType.IMAGE && (
  399. <Image src={item.leftValue} class={styles.img} />
  400. )}
  401. </div>
  402. <div
  403. class={[styles.unitItem, item.right && styles.active]}
  404. id={item.index + '-right'}
  405. onClick={(e: any) => onRightClick(e, item)}
  406. >
  407. {item.rightType === AnswerType.TXT && item.rightValue}
  408. {item.rightType === AnswerType.IMAGE && (
  409. <Image src={item.rightValue} class={styles.img} />
  410. )}
  411. </div>
  412. </div>
  413. ))}
  414. </div>
  415. <div class={styles.resetBtnGroup}>
  416. <Button
  417. round
  418. type="primary"
  419. disabled={props.readOnly}
  420. onClick={() => {
  421. state.drawLineList = []
  422. renderDrawLine(canvasRef.value)
  423. // 清除所有的选中状态
  424. state.options.forEach((item: any) => {
  425. item.left = false
  426. item.right = false
  427. item.leftLocked = false
  428. item.rightLocked = false
  429. })
  430. // 清除所有的选择的内容
  431. state.selectItem = []
  432. }}
  433. >
  434. 重置
  435. </Button>
  436. </div>
  437. </div>
  438. {props.showAnalysis && (
  439. <div class={styles.unitSubject}>
  440. <AnswerAnalysis
  441. answerAnalysis={props.analysis.message}
  442. topic={props.analysis.topic}
  443. userResult={props.analysis.userResult}
  444. />
  445. </div>
  446. )}
  447. </>
  448. )
  449. }
  450. })