Showing
3 changed files
with
187 additions
and
221 deletions
@@ -179,14 +179,22 @@ | @@ -179,14 +179,22 @@ | ||
179 | cropper?.value?.[event]?.(arg); | 179 | cropper?.value?.[event]?.(arg); |
180 | } | 180 | } |
181 | 181 | ||
182 | + const blobToFile = (blob, fileName) => { | ||
183 | + const file = new File([blob], fileName, { type: blob.type }); | ||
184 | + const formData = new FormData(); | ||
185 | + formData.append('file', file); | ||
186 | + return formData; | ||
187 | + }; | ||
188 | + | ||
182 | async function handleOk() { | 189 | async function handleOk() { |
183 | const uploadApi = props.uploadApi; | 190 | const uploadApi = props.uploadApi; |
184 | if (uploadApi && isFunction(uploadApi)) { | 191 | if (uploadApi && isFunction(uploadApi)) { |
185 | const blob = dataURLtoBlob(previewSource.value); | 192 | const blob = dataURLtoBlob(previewSource.value); |
193 | + const base64D = blobToFile(blob, filename); | ||
186 | try { | 194 | try { |
187 | setModalProps({ confirmLoading: true }); | 195 | setModalProps({ confirmLoading: true }); |
188 | - const result = await uploadApi({ name: 'file', file: blob, filename }); | ||
189 | - emit('uploadSuccess', { source: previewSource.value, data: result.data }); | 196 | + const result = await uploadApi(base64D); |
197 | + emit('uploadSuccess', { source: previewSource.value, data: result.fileStaticUri }); | ||
190 | closeModal(); | 198 | closeModal(); |
191 | } finally { | 199 | } finally { |
192 | setModalProps({ confirmLoading: false }); | 200 | setModalProps({ confirmLoading: false }); |
1 | -<template> | ||
2 | - <div :class="getClass" :style="getStyle"> | ||
3 | - <div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal"> | ||
4 | - <div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle"> | ||
5 | - <Icon | ||
6 | - icon="ant-design:cloud-upload-outlined" | ||
7 | - :size="getIconWidth" | ||
8 | - :style="getImageWrapperStyle" | ||
9 | - color="#d6d6d6" | ||
10 | - /> | ||
11 | - </div> | ||
12 | - <img :src="sourceValue" v-if="sourceValue" alt="avatar" /> | ||
13 | - </div> | ||
14 | - <a-button | ||
15 | - :class="`${prefixCls}-upload-btn`" | ||
16 | - @click="openModal" | ||
17 | - v-if="showBtn" | ||
18 | - v-bind="btnProps" | ||
19 | - > | ||
20 | - {{ btnText ? btnText : t('component.cropper.selectImage') }} | ||
21 | - </a-button> | ||
22 | - | ||
23 | - <CopperModal | ||
24 | - @register="register" | ||
25 | - @uploadSuccess="handleUploadSuccess" | ||
26 | - :uploadApi="uploadApi" | ||
27 | - :src="sourceValue" | ||
28 | - /> | ||
29 | - </div> | ||
30 | -</template> | ||
31 | -<script lang="ts"> | ||
32 | - import { | ||
33 | - defineComponent, | ||
34 | - computed, | ||
35 | - CSSProperties, | ||
36 | - unref, | ||
37 | - ref, | ||
38 | - watchEffect, | ||
39 | - watch, | ||
40 | - PropType, | ||
41 | - } from 'vue'; | ||
42 | - import CopperModal from './CopperModal.vue'; | ||
43 | - import { useDesign } from '/@/hooks/web/useDesign'; | ||
44 | - import { useModal } from '/@/components/Modal'; | ||
45 | - import { useMessage } from '/@/hooks/web/useMessage'; | ||
46 | - import { useI18n } from '/@/hooks/web/useI18n'; | ||
47 | - import type { ButtonProps } from '/@/components/Button'; | ||
48 | - import Icon from '/@/components/Icon'; | ||
49 | - | ||
50 | - const props = { | ||
51 | - width: { type: [String, Number], default: '200px' }, | ||
52 | - value: { type: String }, | ||
53 | - showBtn: { type: Boolean, default: true }, | ||
54 | - btnProps: { type: Object as PropType<ButtonProps> }, | ||
55 | - btnText: { type: String, default: '' }, | ||
56 | - uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> }, | ||
57 | - }; | ||
58 | - | ||
59 | - export default defineComponent({ | ||
60 | - name: 'CropperAvatar', | ||
61 | - components: { CopperModal, Icon }, | ||
62 | - props, | ||
63 | - emits: ['update:value', 'change'], | ||
64 | - setup(props, { emit, expose }) { | ||
65 | - const sourceValue = ref(props.value || ''); | ||
66 | - const { prefixCls } = useDesign('cropper-avatar'); | ||
67 | - const [register, { openModal, closeModal }] = useModal(); | ||
68 | - const { createMessage } = useMessage(); | ||
69 | - const { t } = useI18n(); | ||
70 | - | ||
71 | - const getClass = computed(() => [prefixCls]); | ||
72 | - | ||
73 | - const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px'); | ||
74 | - | ||
75 | - const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px'); | ||
76 | - | ||
77 | - const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) })); | ||
78 | - | ||
79 | - const getImageWrapperStyle = computed( | ||
80 | - (): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }) | ||
81 | - ); | ||
82 | - | ||
83 | - watchEffect(() => { | ||
84 | - sourceValue.value = props.value || ''; | ||
85 | - }); | ||
86 | - | ||
87 | - watch( | ||
88 | - () => sourceValue.value, | ||
89 | - (v: string) => { | ||
90 | - emit('update:value', v); | ||
91 | - } | ||
92 | - ); | ||
93 | - | ||
94 | - function handleUploadSuccess(source) { | ||
95 | - sourceValue.value = source; | ||
96 | - emit('change', source); | ||
97 | - createMessage.success(t('component.cropper.uploadSuccess')); | ||
98 | - } | ||
99 | - | ||
100 | - expose({ openModal: openModal.bind(null, true), closeModal }); | ||
101 | - | ||
102 | - return { | ||
103 | - t, | ||
104 | - prefixCls, | ||
105 | - register, | ||
106 | - openModal, | ||
107 | - getIconWidth, | ||
108 | - sourceValue, | ||
109 | - getClass, | ||
110 | - getImageWrapperStyle, | ||
111 | - getStyle, | ||
112 | - handleUploadSuccess, | ||
113 | - }; | ||
114 | - }, | ||
115 | - }); | ||
116 | -</script> | ||
117 | - | ||
118 | -<style lang="less" scoped> | ||
119 | - @prefix-cls: ~'@{namespace}-cropper-avatar'; | ||
120 | - | ||
121 | - .@{prefix-cls} { | ||
122 | - display: inline-block; | ||
123 | - text-align: center; | ||
124 | - | ||
125 | - &-image-wrapper { | ||
126 | - overflow: hidden; | ||
127 | - cursor: pointer; | ||
128 | - background: @component-background; | ||
129 | - border: 1px solid @border-color-base; | ||
130 | - border-radius: 50%; | ||
131 | - | ||
132 | - img { | ||
133 | - width: 100%; | ||
134 | - } | ||
135 | - } | ||
136 | - | ||
137 | - &-image-mask { | ||
138 | - opacity: 0; | ||
139 | - position: absolute; | ||
140 | - width: inherit; | ||
141 | - height: inherit; | ||
142 | - border-radius: inherit; | ||
143 | - border: inherit; | ||
144 | - background: rgba(0, 0, 0, 0.4); | ||
145 | - cursor: pointer; | ||
146 | - -webkit-transition: opacity 0.4s; | ||
147 | - transition: opacity 0.4s; | ||
148 | - | ||
149 | - :deep(svg) { | ||
150 | - margin: auto; | ||
151 | - } | ||
152 | - } | ||
153 | - | ||
154 | - &-image-mask:hover { | ||
155 | - opacity: 40; | ||
156 | - } | ||
157 | - | ||
158 | - &-upload-btn { | ||
159 | - margin: 10px auto; | ||
160 | - } | ||
161 | - } | ||
162 | -</style> | 1 | +<template> |
2 | + <div :class="getClass" :style="getStyle"> | ||
3 | + <div :class="`${prefixCls}-image-wrapper`" :style="getImageWrapperStyle" @click="openModal"> | ||
4 | + <div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle"> | ||
5 | + <Icon | ||
6 | + icon="ant-design:cloud-upload-outlined" | ||
7 | + :size="getIconWidth" | ||
8 | + :style="getImageWrapperStyle" | ||
9 | + color="#d6d6d6" | ||
10 | + /> | ||
11 | + </div> | ||
12 | + <img :src="sourceValue" v-if="sourceValue" alt="avatar" /> | ||
13 | + </div> | ||
14 | + <a-button | ||
15 | + :class="`${prefixCls}-upload-btn`" | ||
16 | + @click="openModal" | ||
17 | + v-if="showBtn" | ||
18 | + v-bind="btnProps" | ||
19 | + > | ||
20 | + {{ btnText ? btnText : t('component.cropper.selectImage') }} | ||
21 | + </a-button> | ||
22 | + | ||
23 | + <CopperModal | ||
24 | + @register="register" | ||
25 | + @uploadSuccess="handleUploadSuccess" | ||
26 | + :uploadApi="uploadApi" | ||
27 | + :src="sourceValue" | ||
28 | + /> | ||
29 | + </div> | ||
30 | +</template> | ||
31 | +<script lang="ts"> | ||
32 | + import { | ||
33 | + defineComponent, | ||
34 | + computed, | ||
35 | + CSSProperties, | ||
36 | + unref, | ||
37 | + ref, | ||
38 | + watchEffect, | ||
39 | + watch, | ||
40 | + PropType, | ||
41 | + } from 'vue'; | ||
42 | + import CopperModal from './CopperModal.vue'; | ||
43 | + import { useDesign } from '/@/hooks/web/useDesign'; | ||
44 | + import { useModal } from '/@/components/Modal'; | ||
45 | + import { useMessage } from '/@/hooks/web/useMessage'; | ||
46 | + import { useI18n } from '/@/hooks/web/useI18n'; | ||
47 | + import type { ButtonProps } from '/@/components/Button'; | ||
48 | + import Icon from '/@/components/Icon'; | ||
49 | + | ||
50 | + const props = { | ||
51 | + width: { type: [String, Number], default: '200px' }, | ||
52 | + value: { type: String }, | ||
53 | + showBtn: { type: Boolean, default: true }, | ||
54 | + btnProps: { type: Object as PropType<ButtonProps> }, | ||
55 | + btnText: { type: String, default: '' }, | ||
56 | + uploadApi: { type: Function as PropType<({ file: Blob, name: string }) => Promise<void>> }, | ||
57 | + }; | ||
58 | + | ||
59 | + export default defineComponent({ | ||
60 | + name: 'CropperAvatar', | ||
61 | + components: { CopperModal, Icon }, | ||
62 | + props, | ||
63 | + emits: ['update:value', 'change'], | ||
64 | + setup(props, { emit, expose }) { | ||
65 | + const sourceValue = ref(props.value || ''); | ||
66 | + const { prefixCls } = useDesign('cropper-avatar'); | ||
67 | + const [register, { openModal, closeModal }] = useModal(); | ||
68 | + const { createMessage } = useMessage(); | ||
69 | + const { t } = useI18n(); | ||
70 | + | ||
71 | + const getClass = computed(() => [prefixCls]); | ||
72 | + | ||
73 | + const getWidth = computed(() => `${props.width}`.replace(/px/, '') + 'px'); | ||
74 | + | ||
75 | + const getIconWidth = computed(() => parseInt(`${props.width}`.replace(/px/, '')) / 2 + 'px'); | ||
76 | + | ||
77 | + const getStyle = computed((): CSSProperties => ({ width: unref(getWidth) })); | ||
78 | + | ||
79 | + const getImageWrapperStyle = computed( | ||
80 | + (): CSSProperties => ({ width: unref(getWidth), height: unref(getWidth) }) | ||
81 | + ); | ||
82 | + | ||
83 | + watchEffect(() => { | ||
84 | + sourceValue.value = props.value || ''; | ||
85 | + }); | ||
86 | + | ||
87 | + watch( | ||
88 | + () => sourceValue.value, | ||
89 | + (v: string) => { | ||
90 | + emit('update:value', v); | ||
91 | + } | ||
92 | + ); | ||
93 | + | ||
94 | + function handleUploadSuccess(source) { | ||
95 | + sourceValue.value = source.data; | ||
96 | + emit('change', source); | ||
97 | + createMessage.success(t('component.cropper.uploadSuccess')); | ||
98 | + } | ||
99 | + | ||
100 | + expose({ openModal: openModal.bind(null, true), closeModal }); | ||
101 | + | ||
102 | + return { | ||
103 | + t, | ||
104 | + prefixCls, | ||
105 | + register, | ||
106 | + openModal, | ||
107 | + getIconWidth, | ||
108 | + sourceValue, | ||
109 | + getClass, | ||
110 | + getImageWrapperStyle, | ||
111 | + getStyle, | ||
112 | + handleUploadSuccess, | ||
113 | + }; | ||
114 | + }, | ||
115 | + }); | ||
116 | +</script> | ||
117 | + | ||
118 | +<style lang="less" scoped> | ||
119 | + @prefix-cls: ~'@{namespace}-cropper-avatar'; | ||
120 | + | ||
121 | + .@{prefix-cls} { | ||
122 | + display: inline-block; | ||
123 | + text-align: center; | ||
124 | + | ||
125 | + &-image-wrapper { | ||
126 | + overflow: hidden; | ||
127 | + cursor: pointer; | ||
128 | + background: @component-background; | ||
129 | + border: 1px solid @border-color-base; | ||
130 | + border-radius: 50%; | ||
131 | + | ||
132 | + img { | ||
133 | + width: 100%; | ||
134 | + } | ||
135 | + } | ||
136 | + | ||
137 | + &-image-mask { | ||
138 | + opacity: 0; | ||
139 | + position: absolute; | ||
140 | + width: inherit; | ||
141 | + height: inherit; | ||
142 | + border-radius: inherit; | ||
143 | + border: inherit; | ||
144 | + background: rgba(0, 0, 0, 0.4); | ||
145 | + cursor: pointer; | ||
146 | + -webkit-transition: opacity 0.4s; | ||
147 | + transition: opacity 0.4s; | ||
148 | + | ||
149 | + :deep(svg) { | ||
150 | + margin: auto; | ||
151 | + } | ||
152 | + } | ||
153 | + | ||
154 | + &-image-mask:hover { | ||
155 | + opacity: 40; | ||
156 | + } | ||
157 | + | ||
158 | + &-upload-btn { | ||
159 | + margin: 10px auto; | ||
160 | + } | ||
161 | + } | ||
162 | +</style> |
@@ -13,30 +13,9 @@ | @@ -13,30 +13,9 @@ | ||
13 | <div class="text-center cursor-pointer"> | 13 | <div class="text-center cursor-pointer"> |
14 | <div class="text-left text-lg border-gray-200 p-2 border">个人头像</div> | 14 | <div class="text-left text-lg border-gray-200 p-2 border">个人头像</div> |
15 | <div class="flex items-center justify-center mt-4"> | 15 | <div class="flex items-center justify-center mt-4"> |
16 | - <Upload | ||
17 | - name="avatar" | ||
18 | - list-type="picture-card" | ||
19 | - class="round !flex justify-center items-center" | ||
20 | - :show-upload-list="false" | ||
21 | - :customRequest="customUpload" | ||
22 | - :before-upload="beforeUpload" | ||
23 | - > | ||
24 | - <img | ||
25 | - class="round" | ||
26 | - v-if="personalPicture || headerImg" | ||
27 | - :src="personalPicture || headerImg" | ||
28 | - alt="avatar" | ||
29 | - /> | ||
30 | - <div v-else> | ||
31 | - <div> | ||
32 | - <LoadingOutlined class="text-3xl" v-if="loading" /> | ||
33 | - <PlusOutlined v-else class="text-3xl" /> | ||
34 | - </div> | ||
35 | - </div> | ||
36 | - </Upload> | 16 | + <CropperAvatar @change="handleChange" :uploadApi="uploadApi" :value="personalPicture" /> |
37 | </div> | 17 | </div> |
38 | </div> | 18 | </div> |
39 | - | ||
40 | <Description @register="registerDesc" class="mt-8 p-4" /> | 19 | <Description @register="registerDesc" class="mt-8 p-4" /> |
41 | </div> | 20 | </div> |
42 | <div class="ml-4 border border-gray-200"> | 21 | <div class="ml-4 border border-gray-200"> |
@@ -49,22 +28,19 @@ | @@ -49,22 +28,19 @@ | ||
49 | </BasicModal> | 28 | </BasicModal> |
50 | </template> | 29 | </template> |
51 | <script lang="ts"> | 30 | <script lang="ts"> |
52 | - import { defineComponent, ref, unref } from 'vue'; | 31 | + import { defineComponent, ref } from 'vue'; |
53 | import { BasicModal, useModalInner } from '/@/components/Modal/index'; | 32 | import { BasicModal, useModalInner } from '/@/components/Modal/index'; |
54 | import { BasicForm, useForm } from '/@/components/Form/index'; | 33 | import { BasicForm, useForm } from '/@/components/Form/index'; |
55 | import { formSchema } from './config'; | 34 | import { formSchema } from './config'; |
56 | import { Description, DescItem, useDescription } from '/@/components/Description/index'; | 35 | import { Description, DescItem, useDescription } from '/@/components/Description/index'; |
57 | import { uploadApi, personalPut } from '/@/api/personal/index'; | 36 | import { uploadApi, personalPut } from '/@/api/personal/index'; |
58 | import { useMessage } from '/@/hooks/web/useMessage'; | 37 | import { useMessage } from '/@/hooks/web/useMessage'; |
59 | - import { Upload } from 'ant-design-vue'; | ||
60 | - import { PlusOutlined } from '@ant-design/icons-vue'; | ||
61 | import { useUserStore } from '/@/store/modules/user'; | 38 | import { useUserStore } from '/@/store/modules/user'; |
62 | - import type { FileItem } from '/@/components/Upload/src/typing'; | ||
63 | - import { LoadingOutlined } from '@ant-design/icons-vue'; | ||
64 | import headerImg from '/@/assets/images/logo.png'; | 39 | import headerImg from '/@/assets/images/logo.png'; |
65 | import { getMyInfo } from '/@/api/sys/user'; | 40 | import { getMyInfo } from '/@/api/sys/user'; |
66 | import { UserInfoModel } from '/@/api/sys/model/userModel'; | 41 | import { UserInfoModel } from '/@/api/sys/model/userModel'; |
67 | import { UserInfo } from '/#/store'; | 42 | import { UserInfo } from '/#/store'; |
43 | + import { CropperAvatar } from '/@/components/Cropper'; | ||
68 | 44 | ||
69 | const schema: DescItem[] = [ | 45 | const schema: DescItem[] = [ |
70 | { | 46 | { |
@@ -95,7 +71,12 @@ | @@ -95,7 +71,12 @@ | ||
95 | 71 | ||
96 | export default defineComponent({ | 72 | export default defineComponent({ |
97 | name: 'Index', | 73 | name: 'Index', |
98 | - components: { BasicModal, BasicForm, Description, Upload, PlusOutlined, LoadingOutlined }, | 74 | + components: { |
75 | + BasicModal, | ||
76 | + BasicForm, | ||
77 | + Description, | ||
78 | + CropperAvatar, | ||
79 | + }, | ||
99 | emits: ['refreshPersonal', 'register'], | 80 | emits: ['refreshPersonal', 'register'], |
100 | setup(_, { emit }) { | 81 | setup(_, { emit }) { |
101 | const loading = ref(false); | 82 | const loading = ref(false); |
@@ -111,31 +92,8 @@ | @@ -111,31 +92,8 @@ | ||
111 | column: 1, | 92 | column: 1, |
112 | bordered: true, | 93 | bordered: true, |
113 | }); | 94 | }); |
114 | - | ||
115 | - const customUpload = async ({ file }) => { | ||
116 | - if (beforeUpload(file)) { | ||
117 | - personalPicture.value = ''; | ||
118 | - loading.value = true; | ||
119 | - const formData = new FormData(); | ||
120 | - formData.append('file', file); | ||
121 | - const response = await uploadApi(formData); | ||
122 | - if (response.fileStaticUri) { | ||
123 | - personalPicture.value = response.fileStaticUri; | ||
124 | - loading.value = false; | ||
125 | - } | ||
126 | - } | ||
127 | - }; | ||
128 | - | ||
129 | - const beforeUpload = (file: FileItem) => { | ||
130 | - const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'; | ||
131 | - if (!isJpgOrPng) { | ||
132 | - createMessage.error('只能上传图片文件!'); | ||
133 | - } | ||
134 | - const isLt2M = (file.size as number) / 1024 / 1024 < 5; | ||
135 | - if (!isLt2M) { | ||
136 | - createMessage.error('图片大小不能超过5MB!'); | ||
137 | - } | ||
138 | - return isJpgOrPng && isLt2M; | 95 | + const handleChange = (e) => { |
96 | + personalPicture.value = e.data; | ||
139 | }; | 97 | }; |
140 | 98 | ||
141 | const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({ | 99 | const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({ |
@@ -146,7 +104,7 @@ | @@ -146,7 +104,7 @@ | ||
146 | const [registerModal, { closeModal }] = useModalInner(async () => { | 104 | const [registerModal, { closeModal }] = useModalInner(async () => { |
147 | const info = await getMyInfo(); | 105 | const info = await getMyInfo(); |
148 | personalInfo.value = info; | 106 | personalInfo.value = info; |
149 | - personalPicture.value = info.avatar; | 107 | + personalPicture.value = info.avatar || headerImg; |
150 | setFieldsValue(info); | 108 | setFieldsValue(info); |
151 | setDescProps({ data: info }); | 109 | setDescProps({ data: info }); |
152 | }); | 110 | }); |
@@ -157,7 +115,7 @@ | @@ -157,7 +115,7 @@ | ||
157 | const record = await personalPut({ | 115 | const record = await personalPut({ |
158 | ...value, | 116 | ...value, |
159 | id: userInfo.userId, | 117 | id: userInfo.userId, |
160 | - avatar: unref(personalPicture), | 118 | + avatar: personalPicture.value, |
161 | }); | 119 | }); |
162 | 120 | ||
163 | userStore.setUserInfo(record as unknown as UserInfo); | 121 | userStore.setUserInfo(record as unknown as UserInfo); |
@@ -171,13 +129,13 @@ | @@ -171,13 +129,13 @@ | ||
171 | personalInfo, | 129 | personalInfo, |
172 | registerDesc, | 130 | registerDesc, |
173 | personalPicture, | 131 | personalPicture, |
174 | - beforeUpload, | ||
175 | - customUpload, | ||
176 | handleSubmit, | 132 | handleSubmit, |
177 | registerModal, | 133 | registerModal, |
178 | registerForm, | 134 | registerForm, |
179 | loading, | 135 | loading, |
180 | headerImg, | 136 | headerImg, |
137 | + uploadApi, | ||
138 | + handleChange, | ||
181 | }; | 139 | }; |
182 | }, | 140 | }, |
183 | }); | 141 | }); |