SketchRuler.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. <template>
  2. <div
  3. id="wrapper"
  4. class="wrapper vue-ruler-wrapper"
  5. :style="{
  6. width: width + thick + 'px',
  7. height: height + thick + 'px'
  8. }"
  9. >
  10. <i
  11. :class="{
  12. 'iconfont-bigscreen': true,
  13. 'icon-eye': isShowReferLine,
  14. 'icon-eye-close': !isShowReferLine
  15. }"
  16. @click="handleCornerClick"
  17. />
  18. <SketchRule
  19. :key="scale"
  20. :lang="lang"
  21. :thick="thick"
  22. :scale="scale"
  23. :width="width"
  24. :height="height"
  25. :start-x="startX"
  26. :start-y="startY"
  27. :shadow="shadow"
  28. :hor-line-arr="[...lines.h, ...presetLines.h]"
  29. :ver-line-arr="[...lines.v, ...presetLines.v]"
  30. :palette="Palette"
  31. :is-show-refer-line="isShowReferLine"
  32. :corner-active="cornerActive"
  33. @handleLine="handleLine"
  34. @onCornerClick="handleCornerClick"
  35. />
  36. <div
  37. id="screens"
  38. ref="screensRef"
  39. :style="{
  40. width: innerWidth + 'px',
  41. height: innerHeight + 'px'
  42. }"
  43. @scroll="throttleScroll"
  44. >
  45. <div
  46. id="screen-container"
  47. ref="containerRef"
  48. class="screen-container grid-bg"
  49. :style="containerRefStyle"
  50. >
  51. <div
  52. id="canvas"
  53. :style="canvasStyle"
  54. >
  55. <slot />
  56. </div>
  57. </div>
  58. <div
  59. id="minimap"
  60. class="minimap"
  61. >
  62. <div class="mapHeader" id="mapHeader">
  63. <div>
  64. <span>小地图</span>
  65. </div>
  66. <div class="showMap" @click="showMinimap">
  67. <i class="el-icon-arrow-down" style="width:20px;height:20px;color:#fff;" v-if="!mapShow"/>
  68. <i class="el-icon-arrow-up" style="width:20px;height:20px;color:#fff;" v-if="mapShow"/>
  69. </div>
  70. </div>
  71. <div
  72. id="selectWin"
  73. class="selectWin"
  74. v-show="mapShow"
  75. >
  76. <div
  77. id="selectionWin"
  78. class="selectionWin"
  79. />
  80. </div>
  81. <div class="miniView" />
  82. </div>
  83. </div>
  84. </div>
  85. </template>
  86. <script>
  87. import SketchRule from 'vue-sketch-ruler'
  88. import { mapState, mapMutations } from 'vuex'
  89. import throttle from 'lodash/throttle'
  90. export default {
  91. components: {
  92. SketchRule
  93. },
  94. props: {
  95. width: {
  96. type: Number,
  97. default: 500
  98. },
  99. height: {
  100. type: Number,
  101. default: 400
  102. },
  103. pageWidth: {
  104. type: Number,
  105. default: 1920
  106. },
  107. pageHeight: {
  108. type: Number,
  109. default: 1080
  110. }
  111. },
  112. data () {
  113. return {
  114. mapShow: true, // 小地图显示与否
  115. canvasLeft: 0, // 存储画布到视口的left距离
  116. canvasTop: 0, // 存储画布到视口的top距离
  117. isDrag: false, // 小地图白块是否拖拽
  118. startX: 0,
  119. startY: 0,
  120. lines: {
  121. h: [],
  122. v: []
  123. },
  124. thick: 20,
  125. lang: 'zh-CN', // 中英文
  126. isShowRuler: true, // 显示标尺
  127. isShowReferLine: true, // 显示参考线
  128. cornerActive: true, // 左上角激活状态
  129. Palette: {
  130. bgColor: 'rgba(225,225,225, 0)',
  131. longfgColor: '#BABBBC',
  132. shortfgColor: '#C8CDD0',
  133. fontColor: '#7D8694',
  134. shadowColor: 'transparent',
  135. lineColor: '#0089d0',
  136. borderColor: '#transparent',
  137. cornerActiveColor: 'rgb(235, 86, 72, 0.6)'
  138. },
  139. containerRefStyle: {
  140. width: this.width + 'px',
  141. height: this.height + 'px'
  142. },
  143. innerHeight: 0,
  144. innerWidth: 0
  145. }
  146. },
  147. watch: {
  148. // 缩放改变的时候,改变startX,startY
  149. scale (scale) {
  150. // 防抖调用方法
  151. this.throttleScroll()
  152. },
  153. pageWidth (pageWidth) {
  154. if (this.fitZoom === this.zoom) {
  155. this.initZoom()
  156. }
  157. },
  158. pageHeight (pageHeight) {
  159. if (this.fitZoom === this.zoom) {
  160. this.initZoom()
  161. }
  162. },
  163. mapShow (value) {
  164. const mapElement = document.getElementById('minimap')
  165. // const selectElement = document.getElementById('selectWin')
  166. if (!value) {
  167. mapElement.style.bottom = parseFloat(window.getComputedStyle(mapElement).bottom) + 150 + 'px'
  168. } else {
  169. mapElement.style.bottom = parseFloat(window.getComputedStyle(mapElement).bottom) - 150 + 'px'
  170. }
  171. }
  172. },
  173. computed: {
  174. ...mapState('bigScreen', {
  175. scale: state => state.zoom / 100,
  176. fitZoom: state => state.fitZoom,
  177. zoom: state => state.zoom
  178. }),
  179. presetLines () {
  180. const presetLine = this.$store.state.bigScreen.presetLine
  181. // { type: 'h', site: y || 0 },
  182. const v = presetLine?.filter(p => p.type === 'h')?.map(p => p.site)
  183. const h = presetLine?.filter(p => p.type === 'v')?.map(p => p.site)
  184. return {
  185. h,
  186. v
  187. }
  188. },
  189. shadow () {
  190. return {
  191. x: 0,
  192. y: 0,
  193. width: this.width,
  194. height: this.height
  195. }
  196. },
  197. canvasStyle () {
  198. return {
  199. width: this.width + 'px',
  200. height: this.height + 'px',
  201. transform: `scale(${this.scale})`,
  202. transformOrigin: '0 0 0'
  203. }
  204. }
  205. },
  206. mounted () {
  207. // 初始化canvasLeft canvasTop
  208. const canvasRect = document.querySelector('#canvas').getBoundingClientRect()
  209. this.canvasLeft = canvasRect.left
  210. this.canvasTop = canvasRect.top
  211. // 监听屏幕改变
  212. this.listenSize()
  213. this.initRuleHeight()
  214. this.throttleScroll()
  215. this.throttleDrag()
  216. },
  217. methods: {
  218. ...mapMutations('bigScreen', [
  219. 'changeZoom',
  220. 'changeFitZoom'
  221. ]),
  222. throttleDrag () {
  223. throttle(() => {
  224. this.dragSelection()
  225. this.viewMapDrag()
  226. }, 100)()
  227. },
  228. // 绑定滑块拖拽效果
  229. dragSelection () {
  230. const that = this
  231. const draggableElement = document.getElementById('selectionWin')
  232. const dragContainer = document.getElementById('selectWin')
  233. const screenElement = document.getElementById('screens')
  234. const screenContainer = document.getElementById('screen-container')
  235. const maxContainer = document.querySelector('.bs-page-design-wrap')
  236. // 鼠标按下的位置
  237. let posX, posY
  238. // 白色拖拽块相对于父盒子初始位置
  239. let initialX, initialY
  240. // 滚动条初始位置
  241. let scrollTop, scrollLeft
  242. draggableElement.addEventListener('mousedown', function (event) {
  243. that.isDrag = true
  244. posX = event.clientX
  245. posY = event.clientY
  246. initialX = draggableElement.getBoundingClientRect().left - dragContainer.getBoundingClientRect().left
  247. initialY = draggableElement.getBoundingClientRect().top - dragContainer.getBoundingClientRect().top
  248. scrollLeft = screenElement.scrollLeft
  249. scrollTop = screenElement.scrollTop
  250. maxContainer.addEventListener('mousemove', function (event) {
  251. if (that.isDrag) {
  252. event.preventDefault()
  253. // 鼠标移动距离
  254. let moveX = event.clientX - posX
  255. let moveY = event.clientY - posY
  256. // 避免白色拖拽移出边框
  257. if (moveX < -initialX) {
  258. moveX = -initialX
  259. } else if (moveX > dragContainer.getBoundingClientRect().width - draggableElement.getBoundingClientRect().width - initialX) {
  260. moveX = dragContainer.getBoundingClientRect().width - draggableElement.getBoundingClientRect().width - initialX
  261. }
  262. if (moveY < -initialY) {
  263. moveY = -initialY
  264. } else if (moveY > dragContainer.getBoundingClientRect().height - draggableElement.getBoundingClientRect().height - initialY) {
  265. moveY = dragContainer.getBoundingClientRect().height - draggableElement.getBoundingClientRect().height - initialY
  266. }
  267. const newX = moveX + initialX
  268. const newY = moveY + initialY
  269. // 移动拖拽白块
  270. draggableElement.style.left = newX + 'px'
  271. draggableElement.style.top = newY + 'px'
  272. // 移动比例
  273. const percentageX = moveX / (dragContainer.getBoundingClientRect().width - draggableElement.getBoundingClientRect().width)
  274. const percentageY = moveY / (dragContainer.getBoundingClientRect().height - draggableElement.getBoundingClientRect().height)
  275. // 进度条需要滚动的距离
  276. const scrollTopLength = percentageY * (screenContainer.getBoundingClientRect().height - screenElement.getBoundingClientRect().height)
  277. const scrollLeftLength = percentageX * (screenContainer.getBoundingClientRect().width - screenElement.getBoundingClientRect().width)
  278. screenElement.scrollLeft = scrollLeft + scrollLeftLength
  279. screenElement.scrollTop = scrollTop + scrollTopLength
  280. }
  281. })
  282. })
  283. maxContainer.addEventListener('mouseup', function (event) {
  284. that.isDrag = false
  285. })
  286. draggableElement.addEventListener('mouseup', function () {
  287. that.isDrag = false
  288. })
  289. // 禁止H5自带的拖拽事件
  290. draggableElement.ondragstart = function (ev) {
  291. ev.preventDefault()
  292. }
  293. draggableElement.ondragend = function (ev) {
  294. ev.preventDefault()
  295. }
  296. screenElement.ondragstart = function (ev) {
  297. ev.preventDefault()
  298. }
  299. screenElement.ondragend = function (ev) {
  300. ev.preventDefault()
  301. }
  302. },
  303. // 控制小地图显示与隐藏
  304. showMinimap () {
  305. this.mapShow = !this.mapShow
  306. },
  307. // 小地图拖拽
  308. viewMapDrag () {
  309. const mapElement = document.getElementById('minimap')
  310. const mapDragElement = document.getElementById('mapHeader')
  311. const pageElement = document.querySelector('.bs-page-design-wrap')
  312. let mapDrag = false
  313. let curX, curY
  314. let curBottom, curRight
  315. mapDragElement.addEventListener('mousedown', function (event) {
  316. mapDrag = true
  317. curX = event.clientX
  318. curY = event.clientY
  319. curBottom = window.getComputedStyle(mapElement).bottom
  320. curRight = window.getComputedStyle(mapElement).right
  321. pageElement.addEventListener('mousemove', function (event) {
  322. if (mapDrag) {
  323. event.preventDefault()
  324. const dragX = event.clientX - curX
  325. const dragY = event.clientY - curY
  326. mapElement.style.bottom = parseInt(curBottom) - dragY + 'px'
  327. mapElement.style.right = parseInt(curRight) - dragX + 'px'
  328. }
  329. })
  330. })
  331. pageElement.addEventListener('mouseup', function () {
  332. mapDrag = false
  333. })
  334. mapDragElement.addEventListener('mouseup', function () {
  335. mapDrag = false
  336. })
  337. },
  338. listenSize () {
  339. window.onresize = throttle(() => {
  340. this.initRuleHeight()
  341. }, 100)
  342. },
  343. initRuleHeight () {
  344. setTimeout(() => {
  345. const screensRect = document
  346. .querySelector('.grid-wrap-box')
  347. ?.getBoundingClientRect()
  348. if (!screensRect) {
  349. return
  350. }
  351. // 30是grid-wrap-box的底部工具栏高度
  352. this.innerHeight = screensRect.height
  353. this.innerWidth = screensRect.width
  354. this.diffX = this.width - screensRect.width
  355. this.diffY = this.height - screensRect.height
  356. this.containerRefStyle = {
  357. width: this.diffX > 0 ? ((this.width + this.diffX + this.thick + 30) + 'px') : (this.width + 'px'),
  358. height: this.diffY > 0 ? ((this.height + this.diffY + this.thick + 30) + 'px') : (this.height + 'px')
  359. }
  360. if (this.fitZoom === this.zoom) {
  361. this.initZoom()
  362. }
  363. })
  364. },
  365. handleLine (lines) {
  366. this.lines = lines
  367. },
  368. handleCornerClick () {
  369. this.isShowReferLine = !this.isShowReferLine
  370. this.cornerActive = !this.cornerActive
  371. },
  372. throttleScroll () {
  373. throttle(() => {
  374. this.handleScroll()
  375. }, 100)()
  376. },
  377. handleScroll () {
  378. const screensRect = document
  379. .querySelector('#screens')
  380. .getBoundingClientRect()
  381. const canvasRect = document
  382. .querySelector('#canvas')
  383. .getBoundingClientRect()
  384. const container = document.querySelector('#selectWin').getBoundingClientRect()
  385. const screenContainer = document.querySelector('#screen-container').getBoundingClientRect()
  386. const draggableElement = document.getElementById('selectionWin')
  387. // 标尺开始的刻度
  388. const startX = (screensRect.left + this.thick - canvasRect.left) / this.scale
  389. const startY = (screensRect.top + this.thick - canvasRect.top) / this.scale
  390. this.startX = startX >> 0
  391. this.startY = startY >> 0
  392. this.$emit('changeStart', {
  393. x: this.startX * this.scale + 50 - this.thick,
  394. y: this.startY * this.scale + 50 - this.thick
  395. })
  396. // 拖动进度条移动小地图
  397. if (!this.isDrag) {
  398. const leftDrag = canvasRect.left - this.canvasLeft
  399. const topDrag = canvasRect.top - this.canvasTop
  400. // 小方块需要移动的距离
  401. const leftLength = leftDrag / (screenContainer.width - screensRect.width - 9) * (container.width - draggableElement.getBoundingClientRect().width)
  402. const topLength = topDrag / (screenContainer.height - screensRect.height - 9) * (container.height - draggableElement.getBoundingClientRect().height)
  403. draggableElement.style.left = -leftLength + 'px'
  404. draggableElement.style.top = -topLength + 'px'
  405. }
  406. },
  407. // 保证画布能完整展示大屏
  408. initZoom () {
  409. // 横向比例
  410. const xRadio = this.innerWidth / (this.pageWidth + 120)
  411. // 纵向比例
  412. const yRadio = this.innerHeight / (this.pageHeight + 120)
  413. // 取最小的适应比例
  414. const scale = Math.floor(Math.min(xRadio * 100, yRadio * 100))
  415. if (scale < 100) {
  416. this.changeZoom(scale)
  417. this.changeFitZoom(scale)
  418. } else {
  419. this.changeZoom(100)
  420. this.changeFitZoom(100)
  421. }
  422. }
  423. }
  424. }
  425. </script>
  426. <style lang="scss" scoped>
  427. @import '../../BigScreenDesign/fonts/iconfont.css';
  428. .wrapper {
  429. box-sizing: border-box;
  430. position: absolute;
  431. .iconfont-bigscreen {
  432. position: absolute;
  433. left: 0;
  434. top: 0;
  435. font-size: 16px;
  436. color: #fff;
  437. z-index: 999;
  438. cursor: pointer;
  439. }
  440. }
  441. #screens {
  442. position: absolute;
  443. overflow: scroll;
  444. // 滚动条美化,始终在最下方和最右方
  445. &::-webkit-scrollbar {
  446. width: 10px;
  447. height: 10px;
  448. }
  449. &::-webkit-scrollbar-thumb {
  450. border-radius: 10px;
  451. background-color: var(--bs-el-background-2) !important;
  452. }
  453. &::-webkit-scrollbar-track {
  454. border-radius: 10px;
  455. background-color: transparent !important;
  456. }
  457. }
  458. .screen-container {
  459. position: absolute;
  460. width: 6000px;
  461. height: 6000px;
  462. }
  463. .minimap{
  464. position: fixed;
  465. bottom: 15px;
  466. right: 15px;
  467. border: 1px solid #f6f7fb;
  468. z-index:10000;
  469. /*cursor: move;*/
  470. }
  471. .minimap .mapHeader{
  472. background-color:#303640;
  473. padding: 0 10px;
  474. display: flex;
  475. justify-content: space-between;
  476. height: 20px;
  477. width: 150px;
  478. font-size: 12px;
  479. border-bottom: 1px solid #fff;
  480. color: #ffffff;
  481. cursor: pointer;
  482. span {
  483. user-select: none;
  484. }
  485. }
  486. .minimap .selectWin{
  487. background-color: #232832;
  488. height: 150px;
  489. width: 150px;
  490. position: relative;
  491. }
  492. .minimap .selectionWin{
  493. position: absolute;
  494. left: 0px;
  495. top: 0px;
  496. width: 30px;
  497. height: 30px;
  498. background-color: white;
  499. opacity: 0.5;
  500. cursor: move;
  501. }
  502. .scale-value {
  503. position: absolute;
  504. left: 0;
  505. bottom: 100%;
  506. }
  507. .button {
  508. position: absolute;
  509. left: 100px;
  510. bottom: 100%;
  511. }
  512. .button-ch {
  513. position: absolute;
  514. left: 200px;
  515. bottom: 100%;
  516. }
  517. .button-en {
  518. position: absolute;
  519. left: 230px;
  520. bottom: 100%;
  521. }
  522. #canvas {
  523. position: absolute;
  524. top: 50px;
  525. left: 50px;
  526. }
  527. ::v-deep .line {
  528. border-left: 1px dashed #0089d0 !important;
  529. border-top: 1px dashed #0089d0 !important;
  530. }
  531. ::v-deep.action {
  532. .value {
  533. background: var(--bs-el-color-primary);
  534. padding: 4px;
  535. color: #fff;
  536. }
  537. .del {
  538. color: var(--bs-el-color-primary);
  539. }
  540. }
  541. ::v-deep .ruler, ::v-deep .corner {
  542. background: var(--bs-background-1);
  543. }
  544. ::v-deep .corner {
  545. z-index: 999;
  546. background: var(--bs-background-1) !important;
  547. }
  548. ::v-deep .mb-ruler {
  549. z-index: 998
  550. }
  551. .grid-bg {
  552. background-color: #2a2e33 !important;
  553. background-image: url(./images/canvas-bg.png);
  554. background-repeat: repeat;
  555. word-spacing: 10px;
  556. }
  557. </style>