Merge pull request #339 from nagisa77/codex/add-user-defined-avatar-cropping

feat: add avatar cropping
This commit is contained in:
Tim
2025-08-04 13:12:19 +08:00
committed by GitHub
4 changed files with 166 additions and 3 deletions

View File

@@ -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",

View File

@@ -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",

View 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>

View File

@@ -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' },