ActionImage.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. <template>
  2. <div
  3. class="main_container"
  4. ref="mainContainerRef"
  5. v-loading="loading"
  6. element-loading-text="图片加载中……"
  7. element-loading-spinner="el-icon-loading"
  8. element-loading-background="#ffffff"
  9. @mousedown="onMouseDown"
  10. @mousemove="onMouseMove"
  11. @mouseup="onMouseUp"
  12. @wheel="onWheel"
  13. >
  14. <canvas
  15. id="paperCanvas"
  16. ref="paperCanvasRef"
  17. class="paper_canvas"
  18. @mouseleave="onCanvasLeave"
  19. ></canvas>
  20. <div id="imgContainer" class="img_container">
  21. <img v-if="paperImgUrl" :src="paperImgUrl" />
  22. </div>
  23. <div class="no_paper_url" v-if="paperImgUrl == ''">
  24. 暂无数据
  25. <!-- 请先制作模版 -->
  26. </div>
  27. </div>
  28. </template>
  29. <script setup>
  30. import { ref, reactive, watch, onBeforeUnmount, onMounted } from 'vue'
  31. import { throttle } from 'lodash'
  32. // import { mmToPx } from '@/utils/common.ts' // 如果需要取消注释
  33. // 定义 Props
  34. const props = defineProps({
  35. drawData: {
  36. type: Array,
  37. default: () => [],
  38. }, // 画的边框的数据
  39. paperInfo: {
  40. type: Object,
  41. default: () => null,
  42. }, // 纸张大小
  43. paperImgUrl: {
  44. type: String,
  45. default: '',
  46. }, // 试卷地址相关信息
  47. currentId: {
  48. type: String,
  49. default: '',
  50. }, // 当前选中的id
  51. isDrag: {
  52. type: Boolean,
  53. default: true,
  54. }, // 是否可以拖动 默认可以拖动
  55. isAbnormal: {
  56. type: Boolean,
  57. default: false,
  58. }, // 是否异常处使用 区别 异常处理使用的地方 正常答案显示蓝色
  59. })
  60. // 定义 Emits
  61. const emit = defineEmits(['GetRectPoint'])
  62. // 响应式数据
  63. const mainContainerRef = ref(null)
  64. const paperCanvasRef = ref(null)
  65. const position = reactive({
  66. x: 0,
  67. y: 0,
  68. }) // 初始canvas图片位置
  69. const startX = ref(0) // 鼠标按下时的初始位置x坐标
  70. const startY = ref(0) // 鼠标按下时的初始位置y坐标
  71. const scale = ref(1) // 画布缩放倍数
  72. const isDragging = ref(false) // 是否拖动画布
  73. const isDrawing = ref(false) // 是否正在画线
  74. const drawType = ref(0) // 1画线 0 拖拽模式
  75. const image = ref(null)
  76. const paperImgInfo = reactive({
  77. width: 0,
  78. height: 0,
  79. })
  80. const canvasInfo = reactive({
  81. width: 0,
  82. height: 0,
  83. }) // 画布的大小
  84. const zoomRate = ref(0) // 图片的缩放比例
  85. const dpr = window.devicePixelRatio || 1
  86. const minScale = 0.7 // 最小缩放值
  87. const maxScale = 4 // 最大缩放值
  88. const rectPoint = reactive({
  89. startX: 0,
  90. startY: 0,
  91. endX: 0,
  92. endY: 0,
  93. }) // 矩形起始坐标点
  94. const showObjectArea = ref(false) // 是否显示对象区域
  95. const addObjectAreaOption = reactive({
  96. derection: 1, // 排列方向 1 横线 2竖向
  97. questionType: 1, // 题类型 1单选 2多选 3 判断
  98. startNumber: 16, // 起始题号
  99. endNumber: 20, // 结束题号
  100. interval: 1, // 题号间隔 默认1
  101. answerNumber: 4, // 选项个数 答案个数
  102. answerWidth: 0, // 选项宽度
  103. answerHeight: 0, // 选项高度
  104. }) // 添加选择题设置信息
  105. const currenPoint = reactive({
  106. x: 0,
  107. y: 0,
  108. w: 0,
  109. h: 0,
  110. })
  111. const containerWidth = ref(0) // 容器宽度
  112. const containerHeight = ref(0) // 容器高度
  113. const isInit = ref(true) // 是否是初始加载
  114. const isShowDraw = ref(true) // 是否显示画框
  115. const loading = ref(false) // 是否正在加载图片
  116. // 模拟 this.$global.floatNum,请根据实际项目替换为真实工具函数
  117. const floatNum = (num, fixed = 2) => {
  118. if (typeof num !== 'number') return 0
  119. return Number(num.toFixed(fixed))
  120. }
  121. // 方法定义
  122. // 显示画框
  123. const ShowDrawData = () => {
  124. isShowDraw.value = !isShowDraw.value
  125. drawImage()
  126. }
  127. // 初始数据加载
  128. const initData = () => {
  129. if (!mainContainerRef.value) return
  130. console.log('初始化数据加载图片地址信息',props.paperImgUrl)
  131. // 获取容器的宽高
  132. const { width, height } = mainContainerRef.value.getBoundingClientRect()
  133. containerHeight.value = Number(height)
  134. containerWidth.value = Number(width)
  135. image.value = new Image()
  136. image.value.src = props.paperImgUrl
  137. // 初始化获取试卷数据
  138. if (props.paperInfo?.width && props.paperInfo?.height) {
  139. // 设置初始数据
  140. paperImgInfo.width = props.paperInfo.width // 获取宽
  141. paperImgInfo.height = props.paperInfo.height // 获取高
  142. image.value.onload = () => {
  143. loading.value = false // 图片加载完成
  144. // 更新缩放率
  145. updateZoomAndPaperInfo()
  146. // 更新画布尺寸
  147. updateCanvasSize()
  148. // 计算中心位置使图片居中 初始加载图片是居中
  149. if (isInit.value) {
  150. centerCanvas()
  151. }
  152. // 加载图片
  153. loadImage()
  154. }
  155. } else {
  156. // 没有值 默认图片的宽高
  157. image.value.onload = () => {
  158. paperImgInfo.width = image.value.width // 获取宽
  159. paperImgInfo.height = image.value.height // 获取高
  160. loading.value = false // 图片加载完成
  161. // 更新缩放率
  162. updateZoomAndPaperInfo()
  163. // 更新画布尺寸
  164. updateCanvasSize()
  165. // 计算中心位置使图片居中
  166. if (isInit.value) {
  167. centerCanvas()
  168. }
  169. // 加载图片
  170. loadImage()
  171. }
  172. }
  173. }
  174. // 切换试卷图片
  175. const chanagePaperImage = () => {
  176. // 更新缩放率
  177. updateZoomAndPaperInfo()
  178. // 更新画布尺寸
  179. updateCanvasSize()
  180. // 加载图片
  181. loadImage()
  182. }
  183. // 适合屏幕
  184. const fitScreen = () => {
  185. scale.value = 1
  186. isInit.value = true
  187. initData() // 初始化屏幕加载
  188. }
  189. // 加载图片
  190. const loadImage = () => {
  191. drawImage() // 绘制边框数据
  192. }
  193. // 图片宽高坐标变化
  194. const ImageInfoChange = () => {
  195. const { width, height } = paperImgInfo
  196. const imgDom = document.getElementById('imgContainer')
  197. if (imgDom) {
  198. imgDom.style.width = width * zoomRate.value * scale.value + 'px'
  199. imgDom.style.height = height * zoomRate.value * scale.value + 'px'
  200. imgDom.style.left = position.x + 'px'
  201. imgDom.style.top = position.y + 'px'
  202. }
  203. }
  204. // 图片加载完成后绘制图片
  205. const drawImage = () => {
  206. const canvas = paperCanvasRef.value
  207. if (!canvas) return
  208. const ctx = canvas.getContext('2d')
  209. if (!ctx) return
  210. ctx.clearRect(0, 0, canvasInfo.width, canvasInfo.height) // 清除边框数据
  211. ImageInfoChange() // 用image图片替代背景
  212. for (let i = 0; i < props.drawData.length; i++) {
  213. let point = {
  214. x: props.drawData[i].x * zoomRate.value * scale.value,
  215. y: props.drawData[i].y * zoomRate.value * scale.value,
  216. width: props.drawData[i].w * zoomRate.value * scale.value,
  217. height: props.drawData[i].h * zoomRate.value * scale.value,
  218. blockName: props.drawData[i].blockName,
  219. page: props.drawData[i].page,
  220. blockArea: props.drawData[i].blockArea,
  221. }
  222. // 外边框数据 mm单位 比如系统卡 使用
  223. if (props.drawData[i].usedCardType == 1) {
  224. // 系统卡
  225. const imageInfo = paperImgInfo
  226. let offsexPoint = {
  227. x: props.drawData[i].x - 30,
  228. y: props.drawData[i].y - 25,
  229. w: props.drawData[i].w,
  230. h: props.drawData[i].h,
  231. } // 相对与第一个黑块的坐标
  232. let templateInfo = {
  233. width: 794 - 30 * 2,
  234. height: 1123 - 25 * 2,
  235. } // 模板去掉边框的尺寸
  236. // 如果长大于宽 就是A3 否则就是A4
  237. if (paperImgInfo.width > paperImgInfo.height) {
  238. // A3 1588*1123
  239. offsexPoint = {
  240. x: props.drawData[i].x - 30,
  241. y: props.drawData[i].y - 25,
  242. w: props.drawData[i].w,
  243. h: props.drawData[i].h,
  244. }
  245. templateInfo = {
  246. width: 1588 - 30 * 2,
  247. height: 1123 - 25 * 2,
  248. }
  249. } else {
  250. // A4 794*1123
  251. offsexPoint = {
  252. x: props.drawData[i].x - 30,
  253. y: props.drawData[i].y - 25,
  254. w: props.drawData[i].w,
  255. h: props.drawData[i].h,
  256. }
  257. templateInfo = {
  258. width: 794 - 30 * 2,
  259. height: 1123 - 25 * 2,
  260. }
  261. }
  262. // 系统卡 需要根据模板的坐标进行转换
  263. const x = parseFloat(
  264. ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
  265. )
  266. const y = parseFloat(
  267. ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
  268. )
  269. const w = parseFloat(
  270. ((props.drawData[i].w / templateInfo.width) * imageInfo.width).toFixed(2)
  271. )
  272. const h = parseFloat(
  273. ((props.drawData[i].h / templateInfo.height) * imageInfo.height).toFixed(2)
  274. )
  275. point = {
  276. x: x * zoomRate.value * scale.value,
  277. y: y * zoomRate.value * scale.value,
  278. width: w * zoomRate.value * scale.value,
  279. height: h * zoomRate.value * scale.value,
  280. blockName: props.drawData[i].blockName,
  281. page: props.drawData[i].page || 1,
  282. blockArea: props.drawData[i].blockArea,
  283. }
  284. }
  285. // 当前表格选择的边框换蓝色显示
  286. if (props.currentId == props.drawData[i].id) {
  287. ctx.strokeStyle = 'blue'
  288. ctx.fillStyle = 'blue'
  289. ctx.lineWidth = 2
  290. } else {
  291. ctx.strokeStyle = 'red'
  292. ctx.fillStyle = 'red'
  293. ctx.lineWidth = 2
  294. }
  295. const topiclist = props.drawData[i].topiclist || []
  296. if (topiclist.length > 0) {
  297. const topicName = String(topiclist?.[0]?.topicName)
  298. // 客观题有题目则不显示外边框
  299. if (topicName.includes('选做区')) {
  300. ctx.strokeRect(point.x, point.y, point.width, point.height) // 绘制边框
  301. }
  302. if (topiclist[0].index < 0) {
  303. // 系统卡选做题显示边框
  304. ctx.strokeRect(point.x, point.y, point.width, point.height) // 绘制边框
  305. }
  306. } else {
  307. ctx.lineWidth = 2
  308. ctx.strokeRect(point.x, point.y, point.width, point.height) // 绘制边框
  309. }
  310. // 客观题小题加载
  311. if (props.drawData[i].topiclist) {
  312. const topiclist = props.drawData[i].topiclist
  313. let derection = 1 // 排列方向 1 横向 2竖向
  314. if (props.drawData[i].areaOption) {
  315. try {
  316. const option = JSON.parse(props.drawData[i].areaOption)
  317. derection = option.derection
  318. } catch (e) {
  319. console.error('解析 areaOption 失败', e)
  320. }
  321. }
  322. if (isShowDraw.value) {
  323. topiclist.forEach((topicItem) => {
  324. ctx.font = '15px Arial'
  325. ctx.fillStyle = 'red'
  326. ctx.textAlign = 'left'
  327. if (String(topicItem.topicName).includes('选做区')) {
  328. // 选做题不显示题目文字
  329. } else {
  330. let namePoint = {
  331. x: topicItem.answerList[0].x,
  332. y: topicItem.answerList[0].y,
  333. }
  334. // 绘制文字 系统卡
  335. if (props.drawData[i].usedCardType == 1) {
  336. const imageInfo = paperImgInfo
  337. let offsexPoint = {
  338. x: topicItem.answerList[0].x - 30,
  339. y: topicItem.answerList[0].y - 25,
  340. }
  341. let templateInfo = {
  342. width: 794 - 30 * 2,
  343. height: 1123 - 25 * 2,
  344. }
  345. if (paperImgInfo.width > paperImgInfo.height) {
  346. // A3
  347. offsexPoint = {
  348. x: topicItem.answerList[0].x - 30,
  349. y: topicItem.answerList[0].y - 25,
  350. }
  351. templateInfo = {
  352. width: 1588 - 30 * 2,
  353. height: 1123 - 25 * 2,
  354. }
  355. } else {
  356. // A4
  357. offsexPoint = {
  358. x: topicItem.answerList[0].x - 30,
  359. y: topicItem.answerList[0].y - 25,
  360. }
  361. templateInfo = {
  362. width: 794 - 30 * 2,
  363. height: 1123 - 25 * 2,
  364. }
  365. }
  366. namePoint.x = parseFloat(
  367. ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
  368. )
  369. namePoint.y = parseFloat(
  370. ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
  371. )
  372. }
  373. if (props.isAbnormal) {
  374. if (topicItem.hasAbnormal) {
  375. ctx.fillStyle = 'red'
  376. } else {
  377. ctx.fillStyle = 'blue'
  378. }
  379. } else {
  380. ctx.fillStyle = 'red'
  381. if (props.currentId == props.drawData[i].id) {
  382. ctx.fillStyle = 'blue'
  383. } else {
  384. ctx.fillStyle = 'red'
  385. }
  386. }
  387. if (derection == 1 || derection == 3) {
  388. // 横向 或者横纵向
  389. ctx.fillText(
  390. topicItem.topicName,
  391. parseFloat(namePoint.x - 30) * zoomRate.value * scale.value,
  392. parseFloat(namePoint.y + 15) * zoomRate.value * scale.value
  393. )
  394. } else {
  395. // 竖向
  396. ctx.fillText(
  397. topicItem.topicName,
  398. parseFloat(namePoint.x + 1) * zoomRate.value * scale.value,
  399. parseFloat(namePoint.y - 22) * zoomRate.value * scale.value
  400. )
  401. }
  402. }
  403. topicItem.answerList.forEach((answerItem) => {
  404. let obj = {
  405. x: answerItem.x * zoomRate.value * scale.value,
  406. y: answerItem.y * zoomRate.value * scale.value,
  407. width: answerItem.w * zoomRate.value * scale.value,
  408. height: answerItem.h * zoomRate.value * scale.value,
  409. }
  410. // 系统卡
  411. if (props.drawData[i].usedCardType == 1) {
  412. const imageInfo = paperImgInfo
  413. let offsexPoint = {
  414. x: answerItem.x - 30,
  415. y: answerItem.y - 25,
  416. w: answerItem.w,
  417. h: answerItem.h,
  418. }
  419. let templateInfo = {
  420. width: 794 - 30 * 2,
  421. height: 1123 - 25 * 2,
  422. }
  423. if (paperImgInfo.width > paperImgInfo.height) {
  424. // A3
  425. offsexPoint = {
  426. x: answerItem.x - 30,
  427. y: answerItem.y - 25,
  428. w: answerItem.w,
  429. h: answerItem.h,
  430. }
  431. templateInfo = {
  432. width: 1588 - 30 * 2,
  433. height: 1123 - 25 * 2,
  434. }
  435. } else {
  436. // A4
  437. offsexPoint = {
  438. x: answerItem.x - 30,
  439. y: answerItem.y - 25,
  440. w: answerItem.w,
  441. h: answerItem.h,
  442. }
  443. templateInfo = {
  444. width: 794 - 30 * 2,
  445. height: 1123 - 25 * 2,
  446. }
  447. }
  448. const x = parseFloat(
  449. ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
  450. )
  451. const y = parseFloat(
  452. ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
  453. )
  454. const w = parseFloat(
  455. ((answerItem.w / templateInfo.width) * imageInfo.width).toFixed(2)
  456. )
  457. const h = parseFloat(
  458. ((answerItem.h / templateInfo.height) * imageInfo.height).toFixed(2)
  459. )
  460. obj = {
  461. x: x * zoomRate.value * scale.value,
  462. y: y * zoomRate.value * scale.value,
  463. width: w * zoomRate.value * scale.value,
  464. height: h * zoomRate.value * scale.value,
  465. }
  466. }
  467. ctx.lineWidth = 1
  468. // 选中 绘制带背景阴影的边框
  469. if (topicItem.hasAbnormal) {
  470. // 有异常的
  471. if (answerItem.isCheck) {
  472. ctx.fillStyle = 'rgba(255,0,0,0.3)' // 红色背景
  473. ctx.strokeStyle = 'red' // 红色边框
  474. ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
  475. ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
  476. } else {
  477. ctx.fillStyle = 'red' // 半透明红色背景
  478. ctx.strokeStyle = 'red' // 边框的颜色
  479. ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
  480. }
  481. } else {
  482. if (props.isAbnormal) {
  483. ctx.fillStyle = 'rgba(0,0,255,0.3)' // 半透明蓝色背景
  484. ctx.strokeStyle = 'blue' // 蓝色边框
  485. } else {
  486. ctx.fillStyle = 'rgba(255,0,0,0.3)' // 红色背景
  487. ctx.strokeStyle = 'red' // 红色边框
  488. if (props.currentId == props.drawData[i].id) {
  489. ctx.strokeStyle = 'blue'
  490. } else {
  491. ctx.strokeStyle = 'red'
  492. }
  493. }
  494. // 选中
  495. if (answerItem?.isCheck) {
  496. ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
  497. ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
  498. } else {
  499. ctx.strokeRect(obj.x, obj.y, obj.width, obj.height)
  500. }
  501. }
  502. })
  503. })
  504. }
  505. }
  506. // 主观题划分区加载
  507. if (props.drawData[i].scoreList) {
  508. const scoreList = props.drawData[i].scoreList
  509. scoreList.forEach((scoreItem) => {
  510. ctx.font = '15px Arial'
  511. ctx.fillStyle = 'red'
  512. ctx.textAlign = 'left'
  513. let obj = {
  514. x: scoreItem.x * zoomRate.value * scale.value,
  515. y: scoreItem.y * zoomRate.value * scale.value,
  516. width: scoreItem.w * zoomRate.value * scale.value,
  517. height: scoreItem.h * zoomRate.value * scale.value,
  518. }
  519. // 系统卡
  520. if (props.drawData[i].usedCardType == 1) {
  521. const imageInfo = paperImgInfo
  522. let offsexPoint = {
  523. x: scoreItem.x - 30,
  524. y: scoreItem.y - 25,
  525. w: scoreItem.w,
  526. h: scoreItem.h,
  527. }
  528. let templateInfo = {
  529. width: 794 - 30 * 2,
  530. height: 1123 - 25 * 2,
  531. }
  532. if (paperImgInfo.width > paperImgInfo.height) {
  533. // A3
  534. offsexPoint = {
  535. x: scoreItem.x - 30,
  536. y: scoreItem.y - 25,
  537. w: scoreItem.w,
  538. h: scoreItem.h,
  539. }
  540. templateInfo = {
  541. width: 1588 - 30 * 2,
  542. height: 1123 - 25 * 2,
  543. }
  544. } else {
  545. // A4
  546. offsexPoint = {
  547. x: scoreItem.x - 30,
  548. y: scoreItem.y - 25,
  549. w: scoreItem.w,
  550. h: scoreItem.h,
  551. }
  552. templateInfo = {
  553. width: 794 - 30 * 2,
  554. height: 1123 - 25 * 2,
  555. }
  556. }
  557. const x = parseFloat(
  558. ((offsexPoint.x / templateInfo.width) * imageInfo.width).toFixed(2)
  559. )
  560. const y = parseFloat(
  561. ((offsexPoint.y / templateInfo.height) * imageInfo.height).toFixed(2)
  562. )
  563. const w = parseFloat(
  564. ((scoreItem.w / templateInfo.width) * imageInfo.width).toFixed(2)
  565. )
  566. const h = parseFloat(
  567. ((scoreItem.h / templateInfo.height) * imageInfo.height).toFixed(2)
  568. )
  569. obj = {
  570. x: x * zoomRate.value * scale.value,
  571. y: y * zoomRate.value * scale.value,
  572. width: w * zoomRate.value * scale.value,
  573. height: h * zoomRate.value * scale.value,
  574. }
  575. }
  576. ctx.lineWidth = 1
  577. ctx.fillStyle = scoreItem.color + '50' // 红色背景
  578. ctx.strokeStyle = scoreItem.color // 红色边框
  579. if (scoreItem.isCheck) {
  580. ctx.fillRect(obj.x, obj.y, obj.width, obj.height)
  581. }
  582. ctx.strokeRect(obj.x, obj.y, obj.width, obj.height) // 绘制答案边框
  583. })
  584. }
  585. ctx.fillStyle = 'rgba(255,0,0,1)' // 半透明红色背景
  586. ctx.font = '15px Arial'
  587. ctx.textAlign = 'left'
  588. // 绘制文字 定位去和客观题组不用显示
  589. if (point.blockArea != 2 && point.blockArea != 8) {
  590. ctx.fillText(point.blockName, point.x, point.y - 5)
  591. }
  592. }
  593. ctx.restore()
  594. }
  595. // 更新缩放率
  596. const updateZoomAndPaperInfo = () => {
  597. const widthZoomRate = containerWidth.value / paperImgInfo.width
  598. const heightZoomRate = containerHeight.value / paperImgInfo.height
  599. // 选择较小的缩放率作为基准,确保图像完整显示在容器内
  600. zoomRate.value = Math.min(widthZoomRate, heightZoomRate)
  601. }
  602. // 更新画布尺寸
  603. const updateCanvasSize = () => {
  604. canvasInfo.width = Math.round(paperImgInfo.width * zoomRate.value * scale.value)
  605. canvasInfo.height = Math.round(paperImgInfo.height * zoomRate.value * scale.value)
  606. if (paperCanvasRef.value) {
  607. paperCanvasRef.value.width = canvasInfo.width
  608. paperCanvasRef.value.height = canvasInfo.height
  609. }
  610. }
  611. // 中心化画布
  612. const centerCanvas = () => {
  613. position.x = (containerWidth.value - canvasInfo.width) / 2
  614. position.y = (containerHeight.value - canvasInfo.height) / 2
  615. if (paperCanvasRef.value) {
  616. paperCanvasRef.value.style.left = `${position.x}px`
  617. paperCanvasRef.value.style.top = `${position.y}px`
  618. }
  619. isInit.value = false // 后面不在执行居中操作
  620. }
  621. // 全局鼠标释放事件处理
  622. const onGlobalMouseUp = () => {
  623. isDragging.value = false
  624. }
  625. // 鼠标按下事件
  626. const onMouseDown = (event) => {
  627. // 只响应左键点击(button值为0表示左键)
  628. if (event.button !== 0) {
  629. isDragging.value = false
  630. return
  631. }
  632. // 确保在开始新的拖拽前,先重置拖拽状态
  633. isDragging.value = false
  634. if (drawType.value == 0) {
  635. if (props.isDrag) {
  636. isDragging.value = true
  637. startX.value = event.clientX - position.x
  638. startY.value = event.clientY - position.y
  639. }
  640. }
  641. if (drawType.value == 1) {
  642. if (event.target.id != 'paperCanvas') {
  643. isDragging.value = true
  644. startX.value = event.clientX - position.x
  645. startY.value = event.clientY - position.y
  646. }
  647. }
  648. }
  649. // 鼠标移动事件
  650. const onMouseMove = (event) => {
  651. if (isDragging.value) {
  652. // 在拖拽模式下,移动画布
  653. position.x = event.clientX - startX.value
  654. position.y = event.clientY - startY.value
  655. if (paperCanvasRef.value) {
  656. paperCanvasRef.value.style.left = `${position.x}px`
  657. paperCanvasRef.value.style.top = `${position.y}px`
  658. }
  659. ImageInfoChange()
  660. }
  661. }
  662. // 鼠标抬起事件
  663. const onMouseUp = (event) => {
  664. // 只响应左键释放
  665. if (event && event.button !== 0) {
  666. return
  667. }
  668. isDragging.value = false // 停止拖拽
  669. }
  670. // 鼠标滚轮事件
  671. const onWheel = (event) => {
  672. event.preventDefault()
  673. // 计算新的缩放比例
  674. const delta = event.deltaY < 0 ? 1 : -1
  675. const newScale = scale.value + delta * 0.1
  676. const clampedScale = Math.max(minScale, Math.min(maxScale, newScale))
  677. // 如果缩放值没有变化,则直接返回
  678. if (clampedScale === scale.value) return
  679. // 获取鼠标在容器中的位置
  680. const containerRect = mainContainerRef.value.getBoundingClientRect()
  681. const mouseX = event.clientX - containerRect.left
  682. const mouseY = event.clientY - containerRect.top
  683. // 计算鼠标相对于当前画布的位置
  684. const mouseRelativeToCanvasX = (mouseX - position.x) / scale.value
  685. const mouseRelativeToCanvasY = (mouseY - position.y) / scale.value
  686. // 更新缩放比例
  687. scale.value = clampedScale
  688. // 更新画布尺寸
  689. updateCanvasSize()
  690. // 重新计算画布位置,使缩放围绕鼠标点进行
  691. position.x = mouseX - mouseRelativeToCanvasX * scale.value
  692. position.y = mouseY - mouseRelativeToCanvasY * scale.value
  693. // 应用新的位置
  694. if (paperCanvasRef.value) {
  695. paperCanvasRef.value.style.left = `${position.x}px`
  696. paperCanvasRef.value.style.top = `${position.y}px`
  697. }
  698. // 更新图片位置信息
  699. ImageInfoChange()
  700. // 重新绘制内容
  701. drawImage()
  702. }
  703. // 鼠标复位
  704. const MouseReset = () => {
  705. drawType.value = 0
  706. const canvas = paperCanvasRef.value
  707. if (canvas) {
  708. canvas.style.cursor = 'pointer'
  709. canvas.onmousedown = null
  710. canvas.onmousemove = null
  711. canvas.onmouseup = null
  712. }
  713. }
  714. // 框选答案区域
  715. const SelectionBox = () => {
  716. drawType.value = 1
  717. const canvas = paperCanvasRef.value
  718. if (canvas) {
  719. canvas.style.cursor = 'crosshair'
  720. canvas.onmousedown = onCanvasDown
  721. canvas.onmousemove = onCanvasMove
  722. canvas.onmouseup = onCanvasUp
  723. }
  724. }
  725. // 画圈模式
  726. const PaintingCircle = () => {
  727. drawType.value = 1
  728. const canvas = paperCanvasRef.value
  729. if (canvas) {
  730. canvas.style.cursor = 'crosshair'
  731. canvas.onmousedown = onCanvasDown
  732. canvas.onmousemove = onCanvasMove
  733. canvas.onmouseup = onCanvasUp
  734. }
  735. }
  736. // 开始画答案区域圈
  737. const StartPaintingCircle = (obj) => {
  738. Object.assign(addObjectAreaOption, obj)
  739. drawType.value = 1
  740. const canvas = paperCanvasRef.value
  741. if (canvas) {
  742. canvas.style.cursor = 'crosshair'
  743. canvas.onmousedown = onCanvasDown
  744. canvas.onmousemove = onCanvasMove
  745. canvas.onmouseup = onCanvasUp
  746. }
  747. }
  748. // canvas上按下事件
  749. const onCanvasDown = (e) => {
  750. const canvas = paperCanvasRef.value
  751. if (canvas) canvas.style.cursor = 'crosshair'
  752. isDrawing.value = true
  753. isDragging.value = false // 禁止拖拽
  754. rectPoint.startX = e.offsetX
  755. rectPoint.startY = e.offsetY
  756. rectPoint.endX = e.offsetX
  757. rectPoint.endY = e.offsetY
  758. }
  759. // canvas上移动事件
  760. const onCanvasMove = (e) => {
  761. if (isDrawing.value) {
  762. const canvas = paperCanvasRef.value
  763. if (!canvas) return
  764. const ctx = canvas.getContext('2d')
  765. if (!ctx) return
  766. // 更新当前鼠标位置
  767. rectPoint.endX = e.offsetX
  768. rectPoint.endY = e.offsetY
  769. // 清除之前的矩形
  770. ctx.clearRect(0, 0, canvas.width, canvas.height)
  771. // 重新绘制之前的边框数据
  772. drawImage()
  773. // 设置矩形样式
  774. ctx.strokeStyle = 'blue'
  775. ctx.lineWidth = 1 // 画框的线的粗细
  776. // 计算矩形的起点和宽高,处理反向框选
  777. const startX = rectPoint.startX
  778. const startY = rectPoint.startY
  779. const width = e.offsetX - startX
  780. const height = e.offsetY - startY
  781. // 绘制矩形(可以处理负宽度和高度)
  782. ctx.strokeRect(startX, startY, width, height)
  783. }
  784. }
  785. // canvas抬起事件
  786. const onCanvasUp = () => {
  787. isDrawing.value = false
  788. // 处理反向框选,确保宽度和高度为正数
  789. let startX = rectPoint.startX
  790. let startY = rectPoint.startY
  791. let endX = rectPoint.endX
  792. let endY = rectPoint.endY
  793. // 确保起点坐标是左上角,终点坐标是右下角
  794. let actualStartX = Math.min(startX, endX)
  795. let actualStartY = Math.min(startY, endY)
  796. let actualEndX = Math.max(startX, endX)
  797. let actualEndY = Math.max(startY, endY)
  798. // 计算实际的宽度和高度(确保为正数)
  799. let width = Math.abs(actualEndX - actualStartX)
  800. let height = Math.abs(actualEndY - actualStartY)
  801. const point = {
  802. x: floatNum(actualStartX / zoomRate.value / scale.value),
  803. y: floatNum(actualStartY / zoomRate.value / scale.value),
  804. w: floatNum(width / zoomRate.value / scale.value),
  805. h: floatNum(height / zoomRate.value / scale.value),
  806. unit: 'px',
  807. }
  808. Object.assign(currenPoint, point)
  809. // 只有当宽度和高度都大于0时才触发事件
  810. if (point.w > 0 && point.h > 0) {
  811. emit('GetRectPoint', point)
  812. }
  813. }
  814. // 鼠标离开画布事件
  815. const onCanvasLeave = () => {
  816. // 如果正在绘制,自动结束绘制
  817. if (isDrawing.value) {
  818. onCanvasUp()
  819. }
  820. }
  821. // 监听窗口大小变化,重新计算表格高度 使用节流防止频繁改变窗口大小导致计算量过大而页面卡顿
  822. const handleResize = throttle(() => {
  823. // 如果需要重新计算,可以在这里 uncomment
  824. // if (mainContainerRef.value) {
  825. // const { width, height } = mainContainerRef.value.getBoundingClientRect()
  826. // containerWidth.value = width
  827. // containerHeight.value = height
  828. // updateZoomAndPaperInfo()
  829. // updateCanvasSize()
  830. // centerCanvas()
  831. // loadImage()
  832. // }
  833. }, 500)
  834. // Watchers
  835. watch(
  836. () => props.paperImgUrl,
  837. (newVal) => {
  838. loading.value = true // 加载图片
  839. initData() // 初始化数据
  840. setTimeout(() => {
  841. if (!props.paperImgUrl) {
  842. loading.value = false // 加载图片完成
  843. }
  844. }, 100)
  845. },
  846. { deep: true, immediate: true }
  847. )
  848. watch(
  849. () => props.drawData,
  850. () => {
  851. drawImage() // 更新边框数据并重新绘制
  852. },
  853. { deep: true }
  854. )
  855. watch(
  856. () => props.currentId,
  857. () => {
  858. drawImage() // 更新边框数据并重新绘制
  859. }
  860. )
  861. // Lifecycle Hooks
  862. onMounted(() => {
  863. window.addEventListener('resize', handleResize)
  864. // 添加全局鼠标释放事件监听器,处理异常情况
  865. window.addEventListener('mouseup', onGlobalMouseUp)
  866. // 初始数据处理加载 如定位 居中 等 第一次居中
  867. initData()
  868. })
  869. onBeforeUnmount(() => {
  870. // 移除监听 防止内存泄漏
  871. window.removeEventListener('resize', handleResize)
  872. window.removeEventListener('mouseup', onGlobalMouseUp)
  873. })
  874. // 暴露方法给父组件(如果需要)
  875. defineExpose({
  876. ShowDrawData,
  877. fitScreen,
  878. MouseReset,
  879. SelectionBox,
  880. PaintingCircle,
  881. StartPaintingCircle,
  882. })
  883. </script>
  884. <style lang="scss" scoped>
  885. .paper_canvas {
  886. position: absolute;
  887. cursor: pointer;
  888. background-color: transparent;
  889. z-index: 10; // 使canvas在图片上方
  890. // border:1px solid green;
  891. }
  892. .img_container {
  893. position: absolute;
  894. cursor: pointer;
  895. z-index: 9;
  896. // border:1px solid red;
  897. img {
  898. width: 100%;
  899. height: 100%;
  900. }
  901. }
  902. .main_container {
  903. position: relative;
  904. overflow: hidden;
  905. }
  906. </style>