Commit 2e4c46dfa145371018eb51306b6e17c032b66f58

Authored by fengwotao
1 parent c1de9a43

pref:优化个人头像上传头像使用组件

@@ -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 });