feat(cropper): upgrade cropperjs from v1 to v2
cropperjs v2 is a complete rewrite using Web Components architecture.
- Cropper.vue: rewrite to use v2 API
- `new Cropper(img, { container })` with Web Components template
- `selection.$toCanvas()` (async) replaces `cropper.getCroppedCanvas()`
- Selection `change` event replaces `crop`/`cropmove` callbacks
- CropperImage `load` event replaces `ready` callback
- CopperModal.vue: update toolbar handlers
- `cropperImage.$rotate()` replaces `cropper.rotate()`
- `cropperImage.$zoom()` replaces `cropper.zoom()`
- `cropperImage.$scale()` replaces `cropper.scaleX/Y()`
- `cropperImage.$resetTransform()` + `selection.$reset()` replaces `cropper.reset()`
- types.ts: replace `Cropper.Data` with inline type (v2 has no Data type)
- Remove v1 CSS import (v2 uses shadow DOM styles)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pull/878/head
parent
2520de56b4
commit
c4908548a1
|
|
@ -44,7 +44,7 @@
|
|||
"benz-amr-recorder": "^1.1.5",
|
||||
"bpmn-js-token-simulation": "^0.39.2",
|
||||
"camunda-bpmn-moddle": "^7.0.1",
|
||||
"cropperjs": "^1.6.2",
|
||||
"cropperjs": "^2.1.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"diagram-js": "^15.9.1",
|
||||
|
|
|
|||
112
pnpm-lock.yaml
112
pnpm-lock.yaml
|
|
@ -57,8 +57,8 @@ importers:
|
|||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
cropperjs:
|
||||
specifier: ^1.6.2
|
||||
version: 1.6.2
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
|
|
@ -988,6 +988,39 @@ packages:
|
|||
resolution: {integrity: sha512-51OWa1Gi6ODOasPmfJPq6js4pZoomima4XLZZCrkldaH2V5Nb3bVhNXPeT6XV0gubbainSpTw4zi68NqAeCNCg==}
|
||||
engines: {node: '>=v18'}
|
||||
|
||||
'@cropper/element-canvas@2.1.0':
|
||||
resolution: {integrity: sha512-el+rfJpZxsD2q5XxDBA4fRczcrOqB65Lb7roqXOq8LKufwf4bPWA9C6DjNJJahh/TP94dsLIEy3tSkgRMDv3Aw==}
|
||||
|
||||
'@cropper/element-crosshair@2.1.0':
|
||||
resolution: {integrity: sha512-0V589dAx8uZAfvJwdINLn76gfPQEafPH94ukjJ76uX0FCUovLaAVX+VRD/MDSYn0Mza/xejzmL9Dhd1DfemvmA==}
|
||||
|
||||
'@cropper/element-grid@2.1.0':
|
||||
resolution: {integrity: sha512-dEnk0rO+vp553LMvsPYgfrqVFcYXeVFrgFeavBYYEhAXtO40p7kN4rmLYLMMjaN+T/Mx2BATv6kUQpALKy2HLw==}
|
||||
|
||||
'@cropper/element-handle@2.1.0':
|
||||
resolution: {integrity: sha512-8BklWA4C/2GGAULupIWleSnGutECvYt3vx9flodqDfZpDEozws4LgLqmmzVuQmVkRVUdLnXdtx28kjgWLtzkHg==}
|
||||
|
||||
'@cropper/element-image@2.1.0':
|
||||
resolution: {integrity: sha512-mXOV8ixJvG0XtTxLebYAKDjEkFbFOQnsF02hXPZk1yQSV0J+LLhN7a2NePrtKnoTsEV19fhhX3UorMoyGGxvzg==}
|
||||
|
||||
'@cropper/element-selection@2.1.0':
|
||||
resolution: {integrity: sha512-mtFtBl6HIa/s9TWohXw+Z5eJoeYTqylrIcHvS7oVv0uM7IyeRwBW65Q7z+KtLfq/LW+2Sw/XDyvR+VN/DawBPw==}
|
||||
|
||||
'@cropper/element-shade@2.1.0':
|
||||
resolution: {integrity: sha512-zMdyqbb0lc0Vd1oj2Z1miIZvhyZG41OXMHvrNt0hNwblh0dVdrvtw48lnFDgRv+672vt2CNx7Q04GuvCQfPlgg==}
|
||||
|
||||
'@cropper/element-viewer@2.1.0':
|
||||
resolution: {integrity: sha512-XnxlQuqHitd1FOFZ6E0yXAF5NYd/LyIvONLLHI9p1rJw747WYKUPxQaSYtFKF7IOizJu/8mMj++Zc1dZ5ZP3YQ==}
|
||||
|
||||
'@cropper/element@2.1.0':
|
||||
resolution: {integrity: sha512-2zELddqHQNmlvkPoiYzE5nxEjPE+C8nXoTPuvV3FvLp3YjBinc7qb73Icg9UXP0o9qC4+h9q96JgGo0AyMO/Ng==}
|
||||
|
||||
'@cropper/elements@2.1.0':
|
||||
resolution: {integrity: sha512-qvzlYDn3VQgPPpsCu6Gi1XUO0v3vpXQFSjjxcVijbXeNsl/eiKrN7H9/CEiRgi5vr8kXfd7ZvgYxBjUBbH+y+w==}
|
||||
|
||||
'@cropper/utils@2.1.0':
|
||||
resolution: {integrity: sha512-wLtpZ4/UWgo+fGmG8NBWge8x5ehjfDe9ovleDfLy8kpwFaw43XXOEXQtRL1UNr0u4JZxaeO8FcXcolRWUUrlRQ==}
|
||||
|
||||
'@csstools/css-calc@3.1.1':
|
||||
resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
|
@ -3046,8 +3079,8 @@ packages:
|
|||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
cropperjs@1.6.2:
|
||||
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
|
||||
cropperjs@2.1.0:
|
||||
resolution: {integrity: sha512-SsSDqdVRl+mjbIBkGWlk1gCGcc+HzBqCbH5EQ+1tkAFUdxq2KUGukXF1RqhmvXrrdrX7PDwSUkWgXS7E36KvGQ==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
|
|
@ -6528,6 +6561,72 @@ snapshots:
|
|||
conventional-commits-parser: 6.3.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@cropper/element-canvas@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element-crosshair@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element-grid@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element-handle@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element-image@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/element-canvas': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element-selection@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/element-canvas': 2.1.0
|
||||
'@cropper/element-image': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element-shade@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/element-canvas': 2.1.0
|
||||
'@cropper/element-selection': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element-viewer@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/element-canvas': 2.1.0
|
||||
'@cropper/element-image': 2.1.0
|
||||
'@cropper/element-selection': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/element@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
'@cropper/elements@2.1.0':
|
||||
dependencies:
|
||||
'@cropper/element': 2.1.0
|
||||
'@cropper/element-canvas': 2.1.0
|
||||
'@cropper/element-crosshair': 2.1.0
|
||||
'@cropper/element-grid': 2.1.0
|
||||
'@cropper/element-handle': 2.1.0
|
||||
'@cropper/element-image': 2.1.0
|
||||
'@cropper/element-selection': 2.1.0
|
||||
'@cropper/element-shade': 2.1.0
|
||||
'@cropper/element-viewer': 2.1.0
|
||||
|
||||
'@cropper/utils@2.1.0': {}
|
||||
|
||||
'@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
|
|
@ -8549,7 +8648,10 @@ snapshots:
|
|||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cropperjs@1.6.2: {}
|
||||
cropperjs@2.1.0:
|
||||
dependencies:
|
||||
'@cropper/elements': 2.1.0
|
||||
'@cropper/utils': 2.1.0
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
|
|
|
|||
|
|
@ -162,13 +162,24 @@ function handleReady(cropperInstance: Cropper) {
|
|||
}
|
||||
|
||||
function handlerToolbar(event: string, arg?: number) {
|
||||
if (event === 'scaleX') {
|
||||
scaleX = arg = scaleX === -1 ? 1 : -1
|
||||
if (!cropper.value) return
|
||||
const cropperImage = cropper.value.getCropperImage()
|
||||
const cropperSelection = cropper.value.getCropperSelection()
|
||||
|
||||
if (event === 'reset') {
|
||||
cropperImage?.$resetTransform()
|
||||
cropperSelection?.$reset()
|
||||
} else if (event === 'rotate') {
|
||||
cropperImage?.$rotate(`${arg}deg`)
|
||||
} else if (event === 'scaleX') {
|
||||
scaleX = scaleX === -1 ? 1 : -1
|
||||
cropperImage?.$scale(scaleX, 1)
|
||||
} else if (event === 'scaleY') {
|
||||
scaleY = scaleY === -1 ? 1 : -1
|
||||
cropperImage?.$scale(1, scaleY)
|
||||
} else if (event === 'zoom') {
|
||||
cropperImage?.$zoom(arg!)
|
||||
}
|
||||
if (event === 'scaleY') {
|
||||
scaleY = arg = scaleY === -1 ? 1 : -1
|
||||
}
|
||||
cropper?.value?.[event]?.(arg)
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
|
|
|
|||
|
|
@ -1,50 +1,23 @@
|
|||
<template>
|
||||
<div :class="getClass" :style="getWrapperStyle">
|
||||
<div ref="containerRef" :class="getClass" :style="getWrapperStyle">
|
||||
<img
|
||||
v-show="isReady"
|
||||
v-show="false"
|
||||
ref="imgElRef"
|
||||
:alt="alt"
|
||||
:crossorigin="crossorigin"
|
||||
:src="src"
|
||||
:style="getImageStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { CSSProperties, PropType } from 'vue'
|
||||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
|
||||
defineOptions({ name: 'Cropper' })
|
||||
|
||||
type Options = Cropper.Options
|
||||
|
||||
const defaultOptions: Options = {
|
||||
aspectRatio: 1,
|
||||
zoomable: true,
|
||||
zoomOnTouch: true,
|
||||
zoomOnWheel: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: true,
|
||||
autoCrop: true,
|
||||
background: true,
|
||||
highlight: true,
|
||||
center: true,
|
||||
responsive: true,
|
||||
restore: true,
|
||||
checkCrossOrigin: true,
|
||||
checkOrientation: true,
|
||||
scalable: true,
|
||||
modal: true,
|
||||
guides: true,
|
||||
movable: true,
|
||||
rotatable: true
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
src: propTypes.string.def(''),
|
||||
alt: propTypes.string.def(''),
|
||||
|
|
@ -56,35 +29,21 @@ const props = defineProps({
|
|||
default: undefined
|
||||
},
|
||||
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
|
||||
options: { type: Object as PropType<Options>, default: () => ({}) }
|
||||
options: { type: Object as PropType<Record<string, any>>, default: () => ({}) }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cropend', 'ready', 'cropendError'])
|
||||
const attrs = useAttrs()
|
||||
const imgElRef = ref<ElRef<HTMLImageElement>>()
|
||||
const cropper = ref<Nullable<Cropper>>()
|
||||
const isReady = ref(false)
|
||||
const imgElRef = ref<HTMLImageElement>()
|
||||
const containerRef = ref<HTMLElement>()
|
||||
const cropper = ref<Cropper>()
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('cropper-image')
|
||||
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)
|
||||
|
||||
const getImageStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
height: props.height,
|
||||
maxWidth: '100%',
|
||||
...props.imageStyle
|
||||
}
|
||||
})
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
attrs.class,
|
||||
{
|
||||
[`${prefixCls}--circled`]: props.circled
|
||||
}
|
||||
]
|
||||
return [prefixCls, attrs.class]
|
||||
})
|
||||
const getWrapperStyle = computed((): CSSProperties => {
|
||||
return { height: `${props.height}`.replace(/px/, '') + 'px' }
|
||||
|
|
@ -98,27 +57,39 @@ onUnmounted(() => {
|
|||
|
||||
async function init() {
|
||||
const imgEl = unref(imgElRef)
|
||||
if (!imgEl) {
|
||||
return
|
||||
}
|
||||
const containerEl = unref(containerRef)
|
||||
if (!imgEl || !containerEl) return
|
||||
|
||||
cropper.value = new Cropper(imgEl, {
|
||||
...defaultOptions,
|
||||
ready: () => {
|
||||
isReady.value = true
|
||||
realTimeCroppered()
|
||||
emit('ready', cropper.value)
|
||||
},
|
||||
crop() {
|
||||
debounceRealTimeCroppered()
|
||||
},
|
||||
zoom() {
|
||||
debounceRealTimeCroppered()
|
||||
},
|
||||
cropmove() {
|
||||
debounceRealTimeCroppered()
|
||||
},
|
||||
container: containerEl,
|
||||
...props.options
|
||||
})
|
||||
|
||||
// Wait for custom elements to be ready, then configure
|
||||
await nextTick()
|
||||
const cropperSelection = cropper.value.getCropperSelection()
|
||||
const cropperImage = cropper.value.getCropperImage()
|
||||
|
||||
if (cropperSelection) {
|
||||
cropperSelection.initialCoverage = 0.5
|
||||
cropperSelection.aspectRatio = 1
|
||||
cropperSelection.movable = true
|
||||
cropperSelection.resizable = true
|
||||
cropperSelection.addEventListener('change', () => {
|
||||
debounceRealTimeCroppered()
|
||||
})
|
||||
}
|
||||
|
||||
if (cropperImage) {
|
||||
cropperImage.addEventListener('transform', () => {
|
||||
debounceRealTimeCroppered()
|
||||
})
|
||||
// Emit ready once image loads
|
||||
cropperImage.addEventListener('load', () => {
|
||||
emit('ready', cropper.value)
|
||||
realTimeCroppered()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Real-time display preview
|
||||
|
|
@ -127,33 +98,40 @@ function realTimeCroppered() {
|
|||
}
|
||||
|
||||
// event: return base64 and width and height information after cropping
|
||||
function croppered() {
|
||||
if (!cropper.value) {
|
||||
return
|
||||
async function croppered() {
|
||||
if (!cropper.value) return
|
||||
|
||||
const selection = cropper.value.getCropperSelection()
|
||||
if (!selection) return
|
||||
|
||||
const imgInfo = { x: selection.x, y: selection.y, width: selection.width, height: selection.height }
|
||||
|
||||
try {
|
||||
let canvas = await selection.$toCanvas()
|
||||
if (props.circled) {
|
||||
canvas = getRoundedCanvas(canvas)
|
||||
}
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return
|
||||
const fileReader = new FileReader()
|
||||
fileReader.readAsDataURL(blob)
|
||||
fileReader.onloadend = (e) => {
|
||||
emit('cropend', {
|
||||
imgBase64: e.target?.result ?? '',
|
||||
imgInfo
|
||||
})
|
||||
}
|
||||
fileReader.onerror = () => {
|
||||
emit('cropendError')
|
||||
}
|
||||
}, 'image/png')
|
||||
} catch {
|
||||
// Selection may not be ready yet
|
||||
}
|
||||
let imgInfo = cropper.value.getData()
|
||||
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
return
|
||||
}
|
||||
let fileReader: FileReader = new FileReader()
|
||||
fileReader.readAsDataURL(blob)
|
||||
fileReader.onloadend = (e) => {
|
||||
emit('cropend', {
|
||||
imgBase64: e.target?.result ?? '',
|
||||
imgInfo
|
||||
})
|
||||
}
|
||||
fileReader.onerror = () => {
|
||||
emit('cropendError')
|
||||
}
|
||||
}, 'image/png')
|
||||
}
|
||||
|
||||
// Get a circular picture canvas
|
||||
function getRoundedCanvas() {
|
||||
const sourceCanvas = cropper.value!.getCroppedCanvas()
|
||||
function getRoundedCanvas(sourceCanvas: HTMLCanvasElement) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')!
|
||||
const width = sourceCanvas.width
|
||||
|
|
@ -169,15 +147,3 @@ function getRoundedCanvas() {
|
|||
return canvas
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
$prefix-cls: #{$namespace}-cropper-image;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
&--circled {
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type Cropper from 'cropperjs'
|
|||
|
||||
export interface CropendResult {
|
||||
imgBase64: string
|
||||
imgInfo: Cropper.Data
|
||||
imgInfo: { x: number; y: number; width: number; height: number }
|
||||
}
|
||||
|
||||
export type { Cropper }
|
||||
|
|
|
|||
Loading…
Reference in New Issue