mirror of
http://112.124.100.131/ebiz-ai/ebiz-ai-knowledge-manage.git
synced 2025-12-22 09:16:51 +08:00
feat(intelligent-agent): 添加智能体头像上传功能
- 新增 cropper 组件用于图片裁剪 - 在智能体信息组件中集成 cropper 组件 - 添加图片上传和裁剪的相关逻辑和接口 - 优化智能体列表和详情页面的头像显示
This commit is contained in:
220
src/components/RenderCropper/components/cropper.vue
Normal file
220
src/components/RenderCropper/components/cropper.vue
Normal 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>
|
||||||
@@ -1,27 +1,37 @@
|
|||||||
<script>
|
<script>
|
||||||
import { agentEdit, agentAdd } from '@/api/intelligent-agent/list'
|
import { agentEdit, agentAdd } from '@/api/intelligent-agent/list'
|
||||||
import { VEmojiPicker } from 'v-emoji-picker'
|
import { VEmojiPicker } from 'v-emoji-picker'
|
||||||
|
import cropper from '@/components/RenderCropper/components/cropper.vue'
|
||||||
export default {
|
export default {
|
||||||
name: 'info',
|
name: 'info',
|
||||||
inject: ['dialog', 'fetchAgentList'],
|
inject: ['dialog', 'fetchAgentList', 'resetList'],
|
||||||
components: {
|
components: {
|
||||||
VEmojiPicker
|
VEmojiPicker,
|
||||||
|
cropper
|
||||||
|
},
|
||||||
|
computed() {
|
||||||
|
return {
|
||||||
|
copyAgent: item => {
|
||||||
|
return this.dialog.agent
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isImage: '0',
|
isImage: '0',
|
||||||
|
popover: false,
|
||||||
background: '',
|
background: '',
|
||||||
chooseBack: [
|
chooseBack: [
|
||||||
'rgb(255, 255, 255)',
|
'#ffffff',
|
||||||
'rgb(228, 251, 204)',
|
'#e4fbcc',
|
||||||
'rgb(239, 241, 245)',
|
'#eff1f5',
|
||||||
'rgb(224, 234, 255)',
|
'#e0eaf0',
|
||||||
'rgb(254, 247, 195)',
|
'#fef7c3',
|
||||||
'rgb(213, 245, 246)',
|
'#d5f5f6',
|
||||||
'rgb(209, 233, 255)',
|
'#d1e9ff',
|
||||||
'rgb(209, 224, 255)',
|
'#d1e0ff',
|
||||||
'rgb(213, 217, 235)',
|
'#d5d9eb',
|
||||||
'rgb(255, 228, 232)'
|
'#ffe4e8'
|
||||||
],
|
],
|
||||||
// 应用类型
|
// 应用类型
|
||||||
agentType: [
|
agentType: [
|
||||||
@@ -57,22 +67,38 @@ export default {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
name: [
|
appName: [
|
||||||
{ required: true, message: '请输入智能体名称', trigger: 'blur' }
|
{ required: true, message: '请输入智能体名称', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
description: [
|
description: [
|
||||||
{ required: false, message: '请输入智能体描述', trigger: 'blur' }
|
{ required: false, message: '请输入智能体描述', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
appType: [
|
||||||
|
{ required: true, message: '请选择应用类型', trigger: 'blur' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
chooseAgentType(item) {
|
||||||
this.$set(this.dialog.agent, 'appType', item.value)
|
this.$set(this.dialog.agent, 'appType', item.value)
|
||||||
},
|
},
|
||||||
|
|
||||||
chooseGround(colors) {
|
chooseGround(colors) {
|
||||||
this.$set(this.dialog.agent, 'background', colors)
|
this.$set(this.dialog.agent, 'backgroundColor', colors)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 提交表单
|
* 提交表单
|
||||||
@@ -83,20 +109,31 @@ export default {
|
|||||||
// 更新表单之后重新获取列表,然后退出 dialog
|
// 更新表单之后重新获取列表,然后退出 dialog
|
||||||
api(this.dialog.agent).then(res => {
|
api(this.dialog.agent).then(res => {
|
||||||
if (res) {
|
if (res) {
|
||||||
|
this.resetList()
|
||||||
this.fetchAgentList()
|
this.fetchAgentList()
|
||||||
this.dialog.visible = false
|
this.dialog.visible = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// 调用保存或者编辑
|
||||||
validateForm() {
|
validateForm() {
|
||||||
this.$refs.form.validate(valid => {
|
this.$refs.form.validate(valid => {
|
||||||
if (valid) this.handleSubmit()
|
if (valid) this.handleSubmit()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// 选择emoji
|
||||||
selectEmoji(e) {
|
selectEmoji(e) {
|
||||||
this.dialog.agent.image = e.data
|
this.dialog.agent.image = e.data
|
||||||
this.dialog.agent.imageType = 'emoji'
|
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-input v-model="dialog.agent.appName" size="medium" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="" prop="imageId" class="mt30 ml20">
|
<el-form-item label="" prop="imageId" class="mt30 ml20">
|
||||||
<el-popover trigger="click">
|
<el-popover trigger="click" v-model="popover">
|
||||||
<div style="width: 28vw">
|
<div style="width: 28vw">
|
||||||
<el-radio-group
|
<el-radio-group
|
||||||
v-model="isImage"
|
v-model="isImage"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
class="flex render-group"
|
class="flex render-group"
|
||||||
|
@change="changeUploadImage"
|
||||||
>
|
>
|
||||||
<el-radio-button label="0" style="flex:1">表情</el-radio-button>
|
<el-radio-button label="0" style="flex:1">表情</el-radio-button>
|
||||||
<el-radio-button label="1" style="flex:1" disabled
|
<el-radio-button label="1" style="flex:1">图片</el-radio-button>
|
||||||
>图片</el-radio-button
|
|
||||||
>
|
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
|
|
||||||
|
<!-- emoji-->
|
||||||
<div class="mt10" v-if="isImage === '0'">
|
<div class="mt10" v-if="isImage === '0'">
|
||||||
<VEmojiPicker @select="selectEmoji" class="emoji " />
|
<VEmojiPicker @select="selectEmoji" class="emoji " />
|
||||||
<div
|
<div
|
||||||
v-if="dialog.agent.image"
|
v-if="dialog.agent.image"
|
||||||
class="flex mt10 back-content justify-content-b"
|
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
|
<div
|
||||||
class="emoji-background"
|
class="emoji-background"
|
||||||
:style="`background:${item}`"
|
:style="`background:${item}`"
|
||||||
@@ -145,15 +186,44 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="emoji-content"
|
class="emoji-content"
|
||||||
slot="reference"
|
slot="reference"
|
||||||
:style="`background:${dialog.agent.background}`"
|
:style="`background:${dialog.agent.backgroundColor}`"
|
||||||
>
|
>
|
||||||
|
<div v-if="dialog.agent.imageType === 'emoji'">
|
||||||
{{ dialog.agent.image }}
|
{{ dialog.agent.image }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else style="width: 100%">
|
||||||
|
<img :src="dialog.agent.image" alt="" style="width: 100%;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,7 +293,7 @@ export default {
|
|||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: rgb(209, 233, 255);
|
background: #d1e9f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-content {
|
.back-content {
|
||||||
@@ -292,4 +362,9 @@ export default {
|
|||||||
max-width: 100%; // 设置最大宽度以限制文本长度
|
max-width: 100%; // 设置最大宽度以限制文本长度
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cropper-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 45vh;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -53,10 +53,15 @@ export default {
|
|||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
dialog: this.dialog,
|
dialog: this.dialog,
|
||||||
fetchAgentList: this.fetchAgentList
|
fetchAgentList: this.fetchAgentList,
|
||||||
|
resetList: this.resetList
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
resetList() {
|
||||||
|
this.list = []
|
||||||
|
this.page = 0
|
||||||
|
},
|
||||||
load() {
|
load() {
|
||||||
if (this.total === this.list.length) {
|
if (this.total === this.list.length) {
|
||||||
return false
|
return false
|
||||||
@@ -85,7 +90,10 @@ export default {
|
|||||||
*/
|
*/
|
||||||
async handleEditAgent(id) {
|
async handleEditAgent(id) {
|
||||||
const { content } = await agentDetail(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.type = 'edit'
|
||||||
this.dialog.title = '编辑智能体'
|
this.dialog.title = '编辑智能体'
|
||||||
// 获取数据之后打开内容详情
|
// 获取数据之后打开内容详情
|
||||||
@@ -120,7 +128,8 @@ export default {
|
|||||||
appType: '',
|
appType: '',
|
||||||
description: '',
|
description: '',
|
||||||
imageType: '',
|
imageType: '',
|
||||||
image: ''
|
image: '',
|
||||||
|
backgroundColor: '#d1e9ff'
|
||||||
}
|
}
|
||||||
this.dialog.title = '创建智能体'
|
this.dialog.title = '创建智能体'
|
||||||
this.dialog.visible = true
|
this.dialog.visible = true
|
||||||
@@ -195,7 +204,16 @@ export default {
|
|||||||
>
|
>
|
||||||
<div class="dataset-header">
|
<div class="dataset-header">
|
||||||
<div class="folder-content">
|
<div class="folder-content">
|
||||||
<div class="folder">
|
<div
|
||||||
|
class="folder"
|
||||||
|
:style="
|
||||||
|
`background:${
|
||||||
|
listItem.imageType === 'emoji'
|
||||||
|
? listItem.backgroundColor
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
"
|
||||||
|
>
|
||||||
<!--listItem.name.获取字符串第一个-->
|
<!--listItem.name.获取字符串第一个-->
|
||||||
{{ listItem.image ? listItem.image : listItem.appName[0] }}
|
{{ listItem.image ? listItem.image : listItem.appName[0] }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user