2025-11-12 03:41:22 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="attachment-options">
|
|
|
|
|
|
<div class="option-item">
|
|
|
|
|
|
<label class="attachment-upload-label" :class="{ disabled: disabled }">
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref="fileInputRef"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
accept=".txt,.md,.docx,.html,.htm"
|
|
|
|
|
|
:disabled="disabled"
|
|
|
|
|
|
@change="handleFileChange"
|
2026-01-15 06:01:34 +08:00
|
|
|
|
style="display: none"
|
2025-11-12 03:41:22 +08:00
|
|
|
|
/>
|
2026-01-24 15:24:25 +08:00
|
|
|
|
<a-tooltip title="支持 txt/md/docx/html 格式 ≤ 5 MB" placement="right">
|
2025-11-12 03:41:22 +08:00
|
|
|
|
<div class="option-content">
|
2026-01-31 14:16:53 +08:00
|
|
|
|
<FileText :size="14" class="option-icon" />
|
2025-11-12 03:41:22 +08:00
|
|
|
|
<span class="option-text">添加附件</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="option-item" @click="handleImageUpload">
|
2025-11-12 14:04:34 +08:00
|
|
|
|
<a-tooltip title="支持 jpg/jpeg/png/gif, ≤ 5 MB" placement="right">
|
2025-11-12 03:41:22 +08:00
|
|
|
|
<div class="option-content">
|
2026-01-31 14:16:53 +08:00
|
|
|
|
<Image :size="14" class="option-icon" />
|
2025-11-12 03:41:22 +08:00
|
|
|
|
<span class="option-text">上传图片</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-01-15 06:01:34 +08:00
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
|
import { FileText, Image } from 'lucide-vue-next'
|
|
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
import { multimodalApi } from '@/apis/agent_api'
|
2025-11-12 03:41:22 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const fileInputRef = ref(null)
|
|
|
|
|
|
const imageInputRef = ref(null)
|
2025-11-12 03:41:22 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
disabled: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-11-12 03:41:22 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const emit = defineEmits(['upload', 'upload-image', 'upload-image-success'])
|
2025-11-12 03:41:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理文件选择变化
|
|
|
|
|
|
const handleFileChange = (event) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const files = event.target.files
|
2025-11-12 03:41:22 +08:00
|
|
|
|
if (files && files.length > 0) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
emit('upload', Array.from(files))
|
2025-11-12 03:41:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 清空文件输入,允许重复选择同一文件
|
2026-01-15 06:01:34 +08:00
|
|
|
|
event.target.value = ''
|
|
|
|
|
|
}
|
2025-11-12 03:41:22 +08:00
|
|
|
|
|
2025-11-12 14:04:34 +08:00
|
|
|
|
// 处理图片上传
|
2025-11-12 03:41:22 +08:00
|
|
|
|
const handleImageUpload = () => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
if (props.disabled) return
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建隐藏的文件输入
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const input = document.createElement('input')
|
|
|
|
|
|
input.type = 'file'
|
|
|
|
|
|
input.accept = 'image/*'
|
|
|
|
|
|
input.multiple = false
|
|
|
|
|
|
input.style.display = 'none'
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
input.onchange = async (event) => {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const file = event.target.files[0]
|
2025-11-12 14:04:34 +08:00
|
|
|
|
if (file) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
await processImageUpload(file)
|
2025-11-12 14:04:34 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
document.body.removeChild(input)
|
|
|
|
|
|
}
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
document.body.appendChild(input)
|
|
|
|
|
|
input.click()
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
emit('upload-image')
|
|
|
|
|
|
}
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理图片上传逻辑
|
|
|
|
|
|
const processImageUpload = async (file) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 验证文件大小(10MB)
|
|
|
|
|
|
if (file.size > 10 * 1024 * 1024) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.error('图片文件过大,请选择小于10MB的图片')
|
|
|
|
|
|
return
|
2025-11-12 14:04:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证文件类型
|
|
|
|
|
|
if (!file.type.startsWith('image/')) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.error('请选择有效的图片文件')
|
|
|
|
|
|
return
|
2025-11-12 14:04:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
message.loading({ content: '正在处理图片...', key: 'image-upload' })
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
2026-01-15 06:01:34 +08:00
|
|
|
|
const result = await multimodalApi.uploadImage(file)
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
message.success({
|
|
|
|
|
|
content: '图片处理成功',
|
|
|
|
|
|
key: 'image-upload',
|
|
|
|
|
|
duration: 2
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 发出上传成功事件,包含处理后的图片数据
|
|
|
|
|
|
emit('upload-image', {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
imageContent: result.image_content,
|
|
|
|
|
|
thumbnailContent: result.thumbnail_content,
|
|
|
|
|
|
width: result.width,
|
|
|
|
|
|
height: result.height,
|
|
|
|
|
|
format: result.format,
|
2025-12-19 04:04:48 +08:00
|
|
|
|
mimeType: result.mime_type || file.type,
|
2025-11-12 14:04:34 +08:00
|
|
|
|
sizeBytes: result.size_bytes,
|
|
|
|
|
|
originalName: file.name
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-11-12 14:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 发出上传成功通知事件,用于关闭选项面板
|
2026-01-15 06:01:34 +08:00
|
|
|
|
emit('upload-image-success')
|
2025-11-12 14:04:34 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
message.error({
|
|
|
|
|
|
content: `图片处理失败: ${result.error}`,
|
|
|
|
|
|
key: 'image-upload'
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-11-12 14:04:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-01-15 06:01:34 +08:00
|
|
|
|
console.error('图片上传失败:', error)
|
2025-11-12 14:04:34 +08:00
|
|
|
|
message.error({
|
|
|
|
|
|
content: `图片上传失败: ${error.message || '未知错误'}`,
|
|
|
|
|
|
key: 'image-upload'
|
2026-01-15 06:01:34 +08:00
|
|
|
|
})
|
2025-11-12 14:04:34 +08:00
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
}
|
2025-11-12 03:41:22 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
|
.attachment-options {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.option-item {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
|
|
|
|
|
|
.option-content {
|
|
|
|
|
|
color: var(--gray-400);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.option-content {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-01-24 15:24:25 +08:00
|
|
|
|
gap: 10px;
|
2026-01-31 14:16:53 +08:00
|
|
|
|
padding: 6px 10px;
|
2025-11-12 03:41:22 +08:00
|
|
|
|
color: var(--gray-700);
|
2026-01-31 14:16:53 +08:00
|
|
|
|
font-size: 12px;
|
2025-11-12 03:41:22 +08:00
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
|
|
|
|
|
|
|
.option-item:hover & {
|
|
|
|
|
|
color: var(--main-color);
|
|
|
|
|
|
background-color: var(--gray-50);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.option-icon {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.option-text {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.attachment-upload-label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
&.disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
|
|
|
|
|
|
.option-content {
|
|
|
|
|
|
color: var(--gray-400);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-15 06:01:34 +08:00
|
|
|
|
</style>
|