feat: 优化图片/视频上传和显示功能
- 支持从本地一次选择多个文件上传 - 图片选择器上传完成后自动关闭 - 输入框和用户消息块显示缩略图(60x60px)而非文件名 - 输入框缩略图悬停时显示删除按钮(右上角 × 符号) - 统一图片/视频预览样式,提升用户体验 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cd3f07bcc8
commit
bd863f6d38
@ -125,6 +125,8 @@ export const uploadMethods = {
|
||||
this.inputAddSelectedImage(item.path);
|
||||
this.upsertImageEntry(item.path, item.filename);
|
||||
});
|
||||
// 上传完成后自动关闭选择窗口
|
||||
this.closeImagePicker();
|
||||
},
|
||||
|
||||
async handleLocalVideoFiles(files) {
|
||||
|
||||
@ -10,10 +10,14 @@
|
||||
<div class="message-text user-bubble-text">
|
||||
<div v-if="msg.content" class="bubble-text">{{ msg.content }}</div>
|
||||
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
||||
<span class="image-name" v-for="img in msg.images" :key="img">{{ formatImageName(img) }}</span>
|
||||
<div class="image-thumbnail-wrapper" v-for="img in msg.images" :key="img">
|
||||
<img :src="getPreviewUrl(img)" :alt="formatImageName(img)" class="image-thumbnail" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="msg.videos && msg.videos.length" class="image-inline-row video-inline-row">
|
||||
<span class="image-name" v-for="video in msg.videos" :key="video">{{ formatImageName(video) }}</span>
|
||||
<div class="image-thumbnail-wrapper" v-for="video in msg.videos" :key="video">
|
||||
<img :src="getPreviewUrl(video)" :alt="formatImageName(video)" class="image-thumbnail" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -430,6 +434,11 @@ function formatImageName(path: string): string {
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
function getPreviewUrl(path: string): string {
|
||||
if (!path) return '';
|
||||
return `/api/gui/files/download?path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
|
||||
const isStackable = (action: any) => action && (action.type === 'thinking' || action.type === 'tool');
|
||||
const isEmptyTextAction = (action: any) => {
|
||||
if (!action || action.type !== 'text') {
|
||||
|
||||
@ -10,19 +10,19 @@
|
||||
'has-text': (inputMessage || '').trim().length > 0
|
||||
}"
|
||||
>
|
||||
<input type="file" ref="fileUploadInput" class="file-input-hidden" @change="onFileChange" />
|
||||
<input type="file" ref="fileUploadInput" class="file-input-hidden" multiple @change="onFileChange" />
|
||||
<div class="input-stack">
|
||||
<div v-if="selectedImages && selectedImages.length" class="image-inline-row">
|
||||
<span class="image-name" v-for="img in selectedImages" :key="img">
|
||||
{{ formatImageName(img) }}
|
||||
<button type="button" class="image-remove-btn" @click.stop="$emit('remove-image', img)">×</button>
|
||||
</span>
|
||||
<div class="image-thumbnail-wrapper" v-for="img in selectedImages" :key="img">
|
||||
<img :src="getPreviewUrl(img)" :alt="formatImageName(img)" class="image-thumbnail" />
|
||||
<button type="button" class="image-remove-btn-hover" @click.stop="$emit('remove-image', img)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedVideos && selectedVideos.length" class="image-inline-row video-inline-row">
|
||||
<span class="image-name" v-for="video in selectedVideos" :key="video">
|
||||
{{ formatImageName(video) }}
|
||||
<button type="button" class="image-remove-btn" @click.stop="$emit('remove-video', video)">×</button>
|
||||
</span>
|
||||
<div class="image-thumbnail-wrapper" v-for="video in selectedVideos" :key="video">
|
||||
<img :src="getPreviewUrl(video)" :alt="formatImageName(video)" class="image-thumbnail" />
|
||||
<button type="button" class="image-remove-btn-hover" @click.stop="$emit('remove-video', video)">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<button
|
||||
@ -190,6 +190,11 @@ const formatImageName = (path: string): string => {
|
||||
return parts[parts.length - 1] || path;
|
||||
};
|
||||
|
||||
const getPreviewUrl = (path: string): string => {
|
||||
if (!path) return '';
|
||||
return `/api/gui/files/download?path=${encodeURIComponent(path)}`;
|
||||
};
|
||||
|
||||
const applyLineMetrics = (lines: number, multiline: boolean) => {
|
||||
inputStore.setInputLineCount(lines);
|
||||
inputStore.setInputMultiline(multiline);
|
||||
@ -269,11 +274,48 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 4px 10px 2px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #7f8792);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.image-name {
|
||||
white-space: nowrap;
|
||||
|
||||
.image-thumbnail-wrapper {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color, #2a2f3a);
|
||||
}
|
||||
|
||||
.image-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-remove-btn-hover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: none;
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
padding: 2px 4px;
|
||||
text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.image-thumbnail-wrapper:hover .image-remove-btn-hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-remove-btn-hover:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -401,14 +401,25 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--claude-text-secondary);
|
||||
line-height: 1.4;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.user-message .message-text.user-bubble-text .image-name {
|
||||
white-space: nowrap;
|
||||
.user-message .message-text.user-bubble-text .image-thumbnail-wrapper {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color, #2a2f3a);
|
||||
}
|
||||
|
||||
.user-message .message-text.user-bubble-text .image-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.assistant-generating-block {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user