feat(upload-ui): 优化附件卡片交互与上传图标展示
This commit is contained in:
parent
88e43c1763
commit
135f3ae5c8
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 13C10.3137 13 13 10.3137 13 7C13 3.68629 10.3137 1 7 1C3.68629 1 1 3.68629 1 7"
|
||||||
|
stroke="url(#paint0_linear_169_1705)"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_169_1705"
|
||||||
|
x1="1"
|
||||||
|
y1="7"
|
||||||
|
x2="7"
|
||||||
|
y2="13"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="white" />
|
||||||
|
<stop offset="1" stop-color="white" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 14,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11" fill="none">
|
||||||
<path d="M0.5 6.49457H12.5" stroke="#999999" stroke-linecap="round" />
|
<path d="M0.5 5.49469H10.5" stroke="#999999" stroke-linecap="round" />
|
||||||
<path d="M6.49414 12.5L6.49414 0.5" stroke="#999999" stroke-linecap="round" />
|
<path d="M5.49512 10.5L5.49512 0.5" stroke="#999999" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<rect width="12" height="12" rx="2" fill="white" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
@ -8,7 +8,14 @@
|
||||||
@remove="removeAttachment" @add-upload="triggerUploadInput" />
|
@remove="removeAttachment" @add-upload="triggerUploadInput" />
|
||||||
|
|
||||||
<!-- 隐藏的文件输入框 -->
|
<!-- 隐藏的文件输入框 -->
|
||||||
<input ref="uploadInputRef" type="file" multiple hidden @change="handleUploadSelect" />
|
<input
|
||||||
|
ref="uploadInputRef"
|
||||||
|
type="file"
|
||||||
|
:accept="uploadAccept"
|
||||||
|
multiple
|
||||||
|
hidden
|
||||||
|
@change="handleUploadSelect"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文本输入框 -->
|
<!-- 文本输入框 -->
|
||||||
|
|
@ -59,11 +66,11 @@
|
||||||
<div class="input-actions right">
|
<div class="input-actions right">
|
||||||
<!-- 发送/停止按钮 -->
|
<!-- 发送/停止按钮 -->
|
||||||
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
|
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
|
||||||
<StopCircle :size="20" />
|
<StopIcon />
|
||||||
</button>
|
</button>
|
||||||
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
|
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
|
||||||
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
|
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
|
||||||
<Loader2 v-if="isProcessingAttachments" :size="20" class="animate-spin" />
|
<LoadingIcon v-if="isProcessingAttachments" class="animate-spin" />
|
||||||
<SendIcon v-else :size="20" />
|
<SendIcon v-else :size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -74,12 +81,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick, onMounted } from "vue";
|
import { ref, computed, watch, nextTick, onMounted } from "vue";
|
||||||
import {
|
import {
|
||||||
StopCircle,
|
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Globe,
|
Globe,
|
||||||
Brain,
|
Brain,
|
||||||
Loader2,
|
|
||||||
Upload,
|
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
import { generateId } from "@/utils/helpers";
|
import { generateId } from "@/utils/helpers";
|
||||||
import type { Attachment } from "@/types/chat";
|
import type { Attachment } from "@/types/chat";
|
||||||
|
|
@ -88,6 +92,8 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
import { useSettingsStore } from "@/stores/settings";
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
import StackedCards from "@/components/ui/StackedCards.vue";
|
import StackedCards from "@/components/ui/StackedCards.vue";
|
||||||
import SendIcon from "../icons/custom/SendIcon.vue";
|
import SendIcon from "../icons/custom/SendIcon.vue";
|
||||||
|
import StopIcon from "../icons/custom/StopIcon.vue";
|
||||||
|
import LoadingIcon from "../icons/custom/LoadingIcon.vue";
|
||||||
|
|
||||||
interface AttachmentWithProgress extends Attachment {
|
interface AttachmentWithProgress extends Attachment {
|
||||||
uploading?: boolean;
|
uploading?: boolean;
|
||||||
|
|
@ -179,6 +185,40 @@ const isUploading = computed(() => attachments.value.some((a) => a.uploading));
|
||||||
const isProcessingAttachments = computed(() =>
|
const isProcessingAttachments = computed(() =>
|
||||||
attachments.value.some((a) => a.uploading || a.deleting),
|
attachments.value.some((a) => a.uploading || a.deleting),
|
||||||
);
|
);
|
||||||
|
const textFileAccept =
|
||||||
|
".txt,.md,.markdown,.pdf,.doc,.docx,.rtf,.csv,.tsv,.json,.xml,.html,.htm,.yaml,.yml,.log,.ini,.conf,.sql,.js,.ts,.jsx,.tsx,.py,.java,.c,.cpp,.h,.hpp,.go,.rs,.sh";
|
||||||
|
const uploadAccept = computed(() => {
|
||||||
|
if (props.supports_vision && props.supports_files) {
|
||||||
|
return `image/*,${textFileAccept}`;
|
||||||
|
}
|
||||||
|
if (props.supports_vision) {
|
||||||
|
return "image/*";
|
||||||
|
}
|
||||||
|
if (props.supports_files) {
|
||||||
|
return textFileAccept;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function getFileExt(fileName: string) {
|
||||||
|
const idx = fileName.lastIndexOf(".");
|
||||||
|
if (idx === -1) return "";
|
||||||
|
return fileName.slice(idx).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUploadTypeByModel(file: File): "image" | "file" | null {
|
||||||
|
const isImage =
|
||||||
|
file.type.startsWith("image/") ||
|
||||||
|
[".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".heic"].includes(
|
||||||
|
getFileExt(file.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
return props.supports_vision ? "image" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.supports_files ? "file" : null;
|
||||||
|
}
|
||||||
const canSend = computed(() => {
|
const canSend = computed(() => {
|
||||||
return (
|
return (
|
||||||
(inputText.value.trim().length > 0 || attachments.value.length > 0) &&
|
(inputText.value.trim().length > 0 || attachments.value.length > 0) &&
|
||||||
|
|
@ -283,16 +323,26 @@ async function handlePaste(event: ClipboardEvent) {
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.type.startsWith("image/")) {
|
if (item.type.startsWith("image/")) {
|
||||||
event.preventDefault();
|
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
await addFileAsAttachment(file, "image");
|
const uploadType = getUploadTypeByModel(file);
|
||||||
|
if (uploadType === "image") {
|
||||||
|
event.preventDefault();
|
||||||
|
await addFileAsAttachment(file, uploadType);
|
||||||
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
|
showThrottledToast("当前模型不支持上传图片");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerUploadInput() {
|
function triggerUploadInput() {
|
||||||
|
if (!props.supports_vision && !props.supports_files) {
|
||||||
|
showThrottledToast("当前模型不支持上传附件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
uploadInputRef.value?.click();
|
uploadInputRef.value?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,8 +353,16 @@ async function handleUploadSelect(event: Event) {
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const type = file.type.startsWith("image/") ? "image" : "file";
|
const uploadType = getUploadTypeByModel(file);
|
||||||
await addFileAsAttachment(file, type);
|
if (!uploadType) {
|
||||||
|
showThrottledToast(
|
||||||
|
file.type.startsWith("image/")
|
||||||
|
? "当前模型不支持上传图片"
|
||||||
|
: "当前模型不支持上传附件文件",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await addFileAsAttachment(file, uploadType);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.value = "";
|
input.value = "";
|
||||||
|
|
@ -568,7 +626,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.send {
|
&.send {
|
||||||
background: #e5e7eb;
|
background: rgba(0, 15, 51, 0.20);
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
|
|
@ -586,21 +644,21 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
background: #000F33;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
background: rgba(0, 15, 51, 0.20);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stop {
|
&.stop {
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
background: #000F33;
|
||||||
color: white;
|
color: white;
|
||||||
animation: pulse 2s infinite;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
|
|
@ -703,15 +761,4 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { NModal, NImage } from 'naive-ui'
|
import { NModal, NImage, NTooltip } from 'naive-ui'
|
||||||
import PlusIcon from '../icons/custom/PlusIcon.vue'
|
import PlusIcon from '../icons/custom/PlusIcon.vue'
|
||||||
export interface CardItem {
|
export interface CardItem {
|
||||||
id: string | number
|
id: string | number
|
||||||
|
|
@ -262,42 +262,85 @@ watch(
|
||||||
@click="expandCards">
|
@click="expandCards">
|
||||||
<div class="cards-wrapper" @mouseenter="isWrapperHovered = true" @mouseleave="isWrapperHovered = false">
|
<div class="cards-wrapper" @mouseenter="isWrapperHovered = true" @mouseleave="isWrapperHovered = false">
|
||||||
<TransitionGroup name="card-spread">
|
<TransitionGroup name="card-spread">
|
||||||
<div v-for="(card, index) in cards" :key="card.id" class="card" :style="cardStyle(index, cards.length)"
|
<NTooltip
|
||||||
@mouseenter="hoveredCardId = card.id" @mouseleave="hoveredCardId = null" @click="handleCardClick(card)">
|
v-for="(card, index) in cards"
|
||||||
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
|
:key="card.id"
|
||||||
<div v-if="card.type === 'image' && getCardImageUrl(card)" class="card-media">
|
trigger="hover"
|
||||||
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
|
placement="top"
|
||||||
<div v-if="card.uploading" class="card-media-uploading">
|
>
|
||||||
<div class="card-media-uploading-spinner" aria-hidden="true" />
|
<template #trigger>
|
||||||
<span class="card-media-uploading-text">上传中</span>
|
<div class="card" :style="cardStyle(index, cards.length)"
|
||||||
|
@mouseenter="hoveredCardId = card.id" @mouseleave="hoveredCardId = null" @click="handleCardClick(card)">
|
||||||
|
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
|
||||||
|
<div v-if="card.type === 'image' && getCardImageUrl(card)" class="card-media">
|
||||||
|
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
|
||||||
|
<div v-if="card.uploading" class="card-media-uploading">
|
||||||
|
<div class="card-media-uploading-spinner" aria-hidden="true" />
|
||||||
|
<span class="card-media-uploading-text">上传中</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="card.deleting" class="card-media-deleting">
|
||||||
|
<div class="card-media-deleting-spinner" aria-hidden="true" />
|
||||||
|
<span class="card-media-deleting-text">删除中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="card.type === 'image'" class="card-preview">
|
||||||
|
<div v-if="getCardImageUrl(card)" class="card-preview-image">
|
||||||
|
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
|
||||||
|
</div>
|
||||||
|
<div class="card-preview-fallback">
|
||||||
|
<span class="card-icon">
|
||||||
|
{{ getCardIcon(card) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="card.deleting" class="card-preview-deleting">
|
||||||
|
<div class="card-preview-deleting-spinner" aria-hidden="true" />
|
||||||
|
<span class="card-preview-deleting-text">删除中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="card-file-preview">
|
||||||
|
<div class="card-file-icon" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<rect x="2.5" y="1.5" width="11" height="13" rx="1.5" stroke="#666666" />
|
||||||
|
<path d="M5 5H11" stroke="#666666" stroke-linecap="round" />
|
||||||
|
<path d="M5 8H11" stroke="#666666" stroke-linecap="round" />
|
||||||
|
<path d="M5 11H11" stroke="#666666" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="card-file-name" :title="getCardTitle(card, index)">
|
||||||
|
{{ getCardTitle(card, index) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="card.deleting" class="card-preview-deleting">
|
||||||
|
<div class="card-preview-deleting-spinner" aria-hidden="true" />
|
||||||
|
<span class="card-preview-deleting-text">删除中</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button v-if="!card.deleting && isWrapperHovered && (hoveredCardId === card.id || (!hoveredCardId && index === 0))"
|
||||||
|
type="button" class="card-delete-btn" title="删除 OSS 文件" @click.stop="removeCard(card)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<rect width="16.0008" height="16" rx="8" fill="#999999" />
|
||||||
|
<path d="M5.49512 5.33374L10.6692 10.6624" stroke="white" stroke-linecap="round" />
|
||||||
|
<path d="M10.5068 5.33813L5.33271 10.6668" stroke="white" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="card.deleting" class="card-media-deleting">
|
</template>
|
||||||
<div class="card-media-deleting-spinner" aria-hidden="true" />
|
{{ getCardTitle(card, index) }}
|
||||||
<span class="card-media-deleting-text">删除中</span>
|
</NTooltip>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="card-preview">
|
|
||||||
<div v-if="getCardImageUrl(card)" class="card-preview-image">
|
|
||||||
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
|
|
||||||
</div>
|
|
||||||
<div class="card-preview-fallback">
|
|
||||||
<span class="card-icon">
|
|
||||||
{{ getCardIcon(card) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="card.deleting" class="card-preview-deleting">
|
|
||||||
<div class="card-preview-deleting-spinner" aria-hidden="true" />
|
|
||||||
<span class="card-preview-deleting-text">删除中</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button v-if="!card.deleting && isWrapperHovered && (hoveredCardId === card.id || (!hoveredCardId && index === 0))"
|
|
||||||
type="button" class="card-delete-btn" title="删除 OSS 文件" @click.stop="removeCard(card)">
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="!isExpanded"
|
||||||
|
type="button"
|
||||||
|
class="cards-upload-fab"
|
||||||
|
:class="{ disabled: !canUpload }"
|
||||||
|
:disabled="!canUpload"
|
||||||
|
:title="canUpload ? '上传附件或图片' : '当前模型不支持上传'"
|
||||||
|
@click.stop="emit('add-upload')"
|
||||||
|
>
|
||||||
|
<PlusIcon :size="12" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<NModal v-model:show="isPreviewVisible" preset="card" :mask-closable="true" :close-on-esc="true"
|
<NModal v-model:show="isPreviewVisible" preset="card" :mask-closable="true" :close-on-esc="true"
|
||||||
:on-close="removePreviewCard" class="image-preview-modal"
|
:on-close="removePreviewCard" class="image-preview-modal"
|
||||||
:content-style="{ width: '100%', height: '100%', padding: '0', overflow: 'hidden' }"
|
:content-style="{ width: '100%', height: '100%', padding: '0', overflow: 'hidden' }"
|
||||||
|
|
@ -329,6 +372,35 @@ watch(
|
||||||
perspective: 1000px;
|
perspective: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cards-upload-fab {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
z-index: 999;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #999999;
|
||||||
|
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.16);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-upload-fab:hover:not(:disabled) {
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards-upload-fab.disabled,
|
||||||
|
.cards-upload-fab:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.stacked-cards-empty {
|
.stacked-cards-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -358,7 +430,8 @@ watch(
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-media,
|
.card-media,
|
||||||
.card-preview {
|
.card-preview,
|
||||||
|
.card-file-preview {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
@ -456,29 +529,31 @@ watch(
|
||||||
|
|
||||||
.card-delete-btn {
|
.card-delete-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.35rem;
|
top: 0.3rem;
|
||||||
right: 0.35rem;
|
right: 0.3rem;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 0.95rem;
|
width: 16.001px;
|
||||||
height: 0.95rem;
|
height: 16px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 50%;
|
||||||
background: rgba(239, 68, 68, 0.9);
|
background: transparent;
|
||||||
color: #fff;
|
|
||||||
font-size: 0.55rem;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
backdrop-filter: blur(10px);
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-delete-btn:hover {
|
.card-delete-btn:hover {
|
||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
background: rgba(220, 38, 38, 0.98);
|
}
|
||||||
|
|
||||||
|
.card-delete-btn svg {
|
||||||
|
width: 16.001px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-media img,
|
.card-media img,
|
||||||
|
|
@ -555,6 +630,38 @@ watch(
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-file-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 6px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-file-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-file-name {
|
||||||
|
width: 100%;
|
||||||
|
color: var(--6-666666, #666);
|
||||||
|
font-family: "Microsoft YaHei";
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.card:hover .card-glow {
|
.card:hover .card-glow {
|
||||||
opacity: 0.15;
|
opacity: 0.15;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue