feat(ui): 统一图片预览为 Naive UI 并修复工具栏交互

This commit is contained in:
肖应宇 2026-04-09 18:01:29 +08:00
parent 32aa5d0f69
commit 3bab2b008f
2 changed files with 43 additions and 111 deletions

View File

@ -757,7 +757,7 @@ display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.03);
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
.dark & {

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { NModal, NImage, NTooltip } from 'naive-ui'
import { NImage, NTooltip } from 'naive-ui'
import PlusIcon from '../icons/custom/PlusIcon.vue'
export interface CardItem {
id: string | number
@ -42,16 +42,7 @@ const isExpanded = ref(false)
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) => {
if (!value) {
closePreview()
}
},
})
const CARD_SCALE = 0.5
// -
@ -138,21 +129,6 @@ function getCardImageUrl(card: CardItem) {
return card.thumbnail || ''
}
function openPreview(card: CardItem) {
if (card.type !== 'image') return
const imageUrl = getCardImageUrl(card)
if (!imageUrl) return
previewCard.value = card
}
function closePreview() {
previewCard.value = null
}
function removePreviewCard() {
closePreview()
}
function removeCard(card: CardItem) {
emit('remove', card.id)
}
@ -170,10 +146,6 @@ function handleCardClick(card: CardItem) {
expandCards()
return
}
if (card.type === 'image') {
openPreview(card)
}
}
const cardStyle = computed(() => (index: number, total: number) => {
@ -214,9 +186,21 @@ const cardStyle = computed(() => (index: number, total: number) => {
const handleDocumentClick = (event: MouseEvent) => {
if (!containerRef.value) return
const target = event.target as HTMLElement | null
// Naive UI body
if (
target?.closest(
'.n-image-preview-container, .n-image-preview-toolbar, .n-image-preview-overlay, .n-image-preview-close',
)
) {
return
}
if (!target) return
//
if (!containerRef.value.contains(event.target as Node)) {
if (!containerRef.value.contains(target)) {
if (isExpanded.value) {
isExpanded.value = false
hoveredCardId.value = null
@ -273,7 +257,13 @@ watch(
@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)" />
<NImage
:src="getCardImageUrl(card)"
:alt="getCardTitle(card, index)"
object-fit="cover"
:preview-disabled="!isExpanded || card.uploading || card.deleting"
:img-props="{ loading: 'lazy' }"
/>
<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>
@ -285,7 +275,13 @@ watch(
</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)" />
<NImage
:src="getCardImageUrl(card)"
:alt="getCardTitle(card, index)"
object-fit="cover"
:preview-disabled="!isExpanded || card.uploading || card.deleting"
:img-props="{ loading: 'lazy' }"
/>
</div>
<div class="card-preview-fallback">
<span class="card-icon">
@ -341,23 +337,6 @@ watch(
<PlusIcon :size="12" />
</button>
<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' }"
:style="{ width: '100vw', height: '100vh', maxWidth: '100vw', margin: '0', padding: '0', borderRadius: '0', background: 'rgba(5, 7, 12, 0.96)' }">
<div class="image-preview-shell">
<NImage v-if="previewCard" :src="getCardImageUrl(previewCard)" :alt="getCardTitle(previewCard, 0)"
object-fit="contain" preview-disabled :img-props="{
style: {
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
},
}" class="image-preview-content" />
</div>
</NModal>
</div>
</template>
@ -420,11 +399,11 @@ watch(
position: absolute;
width: 74%;
height: 100%;
border-radius: 5px;
border-radius: 10px;
border: 1px solid var(--card-border-color, var(--ffffff, #FFF));
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
cursor: pointer;
overflow: hidden;
/* overflow: hidden; */
transition: transform 0.35s ease-out, opacity 0.35s ease-out;
will-change: transform, opacity;
}
@ -439,6 +418,8 @@ watch(
.card-media {
cursor: default;
border-radius: 10px;
overflow: hidden;
}
.card-media-uploading {
@ -529,8 +510,8 @@ watch(
.card-delete-btn {
position: absolute;
top: 0.3rem;
right: 0.3rem;
top: -0.3rem;
right: -0.3rem;
z-index: 4;
display: inline-flex;
align-items: center;
@ -556,8 +537,14 @@ watch(
display: block;
}
.card-media img,
.card-preview-image img {
.card-media :deep(.n-image),
.card-preview-image :deep(.n-image) {
width: 100%;
height: 100%;
}
.card-media :deep(.n-image img),
.card-preview-image :deep(.n-image img) {
width: 100%;
height: 100%;
object-fit: cover;
@ -816,61 +803,6 @@ watch(
transform: translateX(-50%) translateY(6px);
}
.image-preview-modal :deep(.n-card) {
background: transparent;
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.image-preview-modal :deep(.n-card__content) {
flex: 1;
min-width: 0;
min-height: 0;
width: 100%;
height: 100%;
padding: 0;
overflow: hidden;
display: flex;
}
.image-preview-shell {
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image-preview-content {
flex: 1;
width: 100%;
height: 100%;
max-width: calc(100vw - 48px);
max-height: calc(100vh - 48px);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
z-index: 1;
}
.image-preview-content :deep(img) {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
}
/* 响应式 */
@media (max-width: 640px) {
.stacked-cards-container {