|
@@ -0,0 +1,509 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="chat-content">
|
|
|
|
|
+ <div v-if="messages.length > 0" ref="scrollContainerRef" class="item-list">
|
|
|
|
|
+ <Bubble v-for="item in messages" :key="item.id || item.key" :content="item.content" :placement="item.placement"
|
|
|
|
|
+ :loading="item.loading" :shape="item.shape" :variant="item.variant" :is-markdown="item.isMarkdown"
|
|
|
|
|
+ :is-fog="item.isFog" :typing="item.typing" class="message-bubble" maxWidth="800px">
|
|
|
|
|
+ <template #avatar>
|
|
|
|
|
+ <div class="avatar-wrapper">
|
|
|
|
|
+ <Icon v-if="item.role === 'ai'" icon="mingcute:ai-line" :width="22" />
|
|
|
|
|
+ <Icon v-if="item.role === 'user'" icon="ri:user-line" :width="22" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="header-wrapper">
|
|
|
|
|
+ <div class="header-name">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #content>
|
|
|
|
|
+ <div class="msg-content-wrapper">
|
|
|
|
|
+ <div v-if="item.role === 'ai' && hasStructuredBlocks(item)" class="structured-blocks">
|
|
|
|
|
+ <Thinking v-if="shouldShowThinking(item)" :content="getThinkingText(item)"
|
|
|
|
|
+ :status="getThinkingStatus(item)" :model-value="!item.streamCompleted" auto-collapse max-width="100%"
|
|
|
|
|
+ background-color="var(--bg-container)" color="var(--text-primary)" />
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="getToolCalls(item).length" class="structured-block">
|
|
|
|
|
+ <div class="structured-block__head">
|
|
|
|
|
+ <span class="structured-block__title">{{ t('pages.chat.toolCallTitle') }}</span>
|
|
|
|
|
+ <el-tag size="small" type="warning" effect="plain">{{ getToolCalls(item).length }}</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tool-list">
|
|
|
|
|
+ <div v-for="(toolCall, index) in getToolCalls(item)"
|
|
|
|
|
+ :key="`${item.id || item.key}-tool-call-${index}`" class="tool-card">
|
|
|
|
|
+ <div class="tool-card__head">
|
|
|
|
|
+ <div class="tool-card__meta">
|
|
|
|
|
+ <el-tag size="small" type="warning" effect="dark">
|
|
|
|
|
+ {{ getToolName(toolCall.toolName) }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ <span v-if="toolCall.toolCallId" class="tool-card__id">{{ toolCall.toolCallId }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <pre
|
|
|
|
|
+ class="tool-card__content">{{ formatStructuredValue(toolCall.arguments || toolCall.content) }}</pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="getToolResults(item).length" class="structured-block">
|
|
|
|
|
+ <div class="structured-block__head">
|
|
|
|
|
+ <span class="structured-block__title">{{ t('pages.chat.toolResultTitle') }}</span>
|
|
|
|
|
+ <el-tag size="small" type="success" effect="plain">{{ getToolResults(item).length }}</el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tool-list">
|
|
|
|
|
+ <div v-for="(toolResult, index) in getToolResults(item)"
|
|
|
|
|
+ :key="`${item.id || item.key}-tool-result-${index}`" class="tool-card">
|
|
|
|
|
+ <div class="tool-card__head">
|
|
|
|
|
+ <div class="tool-card__meta">
|
|
|
|
|
+ <el-tag :type="toolResult.success === false ? 'danger' : 'success'" size="small" effect="dark">
|
|
|
|
|
+ {{ toolResult.success === false ? t('pages.chat.failedTag') : 'OK' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ <span class="tool-card__name">{{ getToolName(toolResult.toolName) }}</span>
|
|
|
|
|
+ <span v-if="toolResult.toolCallId" class="tool-card__id">{{ toolResult.toolCallId }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="toolResult.durationMs !== undefined" class="tool-card__duration">
|
|
|
|
|
+ {{ toolResult.durationMs }}ms
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="hasToolResultThought(toolResult)" class="tool-card__section">
|
|
|
|
|
+ <div class="tool-card__label">{{ t('pages.chat.thinkTitle') }}</div>
|
|
|
|
|
+ <pre class="tool-card__content">{{ getToolResultThought(toolResult) }}</pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="hasToolResultOutput(toolResult)" class="tool-card__section">
|
|
|
|
|
+ <div class="tool-card__label">{{ t('pages.chat.toolResultTitle') }}</div>
|
|
|
|
|
+ <pre class="tool-card__content">{{ getToolResultOutput(toolResult) }}</pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="hasToolResultItems(toolResult)" class="tool-card__section">
|
|
|
|
|
+ <div class="tool-card__label">content_items</div>
|
|
|
|
|
+ <pre class="tool-card__content">{{ formatStructuredValue(getToolResultItems(toolResult)) }}</pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="toolResult.error" class="tool-card__section">
|
|
|
|
|
+ <div class="tool-card__label">{{ t('pages.chat.failedTag') }}</div>
|
|
|
|
|
+ <pre class="tool-card__content tool-card__content--error">{{ toolResult.error }}</pre>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <XMarkdown v-if="item.content" class="msg-content-text" :markdown="item.content" enableLatex enableBreaks />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div v-if="getMessageImages(item).length" class="file-wrap">
|
|
|
|
|
+ <el-image v-for="(src, index) in getMessageImages(item)" :key="`${item.id || item.key}-file-${index}`"
|
|
|
|
|
+ :src="src" class="file-image" fit="cover" preview-teleported hide-on-click-modal
|
|
|
|
|
+ :preview-src-list="getMessageImages(item)" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="footer-wrapper" v-if="item.role === 'ai' && item.streamCompleted">
|
|
|
|
|
+ <div class="footer-container">
|
|
|
|
|
+ <el-button type="info" :icon="Refresh" size="small" circle @click="handleRetry(item)" />
|
|
|
|
|
+ <el-button color="#626aef" :icon="DocumentCopy" size="small" circle @click="handleCopy(item)" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="footer-time">
|
|
|
|
|
+ {{ item.updateTime }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Bubble>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-else class="empty-state">
|
|
|
|
|
+ <div class="empty-icon">
|
|
|
|
|
+ <SvgIcon name="sparkle" size="60" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="empty-text">{{ t('pages.chat.emptyTitle') }}</div>
|
|
|
|
|
+ <div class="empty-subtext">{{ t('pages.chat.emptySubtitle') }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { nextTick, ref } from 'vue'
|
|
|
|
|
+import { Bubble, Thinking, XMarkdown } from 'vue-element-plus-x'
|
|
|
|
|
+import { useI18n } from '@/composables/useI18n'
|
|
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
+import { DocumentCopy, Refresh } from '@element-plus/icons-vue'
|
|
|
|
|
+import { Icon } from "@repo/ui"
|
|
|
|
|
+import type { BubbleMessage, ChatToolResult } from '../../views/chat/types'
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n()
|
|
|
|
|
+
|
|
|
|
|
+defineProps<{
|
|
|
|
|
+ messages: BubbleMessage[]
|
|
|
|
|
+ loading: boolean
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits<{
|
|
|
|
|
+ retry: [message: BubbleMessage]
|
|
|
|
|
+}>()
|
|
|
|
|
+
|
|
|
|
|
+const getMessageImages = (message: BubbleMessage) => {
|
|
|
|
|
+ const files = message.message_files
|
|
|
|
|
+ const ids = Array.isArray(files) ? files : files ? [files] : []
|
|
|
|
|
+ return ids.filter(Boolean).map((fileId) => `/File/GetImage?fileId=${fileId}`)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getThinkingText = (message: BubbleMessage) => `${message.thinking || ''}`.trim()
|
|
|
|
|
+
|
|
|
|
|
+const shouldShowThinking = (message: BubbleMessage) => !!getThinkingText(message)
|
|
|
|
|
+
|
|
|
|
|
+const getThinkingStatus = (message: BubbleMessage) => {
|
|
|
|
|
+ if (message.error) return 'error'
|
|
|
|
|
+ return message.streamCompleted ? 'end' : 'thinking'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getToolCalls = (message: BubbleMessage) => message.toolCalls || []
|
|
|
|
|
+
|
|
|
|
|
+const getToolResults = (message: BubbleMessage) => message.toolResults || []
|
|
|
|
|
+
|
|
|
|
|
+const getToolName = (name?: string) => `${name || 'tool'}`
|
|
|
|
|
+
|
|
|
|
|
+const getToolResultThought = (toolResult: ChatToolResult) => `${toolResult.thought || ''}`.trim()
|
|
|
|
|
+
|
|
|
|
|
+const hasToolResultThought = (toolResult: ChatToolResult) => !!getToolResultThought(toolResult)
|
|
|
|
|
+
|
|
|
|
|
+const getToolResultOutput = (toolResult: ChatToolResult) => formatStructuredValue(toolResult.output)
|
|
|
|
|
+
|
|
|
|
|
+const hasToolResultOutput = (toolResult: ChatToolResult) => {
|
|
|
|
|
+ const output = toolResult.output
|
|
|
|
|
+ return output !== null && output !== undefined && `${output}`.trim() !== ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getToolResultItems = (toolResult: ChatToolResult) => Array.isArray(toolResult.contentItems) ? toolResult.contentItems : []
|
|
|
|
|
+
|
|
|
|
|
+const hasToolResultItems = (toolResult: ChatToolResult) => getToolResultItems(toolResult).length > 0
|
|
|
|
|
+
|
|
|
|
|
+const hasStructuredBlocks = (message: BubbleMessage) =>
|
|
|
|
|
+ !!(
|
|
|
|
|
+ getThinkingText(message) ||
|
|
|
|
|
+ getToolCalls(message).length ||
|
|
|
|
|
+ getToolResults(message).length
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+const formatStructuredValue = (value: any) => {
|
|
|
|
|
+ if (value === null || value === undefined || value === '') return '-'
|
|
|
|
|
+ if (typeof value === 'string') return value
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.stringify(value, null, 2)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return `${value}`
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+const scrollContainerRef = ref<HTMLDivElement>()
|
|
|
|
|
+
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ scrollToBottom: () => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ const container = scrollContainerRef.value
|
|
|
|
|
+ if (!container) return
|
|
|
|
|
+ container.scrollTop = container.scrollHeight
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const handleRetry = (message: BubbleMessage) => {
|
|
|
|
|
+ emit('retry', message)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleCopy = (message: BubbleMessage) => {
|
|
|
|
|
+ const text = `${message.rawText || message.answerText || message.content || ''}`
|
|
|
|
|
+ window.navigator.clipboard.writeText(text)
|
|
|
|
|
+ ElMessage.success(t('pages.chat.copySuccess'))
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="less" scoped>
|
|
|
|
|
+.chat-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ padding: 18px;
|
|
|
|
|
+
|
|
|
|
|
+ .item-list {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding-right: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .message-bubble+.message-bubble {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .empty-state {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: var(--text-tertiary);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.avatar-wrapper {
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.header-wrapper {
|
|
|
|
|
+ .header-name {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #979797;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.msg-content-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+ color: var(--text-primary);
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text :deep(h3) {
|
|
|
|
|
+ margin: 0 0 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: var(--text-secondary);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text :deep(p) {
|
|
|
|
|
+ margin: 0 0 10px;
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text :deep(blockquote) {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ border-left: 4px solid var(--el-color-primary);
|
|
|
|
|
+ background: var(--bg-container);
|
|
|
|
|
+ border-radius: 0 8px 8px 0;
|
|
|
|
|
+ color: var(--text-secondary);
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text :deep(pre) {
|
|
|
|
|
+ margin: 8px 0;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: var(--bg-container);
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text :deep(code) {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text :deep(ul),
|
|
|
|
|
+ .msg-content-text :deep(ol) {
|
|
|
|
|
+ margin: 0 0 10px 20px;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .msg-content-text :deep(li) {
|
|
|
|
|
+ margin: 4px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.structured-blocks {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.structured-block {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border: 1px solid var(--border-light);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: var(--bg-container);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.structured-block__head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.structured-block__title {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--text-secondary);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.structured-block__body {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+ color: var(--text-primary);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.structured-block__body--thinking {
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.structured-block__markdown :deep(p) {
|
|
|
|
|
+ margin: 0 0 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card {
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border: 1px solid var(--border-light);
|
|
|
|
|
+ background: var(--bg-page);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card__head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card__meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card__name,
|
|
|
|
|
+.tool-card__id,
|
|
|
|
|
+.tool-card__duration {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--text-secondary);
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card__section+.tool-card__section {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card__label {
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--text-secondary);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card__content {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: var(--bg-container);
|
|
|
|
|
+ color: var(--text-primary);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tool-card__content--error {
|
|
|
|
|
+ color: var(--el-color-danger);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.footer-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ .footer-time {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ margin-top: 3px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-wrap {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.file-image {
|
|
|
|
|
+ width: 96px;
|
|
|
|
|
+ height: 96px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ border: 1px solid var(--border-light);
|
|
|
|
|
+ cursor: zoom-in;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.footer-container {
|
|
|
|
|
+ :deep(.el-button + .el-button) {
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-container {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ background: linear-gradient(to right, #fdfcfb 0%, #ffd1ab 100%);
|
|
|
|
|
+ border-radius: 15px;
|
|
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-container span {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ margin-left: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes bounce {
|
|
|
|
|
+
|
|
|
|
|
+ 0%,
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: translateY(5px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 50% {
|
|
|
|
|
+ transform: translateY(-5px);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-container span:nth-child(4n) {
|
|
|
|
|
+ animation: bounce 1.2s ease infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-container span:nth-child(4n + 1) {
|
|
|
|
|
+ animation: bounce 1.2s ease infinite;
|
|
|
|
|
+ animation-delay: 0.3s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-container span:nth-child(4n + 2) {
|
|
|
|
|
+ animation: bounce 1.2s ease infinite;
|
|
|
|
|
+ animation-delay: 0.6s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-container span:nth-child(4n + 3) {
|
|
|
|
|
+ animation: bounce 1.2s ease infinite;
|
|
|
|
|
+ animation-delay: 0.9s;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|