Commit 2e4c46dfa145371018eb51306b6e17c032b66f58

Authored by fengwotao
1 parent c1de9a43

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

... ... @@ -179,14 +179,22 @@
179 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 189 async function handleOk() {
183 190 const uploadApi = props.uploadApi;
184 191 if (uploadApi && isFunction(uploadApi)) {
185 192 const blob = dataURLtoBlob(previewSource.value);
  193 + const base64D = blobToFile(blob, filename);
186 194 try {
187 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 198 closeModal();
191 199 } finally {
192 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 13 <div class="text-center cursor-pointer">
14 14 <div class="text-left text-lg border-gray-200 p-2 border">个人头像</div>
15 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 17 </div>
38 18 </div>
39   -
40 19 <Description @register="registerDesc" class="mt-8 p-4" />
41 20 </div>
42 21 <div class="ml-4 border border-gray-200">
... ... @@ -49,22 +28,19 @@
49 28 </BasicModal>
50 29 </template>
51 30 <script lang="ts">
52   - import { defineComponent, ref, unref } from 'vue';
  31 + import { defineComponent, ref } from 'vue';
53 32 import { BasicModal, useModalInner } from '/@/components/Modal/index';
54 33 import { BasicForm, useForm } from '/@/components/Form/index';
55 34 import { formSchema } from './config';
56 35 import { Description, DescItem, useDescription } from '/@/components/Description/index';
57 36 import { uploadApi, personalPut } from '/@/api/personal/index';
58 37 import { useMessage } from '/@/hooks/web/useMessage';
59   - import { Upload } from 'ant-design-vue';
60   - import { PlusOutlined } from '@ant-design/icons-vue';
61 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 39 import headerImg from '/@/assets/images/logo.png';
65 40 import { getMyInfo } from '/@/api/sys/user';
66 41 import { UserInfoModel } from '/@/api/sys/model/userModel';
67 42 import { UserInfo } from '/#/store';
  43 + import { CropperAvatar } from '/@/components/Cropper';
68 44
69 45 const schema: DescItem[] = [
70 46 {
... ... @@ -95,7 +71,12 @@
95 71
96 72 export default defineComponent({
97 73 name: 'Index',
98   - components: { BasicModal, BasicForm, Description, Upload, PlusOutlined, LoadingOutlined },
  74 + components: {
  75 + BasicModal,
  76 + BasicForm,
  77 + Description,
  78 + CropperAvatar,
  79 + },
99 80 emits: ['refreshPersonal', 'register'],
100 81 setup(_, { emit }) {
101 82 const loading = ref(false);
... ... @@ -111,31 +92,8 @@
111 92 column: 1,
112 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 99 const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
... ... @@ -146,7 +104,7 @@
146 104 const [registerModal, { closeModal }] = useModalInner(async () => {
147 105 const info = await getMyInfo();
148 106 personalInfo.value = info;
149   - personalPicture.value = info.avatar;
  107 + personalPicture.value = info.avatar || headerImg;
150 108 setFieldsValue(info);
151 109 setDescProps({ data: info });
152 110 });
... ... @@ -157,7 +115,7 @@
157 115 const record = await personalPut({
158 116 ...value,
159 117 id: userInfo.userId,
160   - avatar: unref(personalPicture),
  118 + avatar: personalPicture.value,
161 119 });
162 120
163 121 userStore.setUserInfo(record as unknown as UserInfo);
... ... @@ -171,13 +129,13 @@
171 129 personalInfo,
172 130 registerDesc,
173 131 personalPicture,
174   - beforeUpload,
175   - customUpload,
176 132 handleSubmit,
177 133 registerModal,
178 134 registerForm,
179 135 loading,
180 136 headerImg,
  137 + uploadApi,
  138 + handleChange,
181 139 };
182 140 },
183 141 });
... ...