mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-06-07 02:27:34 +08:00
Merge pull request #339 from nagisa77/codex/add-user-defined-avatar-cropping
feat: add avatar cropping
This commit is contained in:
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"cropperjs": "^1.6.2",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"ldrs": "^1.1.7",
|
"ldrs": "^1.1.7",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
@@ -4434,6 +4435,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cropperjs": {
|
||||||
|
"version": "1.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
|
||||||
|
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "6.0.6",
|
"version": "6.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
|
"cropperjs": "^1.6.2",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"ldrs": "^1.1.7",
|
"ldrs": "^1.1.7",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
|||||||
142
frontend/src/components/AvatarCropper.vue
Normal file
142
frontend/src/components/AvatarCropper.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="show" class="cropper-modal">
|
||||||
|
<div class="cropper-body">
|
||||||
|
<div class="cropper-wrapper">
|
||||||
|
<img ref="image" :src="src" alt="to crop" />
|
||||||
|
</div>
|
||||||
|
<div class="cropper-actions">
|
||||||
|
<button class="cropper-btn" @click="$emit('close')">取消</button>
|
||||||
|
<button class="cropper-btn primary" @click="onConfirm">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
import 'cropperjs/dist/cropper.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AvatarCropper',
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'crop'],
|
||||||
|
data() {
|
||||||
|
return { cropper: null }
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(val) {
|
||||||
|
if (val) {
|
||||||
|
this.$nextTick(() => this.init())
|
||||||
|
} else {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.show) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
const image = this.$refs.image
|
||||||
|
this.cropper = new Cropper(image, {
|
||||||
|
aspectRatio: 1,
|
||||||
|
viewMode: 1,
|
||||||
|
autoCropArea: 1,
|
||||||
|
responsive: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
if (this.cropper) {
|
||||||
|
this.cropper.destroy()
|
||||||
|
this.cropper = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
if (!this.cropper) return
|
||||||
|
this.cropper.getCroppedCanvas({ width: 256, height: 256 }).toBlob(blob => {
|
||||||
|
const file = new File([blob], 'avatar.png', { type: 'image/png' })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
this.$emit('crop', { file, url })
|
||||||
|
this.$emit('close')
|
||||||
|
this.destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cropper-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
opacity: 1.0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-body {
|
||||||
|
background: var(--background-color);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-wrapper {
|
||||||
|
width: 80vw;
|
||||||
|
height: 80vw;
|
||||||
|
max-width: 400px;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-wrapper img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cropper-btn.primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.cropper-wrapper {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="settings-page">
|
<div class="settings-page">
|
||||||
|
<AvatarCropper
|
||||||
|
:src="tempAvatar"
|
||||||
|
:show="showCropper"
|
||||||
|
@close="showCropper = false"
|
||||||
|
@crop="onCropped"
|
||||||
|
/>
|
||||||
<div v-if="isLoadingPage" class="loading-page">
|
<div v-if="isLoadingPage" class="loading-page">
|
||||||
<l-hatch size="20" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
<l-hatch size="20" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,11 +64,12 @@ import { API_BASE_URL, toast } from '../main'
|
|||||||
import { getToken, fetchCurrentUser, setToken } from '../utils/auth'
|
import { getToken, fetchCurrentUser, setToken } from '../utils/auth'
|
||||||
import BaseInput from '../components/BaseInput.vue'
|
import BaseInput from '../components/BaseInput.vue'
|
||||||
import Dropdown from '../components/Dropdown.vue'
|
import Dropdown from '../components/Dropdown.vue'
|
||||||
|
import AvatarCropper from '../components/AvatarCropper.vue'
|
||||||
import { hatch } from 'ldrs'
|
import { hatch } from 'ldrs'
|
||||||
hatch.register()
|
hatch.register()
|
||||||
export default {
|
export default {
|
||||||
name: 'SettingsPageView',
|
name: 'SettingsPageView',
|
||||||
components: { BaseInput, Dropdown },
|
components: { BaseInput, Dropdown, AvatarCropper },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -70,6 +77,8 @@ export default {
|
|||||||
usernameError: '',
|
usernameError: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
avatarFile: null,
|
avatarFile: null,
|
||||||
|
tempAvatar: '',
|
||||||
|
showCropper: false,
|
||||||
role: '',
|
role: '',
|
||||||
publishMode: 'DIRECT',
|
publishMode: 'DIRECT',
|
||||||
passwordStrength: 'LOW',
|
passwordStrength: 'LOW',
|
||||||
@@ -100,15 +109,19 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
onAvatarChange(e) {
|
onAvatarChange(e) {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
this.avatarFile = file
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
this.avatar = reader.result
|
this.tempAvatar = reader.result
|
||||||
|
this.showCropper = true
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onCropped({ file, url }) {
|
||||||
|
this.avatarFile = file
|
||||||
|
this.avatar = url
|
||||||
|
},
|
||||||
fetchPublishModes() {
|
fetchPublishModes() {
|
||||||
return Promise.resolve([
|
return Promise.resolve([
|
||||||
{ id: 'DIRECT', name: '直接发布', icon: 'fas fa-bolt' },
|
{ id: 'DIRECT', name: '直接发布', icon: 'fas fa-bolt' },
|
||||||
|
|||||||
Reference in New Issue
Block a user