ForcePilot/web/src/components/AttachmentInputPanel.vue
Wenjie Zhang 8b4b7c230c feat: 添加附件上传中间件和管理功能
- 实现对话线程的附件上传、列出和删除功能
- 引入附件中间件以支持附件内容的上下文注入
- 更新智能体和对话管理器以处理附件元数据
- 增强前端组件以支持文件上传和显示附件状态
- 优化文档以反映新功能和使用示例
2025-11-08 10:51:30 +08:00

169 lines
3.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="attachment-panel">
<label class="attachment-upload" :class="{ disabled: disabled || isUploading }">
<input
type="file"
multiple
accept=".txt,.md,.docx,.html,.htm"
:disabled="disabled || isUploading"
@change="handleFileChange"
/>
<Paperclip size="14" />
<span>{{ isUploading ? '上传中…' : '添加附件' }}</span>
</label>
<p class="attachment-hint" v-if="limits">
支持 {{ extensionsText }}单文件 {{ sizeHint }}
</p>
<div class="attachment-list" v-if="attachments.length">
<div class="attachment-chip" v-for="item in attachments" :key="item.file_id">
<Paperclip size="14" class="chip-icon" />
<span class="chip-name" :title="item.file_name">{{ item.file_name }}</span>
<span class="chip-status" :class="`status-${item.status}`">
{{ statusLabel(item) }}
</span>
<a-tooltip title="移除附件">
<a-button
type="text"
size="small"
class="chip-remove"
:disabled="disabled"
@click="$emit('remove', item.file_id)"
>
<template #icon>
<Trash2 size="14" />
</template>
</a-button>
</a-tooltip>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { Paperclip, Trash2 } from 'lucide-vue-next'
const props = defineProps({
attachments: { type: Array, default: () => [] },
limits: { type: Object, default: () => null },
isUploading: { type: Boolean, default: false },
disabled: { type: Boolean, default: false }
})
const emit = defineEmits(['upload', 'remove'])
const extensionsText = computed(() => {
if (!props.limits?.allowed_extensions?.length) return 'txt/md/docx/html'
return props.limits.allowed_extensions.map(item => item.replace('.', '')).join(' / ')
})
const sizeHint = computed(() => {
if (!props.limits?.max_size_bytes) return '5 MB'
const mb = props.limits.max_size_bytes / (1024 * 1024)
return `${mb.toFixed(1)} MB`
})
const statusLabel = (item) => {
if (item.status === 'parsed') {
return item.truncated ? '已解析(截断)' : '已解析'
}
if (item.status === 'failed') return '解析失败'
return '处理中'
}
const handleFileChange = (event) => {
const files = Array.from(event.target.files || [])
event.target.value = ''
if (!files.length) return
emit('upload', files)
}
</script>
<style scoped>
.attachment-panel {
min-width: 220px;
max-width: 320px;
}
.attachment-upload {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--main-700);
cursor: pointer;
font-weight: 500;
}
.attachment-upload.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.attachment-upload input {
display: none;
}
.attachment-hint {
margin: 4px 0 8px;
font-size: 11px;
color: var(--gray-500);
line-height: 1.4;
}
.attachment-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 220px;
overflow-y: auto;
}
.attachment-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: var(--gray-25);
border: 1px solid var(--gray-100);
border-radius: 6px;
}
.chip-icon {
color: var(--gray-600);
}
.chip-name {
flex: 1;
font-size: 12px;
color: var(--gray-800);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chip-status {
font-size: 11px;
color: var(--gray-600);
}
.chip-status.status-parsed {
color: var(--green-600, #1a7f37);
}
.chip-status.status-failed {
color: var(--red-600, #c62828);
}
.chip-remove {
margin-left: 2px;
color: var(--gray-500);
}
.chip-remove:hover {
color: var(--gray-700);
}
</style>