Commit 91abae8898d5d4b301a236e5f9c6a3f78fc69900
1 parent
0f562e59
refactor: cropper
Showing
13 changed files
with
686 additions
and
276 deletions
yudao-dependencies/pom.xml
| ... | ... | @@ -24,7 +24,7 @@ |
| 24 | 24 | <!-- DB 相关 --> |
| 25 | 25 | <druid.version>1.2.15</druid.version> |
| 26 | 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 | 28 | <dynamic-datasource.version>3.6.0</dynamic-datasource.version> |
| 29 | 29 | <redisson.version>3.18.0</redisson.version> |
| 30 | 30 | <!-- 服务保障相关 --> | ... | ... |
yudao-ui-admin-vue3/package.json
| ... | ... | @@ -25,12 +25,13 @@ |
| 25 | 25 | }, |
| 26 | 26 | "dependencies": { |
| 27 | 27 | "@iconify/iconify": "^3.0.1", |
| 28 | - "@vueuse/core": "^9.7.0", | |
| 28 | + "@vueuse/core": "^9.8.2", | |
| 29 | 29 | "@wangeditor/editor": "^5.1.23", |
| 30 | 30 | "@wangeditor/editor-for-vue": "^5.1.10", |
| 31 | 31 | "@zxcvbn-ts/core": "^2.1.0", |
| 32 | 32 | "animate.css": "^4.1.1", |
| 33 | 33 | "axios": "^1.2.1", |
| 34 | + "cropperjs": "^1.5.13", | |
| 34 | 35 | "crypto-js": "^4.1.1", |
| 35 | 36 | "dayjs": "^1.11.7", |
| 36 | 37 | "echarts": "^5.4.1", |
| ... | ... | @@ -46,7 +47,6 @@ |
| 46 | 47 | "qs": "^6.11.0", |
| 47 | 48 | "url": "^0.11.0", |
| 48 | 49 | "vue": "3.2.45", |
| 49 | - "vue-cropper": "^1.0.3", | |
| 50 | 50 | "vue-i18n": "9.2.2", |
| 51 | 51 | "vue-router": "^4.1.6", |
| 52 | 52 | "vue-types": "^5.0.1", |
| ... | ... | @@ -104,7 +104,7 @@ |
| 104 | 104 | "vite-plugin-svg-icons": "^2.0.1", |
| 105 | 105 | "vite-plugin-vue-setup-extend": "^0.4.0", |
| 106 | 106 | "vite-plugin-windicss": "^1.8.10", |
| 107 | - "vue-tsc": "^1.0.14", | |
| 107 | + "vue-tsc": "^1.0.16", | |
| 108 | 108 | "windicss": "^3.5.6" |
| 109 | 109 | }, |
| 110 | 110 | "engines": { | ... | ... |
yudao-ui-admin-vue3/pnpm-lock.yaml
| ... | ... | @@ -18,7 +18,7 @@ specifiers: |
| 18 | 18 | '@vitejs/plugin-legacy': ^3.0.1 |
| 19 | 19 | '@vitejs/plugin-vue': ^4.0.0 |
| 20 | 20 | '@vitejs/plugin-vue-jsx': ^3.0.0 |
| 21 | - '@vueuse/core': ^9.7.0 | |
| 21 | + '@vueuse/core': ^9.8.2 | |
| 22 | 22 | '@wangeditor/editor': ^5.1.23 |
| 23 | 23 | '@wangeditor/editor-for-vue': ^5.1.10 |
| 24 | 24 | '@zxcvbn-ts/core': ^2.1.0 |
| ... | ... | @@ -26,6 +26,7 @@ specifiers: |
| 26 | 26 | autoprefixer: ^10.4.13 |
| 27 | 27 | axios: ^1.2.1 |
| 28 | 28 | consola: ^2.15.3 |
| 29 | + cropperjs: ^1.5.13 | |
| 29 | 30 | crypto-js: ^4.1.1 |
| 30 | 31 | dayjs: ^1.11.7 |
| 31 | 32 | echarts: ^5.4.1 |
| ... | ... | @@ -72,10 +73,9 @@ specifiers: |
| 72 | 73 | vite-plugin-vue-setup-extend: ^0.4.0 |
| 73 | 74 | vite-plugin-windicss: ^1.8.10 |
| 74 | 75 | vue: 3.2.45 |
| 75 | - vue-cropper: ^1.0.3 | |
| 76 | 76 | vue-i18n: 9.2.2 |
| 77 | 77 | vue-router: ^4.1.6 |
| 78 | - vue-tsc: ^1.0.14 | |
| 78 | + vue-tsc: ^1.0.16 | |
| 79 | 79 | vue-types: ^5.0.1 |
| 80 | 80 | vxe-table: ^4.3.7 |
| 81 | 81 | web-storage-cache: ^1.1.1 |
| ... | ... | @@ -84,12 +84,13 @@ specifiers: |
| 84 | 84 | |
| 85 | 85 | dependencies: |
| 86 | 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 | 88 | '@wangeditor/editor': 5.1.23 |
| 89 | 89 | '@wangeditor/editor-for-vue': 5.1.12_3apfu3xbp6awzuex7ed3sbrv6y |
| 90 | 90 | '@zxcvbn-ts/core': 2.1.0 |
| 91 | 91 | animate.css: 4.1.1 |
| 92 | 92 | axios: 1.2.1 |
| 93 | + cropperjs: 1.5.13 | |
| 93 | 94 | crypto-js: 4.1.1 |
| 94 | 95 | dayjs: 1.11.7 |
| 95 | 96 | echarts: registry.npmmirror.com/echarts/5.4.1 |
| ... | ... | @@ -105,7 +106,6 @@ dependencies: |
| 105 | 106 | qs: 6.11.0 |
| 106 | 107 | url: 0.11.0 |
| 107 | 108 | vue: 3.2.45 |
| 108 | - vue-cropper: 1.0.5 | |
| 109 | 109 | vue-i18n: 9.2.2_vue@3.2.45 |
| 110 | 110 | vue-router: 4.1.6_vue@3.2.45 |
| 111 | 111 | vue-types: 5.0.1_vue@3.2.45 |
| ... | ... | @@ -163,7 +163,7 @@ devDependencies: |
| 163 | 163 | vite-plugin-svg-icons: 2.0.1_vite@4.0.2 |
| 164 | 164 | vite-plugin-vue-setup-extend: 0.4.0_vite@4.0.2 |
| 165 | 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 | 167 | windicss: 3.5.6 |
| 168 | 168 | |
| 169 | 169 | packages: |
| ... | ... | @@ -1079,44 +1079,44 @@ packages: |
| 1079 | 1079 | nanoid: registry.npmmirror.com/nanoid/3.3.4 |
| 1080 | 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 | 1084 | dependencies: |
| 1085 | - '@volar/source-map': 1.0.14 | |
| 1085 | + '@volar/source-map': 1.0.16 | |
| 1086 | 1086 | '@vue/reactivity': 3.2.45 |
| 1087 | 1087 | muggle-string: 0.1.0 |
| 1088 | 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 | 1092 | dependencies: |
| 1093 | 1093 | muggle-string: 0.1.0 |
| 1094 | 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 | 1098 | dependencies: |
| 1099 | - '@volar/language-core': 1.0.14 | |
| 1099 | + '@volar/language-core': 1.0.16 | |
| 1100 | 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 | 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 | 1107 | '@vue/compiler-dom': 3.2.45 |
| 1108 | 1108 | '@vue/compiler-sfc': 3.2.45 |
| 1109 | 1109 | '@vue/reactivity': 3.2.45 |
| 1110 | 1110 | '@vue/shared': 3.2.45 |
| 1111 | - minimatch: 5.1.0 | |
| 1111 | + minimatch: 5.1.2 | |
| 1112 | 1112 | vue-template-compiler: 2.7.14 |
| 1113 | 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 | 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 | 1120 | dev: true |
| 1121 | 1121 | |
| 1122 | 1122 | /@vue/compiler-core/3.2.45: |
| ... | ... | @@ -1196,24 +1196,24 @@ packages: |
| 1196 | 1196 | /@vue/shared/3.2.45: |
| 1197 | 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 | 1201 | dependencies: |
| 1202 | 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 | 1205 | vue-demi: 0.13.11_vue@3.2.45 |
| 1206 | 1206 | transitivePeerDependencies: |
| 1207 | 1207 | - '@vue/composition-api' |
| 1208 | 1208 | - vue |
| 1209 | 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 | 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 | 1217 | dependencies: |
| 1218 | 1218 | vue-demi: 0.13.11_vue@3.2.45 |
| 1219 | 1219 | transitivePeerDependencies: |
| ... | ... | @@ -2019,6 +2019,10 @@ packages: |
| 2019 | 2019 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} |
| 2020 | 2020 | dev: true |
| 2021 | 2021 | |
| 2022 | + /cropperjs/1.5.13: | |
| 2023 | + resolution: {integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==} | |
| 2024 | + dev: false | |
| 2025 | + | |
| 2022 | 2026 | /cross-spawn/7.0.3: |
| 2023 | 2027 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} |
| 2024 | 2028 | engines: {node: '>= 8'} |
| ... | ... | @@ -2319,7 +2323,7 @@ packages: |
| 2319 | 2323 | '@popperjs/core': /@sxzz/popperjs-es/2.11.7 |
| 2320 | 2324 | '@types/lodash': 4.14.189 |
| 2321 | 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 | 2327 | async-validator: 4.2.5 |
| 2324 | 2328 | dayjs: 1.11.7 |
| 2325 | 2329 | escape-html: 1.0.3 |
| ... | ... | @@ -3831,6 +3835,13 @@ packages: |
| 3831 | 3835 | brace-expansion: 2.0.1 |
| 3832 | 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 | 3845 | /minimist-options/4.1.0: |
| 3835 | 3846 | resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} |
| 3836 | 3847 | engines: {node: '>= 6'} |
| ... | ... | @@ -5461,10 +5472,6 @@ packages: |
| 5461 | 5472 | fsevents: 2.3.2 |
| 5462 | 5473 | dev: true |
| 5463 | 5474 | |
| 5464 | - /vue-cropper/1.0.5: | |
| 5465 | - resolution: {integrity: sha512-D4XXdqWmMWRLOIV9LIh7/mkH6OBOMQDFbRjwntkxmAtxOtwpC9U5ZZ6lSXw5F5cbd4g8znDjk6MuCwIL+fZSrA==} | |
| 5466 | - dev: false | |
| 5467 | - | |
| 5468 | 5475 | /vue-demi/0.13.11_vue@3.2.45: |
| 5469 | 5476 | resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} |
| 5470 | 5477 | engines: {node: '>=12'} |
| ... | ... | @@ -5526,14 +5533,14 @@ packages: |
| 5526 | 5533 | he: 1.2.0 |
| 5527 | 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 | 5538 | hasBin: true |
| 5532 | 5539 | peerDependencies: |
| 5533 | 5540 | typescript: '*' |
| 5534 | 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 | 5544 | typescript: 4.9.4 |
| 5538 | 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) => { |
| 73 | 73 | |
| 74 | 74 | // 用户头像上传 |
| 75 | 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
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
yudao-ui-admin-vue3/src/config/axios/index.ts
| ... | ... | @@ -44,7 +44,7 @@ export default { |
| 44 | 44 | }, |
| 45 | 45 | upload: async <T = any>(option: any) => { |
| 46 | 46 | option.headersType = 'multipart/form-data' |
| 47 | - const res = await request({ method: 'PUT', ...option }) | |
| 47 | + const res = await request({ method: 'POST', ...option }) | |
| 48 | 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 | 426 | cfPwdMsg: 'Please Enter Confirm Password', |
| 427 | 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 | 419 | pwdRules: '长度在 6 到 20 个字符', |
| 420 | 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 | 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 | 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 | 11 | </template> |
| 88 | 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 | 14 | import { propTypes } from '@/utils/propTypes' |
| 15 | +import { CropperAvatar } from '@/components/Cropper' | |
| 94 | 16 | import { uploadAvatarApi } from '@/api/system/user/profile' |
| 95 | - | |
| 96 | -const cropper = ref() | |
| 97 | -const dialogVisible = ref(false) | |
| 98 | -const cropperVisible = ref(false) | |
| 99 | 17 | const props = defineProps({ |
| 100 | 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 | 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 | 37 | </style> | ... | ... |