committed by
GitHub
2 changed files with 287 additions and 287 deletions
@ -1,284 +1,284 @@ |
|||
<template> |
|||
<BasicModal |
|||
v-bind="$attrs" |
|||
@register="register" |
|||
:title="t('component.cropper.modalTitle')" |
|||
width="800px" |
|||
:canFullscreen="false" |
|||
@ok="handleOk" |
|||
:okText="t('component.cropper.okText')" |
|||
> |
|||
<div :class="prefixCls"> |
|||
<div :class="`${prefixCls}-left`"> |
|||
<div :class="`${prefixCls}-cropper`"> |
|||
<CropperImage |
|||
v-if="src" |
|||
:src="src" |
|||
height="300px" |
|||
:circled="circled" |
|||
@cropend="handleCropend" |
|||
@ready="handleReady" |
|||
/> |
|||
</div> |
|||
|
|||
<div :class="`${prefixCls}-toolbar`"> |
|||
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload"> |
|||
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom"> |
|||
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" /> |
|||
</Tooltip> |
|||
</Upload> |
|||
<Space> |
|||
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:reload-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('reset')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-left-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', -45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-right-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', 45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-h" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleX')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-v" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleY')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-in-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', 0.1)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-out-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', -0.1)" |
|||
/> |
|||
</Tooltip> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<div :class="`${prefixCls}-right`"> |
|||
<div :class="`${prefixCls}-preview`"> |
|||
<img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" /> |
|||
</div> |
|||
<template v-if="previewSource"> |
|||
<div :class="`${prefixCls}-group`"> |
|||
<Avatar :src="previewSource" size="large" /> |
|||
<Avatar :src="previewSource" :size="48" /> |
|||
<Avatar :src="previewSource" :size="64" /> |
|||
<Avatar :src="previewSource" :size="80" /> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</BasicModal> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { CropendResult, Cropper } from './typing'; |
|||
|
|||
import { defineComponent, ref, PropType } from 'vue'; |
|||
import CropperImage from './Cropper.vue'; |
|||
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { BasicModal, useModalInner } from '/@/components/Modal'; |
|||
import { dataURLtoBlob } from '/@/utils/file/base64Conver'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
type apiFunParams = { file: Blob; name: string; filename: string }; |
|||
|
|||
const props = { |
|||
circled: { type: Boolean, default: true }, |
|||
uploadApi: { |
|||
type: Function as PropType<(params: apiFunParams) => Promise<any>>, |
|||
}, |
|||
src: { type: String }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperModal', |
|||
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip }, |
|||
props, |
|||
emits: ['uploadSuccess', 'register'], |
|||
setup(props, { emit }) { |
|||
let filename = ''; |
|||
const src = ref(props.src || ''); |
|||
const previewSource = ref(''); |
|||
const cropper = ref<Cropper>(); |
|||
let scaleX = 1; |
|||
let scaleY = 1; |
|||
|
|||
const { prefixCls } = useDesign('cropper-am'); |
|||
const [register, { closeModal, setModalProps }] = useModalInner(); |
|||
const { t } = useI18n(); |
|||
|
|||
// Block upload |
|||
function handleBeforeUpload(file: File) { |
|||
const reader = new FileReader(); |
|||
reader.readAsDataURL(file); |
|||
src.value = ''; |
|||
previewSource.value = ''; |
|||
reader.onload = function (e) { |
|||
src.value = (e.target?.result as string) ?? ''; |
|||
filename = file.name; |
|||
}; |
|||
return false; |
|||
} |
|||
|
|||
function handleCropend({ imgBase64 }: CropendResult) { |
|||
previewSource.value = imgBase64; |
|||
} |
|||
|
|||
function handleReady(cropperInstance: Cropper) { |
|||
cropper.value = cropperInstance; |
|||
} |
|||
|
|||
function handlerToolbar(event: string, arg?: number) { |
|||
if (event === 'scaleX') { |
|||
scaleX = arg = scaleX === -1 ? 1 : -1; |
|||
} |
|||
if (event === 'scaleY') { |
|||
scaleY = arg = scaleY === -1 ? 1 : -1; |
|||
} |
|||
cropper?.value?.[event]?.(arg); |
|||
} |
|||
|
|||
async function handleOk() { |
|||
const uploadApi = props.uploadApi; |
|||
if (uploadApi && isFunction(uploadApi)) { |
|||
const blob = dataURLtoBlob(previewSource.value); |
|||
try { |
|||
setModalProps({ confirmLoading: true }); |
|||
const result = await uploadApi({ name: 'file', file: blob, filename }); |
|||
emit('uploadSuccess', { source: previewSource.value, data: result.url }); |
|||
closeModal(); |
|||
} finally { |
|||
setModalProps({ confirmLoading: false }); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
src, |
|||
register, |
|||
previewSource, |
|||
handleBeforeUpload, |
|||
handleCropend, |
|||
handleReady, |
|||
handlerToolbar, |
|||
handleOk, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-cropper-am'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: flex; |
|||
|
|||
&-left, |
|||
&-right { |
|||
height: 340px; |
|||
} |
|||
|
|||
&-left { |
|||
width: 55%; |
|||
} |
|||
|
|||
&-right { |
|||
width: 45%; |
|||
} |
|||
|
|||
&-cropper { |
|||
height: 300px; |
|||
background: #eee; |
|||
background-image: linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
), |
|||
linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
); |
|||
background-position: 0 0, 12px 12px; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
&-toolbar { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
&-preview { |
|||
width: 220px; |
|||
height: 220px; |
|||
margin: 0 auto; |
|||
overflow: hidden; |
|||
border: 1px solid @border-color-base; |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
&-group { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
margin-top: 8px; |
|||
padding-top: 8px; |
|||
border-top: 1px solid @border-color-base; |
|||
} |
|||
} |
|||
</style> |
|||
<template> |
|||
<BasicModal |
|||
v-bind="$attrs" |
|||
@register="register" |
|||
:title="t('component.cropper.modalTitle')" |
|||
width="800px" |
|||
:canFullscreen="false" |
|||
@ok="handleOk" |
|||
:okText="t('component.cropper.okText')" |
|||
> |
|||
<div :class="prefixCls"> |
|||
<div :class="`${prefixCls}-left`"> |
|||
<div :class="`${prefixCls}-cropper`"> |
|||
<CropperImage |
|||
v-if="src" |
|||
:src="src" |
|||
height="300px" |
|||
:circled="circled" |
|||
@cropend="handleCropend" |
|||
@ready="handleReady" |
|||
/> |
|||
</div> |
|||
|
|||
<div :class="`${prefixCls}-toolbar`"> |
|||
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload"> |
|||
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom"> |
|||
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" /> |
|||
</Tooltip> |
|||
</Upload> |
|||
<Space> |
|||
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:reload-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('reset')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-left-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', -45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-right-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('rotate', 45)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-h" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleX')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-v" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('scaleY')" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-in-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', 0.1)" |
|||
/> |
|||
</Tooltip> |
|||
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom"> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-out-outlined" |
|||
size="small" |
|||
:disabled="!src" |
|||
@click="handlerToolbar('zoom', -0.1)" |
|||
/> |
|||
</Tooltip> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<div :class="`${prefixCls}-right`"> |
|||
<div :class="`${prefixCls}-preview`"> |
|||
<img :src="previewSource" v-if="previewSource" :alt="t('component.cropper.preview')" /> |
|||
</div> |
|||
<template v-if="previewSource"> |
|||
<div :class="`${prefixCls}-group`"> |
|||
<Avatar :src="previewSource" size="large" /> |
|||
<Avatar :src="previewSource" :size="48" /> |
|||
<Avatar :src="previewSource" :size="64" /> |
|||
<Avatar :src="previewSource" :size="80" /> |
|||
</div> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
</BasicModal> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { CropendResult, Cropper } from './typing'; |
|||
|
|||
import { defineComponent, ref, PropType } from 'vue'; |
|||
import CropperImage from './Cropper.vue'; |
|||
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { BasicModal, useModalInner } from '/@/components/Modal'; |
|||
import { dataURLtoBlob } from '/@/utils/file/base64Conver'; |
|||
import { isFunction } from '/@/utils/is'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
type apiFunParams = { file: Blob; name: string; filename: string }; |
|||
|
|||
const props = { |
|||
circled: { type: Boolean, default: true }, |
|||
uploadApi: { |
|||
type: Function as PropType<(params: apiFunParams) => Promise<any>>, |
|||
}, |
|||
src: { type: String }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperModal', |
|||
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip }, |
|||
props, |
|||
emits: ['uploadSuccess', 'register'], |
|||
setup(props, { emit }) { |
|||
let filename = ''; |
|||
const src = ref(props.src || ''); |
|||
const previewSource = ref(''); |
|||
const cropper = ref<Cropper>(); |
|||
let scaleX = 1; |
|||
let scaleY = 1; |
|||
|
|||
const { prefixCls } = useDesign('cropper-am'); |
|||
const [register, { closeModal, setModalProps }] = useModalInner(); |
|||
const { t } = useI18n(); |
|||
|
|||
// Block upload |
|||
function handleBeforeUpload(file: File) { |
|||
const reader = new FileReader(); |
|||
reader.readAsDataURL(file); |
|||
src.value = ''; |
|||
previewSource.value = ''; |
|||
reader.onload = function (e) { |
|||
src.value = (e.target?.result as string) ?? ''; |
|||
filename = file.name; |
|||
}; |
|||
return false; |
|||
} |
|||
|
|||
function handleCropend({ imgBase64 }: CropendResult) { |
|||
previewSource.value = imgBase64; |
|||
} |
|||
|
|||
function handleReady(cropperInstance: Cropper) { |
|||
cropper.value = cropperInstance; |
|||
} |
|||
|
|||
function handlerToolbar(event: string, arg?: number) { |
|||
if (event === 'scaleX') { |
|||
scaleX = arg = scaleX === -1 ? 1 : -1; |
|||
} |
|||
if (event === 'scaleY') { |
|||
scaleY = arg = scaleY === -1 ? 1 : -1; |
|||
} |
|||
cropper?.value?.[event]?.(arg); |
|||
} |
|||
|
|||
async function handleOk() { |
|||
const uploadApi = props.uploadApi; |
|||
if (uploadApi && isFunction(uploadApi)) { |
|||
const blob = dataURLtoBlob(previewSource.value); |
|||
try { |
|||
setModalProps({ confirmLoading: true }); |
|||
const result = await uploadApi({ name: 'file', file: blob, filename }); |
|||
emit('uploadSuccess', { source: previewSource.value, data: result.url }); |
|||
closeModal(); |
|||
} finally { |
|||
setModalProps({ confirmLoading: false }); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
src, |
|||
register, |
|||
previewSource, |
|||
handleBeforeUpload, |
|||
handleCropend, |
|||
handleReady, |
|||
handlerToolbar, |
|||
handleOk, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less"> |
|||
@prefix-cls: ~'@{namespace}-cropper-am'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: flex; |
|||
|
|||
&-left, |
|||
&-right { |
|||
height: 340px; |
|||
} |
|||
|
|||
&-left { |
|||
width: 55%; |
|||
} |
|||
|
|||
&-right { |
|||
width: 45%; |
|||
} |
|||
|
|||
&-cropper { |
|||
height: 300px; |
|||
background: #eee; |
|||
background-image: linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
), |
|||
linear-gradient( |
|||
45deg, |
|||
rgb(0 0 0 / 25%) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgb(0 0 0 / 25%) 0 |
|||
); |
|||
background-position: 0 0, 12px 12px; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
&-toolbar { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-top: 10px; |
|||
} |
|||
|
|||
&-preview { |
|||
width: 220px; |
|||
height: 220px; |
|||
margin: 0 auto; |
|||
overflow: hidden; |
|||
border: 1px solid @border-color-base; |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
&-group { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
margin-top: 8px; |
|||
padding-top: 8px; |
|||
border-top: 1px solid @border-color-base; |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue