Commit 91abae8898d5d4b301a236e5f9c6a3f78fc69900

Authored by xingyu
1 parent 0f562e59

refactor: cropper

yudao-dependencies/pom.xml
@@ -24,7 +24,7 @@ @@ -24,7 +24,7 @@
24 <!-- DB 相关 --> 24 <!-- DB 相关 -->
25 <druid.version>1.2.15</druid.version> 25 <druid.version>1.2.15</druid.version>
26 <mybatis-plus.version>3.5.2</mybatis-plus.version> 26 <mybatis-plus.version>3.5.2</mybatis-plus.version>
27 - <mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version> 27 + <mybatis-plus-generator.version>3.5.3</mybatis-plus-generator.version>
28 <dynamic-datasource.version>3.6.0</dynamic-datasource.version> 28 <dynamic-datasource.version>3.6.0</dynamic-datasource.version>
29 <redisson.version>3.18.0</redisson.version> 29 <redisson.version>3.18.0</redisson.version>
30 <!-- 服务保障相关 --> 30 <!-- 服务保障相关 -->
yudao-ui-admin-vue3/package.json
@@ -25,12 +25,13 @@ @@ -25,12 +25,13 @@
25 }, 25 },
26 "dependencies": { 26 "dependencies": {
27 "@iconify/iconify": "^3.0.1", 27 "@iconify/iconify": "^3.0.1",
28 - "@vueuse/core": "^9.7.0", 28 + "@vueuse/core": "^9.8.2",
29 "@wangeditor/editor": "^5.1.23", 29 "@wangeditor/editor": "^5.1.23",
30 "@wangeditor/editor-for-vue": "^5.1.10", 30 "@wangeditor/editor-for-vue": "^5.1.10",
31 "@zxcvbn-ts/core": "^2.1.0", 31 "@zxcvbn-ts/core": "^2.1.0",
32 "animate.css": "^4.1.1", 32 "animate.css": "^4.1.1",
33 "axios": "^1.2.1", 33 "axios": "^1.2.1",
  34 + "cropperjs": "^1.5.13",
34 "crypto-js": "^4.1.1", 35 "crypto-js": "^4.1.1",
35 "dayjs": "^1.11.7", 36 "dayjs": "^1.11.7",
36 "echarts": "^5.4.1", 37 "echarts": "^5.4.1",
@@ -46,7 +47,6 @@ @@ -46,7 +47,6 @@
46 "qs": "^6.11.0", 47 "qs": "^6.11.0",
47 "url": "^0.11.0", 48 "url": "^0.11.0",
48 "vue": "3.2.45", 49 "vue": "3.2.45",
49 - "vue-cropper": "^1.0.3",  
50 "vue-i18n": "9.2.2", 50 "vue-i18n": "9.2.2",
51 "vue-router": "^4.1.6", 51 "vue-router": "^4.1.6",
52 "vue-types": "^5.0.1", 52 "vue-types": "^5.0.1",
@@ -104,7 +104,7 @@ @@ -104,7 +104,7 @@
104 "vite-plugin-svg-icons": "^2.0.1", 104 "vite-plugin-svg-icons": "^2.0.1",
105 "vite-plugin-vue-setup-extend": "^0.4.0", 105 "vite-plugin-vue-setup-extend": "^0.4.0",
106 "vite-plugin-windicss": "^1.8.10", 106 "vite-plugin-windicss": "^1.8.10",
107 - "vue-tsc": "^1.0.14", 107 + "vue-tsc": "^1.0.16",
108 "windicss": "^3.5.6" 108 "windicss": "^3.5.6"
109 }, 109 },
110 "engines": { 110 "engines": {
yudao-ui-admin-vue3/pnpm-lock.yaml
@@ -18,7 +18,7 @@ specifiers: @@ -18,7 +18,7 @@ specifiers:
18 '@vitejs/plugin-legacy': ^3.0.1 18 '@vitejs/plugin-legacy': ^3.0.1
19 '@vitejs/plugin-vue': ^4.0.0 19 '@vitejs/plugin-vue': ^4.0.0
20 '@vitejs/plugin-vue-jsx': ^3.0.0 20 '@vitejs/plugin-vue-jsx': ^3.0.0
21 - '@vueuse/core': ^9.7.0 21 + '@vueuse/core': ^9.8.2
22 '@wangeditor/editor': ^5.1.23 22 '@wangeditor/editor': ^5.1.23
23 '@wangeditor/editor-for-vue': ^5.1.10 23 '@wangeditor/editor-for-vue': ^5.1.10
24 '@zxcvbn-ts/core': ^2.1.0 24 '@zxcvbn-ts/core': ^2.1.0
@@ -26,6 +26,7 @@ specifiers: @@ -26,6 +26,7 @@ specifiers:
26 autoprefixer: ^10.4.13 26 autoprefixer: ^10.4.13
27 axios: ^1.2.1 27 axios: ^1.2.1
28 consola: ^2.15.3 28 consola: ^2.15.3
  29 + cropperjs: ^1.5.13
29 crypto-js: ^4.1.1 30 crypto-js: ^4.1.1
30 dayjs: ^1.11.7 31 dayjs: ^1.11.7
31 echarts: ^5.4.1 32 echarts: ^5.4.1
@@ -72,10 +73,9 @@ specifiers: @@ -72,10 +73,9 @@ specifiers:
72 vite-plugin-vue-setup-extend: ^0.4.0 73 vite-plugin-vue-setup-extend: ^0.4.0
73 vite-plugin-windicss: ^1.8.10 74 vite-plugin-windicss: ^1.8.10
74 vue: 3.2.45 75 vue: 3.2.45
75 - vue-cropper: ^1.0.3  
76 vue-i18n: 9.2.2 76 vue-i18n: 9.2.2
77 vue-router: ^4.1.6 77 vue-router: ^4.1.6
78 - vue-tsc: ^1.0.14 78 + vue-tsc: ^1.0.16
79 vue-types: ^5.0.1 79 vue-types: ^5.0.1
80 vxe-table: ^4.3.7 80 vxe-table: ^4.3.7
81 web-storage-cache: ^1.1.1 81 web-storage-cache: ^1.1.1
@@ -84,12 +84,13 @@ specifiers: @@ -84,12 +84,13 @@ specifiers:
84 84
85 dependencies: 85 dependencies:
86 '@iconify/iconify': 3.0.1 86 '@iconify/iconify': 3.0.1
87 - '@vueuse/core': 9.7.0_vue@3.2.45 87 + '@vueuse/core': 9.8.2_vue@3.2.45
88 '@wangeditor/editor': 5.1.23 88 '@wangeditor/editor': 5.1.23
89 '@wangeditor/editor-for-vue': 5.1.12_3apfu3xbp6awzuex7ed3sbrv6y 89 '@wangeditor/editor-for-vue': 5.1.12_3apfu3xbp6awzuex7ed3sbrv6y
90 '@zxcvbn-ts/core': 2.1.0 90 '@zxcvbn-ts/core': 2.1.0
91 animate.css: 4.1.1 91 animate.css: 4.1.1
92 axios: 1.2.1 92 axios: 1.2.1
  93 + cropperjs: 1.5.13
93 crypto-js: 4.1.1 94 crypto-js: 4.1.1
94 dayjs: 1.11.7 95 dayjs: 1.11.7
95 echarts: registry.npmmirror.com/echarts/5.4.1 96 echarts: registry.npmmirror.com/echarts/5.4.1
@@ -105,7 +106,6 @@ dependencies: @@ -105,7 +106,6 @@ dependencies:
105 qs: 6.11.0 106 qs: 6.11.0
106 url: 0.11.0 107 url: 0.11.0
107 vue: 3.2.45 108 vue: 3.2.45
108 - vue-cropper: 1.0.5  
109 vue-i18n: 9.2.2_vue@3.2.45 109 vue-i18n: 9.2.2_vue@3.2.45
110 vue-router: 4.1.6_vue@3.2.45 110 vue-router: 4.1.6_vue@3.2.45
111 vue-types: 5.0.1_vue@3.2.45 111 vue-types: 5.0.1_vue@3.2.45
@@ -163,7 +163,7 @@ devDependencies: @@ -163,7 +163,7 @@ devDependencies:
163 vite-plugin-svg-icons: 2.0.1_vite@4.0.2 163 vite-plugin-svg-icons: 2.0.1_vite@4.0.2
164 vite-plugin-vue-setup-extend: 0.4.0_vite@4.0.2 164 vite-plugin-vue-setup-extend: 0.4.0_vite@4.0.2
165 vite-plugin-windicss: registry.npmmirror.com/vite-plugin-windicss/1.8.10_vite@4.0.2 165 vite-plugin-windicss: registry.npmmirror.com/vite-plugin-windicss/1.8.10_vite@4.0.2
166 - vue-tsc: 1.0.14_typescript@4.9.4 166 + vue-tsc: 1.0.16_typescript@4.9.4
167 windicss: 3.5.6 167 windicss: 3.5.6
168 168
169 packages: 169 packages:
@@ -1079,44 +1079,44 @@ packages: @@ -1079,44 +1079,44 @@ packages:
1079 nanoid: registry.npmmirror.com/nanoid/3.3.4 1079 nanoid: registry.npmmirror.com/nanoid/3.3.4
1080 dev: false 1080 dev: false
1081 1081
1082 - /@volar/language-core/1.0.14:  
1083 - resolution: {integrity: sha512-j1tMQgw0qCV2amM4qDJNG/zc0yj3ay8HoWNt05IaiCPsULtSSpF/9+F6Izvn0DF7nWOd6MUHTxaQAeZwLfr56Q==} 1082 + /@volar/language-core/1.0.16:
  1083 + resolution: {integrity: sha512-IGnOxWTs4DZ81TDcmxBAkCBxs97hUblwcjpBsTx/pOGGaSSDQRJPn0wL8NYTybEObU0i7lhEpKZ+0vJfdIy1Kg==}
1084 dependencies: 1084 dependencies:
1085 - '@volar/source-map': 1.0.14 1085 + '@volar/source-map': 1.0.16
1086 '@vue/reactivity': 3.2.45 1086 '@vue/reactivity': 3.2.45
1087 muggle-string: 0.1.0 1087 muggle-string: 0.1.0
1088 dev: true 1088 dev: true
1089 1089
1090 - /@volar/source-map/1.0.14:  
1091 - resolution: {integrity: sha512-8pHCbEWHWaSDGb/FM9zRIW1lY1OAo16MENVSQGCgTwz7PWf3Gw6WW3TFVKCtzaFhLjPH0i5e9hALy7vBPbSHoA==} 1090 + /@volar/source-map/1.0.16:
  1091 + resolution: {integrity: sha512-PKjzmQcg8QOGC/1V9tmGh2jcy6bKLhkW5bGidElSr83iDbCzLvldt2/La/QlDxaRCHYLT0MeyuGJBZIChB1dYQ==}
1092 dependencies: 1092 dependencies:
1093 muggle-string: 0.1.0 1093 muggle-string: 0.1.0
1094 dev: true 1094 dev: true
1095 1095
1096 - /@volar/typescript/1.0.14:  
1097 - resolution: {integrity: sha512-67qcjjz7KGFhMCG9EKMA9qJK3BRGQecO4dGyAKfMfClZ/PaVoKfDvJvYo89McGTQ8SeczD48I9TPnaJM0zK8JQ==} 1096 + /@volar/typescript/1.0.16:
  1097 + resolution: {integrity: sha512-Yov+n4oO3iYnuMt9QJAFpJabfTRCzc7KvjlAwBaSuZy+Gc/f9611MgtqAh5/SIGmltFN8dXn1Ijno8ro8I4lyw==}
1098 dependencies: 1098 dependencies:
1099 - '@volar/language-core': 1.0.14 1099 + '@volar/language-core': 1.0.16
1100 dev: true 1100 dev: true
1101 1101
1102 - /@volar/vue-language-core/1.0.14:  
1103 - resolution: {integrity: sha512-grJ4dQ7c/suZmBBmZtw2O2XeDX+rtgpdBtHxMug1NMPRDxj5EZ9WGphWtGnMQj8RyVgpz9ByvV5GbQjk4/wfBw==} 1102 + /@volar/vue-language-core/1.0.16:
  1103 + resolution: {integrity: sha512-sQ/aW1Vuiyy4OQuh2lthyYicruM3qh9VSk/aDh8/bFvM8GoohHZqVpMN3LYldEJ9eT/rN6u4xmYP54vc/EjX4Q==}
1104 dependencies: 1104 dependencies:
1105 - '@volar/language-core': 1.0.14  
1106 - '@volar/source-map': 1.0.14 1105 + '@volar/language-core': 1.0.16
  1106 + '@volar/source-map': 1.0.16
1107 '@vue/compiler-dom': 3.2.45 1107 '@vue/compiler-dom': 3.2.45
1108 '@vue/compiler-sfc': 3.2.45 1108 '@vue/compiler-sfc': 3.2.45
1109 '@vue/reactivity': 3.2.45 1109 '@vue/reactivity': 3.2.45
1110 '@vue/shared': 3.2.45 1110 '@vue/shared': 3.2.45
1111 - minimatch: 5.1.0 1111 + minimatch: 5.1.2
1112 vue-template-compiler: 2.7.14 1112 vue-template-compiler: 2.7.14
1113 dev: true 1113 dev: true
1114 1114
1115 - /@volar/vue-typescript/1.0.14:  
1116 - resolution: {integrity: sha512-2P0QeGLLY05fDTu8GqY8SR2+jldXRTrkQdD2Nc0sVOjMJ7j3RYYY0wJyZ9hCBDuxV4Micc6jdB8nKS0yxQgNvA==} 1115 + /@volar/vue-typescript/1.0.16:
  1116 + resolution: {integrity: sha512-M018Ulg/o2FVktAdlr5b/z4K69bYzekxNUA1o39y5Ur6CObc/o+5eDCCS7gIYijWnx9iNKkSQpWWWblJFv7kHQ==}
1117 dependencies: 1117 dependencies:
1118 - '@volar/typescript': 1.0.14  
1119 - '@volar/vue-language-core': 1.0.14 1118 + '@volar/typescript': 1.0.16
  1119 + '@volar/vue-language-core': 1.0.16
1120 dev: true 1120 dev: true
1121 1121
1122 /@vue/compiler-core/3.2.45: 1122 /@vue/compiler-core/3.2.45:
@@ -1196,24 +1196,24 @@ packages: @@ -1196,24 +1196,24 @@ packages:
1196 /@vue/shared/3.2.45: 1196 /@vue/shared/3.2.45:
1197 resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==} 1197 resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
1198 1198
1199 - /@vueuse/core/9.7.0_vue@3.2.45:  
1200 - resolution: {integrity: sha512-/AGY/t7jJPxCyRoVTygNKoroTiCvRaaZIW+yeSlBCnI7QRpQ9cvXNTdNaSl3GvSyFbn83+XwZwEZvI1OpQfeGw==} 1199 + /@vueuse/core/9.8.2_vue@3.2.45:
  1200 + resolution: {integrity: sha512-aWiCmcYIpPt7xjuqYiceODEMHchDYthrJ4AqI+FXPZrR23PZOqdiktbUVyQl2kGlR3H4i9UJ/uimQrwhz9UouQ==}
1201 dependencies: 1201 dependencies:
1202 '@types/web-bluetooth': 0.0.16 1202 '@types/web-bluetooth': 0.0.16
1203 - '@vueuse/metadata': 9.7.0  
1204 - '@vueuse/shared': 9.7.0_vue@3.2.45 1203 + '@vueuse/metadata': 9.8.2
  1204 + '@vueuse/shared': 9.8.2_vue@3.2.45
1205 vue-demi: 0.13.11_vue@3.2.45 1205 vue-demi: 0.13.11_vue@3.2.45
1206 transitivePeerDependencies: 1206 transitivePeerDependencies:
1207 - '@vue/composition-api' 1207 - '@vue/composition-api'
1208 - vue 1208 - vue
1209 dev: false 1209 dev: false
1210 1210
1211 - /@vueuse/metadata/9.7.0:  
1212 - resolution: {integrity: sha512-M7WsAgw28FNtTH0bzsGuHEtJOPJqPpyeHS6PHq+8UesLgNjZ9waMAntiUrgUQlxt09M4i2lH7y9sRi0jkfeXGA==} 1211 + /@vueuse/metadata/9.8.2:
  1212 + resolution: {integrity: sha512-N4E/BKS+9VsUeD4WLVRU1J2kCOLh+iikBcMtipFcTyL204132vDYHs27zLAVabJYGnhC0dIVGdhg9pbOZiY2TQ==}
1213 dev: false 1213 dev: false
1214 1214
1215 - /@vueuse/shared/9.7.0_vue@3.2.45:  
1216 - resolution: {integrity: sha512-pwmt1y3TJ2s5KqWmkv9ZKEV59GwuZQZk8XLiU+hGswz0jej318ozbea9E4A/A50ksyM26swSFr7sZ9llNPsZHg==} 1215 + /@vueuse/shared/9.8.2_vue@3.2.45:
  1216 + resolution: {integrity: sha512-ACjrPQzowd5dnabNJt9EoGVobco9/ENiA5qP53vjiuxndlJYuc/UegwhXC7KdQbPX4F45a50+45K3g1wNqOzmA==}
1217 dependencies: 1217 dependencies:
1218 vue-demi: 0.13.11_vue@3.2.45 1218 vue-demi: 0.13.11_vue@3.2.45
1219 transitivePeerDependencies: 1219 transitivePeerDependencies:
@@ -2019,6 +2019,10 @@ packages: @@ -2019,6 +2019,10 @@ packages:
2019 resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 2019 resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
2020 dev: true 2020 dev: true
2021 2021
  2022 + /cropperjs/1.5.13:
  2023 + resolution: {integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==}
  2024 + dev: false
  2025 +
2022 /cross-spawn/7.0.3: 2026 /cross-spawn/7.0.3:
2023 resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 2027 resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
2024 engines: {node: '>= 8'} 2028 engines: {node: '>= 8'}
@@ -2319,7 +2323,7 @@ packages: @@ -2319,7 +2323,7 @@ packages:
2319 '@popperjs/core': /@sxzz/popperjs-es/2.11.7 2323 '@popperjs/core': /@sxzz/popperjs-es/2.11.7
2320 '@types/lodash': 4.14.189 2324 '@types/lodash': 4.14.189
2321 '@types/lodash-es': 4.17.6 2325 '@types/lodash-es': 4.17.6
2322 - '@vueuse/core': 9.7.0_vue@3.2.45 2326 + '@vueuse/core': 9.8.2_vue@3.2.45
2323 async-validator: 4.2.5 2327 async-validator: 4.2.5
2324 dayjs: 1.11.7 2328 dayjs: 1.11.7
2325 escape-html: 1.0.3 2329 escape-html: 1.0.3
@@ -3831,6 +3835,13 @@ packages: @@ -3831,6 +3835,13 @@ packages:
3831 brace-expansion: 2.0.1 3835 brace-expansion: 2.0.1
3832 dev: true 3836 dev: true
3833 3837
  3838 + /minimatch/5.1.2:
  3839 + resolution: {integrity: sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==}
  3840 + engines: {node: '>=10'}
  3841 + dependencies:
  3842 + brace-expansion: 2.0.1
  3843 + dev: true
  3844 +
3834 /minimist-options/4.1.0: 3845 /minimist-options/4.1.0:
3835 resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} 3846 resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==}
3836 engines: {node: '>= 6'} 3847 engines: {node: '>= 6'}
@@ -5461,10 +5472,6 @@ packages: @@ -5461,10 +5472,6 @@ packages:
5461 fsevents: 2.3.2 5472 fsevents: 2.3.2
5462 dev: true 5473 dev: true
5463 5474
5464 - /vue-cropper/1.0.5:  
5465 - resolution: {integrity: sha512-D4XXdqWmMWRLOIV9LIh7/mkH6OBOMQDFbRjwntkxmAtxOtwpC9U5ZZ6lSXw5F5cbd4g8znDjk6MuCwIL+fZSrA==}  
5466 - dev: false  
5467 -  
5468 /vue-demi/0.13.11_vue@3.2.45: 5475 /vue-demi/0.13.11_vue@3.2.45:
5469 resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} 5476 resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
5470 engines: {node: '>=12'} 5477 engines: {node: '>=12'}
@@ -5526,14 +5533,14 @@ packages: @@ -5526,14 +5533,14 @@ packages:
5526 he: 1.2.0 5533 he: 1.2.0
5527 dev: true 5534 dev: true
5528 5535
5529 - /vue-tsc/1.0.14_typescript@4.9.4:  
5530 - resolution: {integrity: sha512-HeqtyxMrSRUCnU5nxB0lQc3o7zirMppZ/V6HLL3l4FsObGepH3A3beNmNehpLQs0Gt7DkSWVi3CpVCFgrf+/sQ==} 5536 + /vue-tsc/1.0.16_typescript@4.9.4:
  5537 + resolution: {integrity: sha512-yZaiJBbcKR1rSLhiF9KryAFH7R63po+N/invr2EAHGXxMzZksE5j1zyQKvrYiqK47ZHLAlCR+re/PHqWp/UzTg==}
5531 hasBin: true 5538 hasBin: true
5532 peerDependencies: 5539 peerDependencies:
5533 typescript: '*' 5540 typescript: '*'
5534 dependencies: 5541 dependencies:
5535 - '@volar/vue-language-core': 1.0.14  
5536 - '@volar/vue-typescript': 1.0.14 5542 + '@volar/vue-language-core': 1.0.16
  5543 + '@volar/vue-typescript': 1.0.16
5537 typescript: 4.9.4 5544 typescript: 4.9.4
5538 dev: true 5545 dev: true
5539 5546
yudao-ui-admin-vue3/src/api/system/user/profile.ts
@@ -73,5 +73,5 @@ export const updateUserPwdApi = (oldPassword: string, newPassword: string) =&gt; { @@ -73,5 +73,5 @@ export const updateUserPwdApi = (oldPassword: string, newPassword: string) =&gt; {
73 73
74 // 用户头像上传 74 // 用户头像上传
75 export const uploadAvatarApi = (data) => { 75 export const uploadAvatarApi = (data) => {
76 - return request.put({ url: '/system/user/profile/update-avatar', data }) 76 + return request.upload({ url: '/system/user/profile/update-avatar', data: data })
77 } 77 }
yudao-ui-admin-vue3/src/components/Cropper/index.ts 0 → 100644
  1 +import CropperImage from './src/Cropper.vue'
  2 +import CropperAvatar from './src/CropperAvatar.vue'
  3 +
  4 +export { CropperImage, CropperAvatar }
yudao-ui-admin-vue3/src/components/Cropper/src/CopperModal.vue 0 → 100644
  1 +<template>
  2 + <Dialog
  3 + v-model="dialogVisible"
  4 + :title="t('cropper.modalTitle')"
  5 + width="800px"
  6 + maxHeight="380px"
  7 + :canFullscreen="false"
  8 + >
  9 + <div :class="prefixCls">
  10 + <div :class="`${prefixCls}-left`">
  11 + <div :class="`${prefixCls}-cropper`">
  12 + <CropperImage
  13 + v-if="src"
  14 + :src="src"
  15 + height="300px"
  16 + :circled="circled"
  17 + @cropend="handleCropend"
  18 + @ready="handleReady"
  19 + />
  20 + </div>
  21 +
  22 + <div :class="`${prefixCls}-toolbar`">
  23 + <el-upload :fileList="[]" accept="image/*" :beforeUpload="handleBeforeUpload">
  24 + <el-tooltip :content="t('cropper.selectImage')" placement="bottom">
  25 + <XButton preIcon="ant-design:upload-outlined" type="primary" />
  26 + </el-tooltip>
  27 + </el-upload>
  28 + <el-space>
  29 + <el-tooltip :content="t('cropper.btn_reset')" placement="bottom">
  30 + <XButton
  31 + type="primary"
  32 + preIcon="ant-design:reload-outlined"
  33 + size="small"
  34 + :disabled="!src"
  35 + @click="handlerToolbar('reset')"
  36 + />
  37 + </el-tooltip>
  38 + <el-tooltip :content="t('cropper.btn_rotate_left')" placement="bottom">
  39 + <XButton
  40 + type="primary"
  41 + preIcon="ant-design:rotate-left-outlined"
  42 + size="small"
  43 + :disabled="!src"
  44 + @click="handlerToolbar('rotate', -45)"
  45 + />
  46 + </el-tooltip>
  47 + <el-tooltip :content="t('cropper.btn_rotate_right')" placement="bottom">
  48 + <XButton
  49 + type="primary"
  50 + preIcon="ant-design:rotate-right-outlined"
  51 + size="small"
  52 + :disabled="!src"
  53 + @click="handlerToolbar('rotate', 45)"
  54 + />
  55 + </el-tooltip>
  56 + <el-tooltip :content="t('cropper.btn_scale_x')" placement="bottom">
  57 + <XButton
  58 + type="primary"
  59 + preIcon="vaadin:arrows-long-h"
  60 + size="small"
  61 + :disabled="!src"
  62 + @click="handlerToolbar('scaleX')"
  63 + />
  64 + </el-tooltip>
  65 + <el-tooltip :content="t('cropper.btn_scale_y')" placement="bottom">
  66 + <XButton
  67 + type="primary"
  68 + preIcon="vaadin:arrows-long-v"
  69 + size="small"
  70 + :disabled="!src"
  71 + @click="handlerToolbar('scaleY')"
  72 + />
  73 + </el-tooltip>
  74 + <el-tooltip :content="t('cropper.btn_zoom_in')" placement="bottom">
  75 + <XButton
  76 + type="primary"
  77 + preIcon="ant-design:zoom-in-outlined"
  78 + size="small"
  79 + :disabled="!src"
  80 + @click="handlerToolbar('zoom', 0.1)"
  81 + />
  82 + </el-tooltip>
  83 + <el-tooltip :content="t('cropper.btn_zoom_out')" placement="bottom">
  84 + <XButton
  85 + type="primary"
  86 + preIcon="ant-design:zoom-out-outlined"
  87 + size="small"
  88 + :disabled="!src"
  89 + @click="handlerToolbar('zoom', -0.1)"
  90 + />
  91 + </el-tooltip>
  92 + </el-space>
  93 + </div>
  94 + </div>
  95 + <div :class="`${prefixCls}-right`">
  96 + <div :class="`${prefixCls}-preview`">
  97 + <img :src="previewSource" v-if="previewSource" :alt="t('cropper.preview')" />
  98 + </div>
  99 + <template v-if="previewSource">
  100 + <div :class="`${prefixCls}-group`">
  101 + <el-avatar :src="previewSource" size="large" />
  102 + <el-avatar :src="previewSource" :size="48" />
  103 + <el-avatar :src="previewSource" :size="64" />
  104 + <el-avatar :src="previewSource" :size="80" />
  105 + </div>
  106 + </template>
  107 + </div>
  108 + </div>
  109 + <template #footer>
  110 + <el-button type="primary" @click="handleOk">{{ t('cropper.okText') }}</el-button>
  111 + </template>
  112 + </Dialog>
  113 +</template>
  114 +<script setup lang="ts">
  115 +import { useDesign } from '@/hooks/web/useDesign'
  116 +import { dataURLtoBlob } from '@/utils/filt'
  117 +import { ref } from 'vue'
  118 +import { useI18n } from 'vue-i18n'
  119 +import { ElUpload, ElAvatar, ElTooltip, ElSpace } from 'element-plus'
  120 +import { Dialog } from '@/components/Dialog'
  121 +import { CropperImage } from '@/components/Cropper'
  122 +import type { CropendResult, Cropper } from './types'
  123 +import { propTypes } from '@/utils/propTypes'
  124 +
  125 +const props = defineProps({
  126 + srcValue: propTypes.string.def(''),
  127 + circled: propTypes.bool.def(true)
  128 +})
  129 +const emit = defineEmits(['uploadSuccess'])
  130 +const { t } = useI18n()
  131 +const { getPrefixCls } = useDesign()
  132 +const prefixCls = getPrefixCls('cropper-am')
  133 +
  134 +const src = ref(props.srcValue)
  135 +const previewSource = ref('')
  136 +const cropper = ref<Cropper>()
  137 +const dialogVisible = ref(false)
  138 +let filename = ''
  139 +let scaleX = 1
  140 +let scaleY = 1
  141 +
  142 +// Block upload
  143 +function handleBeforeUpload(file: File) {
  144 + const reader = new FileReader()
  145 + reader.readAsDataURL(file)
  146 + src.value = ''
  147 + previewSource.value = ''
  148 + reader.onload = function (e) {
  149 + src.value = (e.target?.result as string) ?? ''
  150 + filename = file.name
  151 + }
  152 + return false
  153 +}
  154 +
  155 +function handleCropend({ imgBase64 }: CropendResult) {
  156 + previewSource.value = imgBase64
  157 +}
  158 +
  159 +function handleReady(cropperInstance: Cropper) {
  160 + cropper.value = cropperInstance
  161 +}
  162 +
  163 +function handlerToolbar(event: string, arg?: number) {
  164 + if (event === 'scaleX') {
  165 + scaleX = arg = scaleX === -1 ? 1 : -1
  166 + }
  167 + if (event === 'scaleY') {
  168 + scaleY = arg = scaleY === -1 ? 1 : -1
  169 + }
  170 + cropper?.value?.[event]?.(arg)
  171 +}
  172 +
  173 +async function handleOk() {
  174 + const blob = dataURLtoBlob(previewSource.value)
  175 + emit('uploadSuccess', { source: previewSource.value, data: blob, filename: filename })
  176 + closeModal()
  177 +}
  178 +function openModal() {
  179 + dialogVisible.value = true
  180 +}
  181 +function closeModal() {
  182 + dialogVisible.value = false
  183 +}
  184 +defineExpose({ openModal, closeModal })
  185 +</script>
  186 +<style lang="scss">
  187 +$prefix-cls: #{$namespace}-cropper-am;
  188 +
  189 +.#{$prefix-cls} {
  190 + display: flex;
  191 +
  192 + &-left,
  193 + &-right {
  194 + height: 340px;
  195 + }
  196 +
  197 + &-left {
  198 + width: 55%;
  199 + }
  200 +
  201 + &-right {
  202 + width: 45%;
  203 + }
  204 +
  205 + &-cropper {
  206 + height: 300px;
  207 + background: #eee;
  208 + background-image: linear-gradient(
  209 + 45deg,
  210 + rgb(0 0 0 / 25%) 25%,
  211 + transparent 0,
  212 + transparent 75%,
  213 + rgb(0 0 0 / 25%) 0
  214 + ),
  215 + linear-gradient(
  216 + 45deg,
  217 + rgb(0 0 0 / 25%) 25%,
  218 + transparent 0,
  219 + transparent 75%,
  220 + rgb(0 0 0 / 25%) 0
  221 + );
  222 + background-position: 0 0, 12px 12px;
  223 + background-size: 24px 24px;
  224 + }
  225 +
  226 + &-toolbar {
  227 + display: flex;
  228 + justify-content: space-between;
  229 + align-items: center;
  230 + margin-top: 10px;
  231 + }
  232 +
  233 + &-preview {
  234 + width: 220px;
  235 + height: 220px;
  236 + margin: 0 auto;
  237 + overflow: hidden;
  238 + border: 1px solid;
  239 + border-radius: 50%;
  240 +
  241 + img {
  242 + width: 100%;
  243 + height: 100%;
  244 + }
  245 + }
  246 +
  247 + &-group {
  248 + display: flex;
  249 + padding-top: 8px;
  250 + margin-top: 8px;
  251 + border-top: 1px solid;
  252 + justify-content: space-around;
  253 + align-items: center;
  254 + }
  255 +}
  256 +</style>
yudao-ui-admin-vue3/src/components/Cropper/src/Cropper.vue 0 → 100644
  1 +<template>
  2 + <div :class="getClass" :style="getWrapperStyle">
  3 + <img
  4 + v-show="isReady"
  5 + ref="imgElRef"
  6 + :src="src"
  7 + :alt="alt"
  8 + :crossorigin="crossorigin"
  9 + :style="getImageStyle"
  10 + />
  11 + </div>
  12 +</template>
  13 +<script setup lang="ts">
  14 +import {
  15 + computed,
  16 + CSSProperties,
  17 + onMounted,
  18 + onUnmounted,
  19 + PropType,
  20 + ref,
  21 + unref,
  22 + useAttrs
  23 +} from 'vue'
  24 +import Cropper from 'cropperjs'
  25 +import 'cropperjs/dist/cropper.css'
  26 +import { useDesign } from '@/hooks/web/useDesign'
  27 +import { useDebounceFn } from '@vueuse/core'
  28 +import { propTypes } from '@/utils/propTypes'
  29 +
  30 +type Options = Cropper.Options
  31 +
  32 +const defaultOptions: Options = {
  33 + aspectRatio: 1,
  34 + zoomable: true,
  35 + zoomOnTouch: true,
  36 + zoomOnWheel: true,
  37 + cropBoxMovable: true,
  38 + cropBoxResizable: true,
  39 + toggleDragModeOnDblclick: true,
  40 + autoCrop: true,
  41 + background: true,
  42 + highlight: true,
  43 + center: true,
  44 + responsive: true,
  45 + restore: true,
  46 + checkCrossOrigin: true,
  47 + checkOrientation: true,
  48 + scalable: true,
  49 + modal: true,
  50 + guides: true,
  51 + movable: true,
  52 + rotatable: true
  53 +}
  54 +
  55 +const props = defineProps({
  56 + src: propTypes.string.def(''),
  57 + alt: propTypes.string.def(''),
  58 + circled: propTypes.bool.def(false),
  59 + realTimePreview: propTypes.bool.def(true),
  60 + height: propTypes.string.def('360px'),
  61 + crossorigin: {
  62 + type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
  63 + default: undefined
  64 + },
  65 + imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
  66 + options: { type: Object as PropType<Options>, default: () => ({}) }
  67 +})
  68 +
  69 +const emit = defineEmits(['cropend', 'ready', 'cropendError'])
  70 +const attrs = useAttrs()
  71 +const imgElRef = ref<ElRef<HTMLImageElement>>()
  72 +const cropper = ref<Nullable<Cropper>>()
  73 +const isReady = ref(false)
  74 +
  75 +const { getPrefixCls } = useDesign()
  76 +const prefixCls = getPrefixCls('cropper-image')
  77 +const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)
  78 +
  79 +const getImageStyle = computed((): CSSProperties => {
  80 + return {
  81 + height: props.height,
  82 + maxWidth: '100%',
  83 + ...props.imageStyle
  84 + }
  85 +})
  86 +
  87 +const getClass = computed(() => {
  88 + return [
  89 + prefixCls,
  90 + attrs.class,
  91 + {
  92 + [`${prefixCls}--circled`]: props.circled
  93 + }
  94 + ]
  95 +})
  96 +const getWrapperStyle = computed((): CSSProperties => {
  97 + return { height: `${props.height}`.replace(/px/, '') + 'px' }
  98 +})
  99 +
  100 +onMounted(init)
  101 +
  102 +onUnmounted(() => {
  103 + cropper.value?.destroy()
  104 +})
  105 +
  106 +async function init() {
  107 + const imgEl = unref(imgElRef)
  108 + if (!imgEl) {
  109 + return
  110 + }
  111 + cropper.value = new Cropper(imgEl, {
  112 + ...defaultOptions,
  113 + ready: () => {
  114 + isReady.value = true
  115 + realTimeCroppered()
  116 + emit('ready', cropper.value)
  117 + },
  118 + crop() {
  119 + debounceRealTimeCroppered()
  120 + },
  121 + zoom() {
  122 + debounceRealTimeCroppered()
  123 + },
  124 + cropmove() {
  125 + debounceRealTimeCroppered()
  126 + },
  127 + ...props.options
  128 + })
  129 +}
  130 +
  131 +// Real-time display preview
  132 +function realTimeCroppered() {
  133 + props.realTimePreview && croppered()
  134 +}
  135 +
  136 +// event: return base64 and width and height information after cropping
  137 +function croppered() {
  138 + if (!cropper.value) {
  139 + return
  140 + }
  141 + let imgInfo = cropper.value.getData()
  142 + const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
  143 + canvas.toBlob((blob) => {
  144 + if (!blob) {
  145 + return
  146 + }
  147 + let fileReader: FileReader = new FileReader()
  148 + fileReader.readAsDataURL(blob)
  149 + fileReader.onloadend = (e) => {
  150 + emit('cropend', {
  151 + imgBase64: e.target?.result ?? '',
  152 + imgInfo
  153 + })
  154 + }
  155 + fileReader.onerror = () => {
  156 + emit('cropendError')
  157 + }
  158 + }, 'image/png')
  159 +}
  160 +
  161 +// Get a circular picture canvas
  162 +function getRoundedCanvas() {
  163 + const sourceCanvas = cropper.value!.getCroppedCanvas()
  164 + const canvas = document.createElement('canvas')
  165 + const context = canvas.getContext('2d')!
  166 + const width = sourceCanvas.width
  167 + const height = sourceCanvas.height
  168 + canvas.width = width
  169 + canvas.height = height
  170 + context.imageSmoothingEnabled = true
  171 + context.drawImage(sourceCanvas, 0, 0, width, height)
  172 + context.globalCompositeOperation = 'destination-in'
  173 + context.beginPath()
  174 + context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
  175 + context.fill()
  176 + return canvas
  177 +}
  178 +</script>
  179 +<style lang="scss">
  180 +$prefix-cls: #{$namespace}-cropper-image;
  181 +
  182 +.#{$prefix-cls} {
  183 + &--circled {
  184 + .cropper-view-box,
  185 + .cropper-face {
  186 + border-radius: 50%;
  187 + }
  188 + }
  189 +}
  190 +</style>
yudao-ui-admin-vue3/src/components/Cropper/src/CropperAvatar.vue 0 → 100644
  1 +<template>
  2 + <div class="user-info-head" @click="open()">
  3 + <img :src="sourceValue" v-if="sourceValue" class="img-circle img-lg" alt="avatar" />
  4 + <el-button :class="`${prefixCls}-upload-btn`" @click="open()" v-if="showBtn">
  5 + {{ btnText ? btnText : t('cropper.selectImage') }}
  6 + </el-button>
  7 + <CopperModal ref="cropperModel" @upload-success="handleUploadSuccess" :srcValue="sourceValue" />
  8 + </div>
  9 +</template>
  10 +<script setup lang="ts">
  11 +import { useDesign } from '@/hooks/web/useDesign'
  12 +import { useMessage } from '@/hooks/web/useMessage'
  13 +import { propTypes } from '@/utils/propTypes'
  14 +import { ref, watch, watchEffect } from 'vue'
  15 +import { useI18n } from 'vue-i18n'
  16 +import CopperModal from './CopperModal.vue'
  17 +
  18 +const props = defineProps({
  19 + width: propTypes.string.def('200px'),
  20 + value: propTypes.string.def(''),
  21 + showBtn: propTypes.bool.def(true),
  22 + btnText: propTypes.string.def('')
  23 +})
  24 +
  25 +const emit = defineEmits(['update:value', 'change'])
  26 +const sourceValue = ref(props.value)
  27 +const { getPrefixCls } = useDesign()
  28 +const prefixCls = getPrefixCls('cropper-avatar')
  29 +const message = useMessage()
  30 +const { t } = useI18n()
  31 +
  32 +const cropperModel = ref()
  33 +
  34 +watchEffect(() => {
  35 + sourceValue.value = props.value
  36 +})
  37 +
  38 +watch(
  39 + () => sourceValue.value,
  40 + (v: string) => {
  41 + emit('update:value', v)
  42 + }
  43 +)
  44 +
  45 +function handleUploadSuccess({ source, data, filename }) {
  46 + sourceValue.value = source
  47 + emit('change', { source, data, filename })
  48 + message.success(t('cropper.uploadSuccess'))
  49 +}
  50 +
  51 +function open() {
  52 + cropperModel.value.openModal()
  53 +}
  54 +</script>
  55 +<style lang="scss" scoped>
  56 +$prefix-cls: #{$namespace}--cropper-avatar;
  57 +
  58 +.#{$prefix-cls} {
  59 + display: inline-block;
  60 + text-align: center;
  61 +
  62 + &-image-wrapper {
  63 + overflow: hidden;
  64 + cursor: pointer;
  65 + border: 1px solid;
  66 + border-radius: 50%;
  67 +
  68 + img {
  69 + width: 100%;
  70 + }
  71 + }
  72 +
  73 + &-image-mask {
  74 + opacity: 0%;
  75 + position: absolute;
  76 + width: inherit;
  77 + height: inherit;
  78 + border-radius: inherit;
  79 + border: inherit;
  80 + background: rgb(0 0 0 / 40%);
  81 + cursor: pointer;
  82 + transition: opacity 0.4s;
  83 +
  84 + ::v-deep(svg) {
  85 + margin: auto;
  86 + }
  87 + }
  88 +
  89 + &-image-mask:hover {
  90 + opacity: 4000%;
  91 + }
  92 +
  93 + &-upload-btn {
  94 + margin: 10px auto;
  95 + }
  96 +}
  97 +.user-info-head {
  98 + position: relative;
  99 + display: inline-block;
  100 +}
  101 +.img-circle {
  102 + border-radius: 50%;
  103 +}
  104 +.img-lg {
  105 + width: 120px;
  106 + height: 120px;
  107 +}
  108 +.user-info-head:hover:after {
  109 + content: '+';
  110 + position: absolute;
  111 + left: 0;
  112 + right: 0;
  113 + top: 0;
  114 + bottom: 0;
  115 + color: #eee;
  116 + background: rgba(0, 0, 0, 0.5);
  117 + font-size: 24px;
  118 + font-style: normal;
  119 + -webkit-font-smoothing: antialiased;
  120 + -moz-osx-font-smoothing: grayscale;
  121 + cursor: pointer;
  122 + line-height: 110px;
  123 + border-radius: 50%;
  124 +}
  125 +</style>
yudao-ui-admin-vue3/src/components/Cropper/src/types.ts 0 → 100644
  1 +import type Cropper from 'cropperjs'
  2 +
  3 +export interface CropendResult {
  4 + imgBase64: string
  5 + imgInfo: Cropper.Data
  6 +}
  7 +
  8 +export type { Cropper }
yudao-ui-admin-vue3/src/config/axios/index.ts
@@ -44,7 +44,7 @@ export default { @@ -44,7 +44,7 @@ export default {
44 }, 44 },
45 upload: async <T = any>(option: any) => { 45 upload: async <T = any>(option: any) => {
46 option.headersType = 'multipart/form-data' 46 option.headersType = 'multipart/form-data'
47 - const res = await request({ method: 'PUT', ...option }) 47 + const res = await request({ method: 'POST', ...option })
48 return res as unknown as Promise<T> 48 return res as unknown as Promise<T>
49 } 49 }
50 } 50 }
yudao-ui-admin-vue3/src/locales/en.ts
@@ -426,5 +426,19 @@ export default { @@ -426,5 +426,19 @@ export default {
426 cfPwdMsg: 'Please Enter Confirm Password', 426 cfPwdMsg: 'Please Enter Confirm Password',
427 diffPwd: 'The Passwords Entered Twice No Match' 427 diffPwd: 'The Passwords Entered Twice No Match'
428 } 428 }
  429 + },
  430 + cropper: {
  431 + selectImage: 'Select Image',
  432 + uploadSuccess: 'Uploaded success!',
  433 + modalTitle: 'Avatar upload',
  434 + okText: 'Confirm and upload',
  435 + btn_reset: 'Reset',
  436 + btn_rotate_left: 'Counterclockwise rotation',
  437 + btn_rotate_right: 'Clockwise rotation',
  438 + btn_scale_x: 'Flip horizontal',
  439 + btn_scale_y: 'Flip vertical',
  440 + btn_zoom_in: 'Zoom in',
  441 + btn_zoom_out: 'Zoom out',
  442 + preview: 'Preivew'
429 } 443 }
430 } 444 }
yudao-ui-admin-vue3/src/locales/zh-CN.ts
@@ -419,5 +419,19 @@ export default { @@ -419,5 +419,19 @@ export default {
419 pwdRules: '长度在 6 到 20 个字符', 419 pwdRules: '长度在 6 到 20 个字符',
420 diffPwd: '两次输入密码不一致' 420 diffPwd: '两次输入密码不一致'
421 } 421 }
  422 + },
  423 + cropper: {
  424 + selectImage: '选择图片',
  425 + uploadSuccess: '上传成功',
  426 + modalTitle: '头像上传',
  427 + okText: '确认并上传',
  428 + btn_reset: '重置',
  429 + btn_rotate_left: '逆时针旋转',
  430 + btn_rotate_right: '顺时针旋转',
  431 + btn_scale_x: '水平翻转',
  432 + btn_scale_y: '垂直翻转',
  433 + btn_zoom_in: '放大',
  434 + btn_zoom_out: '缩小',
  435 + preview: '预览'
422 } 436 }
423 } 437 }
yudao-ui-admin-vue3/src/views/Profile/components/UserAvatar.vue
1 <template> 1 <template>
2 - <div class="user-info-head" @click="editCropper()">  
3 - <img :src="props.img" title="点击上传头像" class="img-circle img-lg" alt="" /> 2 + <div class="change-avatar">
  3 + <CropperAvatar
  4 + :value="avatar"
  5 + :showBtn="false"
  6 + @change="handelUpload"
  7 + :btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
  8 + width="120px"
  9 + />
4 </div> 10 </div>
5 - <el-dialog  
6 - v-model="dialogVisible"  
7 - title="编辑头像"  
8 - :mask-closable="false"  
9 - width="800px"  
10 - append-to-body  
11 - @opened="cropperVisible = true"  
12 - >  
13 - <el-row>  
14 - <el-col :xs="24" :md="12" :style="{ height: '350px' }">  
15 - <VueCropper  
16 - ref="cropper"  
17 - v-if="cropperVisible"  
18 - :img="options.img"  
19 - :info="true"  
20 - :infoTrue="options.infoTrue"  
21 - :autoCrop="options.autoCrop"  
22 - :autoCropWidth="options.autoCropWidth"  
23 - :autoCropHeight="options.autoCropHeight"  
24 - :fixedNumber="options.fixedNumber"  
25 - :fixedBox="options.fixedBox"  
26 - :centerBox="options.centerBox"  
27 - @real-time="realTime"  
28 - />  
29 - </el-col>  
30 - <el-col :xs="24" :md="12" :style="{ height: '350px' }">  
31 - <div  
32 - class="avatar-upload-preview"  
33 - :style="{  
34 - width: previews.w + 'px',  
35 - height: previews.h + 'px',  
36 - overflow: 'hidden',  
37 - margin: '5px'  
38 - }"  
39 - >  
40 - <div :style="previews.div">  
41 - <img :src="previews.url" :style="previews.img" style="!max-width: 100%" alt="" />  
42 - </div>  
43 - </div>  
44 - </el-col>  
45 - </el-row>  
46 - <template #footer>  
47 - <el-row>  
48 - <el-col :lg="2" :md="2">  
49 - <el-upload  
50 - action="#"  
51 - :http-request="requestUpload"  
52 - :show-file-list="false"  
53 - :before-upload="beforeUpload"  
54 - >  
55 - <el-button size="small">  
56 - <Icon icon="ep:upload-filled" class="mr-5px" />  
57 - 选择  
58 - </el-button>  
59 - </el-upload>  
60 - </el-col>  
61 - <el-col :lg="{ span: 1, offset: 2 }" :md="2">  
62 - <el-button size="small" @click="changeScale(1)">  
63 - <Icon icon="ep:zoom-in" class="mr-5px" />  
64 - </el-button>  
65 - </el-col>  
66 - <el-col :lg="{ span: 1, offset: 1 }" :md="2">  
67 - <el-button size="small" @click="changeScale(-1)">  
68 - <Icon icon="ep:zoom-out" class="mr-5px" />  
69 - </el-button>  
70 - </el-col>  
71 - <el-col :lg="{ span: 1, offset: 1 }" :md="2">  
72 - <el-button size="small" @click="rotateLeft()">  
73 - <Icon icon="ep:arrow-left-bold" class="mr-5px" />  
74 - </el-button>  
75 - </el-col>  
76 - <el-col :lg="{ span: 1, offset: 1 }" :md="2">  
77 - <el-button size="small" @click="rotateRight()">  
78 - <Icon icon="ep:arrow-right-bold" class="mr-5px" />  
79 - </el-button>  
80 - </el-col>  
81 - <el-col :lg="{ span: 2, offset: 6 }" :md="2">  
82 - <el-button size="small" type="primary" @click="uploadImg()">提 交</el-button>  
83 - </el-col>  
84 - </el-row>  
85 - </template>  
86 - </el-dialog>  
87 </template> 11 </template>
88 <script setup lang="ts"> 12 <script setup lang="ts">
89 -import { ref, reactive, watch, Ref, UnwrapNestedRefs } from 'vue'  
90 -import VueCropper from 'vue-cropper/lib/vue-cropper.vue'  
91 -import 'vue-cropper/dist/index.css'  
92 -import { ElRow, ElCol, ElUpload, ElMessage, ElDialog } from 'element-plus' 13 +import { computed } from 'vue'
93 import { propTypes } from '@/utils/propTypes' 14 import { propTypes } from '@/utils/propTypes'
  15 +import { CropperAvatar } from '@/components/Cropper'
94 import { uploadAvatarApi } from '@/api/system/user/profile' 16 import { uploadAvatarApi } from '@/api/system/user/profile'
95 -  
96 -const cropper = ref()  
97 -const dialogVisible = ref(false)  
98 -const cropperVisible = ref(false)  
99 const props = defineProps({ 17 const props = defineProps({
100 img: propTypes.string.def('') 18 img: propTypes.string.def('')
101 }) 19 })
102 -interface Options {  
103 - img: string | ArrayBuffer | null // 裁剪图片的地址  
104 - info: true // 裁剪框的大小信息  
105 - outputSize: number // 裁剪生成图片的质量 [1至0.1]  
106 - outputType: 'jpeg' // 裁剪生成图片的格式  
107 - canScale: boolean // 图片是否允许滚轮缩放  
108 - autoCrop: boolean // 是否默认生成截图框  
109 - autoCropWidth: number // 默认生成截图框宽度  
110 - autoCropHeight: number // 默认生成截图框高度  
111 - fixedBox: boolean // 固定截图框大小 不允许改变  
112 - fixed: boolean // 是否开启截图框宽高固定比例  
113 - fixedNumber: Array<number> // 截图框的宽高比例 需要配合centerBox一起使用才能生效  
114 - full: boolean // 是否输出原图比例的截图  
115 - canMoveBox: boolean // 截图框能否拖动  
116 - original: boolean // 上传图片按照原始比例渲染  
117 - centerBox: boolean // 截图框是否被限制在图片里面  
118 - infoTrue: boolean // true 为展示真实输出图片宽高 false 展示看到的截图框宽高  
119 -}  
120 -const options: UnwrapNestedRefs<Options> = reactive({  
121 - img: '', // 需要剪裁的图片  
122 - autoCrop: true, // 是否默认生成截图框  
123 - autoCropWidth: 200, // 默认生成截图框的宽度  
124 - autoCropHeight: 200, // 默认生成截图框的长度  
125 - fixedBox: false, // 是否固定截图框的大小 不允许改变  
126 - info: true, // 裁剪框的大小信息  
127 - outputSize: 1, // 裁剪生成图片的质量 [1至0.1]  
128 - outputType: 'jpeg', // 裁剪生成图片的格式  
129 - canScale: false, // 图片是否允许滚轮缩放  
130 - fixed: true, // 是否开启截图框宽高固定比例  
131 - fixedNumber: [1, 1], // 截图框的宽高比例 需要配合centerBox一起使用才能生效  
132 - full: true, // 是否输出原图比例的截图  
133 - canMoveBox: false, // 截图框能否拖动  
134 - original: false, // 上传图片按照原始比例渲染  
135 - centerBox: true, // 截图框是否被限制在图片里面  
136 - infoTrue: true // true 为展示真实输出图片宽高 false 展示看到的截图框宽高 20 +const avatar = computed(() => {
  21 + return props.img
137 }) 22 })
138 -const previews: Ref<any> = ref({})  
139 -/** 编辑头像 */  
140 -const editCropper = () => {  
141 - dialogVisible.value = true  
142 -}  
143 -/** 向左旋转 */  
144 -const rotateLeft = () => {  
145 - cropper.value.rotateLeft()  
146 -}  
147 -/** 向右旋转 */  
148 -const rotateRight = () => {  
149 - cropper.value.rotateRight()  
150 -}  
151 -/** 图片缩放 */  
152 -const changeScale = (num: number) => {  
153 - num = num || 1  
154 - cropper.value.changeScale(num)  
155 -}  
156 -// 覆盖默认的上传行为  
157 -const requestUpload: any = () => {}  
158 -/** 上传预处理 */  
159 -const beforeUpload = (file: Blob) => {  
160 - if (file.type.indexOf('image/') == -1) {  
161 - ElMessage('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。')  
162 - } else {  
163 - const reader = new FileReader()  
164 - // 转化为base64  
165 - reader.readAsDataURL(file)  
166 - reader.onload = () => {  
167 - if (reader.result) {  
168 - // 获取到需要剪裁的图片 展示到剪裁框中  
169 - options.img = reader.result as string  
170 - }  
171 - return false  
172 - }  
173 - }  
174 -}  
175 -/** 上传图片 */  
176 -const uploadImg = () => {  
177 - cropper.value.getCropBlob((data: any) => {  
178 - let formData = new FormData()  
179 - formData.append('avatarFile', data)  
180 - uploadAvatarApi(formData).then((res) => {  
181 - options.img = res  
182 - window.location.reload()  
183 - })  
184 - dialogVisible.value = false  
185 - cropperVisible.value = false  
186 - })  
187 -}  
188 -/** 实时预览 */  
189 -const realTime = (data: any) => {  
190 - previews.value = data 23 +
  24 +const handelUpload = async ({ data }) => {
  25 + await uploadAvatarApi({ avatarFile: data })
191 } 26 }
192 -watch(  
193 - () => props.img,  
194 - () => {  
195 - if (props.img) {  
196 - options.img = props.img  
197 - previews.value.img = props.img  
198 - previews.value.url = props.img  
199 - }  
200 - }  
201 -)  
202 </script> 27 </script>
203 28
204 -<style scoped>  
205 -.user-info-head {  
206 - position: relative;  
207 - display: inline-block;  
208 -}  
209 -.img-circle {  
210 - border-radius: 50%;  
211 -}  
212 -.img-lg {  
213 - width: 120px;  
214 - height: 120px;  
215 -}  
216 -.avatar-upload-preview {  
217 - position: absolute;  
218 - top: 50%;  
219 - -webkit-transform: translate(50%, -50%);  
220 - transform: translate(50%, -50%);  
221 - width: 200px;  
222 - height: 200px;  
223 - border-radius: 50%;  
224 - -webkit-box-shadow: 0 0 4px #ccc;  
225 - box-shadow: 0 0 4px #ccc;  
226 - overflow: hidden;  
227 -}  
228 -.user-info-head:hover:after {  
229 - content: '+';  
230 - position: absolute;  
231 - left: 0;  
232 - right: 0;  
233 - top: 0;  
234 - bottom: 0;  
235 - color: #eee;  
236 - background: rgba(0, 0, 0, 0.5);  
237 - font-size: 24px;  
238 - font-style: normal;  
239 - -webkit-font-smoothing: antialiased;  
240 - -moz-osx-font-smoothing: grayscale;  
241 - cursor: pointer;  
242 - line-height: 110px;  
243 - border-radius: 50%; 29 +<style scoped lang="scss">
  30 +.change-avatar {
  31 + img {
  32 + display: block;
  33 + margin-bottom: 15px;
  34 + border-radius: 50%;
  35 + }
244 } 36 }
245 </style> 37 </style>