19 changed files with 550 additions and 121 deletions
@ -1,4 +1,7 @@ |
|||
import type Cropper from 'cropperjs'; |
|||
import { withInstall } from '/@/utils'; |
|||
import cropperImage from './src/Cropper.vue'; |
|||
import avatarCropper from './src/CropperAvatar.vue'; |
|||
|
|||
export type { Cropper }; |
|||
export { default as CropperImage } from './src/Cropper.vue'; |
|||
export * from './src/typing'; |
|||
export const CropperImage = withInstall(cropperImage); |
|||
export const CropperAvatar = withInstall(avatarCropper); |
|||
|
|||
@ -1,15 +0,0 @@ |
|||
<template> |
|||
<div :class="$attrs.class" :style="$attrs.style"> </div> |
|||
</template> |
|||
<script lang="ts"> |
|||
// TODO |
|||
import { defineComponent } from 'vue'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'AvatarCropper', |
|||
props: {}, |
|||
setup() { |
|||
return {}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,258 @@ |
|||
<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"> |
|||
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" /> |
|||
</Upload> |
|||
<Space> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:reload-outlined" |
|||
size="small" |
|||
@click="handlerToolbar('reset')" |
|||
/> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-left-outlined" |
|||
size="small" |
|||
@click="handlerToolbar('rotate', -45)" |
|||
/> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:rotate-right-outlined" |
|||
size="small" |
|||
@click="handlerToolbar('rotate', 45)" |
|||
/> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-h" |
|||
size="small" |
|||
@click="handlerToolbar('scaleX')" |
|||
/> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="vaadin:arrows-long-v" |
|||
size="small" |
|||
@click="handlerToolbar('scaleY')" |
|||
/> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-in-outlined" |
|||
size="small" |
|||
@click="handlerToolbar('zoom', 0.1)" |
|||
/> |
|||
<a-button |
|||
type="primary" |
|||
preIcon="ant-design:zoom-out-outlined" |
|||
size="small" |
|||
@click="handlerToolbar('zoom', -0.1)" |
|||
/> |
|||
</Space> |
|||
</div> |
|||
</div> |
|||
<div :class="`${prefixCls}-right`"> |
|||
<div :class="`${prefixCls}-preview`"> |
|||
<img :src="previewSource" v-if="previewSource" /> |
|||
</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 } from 'vue'; |
|||
import CropperImage from './Cropper.vue'; |
|||
import { Space, Upload, Avatar } 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'; |
|||
|
|||
const props = { |
|||
circled: { type: Boolean, default: true }, |
|||
uploadApi: { |
|||
type: Function as PropType<({ file: Blob, name: stirng, filename: string }) => Promise<any>>, |
|||
}, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperAvatar', |
|||
components: { BasicModal, Space, CropperImage, Upload, Avatar }, |
|||
props, |
|||
emits: ['uploadSuccess', 'register'], |
|||
setup(props, { emit }) { |
|||
let filename = ''; |
|||
const src = ref(''); |
|||
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.data }); |
|||
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, |
|||
rgba(0, 0, 0, 0.25) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgba(0, 0, 0, 0.25) 0 |
|||
), |
|||
linear-gradient( |
|||
45deg, |
|||
rgba(0, 0, 0, 0.25) 25%, |
|||
transparent 0, |
|||
transparent 75%, |
|||
rgba(0, 0, 0, 0.25) 0 |
|||
); |
|||
background-position: 0 0, 12px 12px; |
|||
background-size: 24px 24px; |
|||
} |
|||
|
|||
&-toolbar { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
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; |
|||
padding-top: 8px; |
|||
margin-top: 8px; |
|||
border-top: 1px solid @border-color-base; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,90 @@ |
|||
<template> |
|||
<div :class="getClass" :style="getStyle"> |
|||
<div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal"> |
|||
<img :src="sourceValue" v-if="sourceValue" alt="avatar" /> |
|||
</div> |
|||
<a-button :class="`${prefixCls}-upload-btn`" @click="openModal"> |
|||
{{ t('component.cropper.selectImage') }} |
|||
</a-button> |
|||
<CopperModal @register="register" @uploadSuccess="handleUploadSuccess" :uploadApi="uploadApi" /> |
|||
</div> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, computed, CSSProperties, unref, ref } from 'vue'; |
|||
import CopperModal from './CopperModal.vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { useModal } from '/@/components/Modal'; |
|||
import { useMessage } from '/@/hooks/web/useMessage'; |
|||
import { useI18n } from '/@/hooks/web/useI18n'; |
|||
|
|||
const props = { |
|||
src: { type: String, required: true }, |
|||
width: { type: [String, Number], default: '200px' }, |
|||
uploadApi: { type: Function as PropType<({ file: Blob, name: stirng }) => Promise<void>> }, |
|||
}; |
|||
|
|||
export default defineComponent({ |
|||
name: 'CropperAvatar', |
|||
components: { CopperModal }, |
|||
props, |
|||
setup(props) { |
|||
const sourceValue = ref(''); |
|||
const { prefixCls } = useDesign('cropper-avatar'); |
|||
const [register, { openModal }] = useModal(); |
|||
const { createMessage } = useMessage(); |
|||
const { t } = useI18n(); |
|||
|
|||
const getClass = computed(() => [prefixCls]); |
|||
|
|||
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px'); |
|||
|
|||
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) })); |
|||
|
|||
const getImageWrapperStyle = computed( |
|||
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }) |
|||
); |
|||
|
|||
function handleUploadSuccess({ source }) { |
|||
sourceValue.value = source; |
|||
createMessage.success(t('component.cropper.uploadSuccess')); |
|||
} |
|||
|
|||
return { |
|||
t, |
|||
prefixCls, |
|||
register, |
|||
openModal, |
|||
sourceValue, |
|||
getClass, |
|||
getImageWrapperStyle, |
|||
getStyle, |
|||
handleUploadSuccess, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
@prefix-cls: ~'@{namespace}-cropper-avatar'; |
|||
|
|||
.@{prefix-cls} { |
|||
display: inline-block; |
|||
text-align: center; |
|||
|
|||
&-image-wrapper { |
|||
overflow: hidden; |
|||
cursor: pointer; |
|||
background: @component-background; |
|||
border: 1px solid @border-color-base; |
|||
border-radius: 50%; |
|||
|
|||
img { |
|||
width: 100%; |
|||
} |
|||
} |
|||
|
|||
&-upload-btn { |
|||
margin: 10px auto; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,8 @@ |
|||
import type Cropper from 'cropperjs'; |
|||
|
|||
export interface CropendResult { |
|||
imgBase64: string; |
|||
imgInfo: Cropper.Data; |
|||
} |
|||
|
|||
export type { Cropper }; |
|||
Loading…
Reference in new issue