refactor: cropper
This commit is contained in:
parent
0f562e59c2
commit
91abae8898
|
@ -24,7 +24,7 @@
|
|||
<!-- DB 相关 -->
|
||||
<druid.version>1.2.15</druid.version>
|
||||
<mybatis-plus.version>3.5.2</mybatis-plus.version>
|
||||
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
|
||||
<mybatis-plus-generator.version>3.5.3</mybatis-plus-generator.version>
|
||||
<dynamic-datasource.version>3.6.0</dynamic-datasource.version>
|
||||
<redisson.version>3.18.0</redisson.version>
|
||||
<!-- 服务保障相关 -->
|
||||
|
|
|
@ -25,12 +25,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@iconify/iconify": "^3.0.1",
|
||||
"@vueuse/core": "^9.7.0",
|
||||
"@vueuse/core": "^9.8.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||
"@zxcvbn-ts/core": "^2.1.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.2.1",
|
||||
"cropperjs": "^1.5.13",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"echarts": "^5.4.1",
|
||||
|
@ -46,7 +47,6 @@
|
|||
"qs": "^6.11.0",
|
||||
"url": "^0.11.0",
|
||||
"vue": "3.2.45",
|
||||
"vue-cropper": "^1.0.3",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-types": "^5.0.1",
|
||||
|
@ -104,7 +104,7 @@
|
|||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vite-plugin-windicss": "^1.8.10",
|
||||
"vue-tsc": "^1.0.14",
|
||||
"vue-tsc": "^1.0.16",
|
||||
"windicss": "^3.5.6"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
@ -18,7 +18,7 @@ specifiers:
|
|||
'@vitejs/plugin-legacy': ^3.0.1
|
||||
'@vitejs/plugin-vue': ^4.0.0
|
||||
'@vitejs/plugin-vue-jsx': ^3.0.0
|
||||
'@vueuse/core': ^9.7.0
|
||||
'@vueuse/core': ^9.8.2
|
||||
'@wangeditor/editor': ^5.1.23
|
||||
'@wangeditor/editor-for-vue': ^5.1.10
|
||||
'@zxcvbn-ts/core': ^2.1.0
|
||||
|
@ -26,6 +26,7 @@ specifiers:
|
|||
autoprefixer: ^10.4.13
|
||||
axios: ^1.2.1
|
||||
consola: ^2.15.3
|
||||
cropperjs: ^1.5.13
|
||||
crypto-js: ^4.1.1
|
||||
dayjs: ^1.11.7
|
||||
echarts: ^5.4.1
|
||||
|
@ -72,10 +73,9 @@ specifiers:
|
|||
vite-plugin-vue-setup-extend: ^0.4.0
|
||||
vite-plugin-windicss: ^1.8.10
|
||||
vue: 3.2.45
|
||||
vue-cropper: ^1.0.3
|
||||
vue-i18n: 9.2.2
|
||||
vue-router: ^4.1.6
|
||||
vue-tsc: ^1.0.14
|
||||
vue-tsc: ^1.0.16
|
||||
vue-types: ^5.0.1
|
||||
vxe-table: ^4.3.7
|
||||
web-storage-cache: ^1.1.1
|
||||
|
@ -84,12 +84,13 @@ specifiers:
|
|||
|
||||
dependencies:
|
||||
'@iconify/iconify': 3.0.1
|
||||
'@vueuse/core': 9.7.0_vue@3.2.45
|
||||
'@vueuse/core': 9.8.2_vue@3.2.45
|
||||
'@wangeditor/editor': 5.1.23
|
||||
'@wangeditor/editor-for-vue': 5.1.12_3apfu3xbp6awzuex7ed3sbrv6y
|
||||
'@zxcvbn-ts/core': 2.1.0
|
||||
animate.css: 4.1.1
|
||||
axios: 1.2.1
|
||||
cropperjs: 1.5.13
|
||||
crypto-js: 4.1.1
|
||||
dayjs: 1.11.7
|
||||
echarts: registry.npmmirror.com/echarts/5.4.1
|
||||
|
@ -105,7 +106,6 @@ dependencies:
|
|||
qs: 6.11.0
|
||||
url: 0.11.0
|
||||
vue: 3.2.45
|
||||
vue-cropper: 1.0.5
|
||||
vue-i18n: 9.2.2_vue@3.2.45
|
||||
vue-router: 4.1.6_vue@3.2.45
|
||||
vue-types: 5.0.1_vue@3.2.45
|
||||
|
@ -163,7 +163,7 @@ devDependencies:
|
|||
vite-plugin-svg-icons: 2.0.1_vite@4.0.2
|
||||
vite-plugin-vue-setup-extend: 0.4.0_vite@4.0.2
|
||||
vite-plugin-windicss: registry.npmmirror.com/vite-plugin-windicss/1.8.10_vite@4.0.2
|
||||
vue-tsc: 1.0.14_typescript@4.9.4
|
||||
vue-tsc: 1.0.16_typescript@4.9.4
|
||||
windicss: 3.5.6
|
||||
|
||||
packages:
|
||||
|
@ -1079,44 +1079,44 @@ packages:
|
|||
nanoid: registry.npmmirror.com/nanoid/3.3.4
|
||||
dev: false
|
||||
|
||||
/@volar/language-core/1.0.14:
|
||||
resolution: {integrity: sha512-j1tMQgw0qCV2amM4qDJNG/zc0yj3ay8HoWNt05IaiCPsULtSSpF/9+F6Izvn0DF7nWOd6MUHTxaQAeZwLfr56Q==}
|
||||
/@volar/language-core/1.0.16:
|
||||
resolution: {integrity: sha512-IGnOxWTs4DZ81TDcmxBAkCBxs97hUblwcjpBsTx/pOGGaSSDQRJPn0wL8NYTybEObU0i7lhEpKZ+0vJfdIy1Kg==}
|
||||
dependencies:
|
||||
'@volar/source-map': 1.0.14
|
||||
'@volar/source-map': 1.0.16
|
||||
'@vue/reactivity': 3.2.45
|
||||
muggle-string: 0.1.0
|
||||
dev: true
|
||||
|
||||
/@volar/source-map/1.0.14:
|
||||
resolution: {integrity: sha512-8pHCbEWHWaSDGb/FM9zRIW1lY1OAo16MENVSQGCgTwz7PWf3Gw6WW3TFVKCtzaFhLjPH0i5e9hALy7vBPbSHoA==}
|
||||
/@volar/source-map/1.0.16:
|
||||
resolution: {integrity: sha512-PKjzmQcg8QOGC/1V9tmGh2jcy6bKLhkW5bGidElSr83iDbCzLvldt2/La/QlDxaRCHYLT0MeyuGJBZIChB1dYQ==}
|
||||
dependencies:
|
||||
muggle-string: 0.1.0
|
||||
dev: true
|
||||
|
||||
/@volar/typescript/1.0.14:
|
||||
resolution: {integrity: sha512-67qcjjz7KGFhMCG9EKMA9qJK3BRGQecO4dGyAKfMfClZ/PaVoKfDvJvYo89McGTQ8SeczD48I9TPnaJM0zK8JQ==}
|
||||
/@volar/typescript/1.0.16:
|
||||
resolution: {integrity: sha512-Yov+n4oO3iYnuMt9QJAFpJabfTRCzc7KvjlAwBaSuZy+Gc/f9611MgtqAh5/SIGmltFN8dXn1Ijno8ro8I4lyw==}
|
||||
dependencies:
|
||||
'@volar/language-core': 1.0.14
|
||||
'@volar/language-core': 1.0.16
|
||||
dev: true
|
||||
|
||||
/@volar/vue-language-core/1.0.14:
|
||||
resolution: {integrity: sha512-grJ4dQ7c/suZmBBmZtw2O2XeDX+rtgpdBtHxMug1NMPRDxj5EZ9WGphWtGnMQj8RyVgpz9ByvV5GbQjk4/wfBw==}
|
||||
/@volar/vue-language-core/1.0.16:
|
||||
resolution: {integrity: sha512-sQ/aW1Vuiyy4OQuh2lthyYicruM3qh9VSk/aDh8/bFvM8GoohHZqVpMN3LYldEJ9eT/rN6u4xmYP54vc/EjX4Q==}
|
||||
dependencies:
|
||||
'@volar/language-core': 1.0.14
|
||||
'@volar/source-map': 1.0.14
|
||||
'@volar/language-core': 1.0.16
|
||||
'@volar/source-map': 1.0.16
|
||||
'@vue/compiler-dom': 3.2.45
|
||||
'@vue/compiler-sfc': 3.2.45
|
||||
'@vue/reactivity': 3.2.45
|
||||
'@vue/shared': 3.2.45
|
||||
minimatch: 5.1.0
|
||||
minimatch: 5.1.2
|
||||
vue-template-compiler: 2.7.14
|
||||
dev: true
|
||||
|
||||
/@volar/vue-typescript/1.0.14:
|
||||
resolution: {integrity: sha512-2P0QeGLLY05fDTu8GqY8SR2+jldXRTrkQdD2Nc0sVOjMJ7j3RYYY0wJyZ9hCBDuxV4Micc6jdB8nKS0yxQgNvA==}
|
||||
/@volar/vue-typescript/1.0.16:
|
||||
resolution: {integrity: sha512-M018Ulg/o2FVktAdlr5b/z4K69bYzekxNUA1o39y5Ur6CObc/o+5eDCCS7gIYijWnx9iNKkSQpWWWblJFv7kHQ==}
|
||||
dependencies:
|
||||
'@volar/typescript': 1.0.14
|
||||
'@volar/vue-language-core': 1.0.14
|
||||
'@volar/typescript': 1.0.16
|
||||
'@volar/vue-language-core': 1.0.16
|
||||
dev: true
|
||||
|
||||
/@vue/compiler-core/3.2.45:
|
||||
|
@ -1196,24 +1196,24 @@ packages:
|
|||
/@vue/shared/3.2.45:
|
||||
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
|
||||
|
||||
/@vueuse/core/9.7.0_vue@3.2.45:
|
||||
resolution: {integrity: sha512-/AGY/t7jJPxCyRoVTygNKoroTiCvRaaZIW+yeSlBCnI7QRpQ9cvXNTdNaSl3GvSyFbn83+XwZwEZvI1OpQfeGw==}
|
||||
/@vueuse/core/9.8.2_vue@3.2.45:
|
||||
resolution: {integrity: sha512-aWiCmcYIpPt7xjuqYiceODEMHchDYthrJ4AqI+FXPZrR23PZOqdiktbUVyQl2kGlR3H4i9UJ/uimQrwhz9UouQ==}
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.16
|
||||
'@vueuse/metadata': 9.7.0
|
||||
'@vueuse/shared': 9.7.0_vue@3.2.45
|
||||
'@vueuse/metadata': 9.8.2
|
||||
'@vueuse/shared': 9.8.2_vue@3.2.45
|
||||
vue-demi: 0.13.11_vue@3.2.45
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/metadata/9.7.0:
|
||||
resolution: {integrity: sha512-M7WsAgw28FNtTH0bzsGuHEtJOPJqPpyeHS6PHq+8UesLgNjZ9waMAntiUrgUQlxt09M4i2lH7y9sRi0jkfeXGA==}
|
||||
/@vueuse/metadata/9.8.2:
|
||||
resolution: {integrity: sha512-N4E/BKS+9VsUeD4WLVRU1J2kCOLh+iikBcMtipFcTyL204132vDYHs27zLAVabJYGnhC0dIVGdhg9pbOZiY2TQ==}
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared/9.7.0_vue@3.2.45:
|
||||
resolution: {integrity: sha512-pwmt1y3TJ2s5KqWmkv9ZKEV59GwuZQZk8XLiU+hGswz0jej318ozbea9E4A/A50ksyM26swSFr7sZ9llNPsZHg==}
|
||||
/@vueuse/shared/9.8.2_vue@3.2.45:
|
||||
resolution: {integrity: sha512-ACjrPQzowd5dnabNJt9EoGVobco9/ENiA5qP53vjiuxndlJYuc/UegwhXC7KdQbPX4F45a50+45K3g1wNqOzmA==}
|
||||
dependencies:
|
||||
vue-demi: 0.13.11_vue@3.2.45
|
||||
transitivePeerDependencies:
|
||||
|
@ -2019,6 +2019,10 @@ packages:
|
|||
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||
dev: true
|
||||
|
||||
/cropperjs/1.5.13:
|
||||
resolution: {integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==}
|
||||
dev: false
|
||||
|
||||
/cross-spawn/7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -2319,7 +2323,7 @@ packages:
|
|||
'@popperjs/core': /@sxzz/popperjs-es/2.11.7
|
||||
'@types/lodash': 4.14.189
|
||||
'@types/lodash-es': 4.17.6
|
||||
'@vueuse/core': 9.7.0_vue@3.2.45
|
||||
'@vueuse/core': 9.8.2_vue@3.2.45
|
||||
async-validator: 4.2.5
|
||||
dayjs: 1.11.7
|
||||
escape-html: 1.0.3
|
||||
|
@ -3831,6 +3835,13 @@ packages:
|
|||
brace-expansion: 2.0.1
|
||||
dev: true
|
||||
|
||||
/minimatch/5.1.2:
|
||||
resolution: {integrity: sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
dev: true
|
||||
|
||||
/minimist-options/4.1.0:
|
||||
resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
|
||||
engines: {node: '>= 6'}
|
||||
|
@ -5461,10 +5472,6 @@ packages:
|
|||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/vue-cropper/1.0.5:
|
||||
resolution: {integrity: sha512-D4XXdqWmMWRLOIV9LIh7/mkH6OBOMQDFbRjwntkxmAtxOtwpC9U5ZZ6lSXw5F5cbd4g8znDjk6MuCwIL+fZSrA==}
|
||||
dev: false
|
||||
|
||||
/vue-demi/0.13.11_vue@3.2.45:
|
||||
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -5526,14 +5533,14 @@ packages:
|
|||
he: 1.2.0
|
||||
dev: true
|
||||
|
||||
/vue-tsc/1.0.14_typescript@4.9.4:
|
||||
resolution: {integrity: sha512-HeqtyxMrSRUCnU5nxB0lQc3o7zirMppZ/V6HLL3l4FsObGepH3A3beNmNehpLQs0Gt7DkSWVi3CpVCFgrf+/sQ==}
|
||||
/vue-tsc/1.0.16_typescript@4.9.4:
|
||||
resolution: {integrity: sha512-yZaiJBbcKR1rSLhiF9KryAFH7R63po+N/invr2EAHGXxMzZksE5j1zyQKvrYiqK47ZHLAlCR+re/PHqWp/UzTg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
dependencies:
|
||||
'@volar/vue-language-core': 1.0.14
|
||||
'@volar/vue-typescript': 1.0.14
|
||||
'@volar/vue-language-core': 1.0.16
|
||||
'@volar/vue-typescript': 1.0.16
|
||||
typescript: 4.9.4
|
||||
dev: true
|
||||
|
||||
|
|
|
@ -73,5 +73,5 @@ export const updateUserPwdApi = (oldPassword: string, newPassword: string) => {
|
|||
|
||||
// 用户头像上传
|
||||
export const uploadAvatarApi = (data) => {
|
||||
return request.put({ url: '/system/user/profile/update-avatar', data })
|
||||
return request.upload({ url: '/system/user/profile/update-avatar', data: data })
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import CropperImage from './src/Cropper.vue'
|
||||
import CropperAvatar from './src/CropperAvatar.vue'
|
||||
|
||||
export { CropperImage, CropperAvatar }
|
|
@ -0,0 +1,256 @@
|
|||
<template>
|
||||
<Dialog
|
||||
v-model="dialogVisible"
|
||||
:title="t('cropper.modalTitle')"
|
||||
width="800px"
|
||||
maxHeight="380px"
|
||||
:canFullscreen="false"
|
||||
>
|
||||
<div :class="prefixCls">
|
||||
<div :class="`${prefixCls}-left`">
|
||||
<div :class="`${prefixCls}-cropper`">
|
||||
<CropperImage
|
||||
v-if="src"
|
||||
:src="src"
|
||||
height="300px"
|
||||
:circled="circled"
|
||||
@cropend="handleCropend"
|
||||
@ready="handleReady"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="`${prefixCls}-toolbar`">
|
||||
<el-upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
|
||||
<el-tooltip :content="t('cropper.selectImage')" placement="bottom">
|
||||
<XButton preIcon="ant-design:upload-outlined" type="primary" />
|
||||
</el-tooltip>
|
||||
</el-upload>
|
||||
<el-space>
|
||||
<el-tooltip :content="t('cropper.btn_reset')" placement="bottom">
|
||||
<XButton
|
||||
type="primary"
|
||||
preIcon="ant-design:reload-outlined"
|
||||
size="small"
|
||||
:disabled="!src"
|
||||
@click="handlerToolbar('reset')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="t('cropper.btn_rotate_left')" placement="bottom">
|
||||
<XButton
|
||||
type="primary"
|
||||
preIcon="ant-design:rotate-left-outlined"
|
||||
size="small"
|
||||
:disabled="!src"
|
||||
@click="handlerToolbar('rotate', -45)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="t('cropper.btn_rotate_right')" placement="bottom">
|
||||
<XButton
|
||||
type="primary"
|
||||
preIcon="ant-design:rotate-right-outlined"
|
||||
size="small"
|
||||
:disabled="!src"
|
||||
@click="handlerToolbar('rotate', 45)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="t('cropper.btn_scale_x')" placement="bottom">
|
||||
<XButton
|
||||
type="primary"
|
||||
preIcon="vaadin:arrows-long-h"
|
||||
size="small"
|
||||
:disabled="!src"
|
||||
@click="handlerToolbar('scaleX')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="t('cropper.btn_scale_y')" placement="bottom">
|
||||
<XButton
|
||||
type="primary"
|
||||
preIcon="vaadin:arrows-long-v"
|
||||
size="small"
|
||||
:disabled="!src"
|
||||
@click="handlerToolbar('scaleY')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="t('cropper.btn_zoom_in')" placement="bottom">
|
||||
<XButton
|
||||
type="primary"
|
||||
preIcon="ant-design:zoom-in-outlined"
|
||||
size="small"
|
||||
:disabled="!src"
|
||||
@click="handlerToolbar('zoom', 0.1)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="t('cropper.btn_zoom_out')" placement="bottom">
|
||||
<XButton
|
||||
type="primary"
|
||||
preIcon="ant-design:zoom-out-outlined"
|
||||
size="small"
|
||||
:disabled="!src"
|
||||
@click="handlerToolbar('zoom', -0.1)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-space>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`${prefixCls}-right`">
|
||||
<div :class="`${prefixCls}-preview`">
|
||||
<img :src="previewSource" v-if="previewSource" :alt="t('cropper.preview')" />
|
||||
</div>
|
||||
<template v-if="previewSource">
|
||||
<div :class="`${prefixCls}-group`">
|
||||
<el-avatar :src="previewSource" size="large" />
|
||||
<el-avatar :src="previewSource" :size="48" />
|
||||
<el-avatar :src="previewSource" :size="64" />
|
||||
<el-avatar :src="previewSource" :size="80" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleOk">{{ t('cropper.okText') }}</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { dataURLtoBlob } from '@/utils/filt'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElUpload, ElAvatar, ElTooltip, ElSpace } from 'element-plus'
|
||||
import { Dialog } from '@/components/Dialog'
|
||||
import { CropperImage } from '@/components/Cropper'
|
||||
import type { CropendResult, Cropper } from './types'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
const props = defineProps({
|
||||
srcValue: propTypes.string.def(''),
|
||||
circled: propTypes.bool.def(true)
|
||||
})
|
||||
const emit = defineEmits(['uploadSuccess'])
|
||||
const { t } = useI18n()
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('cropper-am')
|
||||
|
||||
const src = ref(props.srcValue)
|
||||
const previewSource = ref('')
|
||||
const cropper = ref<Cropper>()
|
||||
const dialogVisible = ref(false)
|
||||
let filename = ''
|
||||
let scaleX = 1
|
||||
let scaleY = 1
|
||||
|
||||
// Block upload
|
||||
function handleBeforeUpload(file: File) {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
src.value = ''
|
||||
previewSource.value = ''
|
||||
reader.onload = function (e) {
|
||||
src.value = (e.target?.result as string) ?? ''
|
||||
filename = file.name
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function handleCropend({ imgBase64 }: CropendResult) {
|
||||
previewSource.value = imgBase64
|
||||
}
|
||||
|
||||
function handleReady(cropperInstance: Cropper) {
|
||||
cropper.value = cropperInstance
|
||||
}
|
||||
|
||||
function handlerToolbar(event: string, arg?: number) {
|
||||
if (event === 'scaleX') {
|
||||
scaleX = arg = scaleX === -1 ? 1 : -1
|
||||
}
|
||||
if (event === 'scaleY') {
|
||||
scaleY = arg = scaleY === -1 ? 1 : -1
|
||||
}
|
||||
cropper?.value?.[event]?.(arg)
|
||||
}
|
||||
|
||||
async function handleOk() {
|
||||
const blob = dataURLtoBlob(previewSource.value)
|
||||
emit('uploadSuccess', { source: previewSource.value, data: blob, filename: filename })
|
||||
closeModal()
|
||||
}
|
||||
function openModal() {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
function closeModal() {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
defineExpose({ openModal, closeModal })
|
||||
</script>
|
||||
<style lang="scss">
|
||||
$prefix-cls: #{$namespace}-cropper-am;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
display: flex;
|
||||
|
||||
&-left,
|
||||
&-right {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
&-left {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&-cropper {
|
||||
height: 300px;
|
||||
background: #eee;
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
rgb(0 0 0 / 25%) 25%,
|
||||
transparent 0,
|
||||
transparent 75%,
|
||||
rgb(0 0 0 / 25%) 0
|
||||
),
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgb(0 0 0 / 25%) 25%,
|
||||
transparent 0,
|
||||
transparent 75%,
|
||||
rgb(0 0 0 / 25%) 0
|
||||
);
|
||||
background-position: 0 0, 12px 12px;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&-preview {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-group {
|
||||
display: flex;
|
||||
padding-top: 8px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,190 @@
|
|||
<template>
|
||||
<div :class="getClass" :style="getWrapperStyle">
|
||||
<img
|
||||
v-show="isReady"
|
||||
ref="imgElRef"
|
||||
:src="src"
|
||||
:alt="alt"
|
||||
:crossorigin="crossorigin"
|
||||
:style="getImageStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
CSSProperties,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
PropType,
|
||||
ref,
|
||||
unref,
|
||||
useAttrs
|
||||
} from 'vue'
|
||||
import Cropper from 'cropperjs'
|
||||
import 'cropperjs/dist/cropper.css'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
type Options = Cropper.Options
|
||||
|
||||
const defaultOptions: Options = {
|
||||
aspectRatio: 1,
|
||||
zoomable: true,
|
||||
zoomOnTouch: true,
|
||||
zoomOnWheel: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: true,
|
||||
autoCrop: true,
|
||||
background: true,
|
||||
highlight: true,
|
||||
center: true,
|
||||
responsive: true,
|
||||
restore: true,
|
||||
checkCrossOrigin: true,
|
||||
checkOrientation: true,
|
||||
scalable: true,
|
||||
modal: true,
|
||||
guides: true,
|
||||
movable: true,
|
||||
rotatable: true
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
src: propTypes.string.def(''),
|
||||
alt: propTypes.string.def(''),
|
||||
circled: propTypes.bool.def(false),
|
||||
realTimePreview: propTypes.bool.def(true),
|
||||
height: propTypes.string.def('360px'),
|
||||
crossorigin: {
|
||||
type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
|
||||
default: undefined
|
||||
},
|
||||
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
|
||||
options: { type: Object as PropType<Options>, default: () => ({}) }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cropend', 'ready', 'cropendError'])
|
||||
const attrs = useAttrs()
|
||||
const imgElRef = ref<ElRef<HTMLImageElement>>()
|
||||
const cropper = ref<Nullable<Cropper>>()
|
||||
const isReady = ref(false)
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('cropper-image')
|
||||
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)
|
||||
|
||||
const getImageStyle = computed((): CSSProperties => {
|
||||
return {
|
||||
height: props.height,
|
||||
maxWidth: '100%',
|
||||
...props.imageStyle
|
||||
}
|
||||
})
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
attrs.class,
|
||||
{
|
||||
[`${prefixCls}--circled`]: props.circled
|
||||
}
|
||||
]
|
||||
})
|
||||
const getWrapperStyle = computed((): CSSProperties => {
|
||||
return { height: `${props.height}`.replace(/px/, '') + 'px' }
|
||||
})
|
||||
|
||||
onMounted(init)
|
||||
|
||||
onUnmounted(() => {
|
||||
cropper.value?.destroy()
|
||||
})
|
||||
|
||||
async function init() {
|
||||
const imgEl = unref(imgElRef)
|
||||
if (!imgEl) {
|
||||
return
|
||||
}
|
||||
cropper.value = new Cropper(imgEl, {
|
||||
...defaultOptions,
|
||||
ready: () => {
|
||||
isReady.value = true
|
||||
realTimeCroppered()
|
||||
emit('ready', cropper.value)
|
||||
},
|
||||
crop() {
|
||||
debounceRealTimeCroppered()
|
||||
},
|
||||
zoom() {
|
||||
debounceRealTimeCroppered()
|
||||
},
|
||||
cropmove() {
|
||||
debounceRealTimeCroppered()
|
||||
},
|
||||
...props.options
|
||||
})
|
||||
}
|
||||
|
||||
// Real-time display preview
|
||||
function realTimeCroppered() {
|
||||
props.realTimePreview && croppered()
|
||||
}
|
||||
|
||||
// event: return base64 and width and height information after cropping
|
||||
function croppered() {
|
||||
if (!cropper.value) {
|
||||
return
|
||||
}
|
||||
let imgInfo = cropper.value.getData()
|
||||
const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
return
|
||||
}
|
||||
let fileReader: FileReader = new FileReader()
|
||||
fileReader.readAsDataURL(blob)
|
||||
fileReader.onloadend = (e) => {
|
||||
emit('cropend', {
|
||||
imgBase64: e.target?.result ?? '',
|
||||
imgInfo
|
||||
})
|
||||
}
|
||||
fileReader.onerror = () => {
|
||||
emit('cropendError')
|
||||
}
|
||||
}, 'image/png')
|
||||
}
|
||||
|
||||
// Get a circular picture canvas
|
||||
function getRoundedCanvas() {
|
||||
const sourceCanvas = cropper.value!.getCroppedCanvas()
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')!
|
||||
const width = sourceCanvas.width
|
||||
const height = sourceCanvas.height
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
context.imageSmoothingEnabled = true
|
||||
context.drawImage(sourceCanvas, 0, 0, width, height)
|
||||
context.globalCompositeOperation = 'destination-in'
|
||||
context.beginPath()
|
||||
context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
|
||||
context.fill()
|
||||
return canvas
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
$prefix-cls: #{$namespace}-cropper-image;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
&--circled {
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div class="user-info-head" @click="open()">
|
||||
<img :src="sourceValue" v-if="sourceValue" class="img-circle img-lg" alt="avatar" />
|
||||
<el-button :class="`${prefixCls}-upload-btn`" @click="open()" v-if="showBtn">
|
||||
{{ btnText ? btnText : t('cropper.selectImage') }}
|
||||
</el-button>
|
||||
<CopperModal ref="cropperModel" @upload-success="handleUploadSuccess" :srcValue="sourceValue" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ref, watch, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import CopperModal from './CopperModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
width: propTypes.string.def('200px'),
|
||||
value: propTypes.string.def(''),
|
||||
showBtn: propTypes.bool.def(true),
|
||||
btnText: propTypes.string.def('')
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:value', 'change'])
|
||||
const sourceValue = ref(props.value)
|
||||
const { getPrefixCls } = useDesign()
|
||||
const prefixCls = getPrefixCls('cropper-avatar')
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
const cropperModel = ref()
|
||||
|
||||
watchEffect(() => {
|
||||
sourceValue.value = props.value
|
||||
})
|
||||
|
||||
watch(
|
||||
() => sourceValue.value,
|
||||
(v: string) => {
|
||||
emit('update:value', v)
|
||||
}
|
||||
)
|
||||
|
||||
function handleUploadSuccess({ source, data, filename }) {
|
||||
sourceValue.value = source
|
||||
emit('change', { source, data, filename })
|
||||
message.success(t('cropper.uploadSuccess'))
|
||||
}
|
||||
|
||||
function open() {
|
||||
cropperModel.value.openModal()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
$prefix-cls: #{$namespace}--cropper-avatar;
|
||||
|
||||
.#{$prefix-cls} {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
&-image-wrapper {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-image-mask {
|
||||
opacity: 0%;
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: inherit;
|
||||
border: inherit;
|
||||
background: rgb(0 0 0 / 40%);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.4s;
|
||||
|
||||
::v-deep(svg) {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-image-mask:hover {
|
||||
opacity: 4000%;
|
||||
}
|
||||
|
||||
&-upload-btn {
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
.user-info-head {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.img-lg {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.user-info-head:hover:after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
color: #eee;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
cursor: pointer;
|
||||
line-height: 110px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
import type Cropper from 'cropperjs'
|
||||
|
||||
export interface CropendResult {
|
||||
imgBase64: string
|
||||
imgInfo: Cropper.Data
|
||||
}
|
||||
|
||||
export type { Cropper }
|
|
@ -44,7 +44,7 @@ export default {
|
|||
},
|
||||
upload: async <T = any>(option: any) => {
|
||||
option.headersType = 'multipart/form-data'
|
||||
const res = await request({ method: 'PUT', ...option })
|
||||
const res = await request({ method: 'POST', ...option })
|
||||
return res as unknown as Promise<T>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -426,5 +426,19 @@ export default {
|
|||
cfPwdMsg: 'Please Enter Confirm Password',
|
||||
diffPwd: 'The Passwords Entered Twice No Match'
|
||||
}
|
||||
},
|
||||
cropper: {
|
||||
selectImage: 'Select Image',
|
||||
uploadSuccess: 'Uploaded success!',
|
||||
modalTitle: 'Avatar upload',
|
||||
okText: 'Confirm and upload',
|
||||
btn_reset: 'Reset',
|
||||
btn_rotate_left: 'Counterclockwise rotation',
|
||||
btn_rotate_right: 'Clockwise rotation',
|
||||
btn_scale_x: 'Flip horizontal',
|
||||
btn_scale_y: 'Flip vertical',
|
||||
btn_zoom_in: 'Zoom in',
|
||||
btn_zoom_out: 'Zoom out',
|
||||
preview: 'Preivew'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -419,5 +419,19 @@ export default {
|
|||
pwdRules: '长度在 6 到 20 个字符',
|
||||
diffPwd: '两次输入密码不一致'
|
||||
}
|
||||
},
|
||||
cropper: {
|
||||
selectImage: '选择图片',
|
||||
uploadSuccess: '上传成功',
|
||||
modalTitle: '头像上传',
|
||||
okText: '确认并上传',
|
||||
btn_reset: '重置',
|
||||
btn_rotate_left: '逆时针旋转',
|
||||
btn_rotate_right: '顺时针旋转',
|
||||
btn_scale_x: '水平翻转',
|
||||
btn_scale_y: '垂直翻转',
|
||||
btn_zoom_in: '放大',
|
||||
btn_zoom_out: '缩小',
|
||||
preview: '预览'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,245 +1,37 @@
|
|||
<template>
|
||||
<div class="user-info-head" @click="editCropper()">
|
||||
<img :src="props.img" title="点击上传头像" class="img-circle img-lg" alt="" />
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="编辑头像"
|
||||
:mask-closable="false"
|
||||
width="800px"
|
||||
append-to-body
|
||||
@opened="cropperVisible = true"
|
||||
>
|
||||
<el-row>
|
||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
||||
<VueCropper
|
||||
ref="cropper"
|
||||
v-if="cropperVisible"
|
||||
:img="options.img"
|
||||
:info="true"
|
||||
:infoTrue="options.infoTrue"
|
||||
:autoCrop="options.autoCrop"
|
||||
:autoCropWidth="options.autoCropWidth"
|
||||
:autoCropHeight="options.autoCropHeight"
|
||||
:fixedNumber="options.fixedNumber"
|
||||
:fixedBox="options.fixedBox"
|
||||
:centerBox="options.centerBox"
|
||||
@real-time="realTime"
|
||||
<div class="change-avatar">
|
||||
<CropperAvatar
|
||||
:value="avatar"
|
||||
:showBtn="false"
|
||||
@change="handelUpload"
|
||||
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
|
||||
width="120px"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
||||
<div
|
||||
class="avatar-upload-preview"
|
||||
:style="{
|
||||
width: previews.w + 'px',
|
||||
height: previews.h + 'px',
|
||||
overflow: 'hidden',
|
||||
margin: '5px'
|
||||
}"
|
||||
>
|
||||
<div :style="previews.div">
|
||||
<img :src="previews.url" :style="previews.img" style="!max-width: 100%" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>
|
||||
<el-row>
|
||||
<el-col :lg="2" :md="2">
|
||||
<el-upload
|
||||
action="#"
|
||||
:http-request="requestUpload"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<el-button size="small">
|
||||
<Icon icon="ep:upload-filled" class="mr-5px" />
|
||||
选择
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
|
||||
<el-button size="small" @click="changeScale(1)">
|
||||
<Icon icon="ep:zoom-in" class="mr-5px" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button size="small" @click="changeScale(-1)">
|
||||
<Icon icon="ep:zoom-out" class="mr-5px" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button size="small" @click="rotateLeft()">
|
||||
<Icon icon="ep:arrow-left-bold" class="mr-5px" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button size="small" @click="rotateRight()">
|
||||
<Icon icon="ep:arrow-right-bold" class="mr-5px" />
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
|
||||
<el-button size="small" type="primary" @click="uploadImg()">提 交</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, Ref, UnwrapNestedRefs } from 'vue'
|
||||
import VueCropper from 'vue-cropper/lib/vue-cropper.vue'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { ElRow, ElCol, ElUpload, ElMessage, ElDialog } from 'element-plus'
|
||||
import { computed } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { CropperAvatar } from '@/components/Cropper'
|
||||
import { uploadAvatarApi } from '@/api/system/user/profile'
|
||||
|
||||
const cropper = ref()
|
||||
const dialogVisible = ref(false)
|
||||
const cropperVisible = ref(false)
|
||||
const props = defineProps({
|
||||
img: propTypes.string.def('')
|
||||
})
|
||||
interface Options {
|
||||
img: string | ArrayBuffer | null // 裁剪图片的地址
|
||||
info: true // 裁剪框的大小信息
|
||||
outputSize: number // 裁剪生成图片的质量 [1至0.1]
|
||||
outputType: 'jpeg' // 裁剪生成图片的格式
|
||||
canScale: boolean // 图片是否允许滚轮缩放
|
||||
autoCrop: boolean // 是否默认生成截图框
|
||||
autoCropWidth: number // 默认生成截图框宽度
|
||||
autoCropHeight: number // 默认生成截图框高度
|
||||
fixedBox: boolean // 固定截图框大小 不允许改变
|
||||
fixed: boolean // 是否开启截图框宽高固定比例
|
||||
fixedNumber: Array<number> // 截图框的宽高比例 需要配合centerBox一起使用才能生效
|
||||
full: boolean // 是否输出原图比例的截图
|
||||
canMoveBox: boolean // 截图框能否拖动
|
||||
original: boolean // 上传图片按照原始比例渲染
|
||||
centerBox: boolean // 截图框是否被限制在图片里面
|
||||
infoTrue: boolean // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
|
||||
}
|
||||
const options: UnwrapNestedRefs<Options> = reactive({
|
||||
img: '', // 需要剪裁的图片
|
||||
autoCrop: true, // 是否默认生成截图框
|
||||
autoCropWidth: 200, // 默认生成截图框的宽度
|
||||
autoCropHeight: 200, // 默认生成截图框的长度
|
||||
fixedBox: false, // 是否固定截图框的大小 不允许改变
|
||||
info: true, // 裁剪框的大小信息
|
||||
outputSize: 1, // 裁剪生成图片的质量 [1至0.1]
|
||||
outputType: 'jpeg', // 裁剪生成图片的格式
|
||||
canScale: false, // 图片是否允许滚轮缩放
|
||||
fixed: true, // 是否开启截图框宽高固定比例
|
||||
fixedNumber: [1, 1], // 截图框的宽高比例 需要配合centerBox一起使用才能生效
|
||||
full: true, // 是否输出原图比例的截图
|
||||
canMoveBox: false, // 截图框能否拖动
|
||||
original: false, // 上传图片按照原始比例渲染
|
||||
centerBox: true, // 截图框是否被限制在图片里面
|
||||
infoTrue: true // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
|
||||
const avatar = computed(() => {
|
||||
return props.img
|
||||
})
|
||||
const previews: Ref<any> = ref({})
|
||||
/** 编辑头像 */
|
||||
const editCropper = () => {
|
||||
dialogVisible.value = true
|
||||
|
||||
const handelUpload = async ({ data }) => {
|
||||
await uploadAvatarApi({ avatarFile: data })
|
||||
}
|
||||
/** 向左旋转 */
|
||||
const rotateLeft = () => {
|
||||
cropper.value.rotateLeft()
|
||||
}
|
||||
/** 向右旋转 */
|
||||
const rotateRight = () => {
|
||||
cropper.value.rotateRight()
|
||||
}
|
||||
/** 图片缩放 */
|
||||
const changeScale = (num: number) => {
|
||||
num = num || 1
|
||||
cropper.value.changeScale(num)
|
||||
}
|
||||
// 覆盖默认的上传行为
|
||||
const requestUpload: any = () => {}
|
||||
/** 上传预处理 */
|
||||
const beforeUpload = (file: Blob) => {
|
||||
if (file.type.indexOf('image/') == -1) {
|
||||
ElMessage('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')
|
||||
} else {
|
||||
const reader = new FileReader()
|
||||
// 转化为base64
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => {
|
||||
if (reader.result) {
|
||||
// 获取到需要剪裁的图片 展示到剪裁框中
|
||||
options.img = reader.result as string
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
/** 上传图片 */
|
||||
const uploadImg = () => {
|
||||
cropper.value.getCropBlob((data: any) => {
|
||||
let formData = new FormData()
|
||||
formData.append('avatarFile', data)
|
||||
uploadAvatarApi(formData).then((res) => {
|
||||
options.img = res
|
||||
window.location.reload()
|
||||
})
|
||||
dialogVisible.value = false
|
||||
cropperVisible.value = false
|
||||
})
|
||||
}
|
||||
/** 实时预览 */
|
||||
const realTime = (data: any) => {
|
||||
previews.value = data
|
||||
}
|
||||
watch(
|
||||
() => props.img,
|
||||
() => {
|
||||
if (props.img) {
|
||||
options.img = props.img
|
||||
previews.value.img = props.img
|
||||
previews.value.url = props.img
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-info-head {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.img-circle {
|
||||
<style scoped lang="scss">
|
||||
.change-avatar {
|
||||
img {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.img-lg {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.avatar-upload-preview {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
-webkit-transform: translate(50%, -50%);
|
||||
transform: translate(50%, -50%);
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
-webkit-box-shadow: 0 0 4px #ccc;
|
||||
box-shadow: 0 0 4px #ccc;
|
||||
overflow: hidden;
|
||||
}
|
||||
.user-info-head:hover:after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
color: #eee;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
cursor: pointer;
|
||||
line-height: 110px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue