Browse Source

fixed cropper

pull/709/head
cKey 3 years ago
parent
commit
6a1ce12739
  1. 150
      apps/vue/src/components/Cropper/src/CopperModal.vue
  2. 224
      apps/vue/src/components/Cropper/src/Cropper.vue
  3. 112
      apps/vue/src/components/Cropper/src/CropperAvatar.vue
  4. 146
      apps/vue/src/components/registerGlobComp.ts
  5. 3
      apps/vue/src/main.ts

150
apps/vue/src/components/Cropper/src/CopperModal.vue

@ -22,14 +22,14 @@
</div>
<div :class="`${prefixCls}-toolbar`">
<Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
<Upload :fileList="fileList" accept="image/*" :beforeUpload="handleBeforeUpload">
<Tooltip :title="t('component.cropper.selectImage')" placement="bottom">
<a-button size="small" preIcon="ant-design:upload-outlined" type="primary" />
<Button size="small" preIcon="ant-design:upload-outlined" type="primary" />
</Tooltip>
</Upload>
<Space>
<Tooltip :title="t('component.cropper.btn_reset')" placement="bottom">
<a-button
<Button
type="primary"
preIcon="ant-design:reload-outlined"
size="small"
@ -38,7 +38,7 @@
/>
</Tooltip>
<Tooltip :title="t('component.cropper.btn_rotate_left')" placement="bottom">
<a-button
<Button
type="primary"
preIcon="ant-design:rotate-left-outlined"
size="small"
@ -47,7 +47,7 @@
/>
</Tooltip>
<Tooltip :title="t('component.cropper.btn_rotate_right')" placement="bottom">
<a-button
<Button
type="primary"
preIcon="ant-design:rotate-right-outlined"
size="small"
@ -56,7 +56,7 @@
/>
</Tooltip>
<Tooltip :title="t('component.cropper.btn_scale_x')" placement="bottom">
<a-button
<Button
type="primary"
preIcon="vaadin:arrows-long-h"
size="small"
@ -65,7 +65,7 @@
/>
</Tooltip>
<Tooltip :title="t('component.cropper.btn_scale_y')" placement="bottom">
<a-button
<Button
type="primary"
preIcon="vaadin:arrows-long-v"
size="small"
@ -74,7 +74,7 @@
/>
</Tooltip>
<Tooltip :title="t('component.cropper.btn_zoom_in')" placement="bottom">
<a-button
<Button
type="primary"
preIcon="ant-design:zoom-in-outlined"
size="small"
@ -83,7 +83,7 @@
/>
</Tooltip>
<Tooltip :title="t('component.cropper.btn_zoom_out')" placement="bottom">
<a-button
<Button
type="primary"
preIcon="ant-design:zoom-out-outlined"
size="small"
@ -110,13 +110,15 @@
</div>
</BasicModal>
</template>
<script lang="ts">
<script lang="ts" setup>
import type { CropendResult, Cropper } from './typing';
import type { UploadProps } from 'ant-design-vue';
import { defineComponent, ref } from 'vue';
import { ref } from 'vue';
import CropperImage from './Cropper.vue';
import { Space, Upload, Avatar, Tooltip } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { Button } from '/@/components/Button';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { dataURLtoBlob } from '/@/utils/file/base64Conver';
import { isFunction } from '/@/utils/is';
@ -124,90 +126,70 @@
type apiFunParams = { file: Blob; name: string; filename: string };
const props = {
const emits = defineEmits(['uploadSuccess', 'register']);
const props = defineProps({
circled: { type: Boolean, default: true },
uploadApi: {
type: Function as PropType<(params: apiFunParams) => Promise<any>>,
},
};
export default defineComponent({
name: 'CropperModal',
components: { BasicModal, Space, CropperImage, Upload, Avatar, Tooltip },
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;
});
let filename = '';
const src = ref('');
const previewSource = ref('');
const cropper = ref<Cropper>();
const fileList = ref<UploadProps['fileList']>([]);
let scaleX = 1;
let scaleY = 1;
const { prefixCls } = useDesign('cropper-am');
const [register, { closeModal, setModalProps }] = useModalInner();
const { t } = useI18n();
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;
}
// 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 handleCropend({ imgBase64 }: CropendResult) {
previewSource.value = imgBase64;
}
function handleReady(cropperInstance: Cropper) {
cropper.value = cropperInstance;
}
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);
}
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 });
}
}
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 });
emits('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">

224
apps/vue/src/components/Cropper/src/Cropper.vue

@ -10,9 +10,9 @@
/>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import type { CSSProperties } from 'vue';
import { defineComponent, onMounted, ref, unref, computed, onUnmounted } from 'vue';
import { onMounted, ref, unref, computed, onUnmounted, useAttrs } from 'vue';
import Cropper from 'cropperjs';
import 'cropperjs/dist/cropper.css';
import { useDesign } from '/@/hooks/web/useDesign';
@ -20,6 +20,7 @@
type Options = Cropper.Options;
const emits = defineEmits(['cropend', 'ready', 'cropendError']);
const defaultOptions: Options = {
aspectRatio: 1,
zoomable: true,
@ -43,7 +44,7 @@
rotatable: true,
};
const props = {
const props = defineProps({
src: { type: String, required: true },
alt: { type: String },
circled: { type: Boolean, default: false },
@ -55,124 +56,117 @@
},
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
options: { type: Object as PropType<Options>, default: () => ({}) },
};
});
export default defineComponent({
name: 'CropperImage',
props,
emits: ['cropend', 'ready', 'cropendError'],
setup(props, { attrs, emit }) {
const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Nullable<Cropper>>();
const isReady = ref(false);
const { prefixCls } = useDesign('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,
},
];
});
const getWrapperStyle = computed((): CSSProperties => {
return { height: `${props.height}`.replace(/px/, '') + 'px' };
});
onMounted(init);
onUnmounted(() => {
cropper.value?.destroy();
});
async function init() {
const imgEl = unref(imgElRef);
if (!imgEl) {
return;
}
cropper.value = new Cropper(imgEl, {
...defaultOptions,
ready: () => {
isReady.value = true;
realTimeCroppered();
emit('ready', cropper.value);
},
crop() {
debounceRealTimeCroppered();
},
zoom() {
debounceRealTimeCroppered();
},
cropmove() {
debounceRealTimeCroppered();
},
...props.options,
});
}
const attrs = useAttrs();
// Real-time display preview
function realTimeCroppered() {
props.realTimePreview && croppered();
}
const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Nullable<Cropper>>();
const isReady = ref(false);
// event: return base64 and width and height information after cropping
function croppered() {
if (!cropper.value) {
return;
}
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');
}
const { prefixCls } = useDesign('cropper-image');
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80);
// Get a circular picture canvas
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
const width = sourceCanvas.width;
const height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = 'destination-in';
context.beginPath();
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);
context.fill();
return canvas;
}
const getImageStyle = computed((): CSSProperties => {
return {
height: props.height,
maxWidth: '100%',
...props.imageStyle,
};
});
return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered };
},
const getClass = computed(() => {
return [
prefixCls,
attrs.class,
{
[`${prefixCls}--circled`]: props.circled,
},
];
});
const getWrapperStyle = computed((): CSSProperties => {
return { height: `${props.height}`.replace(/px/, '') + 'px' };
});
onMounted(init);
onUnmounted(() => {
cropper.value?.destroy();
});
async function init() {
const imgEl = unref(imgElRef);
if (!imgEl) {
return;
}
cropper.value = new Cropper(imgEl, {
...defaultOptions,
ready: () => {
isReady.value = true;
realTimeCroppered();
emits('ready', cropper.value);
},
crop() {
debounceRealTimeCroppered();
},
zoom() {
debounceRealTimeCroppered();
},
cropmove() {
debounceRealTimeCroppered();
},
...props.options,
});
}
// Real-time display preview
function realTimeCroppered() {
props.realTimePreview && croppered();
}
// event: return base64 and width and height information after cropping
function croppered() {
if (!cropper.value) {
return;
}
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) => {
emits('cropend', {
imgBase64: e.target?.result ?? '',
imgInfo,
});
};
fileReader.onerror = () => {
emits('cropendError');
};
}, 'image/png');
}
// Get a circular picture canvas
function getRoundedCanvas() {
const sourceCanvas = cropper.value!.getCroppedCanvas();
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
const width = sourceCanvas.width;
const height = sourceCanvas.height;
canvas.width = width;
canvas.height = height;
context.imageSmoothingEnabled = true;
context.drawImage(sourceCanvas, 0, 0, width, height);
context.globalCompositeOperation = 'destination-in';
context.beginPath();
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);
context.fill();
return canvas;
}
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-cropper-image';

112
apps/vue/src/components/Cropper/src/CropperAvatar.vue

@ -11,14 +11,14 @@
</div>
<img :src="sourceValue" v-if="sourceValue" alt="avatar" />
</div>
<a-button
<Button
:class="`${prefixCls}-upload-btn`"
@click="openModal"
v-if="showBtn"
v-bind="btnProps"
>
{{ btnText ? btnText : t('component.cropper.selectImage') }}
</a-button>
</Button>
<CopperModal
@register="register"
@ -28,9 +28,8 @@
/>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import {
defineComponent,
computed,
CSSProperties,
unref,
@ -39,6 +38,7 @@
watch,
PropType,
} from 'vue';
import { Button } from '/@/components/Button';
import CopperModal from './CopperModal.vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useModal } from '/@/components/Modal';
@ -47,72 +47,58 @@
import type { ButtonProps } from '/@/components/Button';
import Icon from '/@/components/Icon';
const props = {
interface File {
file: Blob;
name: string;
fileName?: string;
}
const emits = defineEmits(['update:value', 'change']);
const props = defineProps({
width: { type: [String, Number], default: '200px' },
value: { type: String },
showBtn: { type: Boolean, default: true },
btnProps: { type: Object as PropType<ButtonProps> },
btnText: { type: String, default: '' },
uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> },
};
export default defineComponent({
name: 'CropperAvatar',
components: { CopperModal, Icon },
props,
emits: ['update:value', 'change'],
setup(props, { emit, expose }) {
const sourceValue = ref(props.value || '');
const { prefixCls } = useDesign('cropper-avatar');
const [register, { openModal, closeModal }] = useModal();
const { createMessage } = useMessage();
const { t } = useI18n();
const getClass = computed(() => [prefixCls]);
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px');
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px');
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));
const getImageWrapperStyle = computed(
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }),
);
watchEffect(() => {
sourceValue.value = props.value || '';
});
watch(
() => sourceValue.value,
(v: string) => {
emit('update:value', v);
},
);
function handleUploadSuccess({ source }) {
sourceValue.value = source;
emit('change', source);
createMessage.success(t('component.cropper.uploadSuccess'));
}
uploadApi: { type: Function as PropType<(file: File) => Promise<void>> },
});
expose({ openModal: openModal.bind(null, true), closeModal });
return {
t,
prefixCls,
register,
openModal: openModal as any,
getIconWidth,
sourceValue,
getClass,
getImageWrapperStyle,
getStyle,
handleUploadSuccess,
};
},
const sourceValue = ref(props.value || '');
const { prefixCls } = useDesign('cropper-avatar');
const [register, { openModal, closeModal }] = useModal();
const { createMessage } = useMessage();
const { t } = useI18n();
const getClass = computed(() => [prefixCls]);
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px');
const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px');
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }));
const getImageWrapperStyle = computed(
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }),
);
watchEffect(() => {
sourceValue.value = props.value || '';
});
watch(
() => sourceValue.value,
(v: string) => {
emits('update:value', v);
},
);
function handleUploadSuccess({ source }) {
sourceValue.value = source;
emits('change', source);
createMessage.success(t('component.cropper.uploadSuccess'));
}
defineExpose({ openModal: openModal.bind(null, true), closeModal });
</script>
<style lang="less" scoped>

146
apps/vue/src/components/registerGlobComp.ts

@ -3,17 +3,159 @@ import { Button } from './Button';
import { Input } from './Input';
import {
// Need
Affix,
Anchor,
AutoComplete,
Alert,
Avatar,
BackTop,
Badge,
Button as AntButton,
Breadcrumb,
Calendar,
Card,
Collapse,
Carousel,
Cascader,
Checkbox,
Col,
Comment,
DatePicker,
Descriptions,
Divider,
Dropdown,
Drawer,
Empty,
Form,
Image,
Layout,
List,
Mentions,
Statistic,
Pagination,
Popover,
Progress,
Radio,
Rate,
Result,
Row,
Select,
Skeleton,
Slider,
Space,
Spin,
Steps,
Switch,
Transfer,
Tabs,
Tag,
Timeline,
Tooltip,
Typography,
Upload,
Input as AInput,
InputNumber as AInputNumber,
} from 'ant-design-vue';
const compList = [AntButton.Group];
const compList = [
Affix,
Anchor,
Anchor.Link,
AntButton.Group,
AutoComplete,
Alert,
Avatar,
Avatar.Group,
BackTop,
Badge,
Badge.Ribbon,
Breadcrumb,
Breadcrumb.Item,
Breadcrumb.Separator,
Calendar,
Card,
Card.Grid,
Card.Meta,
Collapse,
Collapse.Panel,
Carousel,
Cascader,
Checkbox,
Checkbox.Group,
Col,
Comment,
DatePicker,
DatePicker.MonthPicker,
DatePicker.QuarterPicker,
DatePicker.RangePicker,
DatePicker.TimePicker,
DatePicker.WeekPicker,
DatePicker.YearPicker,
Descriptions,
Descriptions.Item,
Divider,
Dropdown,
Dropdown.Button,
Drawer,
Empty,
Form,
Form.Item,
Form.ItemRest,
Image,
AInput,
AInput.Group,
AInput.Password,
AInput.Search,
AInput.TextArea,
AInputNumber,
List,
Mentions,
Statistic,
Statistic.Countdown,
Pagination,
Popover,
Progress,
Radio,
Radio.Button,
Radio.Group,
Rate,
Result,
Row,
Select,
Select.OptGroup,
Select.Option,
Skeleton,
Skeleton.Button,
Skeleton.Avatar,
Skeleton.Image,
Skeleton.Input,
Slider,
Space,
Spin,
Steps,
Steps.Step,
Switch,
Transfer,
Tabs,
Tabs.TabPane,
Tag,
Tag.CheckableTag,
Timeline,
Timeline.Item,
Tooltip,
Typography,
Typography.Link,
Typography.Paragraph,
Typography.Text,
Typography.Title,
Upload,
Upload.Dragger,
];
export function registerGlobComp(app: App) {
compList.forEach((comp) => {
app.component(comp.name || comp.displayName, comp);
});
app.use(Input).use(AInput).use(Button).use(Layout);
app.use(Input).use(Button).use(Layout);
}

3
apps/vue/src/main.ts

@ -5,7 +5,6 @@ import 'virtual:windi-utilities.css';
// Register icon sprite
import 'virtual:svg-icons-register';
import App from './App.vue';
import Antd from 'ant-design-vue';
import VueCookies from 'vue-cookies';
import { createApp } from 'vue';
import { initAppConfigStore, initAbpConfigStore } from '/@/logics/initAppConfig';
@ -53,8 +52,6 @@ async function bootstrap() {
app.use(VueCookies);
app.use(Antd);
app.mount('#app');
}

Loading…
Cancel
Save