Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

Commit e1e56e3

Browse files
authored
feat: add cropper-demo (#325)
1 parent baad5a9 commit e1e56e3

File tree

15 files changed

+844
-9
lines changed

15 files changed

+844
-9
lines changed

apps/admin/src/pages/demo/Cropper.vue

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script lang="ts" setup>
2+
import { ref } from 'vue'
3+
import { CropperImage, CropperAvatar } from '@vben/components'
4+
import img from '@/assets/images/header.jpg'
5+
6+
const info = ref('')
7+
const cropperImg = ref('')
8+
const circleInfo = ref('')
9+
const circleImg = ref('')
10+
const avatar = ref('')
11+
function handleCropend({ imgBase64, imgInfo }) {
12+
info.value = imgInfo
13+
cropperImg.value = imgBase64
14+
}
15+
16+
function handleCircleCropend({ imgBase64, imgInfo }) {
17+
circleInfo.value = imgInfo
18+
circleImg.value = imgBase64
19+
}
20+
</script>
21+
22+
<template>
23+
<VbenCard
24+
title="图片裁剪示例"
25+
content="需要开启测试接口服务才能进行上传测试!"
26+
>
27+
<VbenCard title="头像裁剪">
28+
<CropperAvatar :value="avatar" />
29+
</VbenCard>
30+
31+
<VbenCard title="矩形裁剪" class="my-4">
32+
<div class="container p-4">
33+
<div class="cropper-container mr-10">
34+
<CropperImage
35+
ref="refCropper"
36+
:src="img"
37+
@cropend="handleCropend"
38+
style="width: 40vw"
39+
/>
40+
</div>
41+
<img :src="cropperImg" class="croppered" v-if="cropperImg" alt="" />
42+
</div>
43+
<p v-if="cropperImg">裁剪后图片信息:{{ info }}</p>
44+
</VbenCard>
45+
46+
<VbenCard title="圆形裁剪">
47+
<div class="container p-4">
48+
<div class="cropper-container mr-10">
49+
<CropperImage
50+
ref="refCropper"
51+
:src="img"
52+
@cropend="handleCircleCropend"
53+
style="width: 40vw"
54+
circled
55+
/>
56+
</div>
57+
<img :src="circleImg" class="croppered" v-if="circleImg" />
58+
</div>
59+
<p v-if="circleImg">裁剪后图片信息:{{ circleInfo }}</p>
60+
</VbenCard>
61+
</VbenCard>
62+
</template>
63+
64+
<style scoped>
65+
.container {
66+
display: flex;
67+
align-items: center;
68+
width: 100vw;
69+
}
70+
71+
.cropper-container {
72+
width: 40vw;
73+
}
74+
75+
.croppered {
76+
height: 360px;
77+
}
78+
79+
p {
80+
margin: 10px;
81+
}
82+
</style>

packages/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export { default as CountDownInput } from './src/countdown-input/index.vue'
1010
export { default as StrengthMeter } from './src/strength-meter/index.vue'
1111
export { default as ClickOutside } from './src/click-outside/index.vue'
1212
export { default as IconPicker } from './src/icon-picker/index.vue'
13+
export { default as CropperImage } from './src/cropper/index.vue'
14+
export { default as CropperAvatar } from './src/cropper/cropper-avatar.vue'
1315

1416
export { default as CollapseTransition } from './src/transition/collapse-transition.vue'
1517
export { default as CustomTransition } from './src/transition/index'

packages/components/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"@zxcvbn-ts/core": "^3.0.4",
2020
"qrcode": "^1.5.3",
2121
"vue": "3.3.6",
22-
"vue-router": "^4.2.4"
22+
"vue-router": "^4.2.4",
23+
"cropperjs": "^1.6.1"
2324
},
2425
"devDependencies": {
2526
"@iconify/json": "^2.2.115",
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<script lang="ts" setup>
2+
import {
3+
computed,
4+
CSSProperties,
5+
unref,
6+
ref,
7+
watchEffect,
8+
watch,
9+
PropType,
10+
} from 'vue'
11+
import CropperModal from './cropper-modal.vue'
12+
import { useI18n } from '@vben/locale'
13+
import Icon from '../icon/index.vue'
14+
15+
defineOptions({ name: 'CropperAvatar' })
16+
17+
const props = defineProps({
18+
width: { type: [String, Number], default: '200px' },
19+
value: { type: String },
20+
showBtn: { type: Boolean, default: true },
21+
btnProps: { type: Object },
22+
btnText: { type: String, default: '' },
23+
uploadApi: {
24+
type: Function as PropType<
25+
({ file, name }: { file: Blob; name: string }) => Promise<void>
26+
>,
27+
},
28+
29+
size: { type: Number, default: 5 },
30+
})
31+
32+
const emit = defineEmits(['update:value', 'change'])
33+
34+
const sourceValue = ref(props.value || '')
35+
const prefixCls = 'cropper-avatar'
36+
const { t } = useI18n()
37+
const cropperModelRef = ref()
38+
39+
const openModal = () => {
40+
unref(cropperModelRef).showModal = true
41+
}
42+
43+
const closeModal = () => {
44+
unref(cropperModelRef).showModal = false
45+
}
46+
47+
const getClass = computed(() => [prefixCls])
48+
49+
const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px')
50+
51+
const getIconWidth = computed(
52+
() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px',
53+
)
54+
55+
const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) }))
56+
57+
const getImageWrapperStyle = computed(
58+
(): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }),
59+
)
60+
61+
watchEffect(() => {
62+
sourceValue.value = props.value || ''
63+
})
64+
65+
watch(
66+
() => sourceValue.value,
67+
(v: string) => {
68+
emit('update:value', v)
69+
},
70+
)
71+
72+
function handleUploadSuccess({ source, data }) {
73+
sourceValue.value = source
74+
emit('change', { source, data })
75+
console.log(t('component.cropper.uploadSuccess'))
76+
}
77+
78+
defineExpose({ openModal, closeModal })
79+
</script>
80+
81+
<template>
82+
<div :class="getClass" :style="getStyle">
83+
<div
84+
:class="`${prefixCls}-image-wrapper`"
85+
:style="getImageWrapperStyle"
86+
@click="openModal()"
87+
>
88+
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
89+
<Icon
90+
icon="ant-design:cloud-upload-outlined"
91+
:size="getIconWidth"
92+
:style="getImageWrapperStyle"
93+
color="#d6d6d6"
94+
/>
95+
</div>
96+
<img :src="sourceValue" v-if="sourceValue" alt="avatar" />
97+
</div>
98+
<vben-button
99+
:class="`${prefixCls}-upload-btn`"
100+
@click="openModal"
101+
v-if="showBtn"
102+
v-bind="btnProps"
103+
>
104+
{{ btnText ? btnText : t('component.cropper.selectImage') }}
105+
</vben-button>
106+
107+
<CropperModal
108+
ref="cropperModelRef"
109+
@upload-success="handleUploadSuccess"
110+
:uploadApi="uploadApi"
111+
:src="sourceValue"
112+
:size="size"
113+
@close="closeModal"
114+
/>
115+
</div>
116+
</template>
117+
118+
<style lang="less" scoped>
119+
@prefix-cls: ~'cropper-avatar';
120+
121+
.@{prefix-cls} {
122+
display: inline-block;
123+
text-align: center;
124+
125+
&-image-wrapper {
126+
overflow: hidden;
127+
border: 1px solid hsv(0, 0, 85%);
128+
border-radius: 50%;
129+
background: #fff;
130+
cursor: pointer;
131+
132+
img {
133+
width: 100%;
134+
}
135+
}
136+
137+
&-image-mask {
138+
position: absolute;
139+
width: inherit;
140+
height: inherit;
141+
transition: opacity 0.4s;
142+
border: inherit;
143+
border-radius: inherit;
144+
opacity: 0;
145+
background: rgb(0 0 0 / 40%);
146+
cursor: pointer;
147+
148+
::v-deep(svg) {
149+
margin: auto;
150+
}
151+
}
152+
153+
&-image-mask:hover {
154+
opacity: 40;
155+
}
156+
157+
&-upload-btn {
158+
margin: 10px auto;
159+
}
160+
}
161+
</style>

0 commit comments

Comments
 (0)