feat(intelligent-agent): 添加智能体头像上传功能

- 新增 cropper 组件用于图片裁剪
- 在智能体信息组件中集成 cropper 组件
- 添加图片上传和裁剪的相关逻辑和接口
- 优化智能体列表和详情页面的头像显示
This commit is contained in:
陈昱达
2025-04-29 15:01:15 +08:00
parent dcf80a26b1
commit f7e20fe0c4
3 changed files with 340 additions and 27 deletions

View File

@@ -0,0 +1,220 @@
<template>
<div style="width: 100%;height: 100%">
<el-upload
class="upload-picture-action"
v-show="fileList.length <= 0"
action="#"
drag
list-type="picture-card"
:show-file-list="false"
:auto-upload="false"
:limit="1"
:file-list="fileList"
:on-change="change"
>
<p>点击上传图片或者将图片拖拽到此处</p>
</el-upload>
<div
v-if="fileList.length > 0"
class="flex"
style="width: 100%;height: 100%"
>
<cropper-canvas style="width: 100%;flex:1" cover image>
<cropper-image
:src="file.url"
alt="Picture"
rotatable
scalable
skewable
translatable
></cropper-image>
<cropper-shade hidden></cropper-shade>
<cropper-handle action="select" plain></cropper-handle>
<cropper-selection
id="cropperSelection"
movable
resizable
x="150"
y="100"
width="275"
height="275"
ref="selection"
>
<cropper-handle
action="move"
theme-color="rgba(255, 255, 255, 0.35)"
></cropper-handle>
<cropper-handle action="n-resize"></cropper-handle>
<cropper-handle action="e-resize"></cropper-handle>
<cropper-handle action="s-resize"></cropper-handle>
<cropper-handle action="w-resize"></cropper-handle>
<cropper-handle action="ne-resize"></cropper-handle>
<cropper-handle action="nw-resize"></cropper-handle>
<cropper-handle action="se-resize"></cropper-handle>
<cropper-handle action="sw-resize"></cropper-handle>
</cropper-selection>
</cropper-canvas>
<div
class="cropperSelection"
style="flex:0;border-radius: 8px; border: 1px solid #ccc;overflow: hidden"
>
<!-- 修改: 添加 selection 属性并绑定到 file.url -->
<cropper-viewer
style="width: 100%;height: 100%"
selection="#cropperSelection"
initial-aspect-ratio="1.5"
initial-coverage="0.5"
></cropper-viewer>
</div>
</div>
</div>
</template>
<script>
import Cropper from 'cropperjs'
export default {
name: 'index',
data() {
return {
fileList: [],
cropper: null,
imageUrl: '',
visible: false,
file: {
url: ''
}
}
},
props: {},
watch: {},
components: {},
filters: {},
methods: {
handleRemove() {
this.fileList = []
},
change(file) {
this.file = file
this.fileList = [file]
// this.fileList = []
// this.visible = true
// setTimeout(() => {
// this.cropper = new Cropper('#image', {})
// }, 300)
},
reset() {
this.fileList = []
this.file = {}
},
confirm() {
this.$refs.selection.$toCanvas().then(canvas => {
// 根据canvas 生成一个图片 并转换成file
const image = canvas.toDataURL()
this.dataURLtoFile(image, this.file.name).then(res => {
// 文件生成 bolburl
const blobUrl = URL.createObjectURL(res)
res.url = blobUrl
this.file = {}
this.fileList = []
// this.fileList.push(res)
this.$emit('getFiles', [res])
// this.fileList = []
})
// 根据canvas 生成一个图片
})
},
// 修改: 优化 base64 转文件的逻辑,增加压缩功能
dataURLtoFile(dataurl, filename) {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
// 创建临时 canvas 元素用于压缩图片
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
// 返回一个 Promise确保异步操作完成后再返回文件
return new Promise(resolve => {
img.src = dataurl
img.onload = () => {
// 限制最大宽度或高度为 800px可根据需求调整
const maxWidthOrHeight = 800
let width = img.width
let height = img.height
if (width > height) {
if (width > maxWidthOrHeight) {
height *= maxWidthOrHeight / width
width = maxWidthOrHeight
}
} else {
if (height > maxWidthOrHeight) {
width *= maxWidthOrHeight / height
height = maxWidthOrHeight
}
}
canvas.width = width
canvas.height = height
ctx.drawImage(img, 0, 0, width, height)
// 将 canvas 转换为 base64质量设置为 0.8(可根据需求调整)
const compressedDataUrl = canvas.toDataURL(mime, 0.8)
// 将压缩后的 base64 转换为文件
const compressedArr = compressedDataUrl.split(',')
const compressedBstr = atob(compressedArr[1])
const compressedN = compressedBstr.length
const compressedU8arr = new Uint8Array(compressedN)
for (let i = 0; i < compressedN; i++) {
compressedU8arr[i] = compressedBstr.charCodeAt(i)
}
resolve(new File([compressedU8arr], filename, { type: mime }))
}
})
}
},
created() {},
mounted() {},
computed: {}
}
</script>
<style lang="scss">
cropper-canvas {
height: 100%;
}
.cropperSelection {
//width: 20vw;
//max-height: 30vw !important;
}
.upload-picture-action {
height: 100%;
width: 100%;
& .el-upload--picture-card {
height: 100%;
width: 100%;
& .el-upload-dragger {
border: unset;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
& el-upload-dragger {
}
}
}
</style>

View File

@@ -1,27 +1,37 @@
<script>
import { agentEdit, agentAdd } from '@/api/intelligent-agent/list'
import { VEmojiPicker } from 'v-emoji-picker'
import cropper from '@/components/RenderCropper/components/cropper.vue'
export default {
name: 'info',
inject: ['dialog', 'fetchAgentList'],
inject: ['dialog', 'fetchAgentList', 'resetList'],
components: {
VEmojiPicker
VEmojiPicker,
cropper
},
computed() {
return {
copyAgent: item => {
return this.dialog.agent
}
}
},
data() {
return {
isImage: '0',
popover: false,
background: '',
chooseBack: [
'rgb(255, 255, 255)',
'rgb(228, 251, 204)',
'rgb(239, 241, 245)',
'rgb(224, 234, 255)',
'rgb(254, 247, 195)',
'rgb(213, 245, 246)',
'rgb(209, 233, 255)',
'rgb(209, 224, 255)',
'rgb(213, 217, 235)',
'rgb(255, 228, 232)'
'#ffffff',
'#e4fbcc',
'#eff1f5',
'#e0eaf0',
'#fef7c3',
'#d5f5f6',
'#d1e9ff',
'#d1e0ff',
'#d5d9eb',
'#ffe4e8'
],
// 应用类型
agentType: [
@@ -57,22 +67,38 @@ export default {
}
],
rules: {
name: [
appName: [
{ required: true, message: '请输入智能体名称', trigger: 'blur' }
],
description: [
{ required: false, message: '请输入智能体描述', trigger: 'blur' }
],
appType: [
{ required: true, message: '请选择应用类型', trigger: 'blur' }
]
}
}
},
methods: {
getFiles(file) {
this.dialog.agent.image = file[0].url
this.dialog.agent.imageType = 'image'
this.dialog.agent.backgroundColor = null
},
resetImage() {
this.$refs.cropperImage.reset()
// this.popover = false
},
confirmImage() {
this.$refs.cropperImage.confirm()
this.popover = false
},
chooseAgentType(item) {
this.$set(this.dialog.agent, 'appType', item.value)
},
chooseGround(colors) {
this.$set(this.dialog.agent, 'background', colors)
this.$set(this.dialog.agent, 'backgroundColor', colors)
},
/**
* 提交表单
@@ -83,20 +109,31 @@ export default {
// 更新表单之后重新获取列表,然后退出 dialog
api(this.dialog.agent).then(res => {
if (res) {
this.resetList()
this.fetchAgentList()
this.dialog.visible = false
}
})
},
// 调用保存或者编辑
validateForm() {
this.$refs.form.validate(valid => {
if (valid) this.handleSubmit()
})
},
// 选择emoji
selectEmoji(e) {
this.dialog.agent.image = e.data
this.dialog.agent.imageType = 'emoji'
},
// 选择是否是上传图片
changeUploadImage(e) {
// if (e === '0') {
// this.dialog.agent.imageType = 'emoji'
// this.dialog.agent.imageType = 'emoji'
// } else {
// this.dialog.agent.imageType = 'image'
// }
}
}
}
@@ -116,25 +153,29 @@ export default {
<el-input v-model="dialog.agent.appName" size="medium" />
</el-form-item>
<el-form-item label="" prop="imageId" class="mt30 ml20">
<el-popover trigger="click">
<el-popover trigger="click" v-model="popover">
<div style="width: 28vw">
<el-radio-group
v-model="isImage"
style="width: 100%;"
class="flex render-group"
@change="changeUploadImage"
>
<el-radio-button label="0" style="flex:1">表情</el-radio-button>
<el-radio-button label="1" style="flex:1" disabled
>图片</el-radio-button
>
<el-radio-button label="1" style="flex:1">图片</el-radio-button>
</el-radio-group>
<!-- emoji-->
<div class="mt10" v-if="isImage === '0'">
<VEmojiPicker @select="selectEmoji" class="emoji " />
<div
v-if="dialog.agent.image"
class="flex mt10 back-content justify-content-b"
>
<template v-for="item in chooseBack">
<template
v-for="item in chooseBack"
v-if="dialog.agent.imageType === 'emoji'"
>
<div
class="emoji-background"
:style="`background:${item}`"
@@ -145,15 +186,44 @@ export default {
</template>
</div>
</div>
<!-- 图片切割-->
<div v-else class="cropper-image">
<cropper
ref="cropperImage"
style="height: calc(100% - 40px);"
@getFiles="getFiles"
></cropper>
<div class="mt10 flex align-items-c justify-content-b">
<el-button
class="render-button"
style="flex:1"
@click="resetImage"
>取消</el-button
>
<el-button
type="primary"
size="medium"
style="flex:1"
@click="confirmImage"
>确认</el-button
>
</div>
</div>
</div>
<div
class="emoji-content"
slot="reference"
:style="`background:${dialog.agent.background}`"
:style="`background:${dialog.agent.backgroundColor}`"
>
<div v-if="dialog.agent.imageType === 'emoji'">
{{ dialog.agent.image }}
</div>
<div v-else style="width: 100%">
<img :src="dialog.agent.image" alt="" style="width: 100%;" />
</div>
</div>
</el-popover>
</el-form-item>
</div>
@@ -223,7 +293,7 @@ export default {
line-height: 50px;
font-size: 20px;
cursor: pointer;
background: rgb(209, 233, 255);
background: #d1e9f;
}
.back-content {
@@ -292,4 +362,9 @@ export default {
max-width: 100%; // 设置最大宽度以限制文本长度
}
}
.cropper-image {
width: 100%;
height: 45vh;
}
</style>

View File

@@ -53,10 +53,15 @@ export default {
provide() {
return {
dialog: this.dialog,
fetchAgentList: this.fetchAgentList
fetchAgentList: this.fetchAgentList,
resetList: this.resetList
}
},
methods: {
resetList() {
this.list = []
this.page = 0
},
load() {
if (this.total === this.list.length) {
return false
@@ -85,7 +90,10 @@ export default {
*/
async handleEditAgent(id) {
const { content } = await agentDetail(id)
this.dialog.agent = content.content
if (!content.content.backgroundColor) {
content.content.backgroundColor = '#d1e9ff'
}
this.$set(this.dialog, 'agent', content.content)
this.dialog.type = 'edit'
this.dialog.title = '编辑智能体'
// 获取数据之后打开内容详情
@@ -120,7 +128,8 @@ export default {
appType: '',
description: '',
imageType: '',
image: ''
image: '',
backgroundColor: '#d1e9ff'
}
this.dialog.title = '创建智能体'
this.dialog.visible = true
@@ -195,7 +204,16 @@ export default {
>
<div class="dataset-header">
<div class="folder-content">
<div class="folder">
<div
class="folder"
:style="
`background:${
listItem.imageType === 'emoji'
? listItem.backgroundColor
: ''
}`
"
>
<!--listItem.name.获取字符串第一个-->
{{ listItem.image ? listItem.image : listItem.appName[0] }}
</div>