Files
iot-device-management-frontend/apps/web-antd/src/components/cropper/cropper-modal.vue

362 lines
9.6 KiB
Vue
Raw Normal View History

2025-04-22 09:19:19 +08:00
<script lang="ts" setup>
2025-05-06 16:15:56 +08:00
import type { CropendResult, CropperModalProps, CropperType } from './typing';
2025-04-22 09:19:19 +08:00
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
2025-06-17 20:22:24 +08:00
import { IconifyIcon } from '@vben/icons';
2025-05-06 14:47:02 +08:00
import { $t } from '@vben/locales';
2025-04-22 09:19:19 +08:00
import { dataURLtoBlob, isFunction } from '@vben/utils';
2025-04-23 17:05:51 +08:00
import {
Avatar,
Button,
message,
Space,
Tooltip,
Upload,
} from 'ant-design-vue';
2025-04-22 09:19:19 +08:00
import CropperImage from './cropper.vue';
defineOptions({ name: 'CropperModal' });
2025-05-06 16:15:56 +08:00
const props = withDefaults(defineProps<CropperModalProps>(), {
circled: true,
size: 0,
src: '',
uploadApi: () => Promise.resolve(),
2025-04-22 09:19:19 +08:00
});
const emit = defineEmits(['uploadSuccess', 'uploadError', 'register']);
let filename = '';
const src = ref(props.src || '');
const previewSource = ref('');
2025-05-06 16:15:56 +08:00
const cropper = ref<CropperType>();
2025-04-22 09:19:19 +08:00
let scaleX = 1;
let scaleY = 1;
const prefixCls = 'cropper-am';
const [Modal, modalApi] = useVbenModal({
onConfirm: handleOk,
onOpenChange(isOpen) {
if (isOpen) {
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading通过 handleReady
modalLoading(true);
const img = new Image();
img.src = src.value;
img.addEventListener('load', () => {
modalLoading(false);
});
img.addEventListener('error', () => {
modalLoading(false);
});
2025-04-22 09:19:19 +08:00
} else {
// 关闭时,清空右侧预览
previewSource.value = '';
modalLoading(false);
}
},
});
function modalLoading(loading: boolean) {
modalApi.setState({ confirmLoading: loading, loading });
}
// Block upload
function handleBeforeUpload(file: File) {
if (props.size > 0 && file.size > 1024 * 1024 * props.size) {
2025-05-06 14:47:02 +08:00
emit('uploadError', { msg: $t('ui.cropper.imageTooBig') });
2025-04-22 09:19:19 +08:00
return false;
}
const reader = new FileReader();
reader.readAsDataURL(file);
src.value = '';
previewSource.value = '';
reader.addEventListener('load', (e) => {
src.value = (e.target?.result as string) ?? '';
filename = file.name;
});
return false;
}
function handleCropend({ imgBase64 }: CropendResult) {
previewSource.value = imgBase64;
}
2025-05-06 16:15:56 +08:00
function handleReady(cropperInstance: CropperType) {
2025-04-22 09:19:19 +08:00
cropper.value = cropperInstance;
// 画布加载完毕 关闭 loading
modalLoading(false);
}
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 as any)?.[event]?.(arg);
}
async function handleOk() {
const uploadApi = props.uploadApi;
if (uploadApi && isFunction(uploadApi)) {
if (!previewSource.value) {
message.warn('未选择图片');
return;
}
const blob = dataURLtoBlob(previewSource.value);
try {
modalLoading(true);
const url = await uploadApi({ file: blob, filename, name: 'file' });
emit('uploadSuccess', { data: url, source: previewSource.value });
await modalApi.close();
} finally {
modalLoading(false);
}
}
}
</script>
<template>
<Modal
v-bind="$attrs"
2025-05-06 14:47:02 +08:00
:confirm-text="$t('ui.cropper.okText')"
2025-04-22 09:19:19 +08:00
:fullscreen-button="false"
2025-05-06 14:47:02 +08:00
:title="$t('ui.cropper.modalTitle')"
2025-06-17 20:22:24 +08:00
class="w-2/3"
2025-04-22 09:19:19 +08:00
>
<div :class="prefixCls">
<div :class="`${prefixCls}-left`" class="w-full">
<div :class="`${prefixCls}-cropper`">
<CropperImage
v-if="src"
:circled="circled"
:src="src"
height="300px"
@cropend="handleCropend"
@ready="handleReady"
/>
</div>
<div :class="`${prefixCls}-toolbar`">
<Upload
:before-upload="handleBeforeUpload"
:file-list="[]"
accept="image/*"
>
2025-05-06 14:47:02 +08:00
<Tooltip :title="$t('ui.cropper.selectImage')" placement="bottom">
2025-04-22 09:19:19 +08:00
<Button size="small" type="primary">
<template #icon>
<div class="flex items-center justify-center">
2025-06-17 20:22:24 +08:00
<IconifyIcon icon="lucide:upload" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
</Upload>
<Space>
2025-05-06 14:47:02 +08:00
<Tooltip :title="$t('ui.cropper.btn_reset')" placement="bottom">
2025-04-22 09:19:19 +08:00
<Button
:disabled="!src"
size="small"
type="primary"
@click="handlerToolbar('reset')"
>
<template #icon>
<div class="flex items-center justify-center">
2025-06-17 20:22:24 +08:00
<IconifyIcon icon="lucide:rotate-ccw" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
<Tooltip
2025-05-06 14:47:02 +08:00
:title="$t('ui.cropper.btn_rotate_left')"
2025-04-22 09:19:19 +08:00
placement="bottom"
>
<Button
:disabled="!src"
size="small"
type="primary"
@click="handlerToolbar('rotate', -45)"
>
<template #icon>
<div class="flex items-center justify-center">
2025-06-17 20:22:24 +08:00
<IconifyIcon icon="ant-design:rotate-left-outlined" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
<Tooltip
2025-05-06 14:47:02 +08:00
:title="$t('ui.cropper.btn_rotate_right')"
2025-04-22 09:19:19 +08:00
placement="bottom"
>
<Button
:disabled="!src"
size="small"
type="primary"
@click="handlerToolbar('rotate', 45)"
>
<template #icon>
<div class="flex items-center justify-center">
2025-06-17 20:22:24 +08:00
<IconifyIcon icon="ant-design:rotate-right-outlined" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
2025-05-06 14:47:02 +08:00
<Tooltip :title="$t('ui.cropper.btn_scale_x')" placement="bottom">
2025-04-22 09:19:19 +08:00
<Button
:disabled="!src"
size="small"
type="primary"
@click="handlerToolbar('scaleX')"
>
<template #icon>
<div class="flex items-center justify-center">
<IconifyIcon icon="vaadin:arrows-long-h" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
2025-05-06 14:47:02 +08:00
<Tooltip :title="$t('ui.cropper.btn_scale_y')" placement="bottom">
2025-04-22 09:19:19 +08:00
<Button
:disabled="!src"
size="small"
type="primary"
@click="handlerToolbar('scaleY')"
>
<template #icon>
<div class="flex items-center justify-center">
2025-06-17 20:22:24 +08:00
<IconifyIcon icon="vaadin:arrows-long-v" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
2025-05-06 14:47:02 +08:00
<Tooltip :title="$t('ui.cropper.btn_zoom_in')" placement="bottom">
2025-04-22 09:19:19 +08:00
<Button
:disabled="!src"
size="small"
type="primary"
@click="handlerToolbar('zoom', 0.1)"
>
<template #icon>
<div class="flex items-center justify-center">
2025-06-17 20:22:24 +08:00
<IconifyIcon icon="lucide:zoom-in" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
2025-05-06 14:47:02 +08:00
<Tooltip :title="$t('ui.cropper.btn_zoom_out')" placement="bottom">
2025-04-22 09:19:19 +08:00
<Button
:disabled="!src"
size="small"
type="primary"
@click="handlerToolbar('zoom', -0.1)"
>
<template #icon>
<div class="flex items-center justify-center">
2025-06-17 20:22:24 +08:00
<IconifyIcon icon="lucide:zoom-out" />
2025-04-22 09:19:19 +08:00
</div>
</template>
</Button>
</Tooltip>
</Space>
</div>
</div>
<div :class="`${prefixCls}-right`">
<div :class="`${prefixCls}-preview`">
<img
v-if="previewSource"
2025-05-06 14:47:02 +08:00
:alt="$t('ui.cropper.preview')"
2025-04-22 09:19:19 +08:00
:src="previewSource"
/>
</div>
<template v-if="previewSource">
<div :class="`${prefixCls}-group`">
<Avatar :src="previewSource" size="large" />
<Avatar :size="48" :src="previewSource" />
<Avatar :size="64" :src="previewSource" />
<Avatar :size="80" :src="previewSource" />
</div>
</template>
</div>
</div>
</Modal>
</template>
<style lang="scss">
.cropper-am {
display: flex;
&-left,
&-right {
height: 340px;
}
&-left {
width: 55%;
}
&-right {
width: 45%;
}
&-cropper {
height: 300px;
background: #eee;
2025-04-23 17:05:51 +08:00
background-image:
linear-gradient(
2025-04-22 09:19:19 +08:00
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 #eee;
border-radius: 50%;
img {
width: 100%;
height: 100%;
}
}
&-group {
display: flex;
align-items: center;
justify-content: space-around;
padding-top: 8px;
margin-top: 8px;
border-top: 1px solid #eee;
}
}
</style>