imagePreviewer.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. <template>
  2. <div class="image-previewer" :class="[isH5 && 'image-previewer-h5']">
  3. <div class="image-wrapper" ref="image">
  4. <ul
  5. class="image-list"
  6. :style="{
  7. width: `${imageList.length * 100}%`,
  8. transform: `translateX(-${
  9. (currentImageIndex * 100) / imageList.length
  10. }%)`
  11. }"
  12. ref="ul"
  13. >
  14. <li class="image-item" v-for="(item, index) in imageList" :key="index">
  15. <img
  16. class="image-preview"
  17. :style="{
  18. transform: `scale(${zoom}) rotate(${rotate}deg)`
  19. }"
  20. :src="item?.payload?.imageInfoArray[0]?.url"
  21. />
  22. </li>
  23. </ul>
  24. </div>
  25. <i class="icon icon-close" @click="close" v-show="!isH5" />
  26. <div
  27. class="image-button image-button-left"
  28. v-show="!isH5 && currentImageIndex > 0"
  29. @click="goPrev"
  30. >
  31. <i class="icon icon-left-arrow"></i>
  32. </div>
  33. <div
  34. class="image-button image-button-right"
  35. v-show="!isH5 && currentImageIndex < imageList?.length - 1"
  36. @click="goNext"
  37. >
  38. <i class="icon icon-right-arrow"></i>
  39. </div>
  40. <div class="actions-bar">
  41. <i class="icon icon-zoom-in" @click="zoomIn"></i>
  42. <i class="icon icon-zoom-out" @click="zoomOut"></i>
  43. <i class="icon icon-refresh-left" @click="rotateLeft"></i>
  44. <i class="icon icon-refresh-right" @click="rotateRight"></i>
  45. <span class="image-counter">
  46. {{ currentImageIndex + 1 }} / {{ imageList.length }}
  47. </span>
  48. </div>
  49. </div>
  50. </template>
  51. <script setup lang="ts">
  52. import TUIEnv from '../../../../../TUIPlugin/TUIEnv';
  53. import {
  54. defineProps,
  55. ref,
  56. defineEmits,
  57. watchEffect,
  58. onMounted,
  59. onUnmounted
  60. } from 'vue';
  61. import { Message } from '../../interface';
  62. // import { isNumber } from '@vueuse/core';
  63. function isNumber(obj: any) {
  64. return typeof obj === 'number' && isFinite(obj);
  65. }
  66. interface touchesPosition {
  67. pageX1?: number;
  68. pageY1?: number;
  69. pageX2?: number;
  70. pageY2?: number;
  71. }
  72. const props = defineProps({
  73. imageList: {
  74. type: Array,
  75. default: () => [] as Array<Message>
  76. },
  77. currentImage: {
  78. type: Object,
  79. default: () => ({} as Message)
  80. }
  81. });
  82. const emit = defineEmits(['close']);
  83. const zoom = ref(1);
  84. const rotate = ref(0);
  85. const minZoom = ref(0.1);
  86. const currentImageIndex = ref(0);
  87. const image = ref();
  88. const ul = ref();
  89. const { isH5 } = TUIEnv();
  90. // touch
  91. let startX = 0;
  92. let touchStore = {} as touchesPosition;
  93. let moveFlag = false;
  94. let twoTouchesFlag = false;
  95. let timer: number | null = null;
  96. watchEffect(() => {
  97. currentImageIndex.value = props.imageList.findIndex((message: any) => {
  98. return message.ID === props.currentImage.ID;
  99. });
  100. });
  101. const debounce = (func: any, wait = 200) => {
  102. let timer: any;
  103. return function () {
  104. if (timer) clearTimeout(timer);
  105. timer = setTimeout(func, wait);
  106. };
  107. };
  108. const handleTouchStart = (e: any) => {
  109. e.preventDefault();
  110. moveInit(e);
  111. twoTouchesInit(e);
  112. };
  113. const handleTouchMove = (e: any) => {
  114. e.preventDefault();
  115. moveFlag = true;
  116. if (e.touches && e.touches.length === 2) {
  117. twoTouchesFlag = true;
  118. handleTwoTouches(e);
  119. }
  120. };
  121. const handleTouchEnd = (e: any) => {
  122. e.preventDefault();
  123. let moveEndX = 0;
  124. let X = 0;
  125. if (twoTouchesFlag) {
  126. if (!timer) {
  127. twoTouchesFlag = false;
  128. delete touchStore.pageX2;
  129. delete touchStore.pageY2;
  130. timer = setTimeout(() => {
  131. timer = null;
  132. }, 200);
  133. }
  134. return;
  135. }
  136. // H5 touch move to left to go to prev image
  137. // H5 touch move to right to go to next image
  138. if (timer === null) {
  139. switch (moveFlag) {
  140. // touch event
  141. case true:
  142. moveEndX = e?.changedTouches[0]?.pageX;
  143. X = moveEndX - startX;
  144. if (X > 100) {
  145. goPrev();
  146. } else if (X < -100) {
  147. goNext();
  148. }
  149. break;
  150. // click event
  151. case false:
  152. close();
  153. break;
  154. }
  155. timer = setTimeout(() => {
  156. timer = null;
  157. }, 200);
  158. }
  159. };
  160. const handleTouchCancel = (e: any) => {
  161. twoTouchesFlag = false;
  162. delete touchStore.pageX1;
  163. delete touchStore.pageY1;
  164. };
  165. const handleWheel = (e: any) => {
  166. e.preventDefault();
  167. if (Math.abs(e.deltaX) !== 0 && Math.abs(e.deltaY) !== 0) return;
  168. let scale = zoom.value;
  169. scale += e.deltaY * (e.ctrlKey ? -0.01 : 0.002);
  170. scale = Math.min(Math.max(0.125, scale), 4);
  171. zoom.value = scale;
  172. };
  173. const moveInit = (e: any) => {
  174. startX = e?.changedTouches[0]?.pageX;
  175. moveFlag = false;
  176. };
  177. const twoTouchesInit = (e: any) => {
  178. let touch1 = e?.touches[0];
  179. let touch2 = e?.touches[1];
  180. touchStore.pageX1 = touch1?.pageX;
  181. touchStore.pageY1 = touch1?.pageY;
  182. if (touch2) {
  183. touchStore.pageX2 = touch2?.pageX;
  184. touchStore.pageY2 = touch2?.pageY;
  185. }
  186. };
  187. const handleTwoTouches = (e: any) => {
  188. let touch1 = e?.touches[0];
  189. let touch2 = e?.touches[1];
  190. if (touch2) {
  191. if (!isNumber(touchStore.pageX2)) {
  192. touchStore.pageX2 = touch2.pageX;
  193. }
  194. if (!isNumber(touchStore.pageY2)) {
  195. touchStore.pageY2 = touch2.pageY;
  196. }
  197. }
  198. const getDistance = (
  199. startX: number,
  200. startY: number,
  201. stoPx: number,
  202. stopY: number
  203. ) => {
  204. return Math.hypot(stoPx - startX, stopY - startY);
  205. };
  206. if (
  207. !isNumber(touchStore.pageX1) ||
  208. !isNumber(touchStore.pageY1) ||
  209. !isNumber(touchStore.pageX2) ||
  210. !isNumber(touchStore.pageY2)
  211. ) {
  212. return;
  213. }
  214. let touchZoom =
  215. getDistance(touch1.pageX, touch1.pageY, touch2.pageX, touch2.pageY) /
  216. getDistance(
  217. touchStore.pageX1,
  218. touchStore.pageY1,
  219. touchStore.pageX2,
  220. touchStore.pageY2
  221. );
  222. zoom.value = Math.min(Math.max(0.5, zoom.value * touchZoom), 4);
  223. };
  224. onMounted(() => {
  225. image?.value?.addEventListener('touchstart', handleTouchStart);
  226. image?.value?.addEventListener('touchmove', handleTouchMove);
  227. image?.value?.addEventListener('touchend', handleTouchEnd);
  228. image?.value?.addEventListener('touchcancel;', handleTouchCancel);
  229. // web: mouse wheel & mac wheel
  230. image?.value?.addEventListener('wheel', handleWheel);
  231. // web: close on esc keydown
  232. document?.addEventListener('keydown', handleEsc);
  233. });
  234. const handleEsc = (e: any) => {
  235. e.preventDefault();
  236. if (e?.keyCode === 27) {
  237. close();
  238. }
  239. };
  240. const zoomIn = () => {
  241. zoom.value += 0.1;
  242. };
  243. const zoomOut = () => {
  244. zoom.value =
  245. zoom.value - 0.1 > minZoom.value ? zoom.value - 0.1 : minZoom.value;
  246. };
  247. const close = () => {
  248. emit('close');
  249. };
  250. const rotateLeft = () => {
  251. rotate.value -= 90;
  252. };
  253. const rotateRight = () => {
  254. rotate.value += 90;
  255. };
  256. const goNext = () => {
  257. ul.value.style.transition = '0.5s';
  258. currentImageIndex.value < props.imageList.length - 1 &&
  259. currentImageIndex.value++;
  260. initStyle();
  261. };
  262. const goPrev = () => {
  263. ul.value.style.transition = '0.5s';
  264. currentImageIndex.value > 0 && currentImageIndex.value--;
  265. initStyle();
  266. };
  267. const initStyle = () => {
  268. zoom.value = 1;
  269. rotate.value = 0;
  270. };
  271. onUnmounted(() => {
  272. image?.value?.removeEventListener('touchstart', handleTouchStart);
  273. image?.value?.removeEventListener('touchmove', handleTouchMove);
  274. image?.value?.removeEventListener('touchend', handleTouchEnd);
  275. image?.value?.removeEventListener('touchcancel;', handleTouchCancel);
  276. image?.value?.removeEventListener('wheel', handleWheel);
  277. document?.removeEventListener('keydown', handleEsc);
  278. });
  279. </script>
  280. <style lang="scss" scoped>
  281. @import url('../../../../styles/common.scss');
  282. @import url('../../../../styles/icon.scss');
  283. .image-previewer {
  284. position: fixed;
  285. z-index: 12;
  286. width: 100vw;
  287. height: 100vh;
  288. background: rgba(#000000, 0.3);
  289. top: 0;
  290. left: 0;
  291. display: flex;
  292. flex-direction: column;
  293. align-items: center;
  294. .image-wrapper {
  295. position: relative;
  296. width: 100%;
  297. height: 100%;
  298. display: flex;
  299. justify-content: center;
  300. align-items: center;
  301. overflow: hidden;
  302. }
  303. .image-list {
  304. position: absolute;
  305. height: 100%;
  306. left: 0;
  307. padding: 0;
  308. margin: 0;
  309. display: flex;
  310. flex-direction: row;
  311. justify-content: center;
  312. align-content: center;
  313. .image-item {
  314. width: 100%;
  315. height: 100%;
  316. display: flex;
  317. align-items: center;
  318. justify-content: center;
  319. overflow: hidden;
  320. }
  321. }
  322. .image-preview {
  323. max-width: 100%;
  324. max-height: 100%;
  325. transition: transform 0.1s ease 0s;
  326. pointer-events: auto;
  327. }
  328. .image-button {
  329. position: absolute;
  330. cursor: pointer;
  331. width: 40Px;
  332. height: 40Px;
  333. border-radius: 50%;
  334. top: calc(50% - 20Px);
  335. background: rgba(255, 255, 255, 0.8);
  336. &-left {
  337. left: 10Px;
  338. }
  339. &-right {
  340. right: 10Px;
  341. }
  342. .icon {
  343. position: absolute;
  344. bottom: 0;
  345. top: 0;
  346. left: 0;
  347. right: 0;
  348. margin: auto;
  349. line-height: 40Px;
  350. }
  351. }
  352. .icon-close {
  353. position: absolute;
  354. cursor: pointer;
  355. width: 40Px;
  356. height: 40Px;
  357. border-radius: 50%;
  358. top: 3%;
  359. right: 3%;
  360. padding: 6Px;
  361. background: rgba(255, 255, 255, 0.8);
  362. &::before,
  363. &::after {
  364. background-color: #444444;
  365. }
  366. }
  367. }
  368. .image-previewer-h5 {
  369. width: 100%;
  370. height: 100%;
  371. background: #000000;
  372. display: flex;
  373. flex-direction: column;
  374. }
  375. .actions-bar {
  376. display: flex;
  377. justify-content: space-around;
  378. align-items: center;
  379. position: absolute;
  380. bottom: 5%;
  381. padding: 12Px;
  382. border-radius: 6Px;
  383. background: rgba(255, 255, 255, 0.8);
  384. .icon {
  385. position: static;
  386. font-size: 24Px;
  387. cursor: pointer;
  388. margin: 0 6Px;
  389. width: 27Px;
  390. height: 27Px;
  391. margin: 5Px;
  392. }
  393. }
  394. .image-counter {
  395. background: rgba(20, 18, 20, 0.53);
  396. padding: 3Px 5Px;
  397. margin: 5Px;
  398. border-radius: 3Px;
  399. color: #fff;
  400. }
  401. </style>