This commit is contained in:
2026-02-12 19:40:21 +08:00
parent b3335b4b6e
commit 6608b3045e
3 changed files with 130 additions and 87 deletions

View File

@@ -31,7 +31,49 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="头像" prop="avatarUrl">
<UploadImg v-model="formData.avatarUrl" directory="ai_companion_avatar" />
<div class="flex items-center gap-12px">
<el-avatar v-if="formData.avatarUrl" :src="formData.avatarUrl" :size="50" />
<el-button type="primary" plain @click="avatarInputRef?.click()">
{{ formData.avatarUrl ? '重新上传' : '选择图片' }}
</el-button>
<el-button v-if="formData.avatarUrl" type="danger" plain @click="formData.avatarUrl = undefined">
删除
</el-button>
<input
ref="avatarInputRef"
type="file"
accept="image/*"
class="hidden"
@change="onAvatarFileChange"
/>
</div>
<!-- 头像裁剪弹窗 -->
<Dialog v-model="cropperVisible" title="裁剪头像50 x 50" width="800px" :canFullscreen="false">
<div class="flex gap-20px">
<div class="flex-1">
<CropperImage
v-if="avatarCropSrc"
:src="avatarCropSrc"
height="300px"
:options="{ aspectRatio: 1, viewMode: 1 }"
@ready="onCropperReady"
@cropend="onCropend"
/>
</div>
<div class="flex flex-col items-center justify-center">
<div class="w-50px h-50px overflow-hidden border border-gray-300 rounded-full">
<img v-if="avatarPreview" :src="avatarPreview" class="w-full h-full" />
</div>
<span class="mt-8px text-12px text-gray-400">50 x 50 预览</span>
</div>
</div>
<template #footer>
<el-button @click="cropperVisible = false">取消</el-button>
<el-button type="primary" :loading="avatarUploading" @click="confirmCropAvatar">
确认上传
</el-button>
</template>
</Dialog>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -136,6 +178,9 @@
<script setup lang="ts">
import { AiCompanionApi, AiCompanion } from '@/api/keyboard/aicompanion'
import { UploadImg } from '@/components/UploadFile'
import { CropperImage } from '@/components/Cropper'
import * as FileApi from '@/api/infra/file'
import type { Cropper } from 'cropperjs'
defineOptions({ name: 'AiCompanionForm' })
@@ -174,6 +219,51 @@ const formRules = reactive({
})
const formRef = ref()
/** 头像裁剪上传 */
const avatarInputRef = ref<HTMLInputElement>()
const cropperVisible = ref(false)
const avatarCropSrc = ref('')
const avatarPreview = ref('')
const avatarUploading = ref(false)
let cropperInstance: Cropper | null = null
const onAvatarFileChange = (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = (ev) => {
avatarCropSrc.value = (ev.target?.result as string) ?? ''
avatarPreview.value = ''
cropperVisible.value = true
}
reader.readAsDataURL(file)
// 清空 input 以便重复选择同一文件
;(e.target as HTMLInputElement).value = ''
}
const onCropperReady = (cropper: Cropper) => {
cropperInstance = cropper
}
const onCropend = ({ imgBase64 }: { imgBase64: string }) => {
avatarPreview.value = imgBase64
}
const confirmCropAvatar = async () => {
if (!cropperInstance) return
const canvas = cropperInstance.getCroppedCanvas({ width: 50, height: 50 })
canvas.toBlob(async (blob) => {
if (!blob) return
avatarUploading.value = true
try {
const file = new File([blob], 'avatar.png', { type: 'image/png' })
const res = await FileApi.updateFile({ file, directory: 'ai_companion_avatar' })
formData.value.avatarUrl = res.data
cropperVisible.value = false
message.success('头像上传成功')
} finally {
avatarUploading.value = false
}
}, 'image/png')
}
/** 性格标签 */
const newTag = ref('')
const normalizePersonalityTags = (raw: unknown): string[] => {