mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-22 22:21:09 +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",
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"cropperjs": "^1.6.2",
|
||||
"echarts": "^5.6.0",
|
||||
"ldrs": "^1.1.7",
|
||||
"markdown-it": "^14.1.0",
|
||||
@@ -4434,6 +4435,12 @@
|
||||
"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": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"cropperjs": "^1.6.2",
|
||||
"echarts": "^5.6.0",
|
||||
"ldrs": "^1.1.7",
|
||||
"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>
|
||||
<div class="settings-page">
|
||||
<AvatarCropper
|
||||
:src="tempAvatar"
|
||||
:show="showCropper"
|
||||
@close="showCropper = false"
|
||||
@crop="onCropped"
|
||||
/>
|
||||
<div v-if="isLoadingPage" class="loading-page">
|
||||
<l-hatch size="20" stroke="4" speed="3.5" color="var(--primary-color)"></l-hatch>
|
||||
</div>
|
||||
@@ -58,11 +64,12 @@ import { API_BASE_URL, toast } from '../main'
|
||||
import { getToken, fetchCurrentUser, setToken } from '../utils/auth'
|
||||
import BaseInput from '../components/BaseInput.vue'
|
||||
import Dropdown from '../components/Dropdown.vue'
|
||||
import AvatarCropper from '../components/AvatarCropper.vue'
|
||||
import { hatch } from 'ldrs'
|
||||
hatch.register()
|
||||
export default {
|
||||
name: 'SettingsPageView',
|
||||
components: { BaseInput, Dropdown },
|
||||
components: { BaseInput, Dropdown, AvatarCropper },
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
@@ -70,6 +77,8 @@ export default {
|
||||
usernameError: '',
|
||||
avatar: '',
|
||||
avatarFile: null,
|
||||
tempAvatar: '',
|
||||
showCropper: false,
|
||||
role: '',
|
||||
publishMode: 'DIRECT',
|
||||
passwordStrength: 'LOW',
|
||||
@@ -100,15 +109,19 @@ export default {
|
||||
methods: {
|
||||
onAvatarChange(e) {
|
||||
const file = e.target.files[0]
|
||||
this.avatarFile = file
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
this.avatar = reader.result
|
||||
this.tempAvatar = reader.result
|
||||
this.showCropper = true
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
},
|
||||
onCropped({ file, url }) {
|
||||
this.avatarFile = file
|
||||
this.avatar = url
|
||||
},
|
||||
fetchPublishModes() {
|
||||
return Promise.resolve([
|
||||
{ id: 'DIRECT', name: '直接发布', icon: 'fas fa-bolt' },
|
||||
|
||||
Reference in New Issue
Block a user