cl-cropper.uvue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. <template>
  2. <view
  3. class="cl-cropper"
  4. :class="[
  5. {
  6. 'is-disabled': disabled
  7. },
  8. pt.className
  9. ]"
  10. :style="{
  11. width: width + 'px',
  12. height: height + 'px'
  13. }"
  14. >
  15. <view class="cl-cropper__container" :class="[pt.inner?.className]">
  16. <!-- 图片容器 -->
  17. <view
  18. class="cl-cropper__image-container"
  19. :style="imageContainerStyle"
  20. @touchstart="onImageTouchStart"
  21. @touchmove="onImageTouchMove"
  22. @touchend="onImageTouchEnd"
  23. >
  24. <image
  25. class="cl-cropper__image"
  26. :class="[pt.image?.className]"
  27. :src="src"
  28. :style="imageStyle"
  29. mode="aspectFit"
  30. @load="onImageLoad"
  31. ></image>
  32. </view>
  33. <!-- 裁剪框 -->
  34. <view
  35. class="cl-cropper__crop-box"
  36. :class="[pt.cropBox?.className]"
  37. :style="cropBoxStyle"
  38. >
  39. <view class="cl-cropper__crop-mask cl-cropper__crop-mask--top"></view>
  40. <view class="cl-cropper__crop-mask cl-cropper__crop-mask--right"></view>
  41. <view class="cl-cropper__crop-mask cl-cropper__crop-mask--bottom"></view>
  42. <view class="cl-cropper__crop-mask cl-cropper__crop-mask--left"></view>
  43. <!-- 裁剪区域 -->
  44. <view
  45. class="cl-cropper__crop-area"
  46. :class="{ 'is-resizing': isResizing }"
  47. @touchstart="onCropTouchStart"
  48. @touchmove="onCropTouchMove"
  49. @touchend="onCropTouchEnd"
  50. >
  51. <!-- 辅助线 -->
  52. <view class="cl-cropper__guide-lines" v-if="showGuideLines">
  53. <view class="cl-cropper__guide-line cl-cropper__guide-line--h1"></view>
  54. <view class="cl-cropper__guide-line cl-cropper__guide-line--h2"></view>
  55. <view class="cl-cropper__guide-line cl-cropper__guide-line--v1"></view>
  56. <view class="cl-cropper__guide-line cl-cropper__guide-line--v2"></view>
  57. </view>
  58. <!-- 拖拽点 -->
  59. <view
  60. class="cl-cropper__drag-point cl-cropper__drag-point--tl"
  61. @touchstart="onResizeTouchStart"
  62. @touchmove="onResizeTouchMove"
  63. @touchend="onResizeTouchEnd"
  64. data-direction="tl"
  65. ></view>
  66. <view
  67. class="cl-cropper__drag-point cl-cropper__drag-point--tr"
  68. @touchstart="onResizeTouchStart"
  69. @touchmove="onResizeTouchMove"
  70. @touchend="onResizeTouchEnd"
  71. data-direction="tr"
  72. ></view>
  73. <view
  74. class="cl-cropper__drag-point cl-cropper__drag-point--bl"
  75. @touchstart="onResizeTouchStart"
  76. @touchmove="onResizeTouchMove"
  77. @touchend="onResizeTouchEnd"
  78. data-direction="bl"
  79. ></view>
  80. <view
  81. class="cl-cropper__drag-point cl-cropper__drag-point--br"
  82. @touchstart="onResizeTouchStart"
  83. @touchmove="onResizeTouchMove"
  84. @touchend="onResizeTouchEnd"
  85. data-direction="br"
  86. ></view>
  87. </view>
  88. </view>
  89. <!-- 操作按钮 -->
  90. <view class="cl-cropper__buttons" v-if="showButtons">
  91. <cl-button
  92. type="light"
  93. size="small"
  94. :pt="{ className: pt.button?.className }"
  95. @tap="reset"
  96. >
  97. 重置
  98. </cl-button>
  99. <cl-button
  100. type="primary"
  101. size="small"
  102. :pt="{ className: pt.button?.className }"
  103. @tap="crop"
  104. >
  105. 裁剪
  106. </cl-button>
  107. </view>
  108. </view>
  109. </view>
  110. </template>
  111. <script setup lang="ts">
  112. import { computed, ref, reactive, onMounted, type PropType } from "vue";
  113. import type { PassThroughProps } from "../../types";
  114. import { parsePt, parseRpx } from "@/cool";
  115. defineOptions({
  116. name: "cl-cropper"
  117. });
  118. const props = defineProps({
  119. // 透传样式
  120. pt: {
  121. type: Object,
  122. default: () => ({})
  123. },
  124. // 图片源
  125. src: {
  126. type: String,
  127. default: ""
  128. },
  129. // 容器宽度
  130. width: {
  131. type: [String, Number] as PropType<string | number>,
  132. default: 375
  133. },
  134. // 容器高度
  135. height: {
  136. type: [String, Number] as PropType<string | number>,
  137. default: 375
  138. },
  139. // 裁剪框宽度
  140. cropWidth: {
  141. type: [String, Number] as PropType<string | number>,
  142. default: 200
  143. },
  144. // 裁剪框高度
  145. cropHeight: {
  146. type: [String, Number] as PropType<string | number>,
  147. default: 200
  148. },
  149. // 最大缩放比例
  150. maxScale: {
  151. type: Number,
  152. default: 3
  153. },
  154. // 最小缩放比例
  155. minScale: {
  156. type: Number,
  157. default: 0.5
  158. },
  159. // 是否显示操作按钮
  160. showButtons: {
  161. type: Boolean,
  162. default: true
  163. },
  164. // 输出图片质量
  165. quality: {
  166. type: Number,
  167. default: 0.9
  168. },
  169. // 输出图片格式
  170. format: {
  171. type: String as PropType<"jpg" | "png">,
  172. default: "jpg"
  173. },
  174. // 是否禁用
  175. disabled: {
  176. type: Boolean,
  177. default: false
  178. }
  179. });
  180. // 事件定义
  181. const emit = defineEmits(["crop", "load", "error"]);
  182. // 透传样式类型
  183. type PassThrough = {
  184. className?: string;
  185. inner?: PassThroughProps;
  186. image?: PassThroughProps;
  187. cropBox?: PassThroughProps;
  188. button?: PassThroughProps;
  189. };
  190. // 解析透传样式
  191. const pt = computed(() => parsePt<PassThrough>(props.pt));
  192. // 图片信息
  193. const imageInfo = reactive({
  194. width: 0,
  195. height: 0,
  196. loaded: false
  197. });
  198. // 图片变换状态
  199. const imageTransform = reactive({
  200. scale: 1,
  201. translateX: 0,
  202. translateY: 0
  203. });
  204. // 裁剪框状态
  205. const cropBox = reactive({
  206. x: 0,
  207. y: 0,
  208. width: 200,
  209. height: 200
  210. });
  211. // 触摸状态
  212. const touchState = reactive({
  213. startX: 0,
  214. startY: 0,
  215. startDistance: 0,
  216. startScale: 1,
  217. startTranslateX: 0,
  218. startTranslateY: 0,
  219. touching: false,
  220. mode: "", // image, crop, resize
  221. resizeDirection: "" // tl, tr, bl, br
  222. });
  223. // 缩放状态
  224. const isResizing = ref(false);
  225. const showGuideLines = ref(false);
  226. // 计算图片容器样式
  227. const imageContainerStyle = computed(() => {
  228. return {
  229. width: "100%",
  230. height: "100%",
  231. overflow: "hidden"
  232. };
  233. });
  234. // 计算图片样式
  235. const imageStyle = computed(() => {
  236. return {
  237. transform: `translate3d(${imageTransform.translateX}px, ${imageTransform.translateY}px, 0) scale(${imageTransform.scale})`,
  238. transformOrigin: "center center",
  239. transition: touchState.touching ? "none" : "transform 0.3s ease"
  240. };
  241. });
  242. // 计算裁剪框样式
  243. const cropBoxStyle = computed(() => {
  244. return {
  245. left: `${cropBox.x}px`,
  246. top: `${cropBox.y}px`,
  247. width: `${cropBox.width}px`,
  248. height: `${cropBox.height}px`
  249. };
  250. });
  251. // 图片加载完成
  252. function onImageLoad(e: any) {
  253. imageInfo.width = e.detail.width;
  254. imageInfo.height = e.detail.height;
  255. imageInfo.loaded = true;
  256. // 初始化裁剪框位置
  257. initCropBox();
  258. emit("load", e);
  259. }
  260. // 计算图片最小缩放比例(确保图片不小于裁剪框)
  261. function calculateMinScale() {
  262. if (!imageInfo.loaded || !imageInfo.width || !imageInfo.height) {
  263. return props.minScale;
  264. }
  265. const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
  266. const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
  267. // 计算图片在容器中的显示尺寸(保持宽高比)
  268. const imageAspectRatio = imageInfo.width / imageInfo.height;
  269. const containerAspectRatio = containerWidth / containerHeight;
  270. let displayWidth: number;
  271. let displayHeight: number;
  272. if (imageAspectRatio > containerAspectRatio) {
  273. // 图片较宽,以容器宽度为准
  274. displayWidth = containerWidth;
  275. displayHeight = containerWidth / imageAspectRatio;
  276. } else {
  277. // 图片较高,以容器高度为准
  278. displayHeight = containerHeight;
  279. displayWidth = containerHeight * imageAspectRatio;
  280. }
  281. // 计算使图片能够覆盖裁剪框的最小缩放比例
  282. const minScaleX = cropBox.width / displayWidth;
  283. const minScaleY = cropBox.height / displayHeight;
  284. const calculatedMinScale = Math.max(minScaleX, minScaleY);
  285. // 确保不低于用户设置的最小缩放比例
  286. return Math.max(props.minScale, calculatedMinScale);
  287. }
  288. // 初始化裁剪框
  289. function initCropBox() {
  290. const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
  291. const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
  292. cropBox.width = parseFloat(parseRpx(props.cropWidth!).replace("px", ""));
  293. cropBox.height = parseFloat(parseRpx(props.cropHeight!).replace("px", ""));
  294. cropBox.x = (containerWidth - cropBox.width) / 2;
  295. cropBox.y = (containerHeight - cropBox.height) / 2;
  296. // 图片加载完成后,确保当前缩放比例不小于最小要求
  297. if (imageInfo.loaded) {
  298. const minScale = calculateMinScale();
  299. if (imageTransform.scale < minScale) {
  300. imageTransform.scale = minScale;
  301. }
  302. }
  303. }
  304. // 图片触摸开始
  305. function onImageTouchStart(e: TouchEvent) {
  306. if (props.disabled || !imageInfo.loaded) return;
  307. touchState.touching = true;
  308. touchState.mode = "image";
  309. if (e.touches != null && e.touches.length == 1) {
  310. // 单指拖拽
  311. touchState.startX = e.touches[0].clientX;
  312. touchState.startY = e.touches[0].clientY;
  313. touchState.startTranslateX = imageTransform.translateX;
  314. touchState.startTranslateY = imageTransform.translateY;
  315. } else if (e.touches != null && e.touches.length == 2) {
  316. // 双指缩放
  317. const touch1 = e.touches[0];
  318. const touch2 = e.touches[1];
  319. // 计算两指间距离
  320. touchState.startDistance = Math.sqrt(
  321. Math.pow(touch2.clientX - touch1.clientX, 2) +
  322. Math.pow(touch2.clientY - touch1.clientY, 2)
  323. );
  324. touchState.startScale = imageTransform.scale;
  325. // 记录缩放中心点(两指中心)
  326. touchState.startX = (touch1.clientX + touch2.clientX) / 2;
  327. touchState.startY = (touch1.clientY + touch2.clientY) / 2;
  328. touchState.startTranslateX = imageTransform.translateX;
  329. touchState.startTranslateY = imageTransform.translateY;
  330. }
  331. }
  332. // 图片触摸移动
  333. function onImageTouchMove(e: TouchEvent) {
  334. if (props.disabled || !touchState.touching || touchState.mode != "image") return;
  335. e.preventDefault();
  336. if (e.touches != null && e.touches.length == 1) {
  337. // 单指拖拽
  338. const deltaX = e.touches[0].clientX - touchState.startX;
  339. const deltaY = e.touches[0].clientY - touchState.startY;
  340. imageTransform.translateX = touchState.startTranslateX + deltaX;
  341. imageTransform.translateY = touchState.startTranslateY + deltaY;
  342. } else if (e.touches != null && e.touches.length == 2) {
  343. // 双指缩放
  344. const touch1 = e.touches[0];
  345. const touch2 = e.touches[1];
  346. // 计算当前两指间距离
  347. const distance = Math.sqrt(
  348. Math.pow(touch2.clientX - touch1.clientX, 2) +
  349. Math.pow(touch2.clientY - touch1.clientY, 2)
  350. );
  351. // 计算缩放比例
  352. const scale = (distance / touchState.startDistance) * touchState.startScale;
  353. // 使用动态计算的最小缩放比例
  354. const minScale = calculateMinScale();
  355. const newScale = Math.max(minScale, Math.min(props.maxScale, scale));
  356. // 计算当前缩放中心点
  357. const centerX = (touch1.clientX + touch2.clientX) / 2;
  358. const centerY = (touch1.clientY + touch2.clientY) / 2;
  359. // 计算缩放中心相对于容器的偏移
  360. const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
  361. const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
  362. const containerCenterX = containerWidth / 2;
  363. const containerCenterY = containerHeight / 2;
  364. // 缩放时调整位移,使缩放围绕双指中心进行
  365. const scaleDelta = newScale - touchState.startScale;
  366. const offsetX = (centerX - containerCenterX - touchState.startX + containerCenterX) * scaleDelta / touchState.startScale;
  367. const offsetY = (centerY - containerCenterY - touchState.startY + containerCenterY) * scaleDelta / touchState.startScale;
  368. imageTransform.scale = newScale;
  369. imageTransform.translateX = touchState.startTranslateX - offsetX;
  370. imageTransform.translateY = touchState.startTranslateY - offsetY;
  371. }
  372. }
  373. // 图片触摸结束
  374. function onImageTouchEnd(e: TouchEvent) {
  375. touchState.touching = false;
  376. touchState.mode = "";
  377. }
  378. // 裁剪框触摸开始
  379. function onCropTouchStart(e: TouchEvent) {
  380. if (props.disabled) return;
  381. e.stopPropagation();
  382. touchState.touching = true;
  383. touchState.mode = "crop";
  384. if (e.touches != null && e.touches.length == 1) {
  385. touchState.startX = e.touches[0].clientX;
  386. touchState.startY = e.touches[0].clientY;
  387. }
  388. }
  389. // 裁剪框触摸移动
  390. function onCropTouchMove(e: TouchEvent) {
  391. if (props.disabled || !touchState.touching || touchState.mode != "crop") return;
  392. e.preventDefault();
  393. e.stopPropagation();
  394. if (e.touches != null && e.touches.length == 1) {
  395. const deltaX = e.touches[0].clientX - touchState.startX;
  396. const deltaY = e.touches[0].clientY - touchState.startY;
  397. const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
  398. const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
  399. // 限制裁剪框在容器内
  400. cropBox.x = Math.max(0, Math.min(containerWidth - cropBox.width, cropBox.x + deltaX));
  401. cropBox.y = Math.max(0, Math.min(containerHeight - cropBox.height, cropBox.y + deltaY));
  402. touchState.startX = e.touches[0].clientX;
  403. touchState.startY = e.touches[0].clientY;
  404. }
  405. }
  406. // 裁剪框触摸结束
  407. function onCropTouchEnd(e: TouchEvent) {
  408. touchState.touching = false;
  409. touchState.mode = "";
  410. }
  411. // 裁剪框缩放开始
  412. function onResizeTouchStart(e: TouchEvent) {
  413. if (props.disabled) return;
  414. e.stopPropagation();
  415. touchState.touching = true;
  416. touchState.mode = "resize";
  417. isResizing.value = true;
  418. showGuideLines.value = true;
  419. // 从 data-direction 属性获取缩放方向
  420. const target = e.target as HTMLElement;
  421. touchState.resizeDirection = target.getAttribute("data-direction") || "";
  422. if (e.touches != null && e.touches.length == 1) {
  423. touchState.startX = e.touches[0].clientX;
  424. touchState.startY = e.touches[0].clientY;
  425. }
  426. }
  427. // 裁剪框缩放移动
  428. function onResizeTouchMove(e: TouchEvent) {
  429. if (props.disabled || !touchState.touching || touchState.mode != "resize") return;
  430. e.preventDefault();
  431. e.stopPropagation();
  432. if (e.touches != null && e.touches.length == 1) {
  433. const deltaX = e.touches[0].clientX - touchState.startX;
  434. const deltaY = e.touches[0].clientY - touchState.startY;
  435. const containerWidth = parseFloat(parseRpx(props.width!).replace("px", ""));
  436. const containerHeight = parseFloat(parseRpx(props.height!).replace("px", ""));
  437. // 最小裁剪框尺寸
  438. const minSize = 50;
  439. let newX = cropBox.x;
  440. let newY = cropBox.y;
  441. let newWidth = cropBox.width;
  442. let newHeight = cropBox.height;
  443. // 根据拖拽方向调整裁剪框
  444. switch (touchState.resizeDirection) {
  445. case "tl": // 左上角
  446. newX = Math.max(0, cropBox.x + deltaX);
  447. newY = Math.max(0, cropBox.y + deltaY);
  448. newWidth = cropBox.width - deltaX;
  449. newHeight = cropBox.height - deltaY;
  450. break;
  451. case "tr": // 右上角
  452. newY = Math.max(0, cropBox.y + deltaY);
  453. newWidth = cropBox.width + deltaX;
  454. newHeight = cropBox.height - deltaY;
  455. break;
  456. case "bl": // 左下角
  457. newX = Math.max(0, cropBox.x + deltaX);
  458. newWidth = cropBox.width - deltaX;
  459. newHeight = cropBox.height + deltaY;
  460. break;
  461. case "br": // 右下角
  462. newWidth = cropBox.width + deltaX;
  463. newHeight = cropBox.height + deltaY;
  464. break;
  465. }
  466. // 确保尺寸不小于最小值且不超出容器
  467. if (newWidth >= minSize && newHeight >= minSize) {
  468. // 检查是否超出容器边界
  469. if (newX >= 0 && newY >= 0 &&
  470. newX + newWidth <= containerWidth &&
  471. newY + newHeight <= containerHeight) {
  472. cropBox.x = newX;
  473. cropBox.y = newY;
  474. cropBox.width = newWidth;
  475. cropBox.height = newHeight;
  476. // 当裁剪框大小改变时,检查图片是否需要放大
  477. const minScale = calculateMinScale();
  478. if (imageTransform.scale < minScale) {
  479. imageTransform.scale = minScale;
  480. }
  481. // 更新起始位置
  482. touchState.startX = e.touches[0].clientX;
  483. touchState.startY = e.touches[0].clientY;
  484. }
  485. }
  486. }
  487. }
  488. // 裁剪框缩放结束
  489. function onResizeTouchEnd(e: TouchEvent) {
  490. touchState.touching = false;
  491. touchState.mode = "";
  492. touchState.resizeDirection = "";
  493. isResizing.value = false;
  494. // 延迟隐藏辅助线,给用户一点时间看到最终位置
  495. setTimeout(() => {
  496. showGuideLines.value = false;
  497. }, 500);
  498. }
  499. // 重置
  500. function reset() {
  501. // 重置位移
  502. imageTransform.translateX = 0;
  503. imageTransform.translateY = 0;
  504. // 重置裁剪框
  505. initCropBox();
  506. // 设置合适的初始缩放比例(确保图片能覆盖裁剪框)
  507. if (imageInfo.loaded) {
  508. const minScale = calculateMinScale();
  509. imageTransform.scale = Math.max(1, minScale);
  510. } else {
  511. imageTransform.scale = 1;
  512. }
  513. }
  514. // 执行裁剪
  515. function crop() {
  516. if (!imageInfo.loaded) {
  517. emit("error", "图片未加载完成");
  518. return;
  519. }
  520. }
  521. // 初始化
  522. onMounted(() => {
  523. if (props.src != "") {
  524. initCropBox();
  525. }
  526. });
  527. </script>
  528. <style lang="scss" scoped>
  529. .cl-cropper {
  530. @apply relative overflow-hidden bg-surface-100 rounded-xl;
  531. &.is-disabled {
  532. @apply opacity-50;
  533. }
  534. &__container {
  535. @apply relative w-full h-full;
  536. }
  537. &__image-container {
  538. @apply absolute top-0 left-0 flex items-center justify-center;
  539. }
  540. &__image {
  541. @apply max-w-full max-h-full;
  542. }
  543. &__crop-box {
  544. @apply absolute;
  545. }
  546. &__crop-mask {
  547. @apply absolute bg-black opacity-50;
  548. &--top {
  549. @apply top-0 left-0 w-full;
  550. height: var(--crop-top);
  551. }
  552. &--right {
  553. @apply top-0 right-0 h-full;
  554. width: var(--crop-right);
  555. }
  556. &--bottom {
  557. @apply bottom-0 left-0 w-full;
  558. height: var(--crop-bottom);
  559. }
  560. &--left {
  561. @apply top-0 left-0 h-full;
  562. width: var(--crop-left);
  563. }
  564. }
  565. &__crop-area {
  566. @apply relative w-full h-full border-2 border-white border-solid transition-all duration-300;
  567. &.is-resizing {
  568. @apply border-primary-500;
  569. }
  570. }
  571. &__guide-lines {
  572. @apply absolute top-0 left-0 w-full h-full pointer-events-none;
  573. }
  574. &__guide-line {
  575. @apply absolute bg-white opacity-70;
  576. &--h1 {
  577. @apply top-1/3 left-0 w-full h-px;
  578. }
  579. &--h2 {
  580. @apply top-2/3 left-0 w-full h-px;
  581. }
  582. &--v1 {
  583. @apply left-1/3 top-0 w-px h-full;
  584. }
  585. &--v2 {
  586. @apply left-2/3 top-0 w-px h-full;
  587. }
  588. }
  589. &__drag-point {
  590. @apply absolute w-3 h-3 bg-white border border-surface-300 border-solid cursor-move transition-all duration-200;
  591. &:hover {
  592. @apply scale-125 border-primary-500;
  593. }
  594. &--tl {
  595. @apply -top-1 -left-1;
  596. cursor: nw-resize;
  597. }
  598. &--tr {
  599. @apply -top-1 -right-1;
  600. cursor: ne-resize;
  601. }
  602. &--bl {
  603. @apply -bottom-1 -left-1;
  604. cursor: sw-resize;
  605. }
  606. &--br {
  607. @apply -bottom-1 -right-1;
  608. cursor: se-resize;
  609. }
  610. // 缩放时高亮显示
  611. .is-resizing & {
  612. @apply scale-125 border-primary-500 shadow-md;
  613. }
  614. }
  615. &__buttons {
  616. @apply absolute bottom-4 left-0 right-0 flex justify-center space-x-4;
  617. }
  618. }
  619. </style>