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.inputAddSelectedImage(item.path);
|
||||||
this.upsertImageEntry(item.path, item.filename);
|
this.upsertImageEntry(item.path, item.filename);
|
||||||
});
|
});
|
||||||
|
// 上传完成后自动关闭选择窗口
|
||||||
|
this.closeImagePicker();
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleLocalVideoFiles(files) {
|
async handleLocalVideoFiles(files) {
|
||||||
|
|||||||
@ -10,10 +10,14 @@
|
|||||||
<div class="message-text user-bubble-text">
|
<div class="message-text user-bubble-text">
|
||||||
<div v-if="msg.content" class="bubble-text">{{ msg.content }}</div>
|
<div v-if="msg.content" class="bubble-text">{{ msg.content }}</div>
|
||||||
<div v-if="msg.images && msg.images.length" class="image-inline-row">
|
<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>
|
||||||
<div v-if="msg.videos && msg.videos.length" class="image-inline-row video-inline-row">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -430,6 +434,11 @@ function formatImageName(path: string): string {
|
|||||||
return parts[parts.length - 1] || path;
|
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 isStackable = (action: any) => action && (action.type === 'thinking' || action.type === 'tool');
|
||||||
const isEmptyTextAction = (action: any) => {
|
const isEmptyTextAction = (action: any) => {
|
||||||
if (!action || action.type !== 'text') {
|
if (!action || action.type !== 'text') {
|
||||||
|
|||||||
@ -10,19 +10,19 @@
|
|||||||
'has-text': (inputMessage || '').trim().length > 0
|
'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 class="input-stack">
|
||||||
<div v-if="selectedImages && selectedImages.length" class="image-inline-row">
|
<div v-if="selectedImages && selectedImages.length" class="image-inline-row">
|
||||||
<span class="image-name" v-for="img in selectedImages" :key="img">
|
<div class="image-thumbnail-wrapper" v-for="img in selectedImages" :key="img">
|
||||||
{{ formatImageName(img) }}
|
<img :src="getPreviewUrl(img)" :alt="formatImageName(img)" class="image-thumbnail" />
|
||||||
<button type="button" class="image-remove-btn" @click.stop="$emit('remove-image', img)">×</button>
|
<button type="button" class="image-remove-btn-hover" @click.stop="$emit('remove-image', img)">×</button>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedVideos && selectedVideos.length" class="image-inline-row video-inline-row">
|
<div v-if="selectedVideos && selectedVideos.length" class="image-inline-row video-inline-row">
|
||||||
<span class="image-name" v-for="video in selectedVideos" :key="video">
|
<div class="image-thumbnail-wrapper" v-for="video in selectedVideos" :key="video">
|
||||||
{{ formatImageName(video) }}
|
<img :src="getPreviewUrl(video)" :alt="formatImageName(video)" class="image-thumbnail" />
|
||||||
<button type="button" class="image-remove-btn" @click.stop="$emit('remove-video', video)">×</button>
|
<button type="button" class="image-remove-btn-hover" @click.stop="$emit('remove-video', video)">×</button>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<button
|
<button
|
||||||
@ -190,6 +190,11 @@ const formatImageName = (path: string): string => {
|
|||||||
return parts[parts.length - 1] || path;
|
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) => {
|
const applyLineMetrics = (lines: number, multiline: boolean) => {
|
||||||
inputStore.setInputLineCount(lines);
|
inputStore.setInputLineCount(lines);
|
||||||
inputStore.setInputMultiline(multiline);
|
inputStore.setInputMultiline(multiline);
|
||||||
@ -269,11 +274,48 @@ onMounted(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 4px 10px 2px;
|
padding: 4px 10px 2px;
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary, #7f8792);
|
|
||||||
line-height: 1.4;
|
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>
|
</style>
|
||||||
|
|||||||
@ -401,14 +401,25 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: 12px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message .message-text.user-bubble-text .image-name {
|
.user-message .message-text.user-bubble-text .image-thumbnail-wrapper {
|
||||||
white-space: nowrap;
|
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 {
|
.assistant-generating-block {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user