PreviewModal.vue 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. <script setup lang="ts">
  2. import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
  3. import PreviewInspector from './PreviewInspector.vue'
  4. import type { UiLibraryItem } from './types'
  5. const props = withDefaults(defineProps<{ item: UiLibraryItem; items?: UiLibraryItem[] }>(), { items: () => [] })
  6. const emit = defineEmits<{ close: []; addToComposer: [item: UiLibraryItem]; source: [item: UiLibraryItem] }>()
  7. const mediaItems = computed(() => {
  8. const candidates = props.items.filter(item => item.kind === 'image' || item.kind === 'video')
  9. return candidates.some(item => item.id === props.item.id) ? candidates : [props.item]
  10. })
  11. const activeIndex = ref(0)
  12. const activeItem = computed(() => mediaItems.value[activeIndex.value] ?? props.item)
  13. const canPrevious = computed(() => activeIndex.value > 0)
  14. const canNext = computed(() => activeIndex.value < mediaItems.value.length - 1)
  15. let swipeStart: number | undefined
  16. function resetActiveItem() {
  17. const index = mediaItems.value.findIndex(item => item.id === props.item.id)
  18. activeIndex.value = index >= 0 ? index : 0
  19. }
  20. function previous() { if (canPrevious.value) activeIndex.value -= 1 }
  21. function next() { if (canNext.value) activeIndex.value += 1 }
  22. function startSwipe(event: PointerEvent) {
  23. if (event.button !== 0 && event.pointerType === 'mouse') return
  24. if ((event.target as HTMLElement).closest('button, a, audio')) return
  25. swipeStart = event.clientX
  26. }
  27. function finishSwipe(event: PointerEvent) {
  28. if (swipeStart === undefined) return
  29. const delta = event.clientX - swipeStart
  30. swipeStart = undefined
  31. if (Math.abs(delta) < 48) return
  32. if (delta < 0) next()
  33. else previous()
  34. }
  35. function onKeydown(event: KeyboardEvent) {
  36. if (event.key !== 'Escape') return
  37. event.preventDefault()
  38. emit('close')
  39. }
  40. watch([() => props.item.id, mediaItems], resetActiveItem, { immediate: true })
  41. onMounted(() => window.addEventListener('keydown', onKeydown))
  42. onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
  43. </script>
  44. <template>
  45. <Teleport to="body">
  46. <Transition name="preview-modal">
  47. <div class="preview-modal-layer" @mousedown.self="emit('close')">
  48. <section class="preview-modal" role="dialog" aria-modal="true" :aria-label="`预览 ${activeItem.title || activeItem.name}`" @pointerdown="startSwipe" @pointerup="finishSwipe" @pointercancel="swipeStart = undefined">
  49. <PreviewInspector :item="activeItem" :can-previous="canPrevious" :can-next="canNext" show-close @close="emit('close')" @previous="previous" @next="next" @add-to-composer="emit('addToComposer', $event)" @source="emit('source', $event)" />
  50. </section>
  51. </div>
  52. </Transition>
  53. </Teleport>
  54. </template>
  55. <style scoped>
  56. .preview-modal-layer { position: fixed; z-index: 90; inset: 0; display: grid; place-items: center; padding: 28px; background: var(--scrim); backdrop-filter: blur(4px); }
  57. .preview-modal { display: flex; width: min(960px, calc(100vw - 56px)); height: min(820px, calc(100vh - 56px)); overflow: hidden; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); box-shadow: 0 24px 70px rgba(0,0,0,.28); }
  58. .preview-modal :deep(.preview-inspector) { width: 100%; }
  59. .preview-modal :deep(.preview-stage) { min-height: min(58vh, 560px); }
  60. .preview-modal-enter-active, .preview-modal-leave-active { transition: opacity 160ms ease; }
  61. .preview-modal-enter-active .preview-modal, .preview-modal-leave-active .preview-modal { transition: transform 180ms var(--ease-out), opacity 150ms ease; }
  62. .preview-modal-enter-from, .preview-modal-leave-to { opacity: 0; }
  63. .preview-modal-enter-from .preview-modal, .preview-modal-leave-to .preview-modal { opacity: 0; transform: translateY(8px) scale(.985); }
  64. @media (max-width: 600px) { .preview-modal-layer { padding: 0; }.preview-modal { width: 100vw; max-height: 100vh; min-height: 100vh; border: 0; border-radius: 0; }.preview-modal :deep(.preview-stage) { min-height: 46vh; } }
  65. @media (prefers-reduced-motion: reduce) { .preview-modal-enter-active, .preview-modal-leave-active, .preview-modal-enter-active .preview-modal, .preview-modal-leave-active .preview-modal { transition: none; } }
  66. </style>