feat(input): 将 ChatInput 容器改为 grid 布局,修复 textarea 溢出滚动抖动
This commit is contained in:
parent
64c441ba36
commit
8261415b40
|
|
@ -4,14 +4,7 @@
|
|||
<div class="input-area">
|
||||
<!-- 左侧功能按钮 -->
|
||||
<div class="input-actions left">
|
||||
<button v-if="attachments.length === 0" type="button" class="action-btn upload" :style="{height: 'auto',width: 'auto',borderRadius: '0'}"
|
||||
:class="{ disabled: !supports_files && !supports_vision }" :disabled="!supports_files && !supports_vision"
|
||||
:title="supports_files || supports_vision ? '上传附件或图片' : '当前模型不支持上传'" @click="triggerUploadInput">
|
||||
<div class="upload-action-div" :class="{ disabled: !supports_files && !supports_vision }">
|
||||
<PlusIcon :size="13" />
|
||||
</div>
|
||||
</button>
|
||||
<StackedCards v-else :cards="attachments" :supports-files="supports_files" :supports-vision="supports_vision"
|
||||
<StackedCards :cards="attachments" :supports-files="supports_files" :supports-vision="supports_vision"
|
||||
@remove="removeAttachment" @add-upload="triggerUploadInput" />
|
||||
|
||||
<!-- 隐藏的文件输入框 -->
|
||||
|
|
@ -68,8 +61,8 @@
|
|||
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
|
||||
<StopCircle :size="20" />
|
||||
</button>
|
||||
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }" :disabled="!canSend"
|
||||
:title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
|
||||
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
|
||||
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
|
||||
<Loader2 v-if="isProcessingAttachments" :size="20" class="animate-spin" />
|
||||
<SendIcon v-else :size="20" />
|
||||
</button>
|
||||
|
|
@ -94,7 +87,6 @@ import { chatApi } from "@/services/api";
|
|||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useSettingsStore } from "@/stores/settings";
|
||||
import StackedCards from "@/components/ui/StackedCards.vue";
|
||||
import PlusIcon from "../icons/custom/PlusIcon.vue";
|
||||
import SendIcon from "../icons/custom/SendIcon.vue";
|
||||
|
||||
interface AttachmentWithProgress extends Attachment {
|
||||
|
|
@ -202,7 +194,7 @@ function autoResize() {
|
|||
if (!textarea) return;
|
||||
|
||||
textarea.style.height = "auto";
|
||||
const maxHeight = isExpanded.value ? 400 : 160;
|
||||
const maxHeight = isExpanded.value ? 400 : 116;
|
||||
// 增加1px是为了避免滚动条出现
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight) + 1}px`;
|
||||
}
|
||||
|
|
@ -498,17 +490,11 @@ onMounted(() => {
|
|||
// border: 2px solid #e2e8f0;
|
||||
height: 200px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.dark & {
|
||||
background: #1e1e2e;
|
||||
border-color: #374151;
|
||||
}
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
border-color: #374151;
|
||||
|
||||
// &.is-focused {
|
||||
// border-color: #3b82f6;
|
||||
|
|
@ -527,6 +513,7 @@ onMounted(() => {
|
|||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
min-height: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -624,11 +611,14 @@ onMounted(() => {
|
|||
.textarea-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 25px;
|
||||
max-height: 160px;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
padding: 8px 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
|
@ -653,7 +643,6 @@ onMounted(() => {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
// background: #fafbfc;
|
||||
|
||||
.dark & {
|
||||
|
|
@ -714,27 +703,6 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.upload-action-div {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 0;
|
||||
width: 50px;
|
||||
height: 70px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.upload-action-div.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
background: #f3f4f6;
|
||||
border: 1px dashed #cbd5e1;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.upload-action-div.disabled :deep(svg) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
<template>
|
||||
<div
|
||||
class="message-bubble"
|
||||
:class="[
|
||||
`role-${message.role}`,
|
||||
{
|
||||
'is-streaming': message.isStreaming,
|
||||
'is-end': !message.isEnd && message.role !== 'user',
|
||||
'is-error': message.isError,
|
||||
compact: compact,
|
||||
'message-select-mode': isMessageSelectMode,
|
||||
'message-selected': isSelected,
|
||||
},
|
||||
]"
|
||||
@click="handleBubbleClick"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
>
|
||||
<div class="message-bubble" :class="[
|
||||
`role-${message.role}`,
|
||||
{
|
||||
'is-streaming': message.isStreaming,
|
||||
'is-end': !message.isEnd && message.role !== 'user',
|
||||
'is-error': message.isError,
|
||||
compact: compact,
|
||||
'message-select-mode': isMessageSelectMode,
|
||||
'message-selected': isSelected,
|
||||
},
|
||||
]" @click="handleBubbleClick" @mouseenter="isHovered = true" @mouseleave="isHovered = false">
|
||||
<!-- 消息选择模式复选框 -->
|
||||
<div v-if="isMessageSelectMode" class="message-checkbox" @click.stop="handleToggleSelect">
|
||||
<div class="checkbox" :class="{ checked: isSelected }">
|
||||
|
|
@ -24,12 +18,12 @@
|
|||
</div>
|
||||
|
||||
<!-- 头像 -->
|
||||
<div class="avatar">
|
||||
<!-- <div class="avatar">
|
||||
<div class="avatar-inner" :class="message.role">
|
||||
<Bot v-if="message.role === 'assistant'" :size="20" />
|
||||
<User v-else :size="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 消息内容区域 -->
|
||||
<div class="message-content-wrapper">
|
||||
|
|
@ -44,7 +38,7 @@
|
|||
</div> -->
|
||||
|
||||
<!-- 消息主体 -->
|
||||
<div class="message-body">
|
||||
<div :class="message.role === 'assistant' ? 'message-body assistant' : 'message-body user'">
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="message.isError" class="error-content">
|
||||
<AlertCircle :size="18" />
|
||||
|
|
@ -59,30 +53,17 @@
|
|||
<template v-else>
|
||||
<!-- 文本内容 - 使用 markstream-vue -->
|
||||
<div v-if="message.content.text" class="text-content markstream-vue">
|
||||
<MarkdownRender
|
||||
v-if="message.role !== 'user'"
|
||||
:content="message.content.text"
|
||||
:custom-html-tags="['think']"
|
||||
custom-id="playground-demo"
|
||||
:escape-html-tags="['question', 'answer']"
|
||||
@copy="textCopy"
|
||||
/>
|
||||
<MarkdownRender v-if="message.role !== 'user'" :content="message.content.text" :custom-html-tags="['think']"
|
||||
custom-id="playground-demo" :escape-html-tags="['question', 'answer']" @copy="textCopy" />
|
||||
<div v-else style="white-space: pre-wrap">
|
||||
{{ message.content.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推荐选项 -->
|
||||
<div
|
||||
v-if="message.content.suggestions?.length && isNew"
|
||||
class="suggestions"
|
||||
>
|
||||
<button
|
||||
v-for="suggestion in message.content.suggestions"
|
||||
:key="suggestion.id"
|
||||
class="suggestion-btn"
|
||||
@click="$emit('select-suggestion', suggestion)"
|
||||
>
|
||||
<div v-if="message.content.suggestions?.length && isNew" class="suggestions">
|
||||
<button v-for="suggestion in message.content.suggestions" :key="suggestion.id" class="suggestion-btn"
|
||||
@click="$emit('select-suggestion', suggestion)">
|
||||
<Zap :size="14" />
|
||||
{{ suggestion.text }}
|
||||
</button>
|
||||
|
|
@ -90,12 +71,8 @@
|
|||
|
||||
<!-- 图片展示 -->
|
||||
<div v-if="message.content.images?.length" class="images-grid">
|
||||
<div
|
||||
v-for="(image, index) in message.content.images"
|
||||
:key="image.id"
|
||||
class="image-item"
|
||||
@click="$emit('preview-image', image, index)"
|
||||
>
|
||||
<div v-for="(image, index) in message.content.images" :key="image.id" class="image-item"
|
||||
@click="$emit('preview-image', image, index)">
|
||||
<img :src="image.url" :alt="image.name" loading="lazy" />
|
||||
<div class="image-overlay">
|
||||
<Maximize2 :size="18" />
|
||||
|
|
@ -105,25 +82,14 @@
|
|||
|
||||
<!-- 单个视频 -->
|
||||
<div v-if="message.content.videos?.length === 1" class="single-video">
|
||||
<video
|
||||
:src="message.content.videos[0].url"
|
||||
:poster="message.content.videos[0].poster"
|
||||
controls
|
||||
preload="metadata"
|
||||
/>
|
||||
<video :src="message.content.videos[0].url" :poster="message.content.videos[0].poster" controls
|
||||
preload="metadata" />
|
||||
</div>
|
||||
|
||||
<!-- 多个视频 -->
|
||||
<div
|
||||
v-if="message.content.videos && message.content.videos.length > 1"
|
||||
class="videos-grid"
|
||||
>
|
||||
<div
|
||||
v-for="video in message.content.videos"
|
||||
:key="video.id"
|
||||
class="video-item"
|
||||
@click="$emit('play-video', video)"
|
||||
>
|
||||
<div v-if="message.content.videos && message.content.videos.length > 1" class="videos-grid">
|
||||
<div v-for="video in message.content.videos" :key="video.id" class="video-item"
|
||||
@click="$emit('play-video', video)">
|
||||
<img :src="video.poster" :alt="video.title" />
|
||||
<div class="video-overlay">
|
||||
<Play :size="32" />
|
||||
|
|
@ -136,11 +102,7 @@
|
|||
|
||||
<!-- 附件列表 -->
|
||||
<div v-if="message.content.files?.length" class="files-list">
|
||||
<div
|
||||
v-for="file in message.content.files"
|
||||
:key="file.id"
|
||||
class="file-item"
|
||||
>
|
||||
<div v-for="file in message.content.files" :key="file.id" class="file-item">
|
||||
<div class="file-icon">
|
||||
{{ getFileEmoji(file.mimeType) }}
|
||||
</div>
|
||||
|
|
@ -159,37 +121,23 @@
|
|||
</template>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div
|
||||
v-if="message.isStreaming && !message.content.text"
|
||||
class="loading-dots"
|
||||
>
|
||||
<!-- <div v-if="message.isStreaming && !message.content.text" class="loading-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<MessageActions
|
||||
v-if="
|
||||
message.role === 'assistant' &&
|
||||
!message.isStreaming &&
|
||||
!message.isError &&
|
||||
!readonly &&
|
||||
!isMessageSelectMode
|
||||
"
|
||||
:content="message.content.text || ''"
|
||||
:feedback="message.feedback"
|
||||
:show-regenerate="true"
|
||||
:is-hovered="isHovered"
|
||||
:is-new="isNew"
|
||||
:is-break="message.isBreak"
|
||||
@copy="handleCopy"
|
||||
@like="handleLike"
|
||||
@dislike="handleDislike"
|
||||
@regenerate="$emit('regenerate')"
|
||||
@share="handleShareClick"
|
||||
/>
|
||||
<MessageActions v-if="
|
||||
message.role === 'assistant' &&
|
||||
!message.isStreaming &&
|
||||
!message.isError &&
|
||||
!readonly &&
|
||||
!isMessageSelectMode
|
||||
" :content="message.content.text || ''" :feedback="message.feedback" :show-regenerate="true"
|
||||
:is-hovered="isHovered" :is-new="isNew" :is-break="message.isBreak" @copy="handleCopy" @like="handleLike"
|
||||
@dislike="handleDislike" @regenerate="$emit('regenerate')" @share="handleShareClick" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -310,7 +258,7 @@ setCustomComponents("playground-demo", {
|
|||
.message-bubble {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 20px 10%;
|
||||
padding: 20px 22%;
|
||||
animation: fadeIn 0.3s ease;
|
||||
|
||||
// 消息选择模式
|
||||
|
|
@ -347,9 +295,9 @@ setCustomComponents("playground-demo", {
|
|||
}
|
||||
|
||||
.message-body {
|
||||
background: #f8fafc;
|
||||
color: #1f2937;
|
||||
border-radius: 20px 20px 4px 20px;
|
||||
border-radius: 10px 2px 10px 10px;
|
||||
background: #EEEFF0;
|
||||
|
||||
}
|
||||
|
||||
.text-content {
|
||||
|
|
@ -366,8 +314,10 @@ setCustomComponents("playground-demo", {
|
|||
|
||||
&.role-assistant {
|
||||
.message-body {
|
||||
background: #f8fafc;
|
||||
border-radius: 20px 20px 20px 4px;
|
||||
padding: 15px 20px;
|
||||
gap: 15px;
|
||||
border-radius: 2px 10px 10px 10px;
|
||||
background: var(---F8F9FA, #F8F9FA);
|
||||
|
||||
.dark & {
|
||||
background: #2d2d3d;
|
||||
|
|
@ -454,7 +404,6 @@ setCustomComponents("playground-demo", {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 75%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -491,7 +440,7 @@ setCustomComponents("playground-demo", {
|
|||
|
||||
// markstream-vue 样式覆盖
|
||||
.text-content {
|
||||
:deep(p) {
|
||||
:deep(p) {
|
||||
margin: 0 0 12px;
|
||||
|
||||
&:last-child {
|
||||
|
|
@ -550,6 +499,7 @@ setCustomComponents("playground-demo", {
|
|||
}
|
||||
|
||||
.dark & {
|
||||
|
||||
th,
|
||||
td {
|
||||
border-color: #374151;
|
||||
|
|
@ -589,12 +539,15 @@ setCustomComponents("playground-demo", {
|
|||
:deep(h1) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
:deep(h4) {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
|
@ -851,6 +804,7 @@ setCustomComponents("playground-demo", {
|
|||
&:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
|
@ -862,6 +816,7 @@ setCustomComponents("playground-demo", {
|
|||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
|
@ -869,11 +824,13 @@ setCustomComponents("playground-demo", {
|
|||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.8);
|
||||
|
|
@ -881,12 +838,14 @@ setCustomComponents("playground-demo", {
|
|||
}
|
||||
|
||||
@keyframes pulseDot {
|
||||
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
|
|
|
|||
|
|
@ -37,13 +37,11 @@ async function textCopy(data: any) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="thinking-node p-4 my-4 bg-blue-50 dark:bg-blue-900/40 rounded-md border-l-4 border-blue-400"
|
||||
>
|
||||
<div class="thinking-node ">
|
||||
<!-- 可点击的标题栏 -->
|
||||
<div class="thinking-header" @click="toggleCollapse">
|
||||
<div class="flex-shrink-0">
|
||||
<!-- 思考图标 -->
|
||||
<!-- 思考图标 -->
|
||||
<!-- <div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-blue-200 dark:bg-blue-700 flex items-center justify-center text-blue-700 dark:text-blue-100"
|
||||
>
|
||||
|
|
@ -63,38 +61,24 @@ async function textCopy(data: any) {
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="thinking-title">
|
||||
<strong class="text-sm">💭 深度思考</strong>
|
||||
<!-- TODO: 深度思考样式 -->
|
||||
<span class="text-lg"> 深度思考</span>
|
||||
<!-- 加载动画 -->
|
||||
<span
|
||||
v-if="node.loading"
|
||||
class="thinking-dots visible"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span v-if="node.loading" class="thinking-dots visible" aria-hidden="true">
|
||||
<span class="dot dot-1" />
|
||||
<span class="dot dot-2" />
|
||||
<span class="dot dot-3" />
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="thinking-status text-xs text-slate-500 dark:text-slate-300"
|
||||
>
|
||||
<span v-else class="thinking-status ">
|
||||
已完成
|
||||
</span>
|
||||
</div>
|
||||
<!-- 折叠/展开箭头 -->
|
||||
<div class="collapse-arrow" :class="{ collapsed }">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -102,9 +86,7 @@ async function textCopy(data: any) {
|
|||
|
||||
<!-- 可折叠的内容区域 -->
|
||||
<div class="thinking-content" :class="{ collapsed }">
|
||||
<div
|
||||
class="mt-3 text-sm leading-relaxed text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<div class="mt-3 text-sm leading-relaxed dark:text-slate-100">
|
||||
<MarkdownRender :content="node.content" @copy="textCopy" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -114,7 +96,9 @@ async function textCopy(data: any) {
|
|||
<style scoped>
|
||||
.thinking-node {
|
||||
color: #0f172a;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dark .thinking-node {
|
||||
color: #e6f0ff;
|
||||
}
|
||||
|
|
@ -123,6 +107,8 @@ async function textCopy(data: any) {
|
|||
.thinking-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
|
@ -136,7 +122,13 @@ async function textCopy(data: any) {
|
|||
}
|
||||
|
||||
.thinking-status {
|
||||
font-style: italic;
|
||||
/* font-style: italic; */
|
||||
color: var(--6B-BBBBBB, #BBB);
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
/* 折叠箭头 */
|
||||
|
|
@ -150,18 +142,28 @@ async function textCopy(data: any) {
|
|||
transition: transform 0.25s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.collapse-arrow:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.dark .collapse-arrow:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.collapse-arrow.collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* 可折叠内容 */
|
||||
.thinking-content {
|
||||
color: var(--9-999999, #999);
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
/* 153.846% */
|
||||
max-height: 2000px;
|
||||
overflow: auto;
|
||||
transition:
|
||||
|
|
@ -169,6 +171,7 @@ async function textCopy(data: any) {
|
|||
opacity 0.25s ease;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.thinking-content.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
|
|
@ -181,6 +184,7 @@ async function textCopy(data: any) {
|
|||
gap: 6px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.thinking-dots .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
|
@ -188,30 +192,36 @@ async function textCopy(data: any) {
|
|||
background: #1e3a8a;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.thinking-dots.visible .dot-1 {
|
||||
animation: think-bounce 1s infinite ease-in-out;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.thinking-dots.visible .dot-2 {
|
||||
animation: think-bounce 1s infinite ease-in-out;
|
||||
animation-delay: 0.12s;
|
||||
}
|
||||
|
||||
.thinking-dots.visible .dot-3 {
|
||||
animation: think-bounce 1s infinite ease-in-out;
|
||||
animation-delay: 0.24s;
|
||||
}
|
||||
|
||||
.dark .thinking-dots .dot {
|
||||
background: #bfdbfe;
|
||||
opacity: 0.28;
|
||||
}
|
||||
|
||||
@keyframes think-bounce {
|
||||
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translateY(-6px);
|
||||
opacity: 1;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { NModal, NImage } from 'naive-ui'
|
||||
import PlusIcon from '../icons/custom/PlusIcon.vue'
|
||||
export interface CardItem {
|
||||
|
|
@ -43,6 +43,7 @@ const isWrapperHovered = ref(false)
|
|||
const hoveredCardId = ref<string | number | null>(null)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const previewCard = ref<CardItem | null>(null)
|
||||
const canUpload = computed(() => props.supportsFiles || props.supportsVision)
|
||||
const isPreviewVisible = computed({
|
||||
get: () => !!previewCard.value,
|
||||
set: (value: boolean) => {
|
||||
|
|
@ -235,10 +236,30 @@ onMounted(() => {
|
|||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.cards.length,
|
||||
(length) => {
|
||||
if (length === 0) {
|
||||
isExpanded.value = false
|
||||
hoveredCardId.value = null
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="stacked-cards-container" :class="{ 'is-expanded': isExpanded }" @click="expandCards">
|
||||
<div v-if="cards.length === 0" class="stacked-cards-empty" aria-label="上传附件操作">
|
||||
<button type="button" class="stacked-upload-btn" :class="{ disabled: !canUpload }" :disabled="!canUpload"
|
||||
:title="canUpload ? '上传附件或图片' : '当前模型不支持上传'" @click.stop="emit('add-upload')">
|
||||
<div class="upload-action-div" :class="{ disabled: !canUpload }">
|
||||
<PlusIcon :size="13" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else ref="containerRef" class="stacked-cards-container" :class="{ 'is-expanded': isExpanded }"
|
||||
@click="expandCards">
|
||||
<div class="cards-wrapper" @mouseenter="isWrapperHovered = true" @mouseleave="isWrapperHovered = false">
|
||||
<TransitionGroup name="card-spread">
|
||||
<div v-for="(card, index) in cards" :key="card.id" class="card" :style="cardStyle(index, cards.length)"
|
||||
|
|
@ -277,18 +298,6 @@ onUnmounted(() => {
|
|||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
<Transition name="upload-actions-fade">
|
||||
<div v-if="!isExpanded" class="upload-actions" aria-label="上传附件操作" @click.stop>
|
||||
<button type="button" class="upload-action-btn upload"
|
||||
:class="{ disabled: !(props.supportsFiles || props.supportsVision) }"
|
||||
:disabled="!(props.supportsFiles || props.supportsVision)" title="上传附件或图片" @click.stop="emit('add-upload')">
|
||||
<span class="upload-action-icon">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<NModal v-model:show="isPreviewVisible" preset="card" :mask-closable="true" :close-on-esc="true"
|
||||
:on-close="removePreviewCard" class="image-preview-modal"
|
||||
:content-style="{ width: '100%', height: '100%', padding: '0', overflow: 'hidden' }"
|
||||
|
|
@ -319,6 +328,12 @@ onUnmounted(() => {
|
|||
justify-content: center;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.stacked-cards-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* 卡片大小 */
|
||||
.cards-wrapper {
|
||||
position: relative;
|
||||
|
|
@ -331,7 +346,7 @@ onUnmounted(() => {
|
|||
|
||||
.card {
|
||||
position: absolute;
|
||||
width: 90%;
|
||||
width: 74%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--card-border-color, var(--ffffff, #FFF));
|
||||
|
|
@ -635,50 +650,46 @@ onUnmounted(() => {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
position: absolute;
|
||||
right: -2rem;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.upload-action-btn {
|
||||
.stacked-upload-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
padding: 0 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255);
|
||||
color: #e4e4e7;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease, border-color 0.2s ease;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.upload-action-btn:hover:not(:disabled) {
|
||||
.stacked-upload-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.upload-action-btn.file {
|
||||
border-color: rgba(6, 182, 212, 0.22);
|
||||
}
|
||||
|
||||
.upload-action-btn.image {
|
||||
border-color: rgba(139, 92, 246, 0.22);
|
||||
}
|
||||
|
||||
.upload-action-btn.disabled,
|
||||
.upload-action-btn:disabled {
|
||||
.stacked-upload-btn.disabled,
|
||||
.stacked-upload-btn:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.upload-action-icon {
|
||||
font-size: 8px;
|
||||
line-height: 1;
|
||||
.upload-action-div {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 0;
|
||||
width: 50px;
|
||||
height: 70px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.upload-action-div.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
background: #f3f4f6;
|
||||
border: 1px dashed #cbd5e1;
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.upload-action-div.disabled :deep(svg) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.upload-action-text {
|
||||
|
|
@ -774,9 +785,14 @@ onUnmounted(() => {
|
|||
height: 36px;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
bottom: 0.75rem;
|
||||
gap: 0.25rem;
|
||||
.upload-action-div {
|
||||
width: 75px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.stacked-cards-empty {
|
||||
width: 75px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.upload-action-btn {
|
||||
|
|
|
|||
Loading…
Reference in New Issue