feat(ui): 统一图片预览为 Naive UI 并修复工具栏交互
This commit is contained in:
parent
32aa5d0f69
commit
3bab2b008f
|
|
@ -757,7 +757,7 @@ display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
|
|
|
||||||
|
|
@ -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, NTooltip } from 'naive-ui'
|
import { 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
|
||||||
|
|
@ -42,16 +42,7 @@ const isExpanded = ref(false)
|
||||||
const isWrapperHovered = ref(false)
|
const isWrapperHovered = ref(false)
|
||||||
const hoveredCardId = ref<string | number | null>(null)
|
const hoveredCardId = ref<string | number | null>(null)
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
const previewCard = ref<CardItem | null>(null)
|
|
||||||
const canUpload = computed(() => props.supportsFiles || props.supportsVision)
|
const canUpload = computed(() => props.supportsFiles || props.supportsVision)
|
||||||
const isPreviewVisible = computed({
|
|
||||||
get: () => !!previewCard.value,
|
|
||||||
set: (value: boolean) => {
|
|
||||||
if (!value) {
|
|
||||||
closePreview()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const CARD_SCALE = 0.5
|
const CARD_SCALE = 0.5
|
||||||
|
|
||||||
// 默认颜色调色板 - 霓虹色系
|
// 默认颜色调色板 - 霓虹色系
|
||||||
|
|
@ -138,21 +129,6 @@ function getCardImageUrl(card: CardItem) {
|
||||||
return card.thumbnail || ''
|
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) {
|
function removeCard(card: CardItem) {
|
||||||
emit('remove', card.id)
|
emit('remove', card.id)
|
||||||
}
|
}
|
||||||
|
|
@ -170,10 +146,6 @@ function handleCardClick(card: CardItem) {
|
||||||
expandCards()
|
expandCards()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.type === 'image') {
|
|
||||||
openPreview(card)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardStyle = computed(() => (index: number, total: number) => {
|
const cardStyle = computed(() => (index: number, total: number) => {
|
||||||
|
|
@ -214,9 +186,21 @@ const cardStyle = computed(() => (index: number, total: number) => {
|
||||||
|
|
||||||
const handleDocumentClick = (event: MouseEvent) => {
|
const handleDocumentClick = (event: MouseEvent) => {
|
||||||
if (!containerRef.value) return
|
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) {
|
if (isExpanded.value) {
|
||||||
isExpanded.value = false
|
isExpanded.value = false
|
||||||
hoveredCardId.value = null
|
hoveredCardId.value = null
|
||||||
|
|
@ -273,7 +257,13 @@ watch(
|
||||||
@mouseenter="hoveredCardId = card.id" @mouseleave="hoveredCardId = null" @click="handleCardClick(card)">
|
@mouseenter="hoveredCardId = card.id" @mouseleave="hoveredCardId = null" @click="handleCardClick(card)">
|
||||||
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
|
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
|
||||||
<div v-if="card.type === 'image' && getCardImageUrl(card)" class="card-media">
|
<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 v-if="card.uploading" class="card-media-uploading">
|
||||||
<div class="card-media-uploading-spinner" aria-hidden="true" />
|
<div class="card-media-uploading-spinner" aria-hidden="true" />
|
||||||
<span class="card-media-uploading-text">上传中</span>
|
<span class="card-media-uploading-text">上传中</span>
|
||||||
|
|
@ -285,7 +275,13 @@ watch(
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="card.type === 'image'" class="card-preview">
|
<div v-else-if="card.type === 'image'" class="card-preview">
|
||||||
<div v-if="getCardImageUrl(card)" class="card-preview-image">
|
<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>
|
||||||
<div class="card-preview-fallback">
|
<div class="card-preview-fallback">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
|
|
@ -341,23 +337,6 @@ watch(
|
||||||
<PlusIcon :size="12" />
|
<PlusIcon :size="12" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -420,11 +399,11 @@ watch(
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 74%;
|
width: 74%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--card-border-color, var(--ffffff, #FFF));
|
border: 1px solid var(--card-border-color, var(--ffffff, #FFF));
|
||||||
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
/* overflow: hidden; */
|
||||||
transition: transform 0.35s ease-out, opacity 0.35s ease-out;
|
transition: transform 0.35s ease-out, opacity 0.35s ease-out;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
@ -439,6 +418,8 @@ watch(
|
||||||
|
|
||||||
.card-media {
|
.card-media {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-media-uploading {
|
.card-media-uploading {
|
||||||
|
|
@ -529,8 +510,8 @@ watch(
|
||||||
|
|
||||||
.card-delete-btn {
|
.card-delete-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.3rem;
|
top: -0.3rem;
|
||||||
right: 0.3rem;
|
right: -0.3rem;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -556,8 +537,14 @@ watch(
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-media img,
|
.card-media :deep(.n-image),
|
||||||
.card-preview-image img {
|
.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%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
@ -816,61 +803,6 @@ watch(
|
||||||
transform: translateX(-50%) translateY(6px);
|
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) {
|
@media (max-width: 640px) {
|
||||||
.stacked-cards-container {
|
.stacked-cards-container {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue