From 4ce77ececcd75515bb21257e32be62d7e31087cf Mon Sep 17 00:00:00 2001 From: Gsh <15170702455@163.com> Date: Mon, 19 Jan 2026 22:15:01 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E5=AF=B9=E8=AF=9D=E6=A1=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=B2=98=E8=B4=B4=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Yi.Ai.Vue3/.claude/settings.local.json | 5 +- .../pages/chat/layouts/chatDefaul/index.vue | 181 +++++++++- .../pages/chat/layouts/chatWithId/index.vue | 337 +++++++++++++++++- 3 files changed, 520 insertions(+), 3 deletions(-) diff --git a/Yi.Ai.Vue3/.claude/settings.local.json b/Yi.Ai.Vue3/.claude/settings.local.json index 13e6d318..776c8df4 100644 --- a/Yi.Ai.Vue3/.claude/settings.local.json +++ b/Yi.Ai.Vue3/.claude/settings.local.json @@ -3,7 +3,10 @@ "allow": [ "Bash(npx vue-tsc --noEmit)", "Bash(timeout 60 npx vue-tsc:*)", - "Bash(npm run dev:*)" + "Bash(npm run dev:*)", + "Bash(taskkill:*)", + "Bash(timeout /t 5 /nobreak)", + "Bash(git checkout:*)" ], "deny": [], "ask": [] diff --git a/Yi.Ai.Vue3/src/pages/chat/layouts/chatDefaul/index.vue b/Yi.Ai.Vue3/src/pages/chat/layouts/chatDefaul/index.vue index faa0ef01..3eefcc93 100644 --- a/Yi.Ai.Vue3/src/pages/chat/layouts/chatDefaul/index.vue +++ b/Yi.Ai.Vue3/src/pages/chat/layouts/chatDefaul/index.vue @@ -4,7 +4,7 @@ import type { FilesCardProps } from 'vue-element-plus-x/types/FilesCard'; import { ArrowLeftBold, ArrowRightBold, Loading } from '@element-plus/icons-vue'; import { useDebounceFn } from '@vueuse/core'; import { ElMessage } from 'element-plus'; -import { computed, nextTick, ref, watch } from 'vue'; +import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'; import ModelSelect from '@/components/ModelSelect/index.vue'; import WelecomeText from '@/components/WelecomeText/index.vue'; import Collapse from '@/layouts/components/Header/components/Collapse.vue'; @@ -26,6 +26,10 @@ const senderValue = ref(''); // 输入框内容 const senderRef = ref(); // Sender 组件引用 const isSending = ref(false); // 发送状态标志 +// 文件处理相关常量 +const MAX_FILE_SIZE = 3 * 1024 * 1024; +const MAX_TOTAL_CONTENT_LENGTH = 150000; + /** * 防抖发送消息函数 */ @@ -107,6 +111,181 @@ watch( }); }, ); + +/** + * 压缩图片 + */ +function compressImage(file: File, maxWidth = 1024, maxHeight = 1024, quality = 0.8): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + let width = img.width; + let height = img.height; + + if (width > maxWidth || height > maxHeight) { + const ratio = Math.min(maxWidth / width, maxHeight / height); + width = width * ratio; + height = height * ratio; + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d')!; + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob( + (blob) => { + if (blob) { + resolve(blob); + } + else { + reject(new Error('压缩失败')); + } + }, + file.type, + quality, + ); + }; + img.onerror = reject; + img.src = e.target?.result as string; + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} + +/** + * 将 Blob 转换为 base64 + */ +function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result as string); + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} + +/** + * 处理粘贴事件 + */ +async function handlePaste(event: ClipboardEvent) { + const items = event.clipboardData?.items; + if (!items) + return; + + const files: File[] = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.kind === 'file') { + const file = item.getAsFile(); + if (file) { + files.push(file); + } + } + } + + if (files.length === 0) + return; + + event.preventDefault(); + + // 计算已有文件的总内容长度 + let totalContentLength = 0; + filesStore.filesList.forEach((f) => { + if (f.fileType === 'text' && f.fileContent) { + totalContentLength += f.fileContent.length; + } + if (f.fileType === 'image' && f.base64) { + totalContentLength += Math.floor(f.base64.length * 0.5); + } + }); + + const arr: any[] = []; + + for (const file of files) { + // 验证文件大小 + if (file.size > MAX_FILE_SIZE) { + ElMessage.error(`文件 ${file.name} 超过 3MB 限制,已跳过`); + continue; + } + + const isImage = file.type.startsWith('image/'); + + if (isImage) { + try { + const compressionLevels = [ + { maxWidth: 800, maxHeight: 800, quality: 0.6 }, + { maxWidth: 600, maxHeight: 600, quality: 0.5 }, + { maxWidth: 400, maxHeight: 400, quality: 0.4 }, + ]; + + let compressedBlob: Blob | null = null; + let base64 = ''; + + for (const level of compressionLevels) { + compressedBlob = await compressImage(file, level.maxWidth, level.maxHeight, level.quality); + base64 = await blobToBase64(compressedBlob); + + const estimatedLength = Math.floor(base64.length * 0.5); + if (totalContentLength + estimatedLength <= MAX_TOTAL_CONTENT_LENGTH) { + totalContentLength += estimatedLength; + break; + } + + compressedBlob = null; + } + + if (!compressedBlob) { + ElMessage.error(`${file.name} 图片内容过大,请压缩后上传`); + continue; + } + + arr.push({ + uid: crypto.randomUUID(), + name: file.name, + fileSize: file.size, + file, + maxWidth: '200px', + showDelIcon: true, + imgPreview: true, + imgVariant: 'square', + url: base64, + isUploaded: true, + base64, + fileType: 'image', + }); + } + catch (error) { + console.error('处理图片失败:', error); + ElMessage.error(`${file.name} 处理失败`); + } + } + else { + ElMessage.warning(`${file.name} 不支持粘贴,请使用上传按钮`); + } + } + + if (arr.length > 0) { + filesStore.setFilesList([...filesStore.filesList, ...arr]); + ElMessage.success(`已添加 ${arr.length} 个文件`); + } +} + +// 监听粘贴事件 +onMounted(() => { + document.addEventListener('paste', handlePaste); +}); + +onUnmounted(() => { + document.removeEventListener('paste', handlePaste); +}); @@ -489,15 +523,22 @@ watch(isDialogVisible, async (newValue) => { .activity-item { position: relative; - padding: 0; + padding: 16px; background: #fff; border-radius: 16px; - overflow: hidden; + overflow: visible; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); border: 2px solid transparent; background-clip: padding-box; + // 清除浮动,确保父容器高度正确 + &::after { + content: ''; + display: table; + clear: both; + } + &::before { content: ''; position: absolute; @@ -525,9 +566,8 @@ watch(isDialogVisible, async (newValue) => { opacity: 1; } - .activity-image { - transform: scale(1.1); - filter: brightness(1.05); + .activity-image-wrapper { + box-shadow: 0 8px 24px rgba(102, 126, 234, 0.2); } .detail-link { @@ -545,10 +585,31 @@ watch(isDialogVisible, async (newValue) => { .activity-image-wrapper { position: relative; - width: 100%; - height: 220px; + float: right; + width: 160px; + height: 160px; + margin-left: 16px; + margin-bottom: 8px; + border-radius: 12px; overflow: hidden; background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%); + shape-outside: inset(0); + transition: box-shadow 0.3s; + cursor: pointer; + z-index: 1; // 确保在内容之上 + + &:hover { + box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3); + } + + &:hover .activity-image { + transform: scale(1.1); + } + + &:hover .zoom-icon { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } &::before { content: ''; @@ -560,6 +621,7 @@ watch(isDialogVisible, async (newValue) => { background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); z-index: 1; animation: shine 3s infinite; + pointer-events: none; // 确保不拦截鼠标事件 } &::after { @@ -587,22 +649,22 @@ watch(isDialogVisible, async (newValue) => { .activity-image { width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; // 等比例缩放,完整显示图片,不裁剪 transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .activity-status-badge { position: absolute; - top: 16px; - right: 16px; + top: 12px; + right: 12px; z-index: 2; animation: fadeInScale 0.5s ease-out 0.3s both; :deep(.el-tag) { - border-radius: 20px; - padding: 7px 18px; + border-radius: 16px; + padding: 5px 14px; font-weight: 700; - font-size: 13px; + font-size: 11px; border: none; backdrop-filter: blur(12px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); @@ -648,24 +710,52 @@ watch(isDialogVisible, async (newValue) => { } } + .zoom-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.8); + width: 48px; + height: 48px; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; // 不拦截鼠标事件,让父容器接收hover + z-index: 10; // 提高层级,确保在状态标签之上 + + svg { + width: 24px; + height: 24px; + color: #fff; + stroke-width: 2; + } + } + .activity-body { - padding: 20px; + // 内容会自动环绕浮动的图片 + position: relative; + z-index: 0; // 确保在浮动图片之下 } .activity-header { display: flex; align-items: center; gap: 12px; - margin-bottom: 12px; + margin-bottom: 8px; } .activity-title { margin: 0; - font-size: 18px; + font-size: 16px; font-weight: 700; color: #1a1a1a; flex: 1; - line-height: 1.4; + line-height: 1.3; background: linear-gradient(135deg, #1a1a1a 0%, #4a5568 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; @@ -679,23 +769,23 @@ watch(isDialogVisible, async (newValue) => { .activity-content-list { padding: 0; - margin: 0 0 16px 0; + margin: 0 0 12px 0; color: #4a5568; - font-size: 14px; - line-height: 1.8; + font-size: 13px; + line-height: 1.6; .content-line { - margin: 8px 0; - padding-left: 18px; + margin: 6px 0; + padding-left: 16px; position: relative; &::before { content: ''; position: absolute; left: 0; - top: 11px; - width: 6px; - height: 6px; + top: 9px; + width: 5px; + height: 5px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; } @@ -715,8 +805,9 @@ watch(isDialogVisible, async (newValue) => { justify-content: space-between; align-items: center; gap: 16px; - padding-top: 16px; + padding-top: 12px; border-top: 1px dashed #e8e9eb; + clear: both; // 清除浮动,始终在新行显示,占满100%宽度 } .activity-time-range { @@ -727,7 +818,7 @@ watch(isDialogVisible, async (newValue) => { } .activity-time { - font-size: 13px; + font-size: 12px; color: #718096; display: flex; align-items: center; @@ -735,19 +826,19 @@ watch(isDialogVisible, async (newValue) => { &:first-child::before { content: '🕐'; - font-size: 14px; + font-size: 13px; } } .detail-link { display: inline-flex; align-items: center; - gap: 8px; - padding: 10px 18px; + gap: 6px; + padding: 8px 16px; background: linear-gradient(135deg, #f0f1f3 0%, #e8eaed 100%); color: #667eea; - border-radius: 24px; - font-size: 13px; + border-radius: 20px; + font-size: 12px; font-weight: 700; text-decoration: none; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); @@ -1095,16 +1186,27 @@ watch(isDialogVisible, async (newValue) => { .activity-item { border-radius: 10px; - } - - .activity-image-wrapper { - height: 180px; + padding: 0; } .activity-body { padding: 16px; } + .activity-image-wrapper { + float: none; + width: 100%; + height: 180px; + min-height: 180px; + margin: 0 0 16px 0; + border-radius: 10px 10px 0 0; + } + + .activity-status-badge { + top: 12px; + right: 12px; + } + .activity-header { margin-bottom: 10px; } @@ -1183,7 +1285,18 @@ watch(isDialogVisible, async (newValue) => { .activity-content { .activity-image-wrapper { + float: none; + width: 100%; height: 200px; + margin: 0 0 16px 0; + } + + .activity-item { + padding: 0; + } + + .activity-body { + padding: 18px; } } diff --git a/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue b/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue index 5e328ee7..2da2ecfa 100644 --- a/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue +++ b/Yi.Ai.Vue3/src/pages/chat/image/components/ImageGenerator.vue @@ -46,6 +46,8 @@ const generating = ref(false); const currentTaskId = ref(''); const currentTask = ref(null); const showViewer = ref(false); +const referenceImageViewerVisible = ref(false); +const referenceImagePreviewUrl = ref(''); let pollTimer: any = null; let debounceTimer: any = null; @@ -130,6 +132,17 @@ function handleRemove(file: UploadFile) { fileList.value.splice(index, 1); } +function handlePreview(file: UploadFile) { + if (file.url) { + referenceImagePreviewUrl.value = file.url; + referenceImageViewerVisible.value = true; + } +} + +function closeReferenceImageViewer() { + referenceImageViewerVisible.value = false; +} + // Handle paste event for reference images function handlePaste(event: ClipboardEvent) { const items = event.clipboardData?.items; @@ -510,6 +523,7 @@ onUnmounted(() => { :limit="2" :on-change="handleFileChange" :on-remove="handleRemove" + :on-preview="handlePreview" accept=".jpg,.jpeg,.png,.bmp,.webp" :class="{ 'hide-upload-btn': fileList.length >= 2 }" > @@ -644,6 +658,15 @@ onUnmounted(() => { + + + + +