Commit 8e410fc6401847d8e5545468b5ce6fd7ce9fc5cc
1 parent
de12babd
feat: add CropperAvatar component
Showing
19 changed files
with
550 additions
and
121 deletions
CHANGELOG.zh_CN.md
package.json
src/components/Application/src/search/AppSearchModal.vue
... | ... | @@ -4,7 +4,7 @@ |
4 | 4 | <div :class="getClass" @click.stop v-if="visible"> |
5 | 5 | <div :class="`${prefixCls}-content`" v-click-outside="handleClose"> |
6 | 6 | <div :class="`${prefixCls}-input__wrapper`"> |
7 | - <Input | |
7 | + <a-input | |
8 | 8 | :class="`${prefixCls}-input`" |
9 | 9 | :placeholder="t('common.searchText')" |
10 | 10 | ref="inputRef" |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | <template #prefix> |
15 | 15 | <SearchOutlined /> |
16 | 16 | </template> |
17 | - </Input> | |
17 | + </a-input> | |
18 | 18 | <span :class="`${prefixCls}-cancel`" @click="handleClose"> |
19 | 19 | {{ t('common.cancelText') }} |
20 | 20 | </span> |
... | ... | @@ -59,7 +59,6 @@ |
59 | 59 | <script lang="ts"> |
60 | 60 | import { defineComponent, computed, unref, ref, watch, nextTick } from 'vue'; |
61 | 61 | import { SearchOutlined } from '@ant-design/icons-vue'; |
62 | - import { Input } from 'ant-design-vue'; | |
63 | 62 | import AppSearchFooter from './AppSearchFooter.vue'; |
64 | 63 | import Icon from '/@/components/Icon'; |
65 | 64 | import clickOutside from '/@/directives/clickOutside'; |
... | ... | @@ -75,7 +74,7 @@ |
75 | 74 | |
76 | 75 | export default defineComponent({ |
77 | 76 | name: 'AppSearchModal', |
78 | - components: { Icon, SearchOutlined, AppSearchFooter, Input }, | |
77 | + components: { Icon, SearchOutlined, AppSearchFooter }, | |
79 | 78 | directives: { |
80 | 79 | clickOutside, |
81 | 80 | }, | ... | ... |
src/components/CountDown/src/CountdownInput.vue
1 | 1 | <template> |
2 | - <AInput v-bind="$attrs" :class="prefixCls" :size="size" :value="state"> | |
2 | + <a-input v-bind="$attrs" :class="prefixCls" :size="size" :value="state"> | |
3 | 3 | <template #addonAfter> |
4 | 4 | <CountButton :size="size" :count="count" :value="state" :beforeStartFunc="sendCodeApi" /> |
5 | 5 | </template> |
6 | - </AInput> | |
6 | + </a-input> | |
7 | 7 | </template> |
8 | 8 | <script lang="ts"> |
9 | 9 | import { defineComponent, PropType } from 'vue'; |
10 | - import { Input } from 'ant-design-vue'; | |
11 | 10 | import CountButton from './CountButton.vue'; |
12 | 11 | import { useDesign } from '/@/hooks/web/useDesign'; |
13 | 12 | import { useRuleFormItem } from '/@/hooks/component/useFormItem'; |
... | ... | @@ -24,7 +23,7 @@ |
24 | 23 | |
25 | 24 | export default defineComponent({ |
26 | 25 | name: 'CountDownInput', |
27 | - components: { [Input.name]: Input, CountButton }, | |
26 | + components: { CountButton }, | |
28 | 27 | inheritAttrs: false, |
29 | 28 | props, |
30 | 29 | setup(props) { | ... | ... |
src/components/Cropper/index.ts
1 | -import type Cropper from 'cropperjs'; | |
1 | +import { withInstall } from '/@/utils'; | |
2 | +import cropperImage from './src/Cropper.vue'; | |
3 | +import avatarCropper from './src/CropperAvatar.vue'; | |
2 | 4 | |
3 | -export type { Cropper }; | |
4 | -export { default as CropperImage } from './src/Cropper.vue'; | |
5 | +export * from './src/typing'; | |
6 | +export const CropperImage = withInstall(cropperImage); | |
7 | +export const CropperAvatar = withInstall(avatarCropper); | ... | ... |
src/components/Cropper/src/AvatarCropper.vue deleted
100644 → 0
1 | -<template> | |
2 | - <div :class="$attrs.class" :style="$attrs.style"> </div> | |
3 | -</template> | |
4 | -<script lang="ts"> | |
5 | - // TODO | |
6 | - import { defineComponent } from 'vue'; | |
7 | - | |
8 | - export default defineComponent({ | |
9 | - name: 'AvatarCropper', | |
10 | - props: {}, | |
11 | - setup() { | |
12 | - return {}; | |
13 | - }, | |
14 | - }); | |
15 | -</script> |
src/components/Cropper/src/CopperModal.vue
0 → 100644
1 | +<template> | |
2 | + <BasicModal | |
3 | + v-bind="$attrs" | |
4 | + @register="register" | |
5 | + :title="t('component.cropper.modalTitle')" | |
6 | + width="800px" | |
7 | + :canFullscreen="false" | |
8 | + @ok="handleOk" | |
9 | + :okText="t('component.cropper.okText')" | |
10 | + > | |
11 | + <div :class="prefixCls"> | |
12 | + <div :class="`${prefixCls}-left`"> | |
13 | + <div :class="`${prefixCls}-cropper`"> | |
14 | + <CropperImage | |
15 | + v-if="src" | |
16 | + :src="src" | |
17 | + height="300px" | |
18 | + :circled="circled" | |
19 | + @cropend="handleCropend" | |
20 | + @ready="handleReady" | |
21 | + /> | |
22 | + </div> | |
23 | + | |
24 | + <div :class="`${prefixCls}-toolbar`"> | |
25 | + <Upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload"> | |
26 | + <a-button size="small" preIcon="ant-design:upload-outlined" type="primary" /> | |
27 | + </Upload> | |
28 | + <Space> | |
29 | + <a-button | |
30 | + type="primary" | |
31 | + preIcon="ant-design:reload-outlined" | |
32 | + size="small" | |
33 | + @click="handlerToolbar('reset')" | |
34 | + /> | |
35 | + <a-button | |
36 | + type="primary" | |
37 | + preIcon="ant-design:rotate-left-outlined" | |
38 | + size="small" | |
39 | + @click="handlerToolbar('rotate', -45)" | |
40 | + /> | |
41 | + <a-button | |
42 | + type="primary" | |
43 | + preIcon="ant-design:rotate-right-outlined" | |
44 | + size="small" | |
45 | + @click="handlerToolbar('rotate', 45)" | |
46 | + /> | |
47 | + <a-button | |
48 | + type="primary" | |
49 | + preIcon="vaadin:arrows-long-h" | |
50 | + size="small" | |
51 | + @click="handlerToolbar('scaleX')" | |
52 | + /> | |
53 | + <a-button | |
54 | + type="primary" | |
55 | + preIcon="vaadin:arrows-long-v" | |
56 | + size="small" | |
57 | + @click="handlerToolbar('scaleY')" | |
58 | + /> | |
59 | + <a-button | |
60 | + type="primary" | |
61 | + preIcon="ant-design:zoom-in-outlined" | |
62 | + size="small" | |
63 | + @click="handlerToolbar('zoom', 0.1)" | |
64 | + /> | |
65 | + <a-button | |
66 | + type="primary" | |
67 | + preIcon="ant-design:zoom-out-outlined" | |
68 | + size="small" | |
69 | + @click="handlerToolbar('zoom', -0.1)" | |
70 | + /> | |
71 | + </Space> | |
72 | + </div> | |
73 | + </div> | |
74 | + <div :class="`${prefixCls}-right`"> | |
75 | + <div :class="`${prefixCls}-preview`"> | |
76 | + <img :src="previewSource" v-if="previewSource" /> | |
77 | + </div> | |
78 | + <template v-if="previewSource"> | |
79 | + <div :class="`${prefixCls}-group`"> | |
80 | + <Avatar :src="previewSource" size="large" /> | |
81 | + <Avatar :src="previewSource" :size="48" /> | |
82 | + <Avatar :src="previewSource" :size="64" /> | |
83 | + <Avatar :src="previewSource" :size="80" /> | |
84 | + </div> | |
85 | + </template> | |
86 | + </div> | |
87 | + </div> | |
88 | + </BasicModal> | |
89 | +</template> | |
90 | +<script lang="ts"> | |
91 | + import type { CropendResult, Cropper } from './typing'; | |
92 | + | |
93 | + import { defineComponent, ref } from 'vue'; | |
94 | + import CropperImage from './Cropper.vue'; | |
95 | + import { Space, Upload, Avatar } from 'ant-design-vue'; | |
96 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
97 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
98 | + import { dataURLtoBlob } from '/@/utils/file/base64Conver'; | |
99 | + import { isFunction } from '/@/utils/is'; | |
100 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
101 | + | |
102 | + const props = { | |
103 | + circled: { type: Boolean, default: true }, | |
104 | + uploadApi: { | |
105 | + type: Function as PropType<({ file: Blob, name: stirng, filename: string }) => Promise<any>>, | |
106 | + }, | |
107 | + }; | |
108 | + | |
109 | + export default defineComponent({ | |
110 | + name: 'CropperAvatar', | |
111 | + components: { BasicModal, Space, CropperImage, Upload, Avatar }, | |
112 | + props, | |
113 | + emits: ['uploadSuccess', 'register'], | |
114 | + setup(props, { emit }) { | |
115 | + let filename = ''; | |
116 | + const src = ref(''); | |
117 | + const previewSource = ref(''); | |
118 | + const cropper = ref<Cropper>(); | |
119 | + let scaleX = 1; | |
120 | + let scaleY = 1; | |
121 | + | |
122 | + const { prefixCls } = useDesign('cropper-am'); | |
123 | + const [register, { closeModal, setModalProps }] = useModalInner(); | |
124 | + const { t } = useI18n(); | |
125 | + | |
126 | + // Block upload | |
127 | + function handleBeforeUpload(file: File) { | |
128 | + const reader = new FileReader(); | |
129 | + reader.readAsDataURL(file); | |
130 | + src.value = ''; | |
131 | + previewSource.value = ''; | |
132 | + reader.onload = function (e) { | |
133 | + src.value = (e.target?.result as string) ?? ''; | |
134 | + filename = file.name; | |
135 | + }; | |
136 | + return false; | |
137 | + } | |
138 | + | |
139 | + function handleCropend({ imgBase64 }: CropendResult) { | |
140 | + previewSource.value = imgBase64; | |
141 | + } | |
142 | + | |
143 | + function handleReady(cropperInstance: Cropper) { | |
144 | + cropper.value = cropperInstance; | |
145 | + } | |
146 | + | |
147 | + function handlerToolbar(event: string, arg?: number) { | |
148 | + if (event === 'scaleX') { | |
149 | + scaleX = arg = scaleX === -1 ? 1 : -1; | |
150 | + } | |
151 | + if (event === 'scaleY') { | |
152 | + scaleY = arg = scaleY === -1 ? 1 : -1; | |
153 | + } | |
154 | + cropper?.value?.[event]?.(arg); | |
155 | + } | |
156 | + | |
157 | + async function handleOk() { | |
158 | + const uploadApi = props.uploadApi; | |
159 | + if (uploadApi && isFunction(uploadApi)) { | |
160 | + const blob = dataURLtoBlob(previewSource.value); | |
161 | + try { | |
162 | + setModalProps({ confirmLoading: true }); | |
163 | + const result = await uploadApi({ name: 'file', file: blob, filename }); | |
164 | + emit('uploadSuccess', { source: previewSource.value, data: result.data }); | |
165 | + closeModal(); | |
166 | + } finally { | |
167 | + setModalProps({ confirmLoading: false }); | |
168 | + } | |
169 | + } | |
170 | + } | |
171 | + | |
172 | + return { | |
173 | + t, | |
174 | + prefixCls, | |
175 | + src, | |
176 | + register, | |
177 | + previewSource, | |
178 | + handleBeforeUpload, | |
179 | + handleCropend, | |
180 | + handleReady, | |
181 | + handlerToolbar, | |
182 | + handleOk, | |
183 | + }; | |
184 | + }, | |
185 | + }); | |
186 | +</script> | |
187 | + | |
188 | +<style lang="less"> | |
189 | + @prefix-cls: ~'@{namespace}-cropper-am'; | |
190 | + | |
191 | + .@{prefix-cls} { | |
192 | + display: flex; | |
193 | + | |
194 | + &-left, | |
195 | + &-right { | |
196 | + height: 340px; | |
197 | + } | |
198 | + | |
199 | + &-left { | |
200 | + width: 55%; | |
201 | + } | |
202 | + | |
203 | + &-right { | |
204 | + width: 45%; | |
205 | + } | |
206 | + | |
207 | + &-cropper { | |
208 | + height: 300px; | |
209 | + background: #eee; | |
210 | + background-image: linear-gradient( | |
211 | + 45deg, | |
212 | + rgba(0, 0, 0, 0.25) 25%, | |
213 | + transparent 0, | |
214 | + transparent 75%, | |
215 | + rgba(0, 0, 0, 0.25) 0 | |
216 | + ), | |
217 | + linear-gradient( | |
218 | + 45deg, | |
219 | + rgba(0, 0, 0, 0.25) 25%, | |
220 | + transparent 0, | |
221 | + transparent 75%, | |
222 | + rgba(0, 0, 0, 0.25) 0 | |
223 | + ); | |
224 | + background-position: 0 0, 12px 12px; | |
225 | + background-size: 24px 24px; | |
226 | + } | |
227 | + | |
228 | + &-toolbar { | |
229 | + display: flex; | |
230 | + justify-content: space-between; | |
231 | + align-items: center; | |
232 | + margin-top: 10px; | |
233 | + } | |
234 | + | |
235 | + &-preview { | |
236 | + width: 220px; | |
237 | + height: 220px; | |
238 | + margin: 0 auto; | |
239 | + overflow: hidden; | |
240 | + border: 1px solid @border-color-base; | |
241 | + border-radius: 50%; | |
242 | + | |
243 | + img { | |
244 | + width: 100%; | |
245 | + height: 100%; | |
246 | + } | |
247 | + } | |
248 | + | |
249 | + &-group { | |
250 | + display: flex; | |
251 | + padding-top: 8px; | |
252 | + margin-top: 8px; | |
253 | + border-top: 1px solid @border-color-base; | |
254 | + justify-content: space-around; | |
255 | + align-items: center; | |
256 | + } | |
257 | + } | |
258 | +</style> | ... | ... |
src/components/Cropper/src/Cropper.vue
1 | 1 | <template> |
2 | - <div :class="$attrs.class" :style="getWrapperStyle"> | |
2 | + <div :class="getClass" :style="getWrapperStyle"> | |
3 | 3 | <img |
4 | 4 | v-show="isReady" |
5 | 5 | ref="imgElRef" |
... | ... | @@ -12,16 +12,16 @@ |
12 | 12 | </template> |
13 | 13 | <script lang="ts"> |
14 | 14 | import type { CSSProperties } from 'vue'; |
15 | - | |
16 | 15 | import { defineComponent, onMounted, ref, unref, computed } from 'vue'; |
17 | - | |
18 | 16 | import Cropper from 'cropperjs'; |
19 | 17 | import 'cropperjs/dist/cropper.css'; |
18 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
19 | + import { useThrottleFn } from '@vueuse/shared'; | |
20 | 20 | |
21 | 21 | type Options = Cropper.Options; |
22 | 22 | |
23 | 23 | const defaultOptions: Options = { |
24 | - aspectRatio: 16 / 9, | |
24 | + aspectRatio: 1, | |
25 | 25 | zoomable: true, |
26 | 26 | zoomOnTouch: true, |
27 | 27 | zoomOnWheel: true, |
... | ... | @@ -43,40 +43,33 @@ |
43 | 43 | rotatable: true, |
44 | 44 | }; |
45 | 45 | |
46 | - export default defineComponent({ | |
47 | - name: 'CropperImage', | |
48 | - props: { | |
49 | - src: { | |
50 | - type: String, | |
51 | - required: true, | |
52 | - }, | |
53 | - alt: { | |
54 | - type: String, | |
55 | - }, | |
56 | - height: { | |
57 | - type: [String, Number], | |
58 | - default: '360px', | |
59 | - }, | |
60 | - crossorigin: { | |
61 | - type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>, | |
62 | - default: undefined, | |
63 | - }, | |
64 | - imageStyle: { | |
65 | - type: Object as PropType<CSSProperties>, | |
66 | - default: () => ({}), | |
67 | - }, | |
68 | - options: { | |
69 | - type: Object as PropType<Options>, | |
70 | - default: () => ({}), | |
71 | - }, | |
46 | + const props = { | |
47 | + src: { type: String, required: true }, | |
48 | + alt: { type: String }, | |
49 | + circled: { type: Boolean, default: false }, | |
50 | + | |
51 | + realTimePreview: { type: Boolean, default: true }, | |
52 | + height: { type: [String, Number], default: '360px' }, | |
53 | + crossorigin: { | |
54 | + type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>, | |
55 | + default: undefined, | |
72 | 56 | }, |
73 | - emits: ['cropperedInfo'], | |
74 | - setup(props, ctx) { | |
75 | - const imgElRef = ref<ElRef<HTMLImageElement>>(null); | |
76 | - const cropper: any = ref<Nullable<Cropper>>(null); | |
57 | + imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) }, | |
58 | + options: { type: Object as PropType<Options>, default: () => ({}) }, | |
59 | + }; | |
77 | 60 | |
61 | + export default defineComponent({ | |
62 | + name: 'CropperImage', | |
63 | + props, | |
64 | + emits: ['cropend', 'ready', 'cropendError'], | |
65 | + setup(props, { attrs, emit }) { | |
66 | + const imgElRef = ref<ElRef<HTMLImageElement>>(); | |
67 | + const cropper = ref<Nullable<Cropper>>(); | |
78 | 68 | const isReady = ref(false); |
79 | 69 | |
70 | + const { prefixCls } = useDesign('cropper-image'); | |
71 | + const throttleRealTimeCroppered = useThrottleFn(realTimeCroppered, 80); | |
72 | + | |
80 | 73 | const getImageStyle = computed((): CSSProperties => { |
81 | 74 | return { |
82 | 75 | height: props.height, |
... | ... | @@ -85,11 +78,22 @@ |
85 | 78 | }; |
86 | 79 | }); |
87 | 80 | |
81 | + const getClass = computed(() => { | |
82 | + return [ | |
83 | + prefixCls, | |
84 | + attrs.class, | |
85 | + { | |
86 | + [`${prefixCls}--circled`]: props.circled, | |
87 | + }, | |
88 | + ]; | |
89 | + }); | |
90 | + | |
88 | 91 | const getWrapperStyle = computed((): CSSProperties => { |
89 | - const { height } = props; | |
90 | - return { height: `${height}`.replace(/px/, '') + 'px' }; | |
92 | + return { height: `${props.height}`.replace(/px/, '') + 'px' }; | |
91 | 93 | }); |
92 | 94 | |
95 | + onMounted(init); | |
96 | + | |
93 | 97 | async function init() { |
94 | 98 | const imgEl = unref(imgElRef); |
95 | 99 | if (!imgEl) { |
... | ... | @@ -99,29 +103,83 @@ |
99 | 103 | ...defaultOptions, |
100 | 104 | ready: () => { |
101 | 105 | isReady.value = true; |
106 | + realTimeCroppered(); | |
107 | + emit('ready', cropper.value); | |
108 | + }, | |
109 | + crop() { | |
110 | + throttleRealTimeCroppered(); | |
111 | + }, | |
112 | + zoom() { | |
113 | + throttleRealTimeCroppered(); | |
114 | + }, | |
115 | + cropmove() { | |
116 | + throttleRealTimeCroppered(); | |
102 | 117 | }, |
103 | 118 | ...props.options, |
104 | 119 | }); |
105 | 120 | } |
106 | 121 | |
122 | + // Real-time display preview | |
123 | + function realTimeCroppered() { | |
124 | + props.realTimePreview && croppered(); | |
125 | + } | |
126 | + | |
107 | 127 | // event: return base64 and width and height information after cropping |
108 | - const croppered = (): void => { | |
128 | + function croppered() { | |
129 | + if (!cropper.value) { | |
130 | + return; | |
131 | + } | |
109 | 132 | let imgInfo = cropper.value.getData(); |
110 | - cropper.value.getCroppedCanvas().toBlob((blob) => { | |
133 | + const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas(); | |
134 | + canvas.toBlob((blob) => { | |
135 | + if (!blob) { | |
136 | + return; | |
137 | + } | |
111 | 138 | let fileReader: FileReader = new FileReader(); |
139 | + fileReader.readAsDataURL(blob); | |
112 | 140 | fileReader.onloadend = (e) => { |
113 | - ctx.emit('cropperedInfo', { | |
141 | + emit('cropend', { | |
114 | 142 | imgBase64: e.target?.result ?? '', |
115 | 143 | imgInfo, |
116 | 144 | }); |
117 | 145 | }; |
118 | - fileReader.readAsDataURL(blob); | |
119 | - }, 'image/jpeg'); | |
120 | - }; | |
146 | + fileReader.onerror = () => { | |
147 | + emit('cropendError'); | |
148 | + }; | |
149 | + }, 'image/png'); | |
150 | + } | |
121 | 151 | |
122 | - onMounted(init); | |
152 | + // Get a circular picture canvas | |
153 | + function getRoundedCanvas() { | |
154 | + const sourceCanvas = cropper.value!.getCroppedCanvas(); | |
155 | + const canvas = document.createElement('canvas'); | |
156 | + const context = canvas.getContext('2d')!; | |
157 | + const width = sourceCanvas.width; | |
158 | + const height = sourceCanvas.height; | |
159 | + canvas.width = width; | |
160 | + canvas.height = height; | |
161 | + context.imageSmoothingEnabled = true; | |
162 | + context.drawImage(sourceCanvas, 0, 0, width, height); | |
163 | + context.globalCompositeOperation = 'destination-in'; | |
164 | + context.beginPath(); | |
165 | + context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true); | |
166 | + context.fill(); | |
167 | + return canvas; | |
168 | + } | |
123 | 169 | |
124 | - return { imgElRef, getWrapperStyle, getImageStyle, isReady, croppered }; | |
170 | + return { getClass, imgElRef, getWrapperStyle, getImageStyle, isReady, croppered }; | |
125 | 171 | }, |
126 | 172 | }); |
127 | 173 | </script> |
174 | +<style lang="less"> | |
175 | + @prefix-cls: ~'@{namespace}-cropper-image'; | |
176 | + | |
177 | + .@{prefix-cls} { | |
178 | + &--circled { | |
179 | + .cropper-view-box, | |
180 | + .cropper-face { | |
181 | + border-radius: 50%; | |
182 | + } | |
183 | + } | |
184 | + } | |
185 | +</style> | ... | ... |
src/components/Cropper/src/CropperAvatar.vue
0 → 100644
1 | +<template> | |
2 | + <div :class="getClass" :style="getStyle"> | |
3 | + <div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal"> | |
4 | + <img :src="sourceValue" v-if="sourceValue" alt="avatar" /> | |
5 | + </div> | |
6 | + <a-button :class="`${prefixCls}-upload-btn`" @click="openModal"> | |
7 | + {{ t('component.cropper.selectImage') }} | |
8 | + </a-button> | |
9 | + <CopperModal @register="register" @uploadSuccess="handleUploadSuccess" :uploadApi="uploadApi" /> | |
10 | + </div> | |
11 | +</template> | |
12 | +<script lang="ts"> | |
13 | + import { defineComponent, computed, CSSProperties, unref, ref } from 'vue'; | |
14 | + import CopperModal from './CopperModal.vue'; | |
15 | + import { useDesign } from '/@/hooks/web/useDesign'; | |
16 | + import { useModal } from '/@/components/Modal'; | |
17 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
18 | + import { useI18n } from '/@/hooks/web/useI18n'; | |
19 | + | |
20 | + const props = { | |
21 | + src: { type: String, required: true }, | |
22 | + width: { type: [String, Number], default: '200px' }, | |
23 | + uploadApi: { type: Function as PropType<({ file: Blob, name: stirng }) => Promise<void>> }, | |
24 | + }; | |
25 | + | |
26 | + export default defineComponent({ | |
27 | + name: 'CropperAvatar', | |
28 | + components: { CopperModal }, | |
29 | + props, | |
30 | + setup(props) { | |
31 | + const sourceValue = ref(''); | |
32 | + const { prefixCls } = useDesign('cropper-avatar'); | |
33 | + const [register, { openModal }] = useModal(); | |
34 | + const { createMessage } = useMessage(); | |
35 | + const { t } = useI18n(); | |
36 | + | |
37 | + const getClass = computed(() => [prefixCls]); | |
38 | + | |
39 | + const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px'); | |
40 | + | |
41 | + const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) })); | |
42 | + | |
43 | + const getImageWrapperStyle = computed( | |
44 | + (): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }) | |
45 | + ); | |
46 | + | |
47 | + function handleUploadSuccess({ source }) { | |
48 | + sourceValue.value = source; | |
49 | + createMessage.success(t('component.cropper.uploadSuccess')); | |
50 | + } | |
51 | + | |
52 | + return { | |
53 | + t, | |
54 | + prefixCls, | |
55 | + register, | |
56 | + openModal, | |
57 | + sourceValue, | |
58 | + getClass, | |
59 | + getImageWrapperStyle, | |
60 | + getStyle, | |
61 | + handleUploadSuccess, | |
62 | + }; | |
63 | + }, | |
64 | + }); | |
65 | +</script> | |
66 | + | |
67 | +<style lang="less" scoped> | |
68 | + @prefix-cls: ~'@{namespace}-cropper-avatar'; | |
69 | + | |
70 | + .@{prefix-cls} { | |
71 | + display: inline-block; | |
72 | + text-align: center; | |
73 | + | |
74 | + &-image-wrapper { | |
75 | + overflow: hidden; | |
76 | + cursor: pointer; | |
77 | + background: @component-background; | |
78 | + border: 1px solid @border-color-base; | |
79 | + border-radius: 50%; | |
80 | + | |
81 | + img { | |
82 | + width: 100%; | |
83 | + } | |
84 | + } | |
85 | + | |
86 | + &-upload-btn { | |
87 | + margin: 10px auto; | |
88 | + } | |
89 | + } | |
90 | +</style> | ... | ... |
src/components/Cropper/src/typing.ts
0 → 100644
src/locales/lang/en/component.ts
... | ... | @@ -8,6 +8,12 @@ export default { |
8 | 8 | normalText: 'Get SMS code', |
9 | 9 | sendText: 'Reacquire in {0}s', |
10 | 10 | }, |
11 | + cropper: { | |
12 | + selectImage: 'Select Image', | |
13 | + uploadSuccess: 'Uploaded success!', | |
14 | + modalTitle: 'Avatar upload', | |
15 | + okText: 'Confirm and upload', | |
16 | + }, | |
11 | 17 | drawer: { |
12 | 18 | loadingText: 'Loading...', |
13 | 19 | cancelText: 'Close', | ... | ... |
src/locales/lang/zh_CN/component.ts
... | ... | @@ -8,6 +8,12 @@ export default { |
8 | 8 | normalText: '获取验证码', |
9 | 9 | sendText: '{0}秒后重新获取', |
10 | 10 | }, |
11 | + cropper: { | |
12 | + selectImage: '选择图片', | |
13 | + uploadSuccess: '上传成功', | |
14 | + modalTitle: '头像上传', | |
15 | + okText: '确认并上传', | |
16 | + }, | |
11 | 17 | drawer: { |
12 | 18 | loadingText: '加载中...', |
13 | 19 | cancelText: '关闭', | ... | ... |
src/router/menus/modules/demo/comp.ts
... | ... | @@ -6,6 +6,7 @@ const menu: MenuModule = { |
6 | 6 | menu: { |
7 | 7 | name: t('routes.demo.comp.comp'), |
8 | 8 | path: '/comp', |
9 | + tag: { dot: true }, | |
9 | 10 | children: [ |
10 | 11 | { |
11 | 12 | path: 'basic', |
... | ... | @@ -124,6 +125,9 @@ const menu: MenuModule = { |
124 | 125 | { |
125 | 126 | path: 'cropper', |
126 | 127 | name: t('routes.demo.comp.cropperImage'), |
128 | + tag: { | |
129 | + content: 'new', | |
130 | + }, | |
127 | 131 | }, |
128 | 132 | { |
129 | 133 | path: 'countTo', |
... | ... | @@ -192,9 +196,6 @@ const menu: MenuModule = { |
192 | 196 | { |
193 | 197 | path: 'json', |
194 | 198 | name: t('routes.demo.editor.jsonEditor'), |
195 | - tag: { | |
196 | - content: 'new', | |
197 | - }, | |
198 | 199 | }, |
199 | 200 | { |
200 | 201 | path: 'markdown', | ... | ... |
src/router/menus/modules/demo/feat.ts
... | ... | @@ -6,9 +6,6 @@ const menu: MenuModule = { |
6 | 6 | menu: { |
7 | 7 | name: t('routes.demo.feat.feat'), |
8 | 8 | path: '/feat', |
9 | - tag: { | |
10 | - dot: true, | |
11 | - }, | |
12 | 9 | children: [ |
13 | 10 | { |
14 | 11 | path: 'icon', |
... | ... | @@ -21,9 +18,6 @@ const menu: MenuModule = { |
21 | 18 | { |
22 | 19 | name: t('routes.demo.feat.sessionTimeout'), |
23 | 20 | path: 'session-timeout', |
24 | - tag: { | |
25 | - content: 'new', | |
26 | - }, | |
27 | 21 | }, |
28 | 22 | { |
29 | 23 | path: 'tabs', | ... | ... |
src/views/demo/comp/cropper/index.vue
1 | 1 | <template> |
2 | - <PageWrapper title="图片裁剪示例" contentBackground> | |
3 | - <div class="container"> | |
4 | - <div class="cropper-container"> | |
5 | - <CropperImage | |
6 | - ref="refCropper" | |
7 | - src="https://fengyuanchen.github.io/cropperjs/images/picture.jpg" | |
8 | - @cropperedInfo="cropperedInfo" | |
9 | - style="width: 40vw" | |
10 | - /> | |
2 | + <PageWrapper title="图片裁剪示例" content="需要开启测试接口服务才能进行上传测试!"> | |
3 | + <CollapseContainer title="头像裁剪"> | |
4 | + <CropperAvatar :src="cropperImg" :uploadApi="uploadApi" /> | |
5 | + </CollapseContainer> | |
6 | + | |
7 | + <CollapseContainer title="矩形裁剪" class="my-4"> | |
8 | + <div class="container p-4"> | |
9 | + <div class="cropper-container mr-10"> | |
10 | + <CropperImage ref="refCropper" :src="img" @cropend="handleCropend" style="width: 40vw" /> | |
11 | + </div> | |
12 | + <img :src="cropperImg" class="croppered" v-if="cropperImg" /> | |
13 | + </div> | |
14 | + <p v-if="cropperImg">裁剪后图片信息:{{ info }}</p> | |
15 | + </CollapseContainer> | |
16 | + | |
17 | + <CollapseContainer title="圆形裁剪"> | |
18 | + <div class="container p-4"> | |
19 | + <div class="cropper-container mr-10"> | |
20 | + <CropperImage | |
21 | + ref="refCropper" | |
22 | + :src="img" | |
23 | + @cropend="handleCircleCropend" | |
24 | + style="width: 40vw" | |
25 | + circled | |
26 | + /> | |
27 | + </div> | |
28 | + <img :src="circleImg" class="croppered" v-if="circleImg" /> | |
11 | 29 | </div> |
12 | - <a-button type="primary" @click="onCropper"> 裁剪 </a-button> | |
13 | - <img :src="cropperImg" class="croppered" v-if="cropperImg" /> | |
14 | - </div> | |
15 | - <p v-if="cropperImg">裁剪后图片信息:{{ info }}</p> | |
30 | + <p v-if="circleImg">裁剪后图片信息:{{ circleInfo }}</p> | |
31 | + </CollapseContainer> | |
16 | 32 | </PageWrapper> |
17 | 33 | </template> |
18 | 34 | <script lang="ts"> |
19 | - import { defineComponent, ref, unref } from 'vue'; | |
35 | + import { defineComponent, ref } from 'vue'; | |
20 | 36 | import { PageWrapper } from '/@/components/Page'; |
21 | - import { CropperImage } from '/@/components/Cropper'; | |
37 | + import { CollapseContainer } from '/@/components/Container/index'; | |
38 | + import { CropperImage, CropperAvatar } from '/@/components/Cropper'; | |
39 | + import { uploadApi } from '/@/api/sys/upload'; | |
22 | 40 | import img from '/@/assets/images/header.jpg'; |
23 | - import { templateRef } from '@vueuse/core'; | |
24 | 41 | |
25 | 42 | export default defineComponent({ |
26 | 43 | components: { |
27 | 44 | PageWrapper, |
28 | 45 | CropperImage, |
46 | + CollapseContainer, | |
47 | + CropperAvatar, | |
29 | 48 | }, |
30 | 49 | setup() { |
31 | - let info = ref(''); | |
32 | - let cropperImg = ref(''); | |
33 | - const refCropper = templateRef<HTMLElement | null>('refCropper', null); | |
50 | + const info = ref(''); | |
51 | + const cropperImg = ref(''); | |
52 | + const circleInfo = ref(''); | |
53 | + const circleImg = ref(''); | |
34 | 54 | |
35 | - const onCropper = (): void => { | |
36 | - unref(refCropper).croppered(); | |
37 | - }; | |
38 | - | |
39 | - function cropperedInfo({ imgBase64, imgInfo }) { | |
55 | + function handleCropend({ imgBase64, imgInfo }) { | |
40 | 56 | info.value = imgInfo; |
41 | 57 | cropperImg.value = imgBase64; |
42 | 58 | } |
43 | 59 | |
60 | + function handleCircleCropend({ imgBase64, imgInfo }) { | |
61 | + circleInfo.value = imgInfo; | |
62 | + circleImg.value = imgBase64; | |
63 | + } | |
64 | + | |
44 | 65 | return { |
45 | 66 | img, |
46 | 67 | info, |
68 | + circleInfo, | |
47 | 69 | cropperImg, |
48 | - onCropper, | |
49 | - cropperedInfo, | |
70 | + circleImg, | |
71 | + handleCropend, | |
72 | + handleCircleCropend, | |
73 | + uploadApi, | |
50 | 74 | }; |
51 | 75 | }, |
52 | 76 | }); | ... | ... |
src/views/demo/comp/upload/index.vue
... | ... | @@ -21,7 +21,6 @@ |
21 | 21 | import { BasicForm, FormSchema, useForm } from '/@/components/Form/index'; |
22 | 22 | import { PageWrapper } from '/@/components/Page'; |
23 | 23 | import { Alert } from 'ant-design-vue'; |
24 | - | |
25 | 24 | import { uploadApi } from '/@/api/sys/upload'; |
26 | 25 | |
27 | 26 | const schemas: FormSchema[] = [ | ... | ... |
src/views/demo/feat/copy/index.vue
... | ... | @@ -14,11 +14,10 @@ |
14 | 14 | import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard'; |
15 | 15 | import { useMessage } from '/@/hooks/web/useMessage'; |
16 | 16 | import { PageWrapper } from '/@/components/Page'; |
17 | - import { Input } from 'ant-design-vue'; | |
18 | 17 | |
19 | 18 | export default defineComponent({ |
20 | 19 | name: 'Copy', |
21 | - components: { CollapseContainer, PageWrapper, [Input.name]: Input }, | |
20 | + components: { CollapseContainer, PageWrapper }, | |
22 | 21 | setup() { |
23 | 22 | const valueRef = ref(''); |
24 | 23 | const { createMessage } = useMessage(); | ... | ... |
test/server/service/FileService.ts
... | ... | @@ -10,8 +10,6 @@ export default class UserService { |
10 | 10 | let fileReader, fileResource, writeStream; |
11 | 11 | |
12 | 12 | const fileFunc = function (file) { |
13 | - console.log(file); | |
14 | - | |
15 | 13 | fileReader = fs.createReadStream(file.path); |
16 | 14 | fileResource = filePath + `/${file.name}`; |
17 | 15 | console.log(fileResource); | ... | ... |
yarn.lock
... | ... | @@ -2188,10 +2188,10 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: |
2188 | 2188 | dependencies: |
2189 | 2189 | color-convert "^2.0.1" |
2190 | 2190 | |
2191 | -ant-design-vue@2.1.2: | |
2192 | - version "2.1.2" | |
2193 | - resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.1.2.tgz#2065d7e63199c0c584919458af57b6a0b597f677" | |
2194 | - integrity sha512-gDG0wauGVt4LE63behrJaIcq4BB+dgs+dpj9jz17IgKr2MPYSEeKetU/x9Kk8d58cGonz4Ulncg7fBZJ7EljsQ== | |
2191 | +ant-design-vue@2.1.6: | |
2192 | + version "2.1.6" | |
2193 | + resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.1.6.tgz#c51cdc858e1b1b8b569f5435eb487f53a3f1745e" | |
2194 | + integrity sha512-qICxb6Y4f7QuSuh/jbLhZA9SkUBnP9xYfy/E6yD7+1fg04aAzmRK8oLv8ETuGTrROVdSVeic9v/NS2BXEuuARg== | |
2195 | 2195 | dependencies: |
2196 | 2196 | "@ant-design-vue/use" "^0.0.1-0" |
2197 | 2197 | "@ant-design/icons-vue" "^6.0.0" |
... | ... | @@ -2201,7 +2201,7 @@ ant-design-vue@2.1.2: |
2201 | 2201 | async-validator "^3.3.0" |
2202 | 2202 | dom-align "^1.10.4" |
2203 | 2203 | dom-scroll-into-view "^2.0.0" |
2204 | - is-mobile "^2.2.1" | |
2204 | + lodash "^4.17.21" | |
2205 | 2205 | lodash-es "^4.17.15" |
2206 | 2206 | moment "^2.27.0" |
2207 | 2207 | omit.js "^2.0.0" |
... | ... | @@ -6079,11 +6079,6 @@ is-jpg@^2.0.0: |
6079 | 6079 | resolved "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz#2e1997fa6e9166eaac0242daae443403e4ef1d97" |
6080 | 6080 | integrity sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc= |
6081 | 6081 | |
6082 | -is-mobile@^2.2.1: | |
6083 | - version "2.2.2" | |
6084 | - resolved "https://registry.npmjs.org/is-mobile/-/is-mobile-2.2.2.tgz#f6c9c5d50ee01254ce05e739bdd835f1ed4e9954" | |
6085 | - integrity sha512-wW/SXnYJkTjs++tVK5b6kVITZpAZPtUrt9SF80vvxGiF/Oywal+COk1jlRkiVq15RFNEQKQY31TkV24/1T5cVg== | |
6086 | - | |
6087 | 6082 | is-module@^1.0.0: |
6088 | 6083 | version "1.0.0" |
6089 | 6084 | resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" | ... | ... |