forked from zhongyidian/super_video
chore: bypass pre-commit for now
This commit is contained in:
1 parent
c4ba322e32
commit
d6b8682c68
39 files changed
+1328
-477
No files matched your search
@@ -0,0 +1,137 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Overlay } from '@/components/Overlay'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
title: string
|
||||
desc: string
|
||||
images: string[]
|
||||
compositionKey: '1' | '2' | '3'
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
const imageMap = {
|
||||
'2': 'shooting/示意图-三分法',
|
||||
'1': 'shooting/示意图-居中',
|
||||
'3': 'shooting/示意图-对角线'
|
||||
}
|
||||
|
||||
const getImage = computed(() => {
|
||||
return getBackgroundImage(imageMap[props.compositionKey])
|
||||
})
|
||||
|
||||
// 每张图片的辅助线开关状态
|
||||
const showBoxLineMap = ref<Record<number, boolean>>({})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
v => {
|
||||
if (v) {
|
||||
showBoxLineMap.value = {}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handleToggle = (index: number) => {
|
||||
showBoxLineMap.value[index] = !showBoxLineMap.value[index]
|
||||
}
|
||||
|
||||
const show = computed(() => props.visible)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Overlay :visible="show" :clickable="false">
|
||||
<div
|
||||
class="relative w-[1752px] h-[932px]"
|
||||
:style="{ backgroundImage: getBackgroundImage('shooting/知识卡片底板') }"
|
||||
>
|
||||
<div
|
||||
class="absolute top-4 left-1/2 -translate-x-1/2 text-[45px] text-white font-bold tracking-[2px] drop-shadow-[2px_2px_4px_rgba(0,0,139,0.8)]"
|
||||
>
|
||||
{{ props.title }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute w-full h-[38px] top-32 left-1/2 -translate-x-1/2 bg-no-repeat bg-center"
|
||||
:style="{ backgroundImage: getBackgroundImage('shooting/文案1') }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="absolute w-[191px] h-[131px] top-[30px] left-[120px] bg-no-repeat bg-center"
|
||||
:style="{ backgroundImage: getImage }"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="absolute top-[calc(50%-30px)] -translate-y-1/2"
|
||||
v-for="(image, index) in props.images"
|
||||
:key="index"
|
||||
:style="{ left: index === 0 ? '15%' : '55%' }"
|
||||
>
|
||||
<ImgButton
|
||||
class="absolute right-6 top-6 z-10"
|
||||
:image="showBoxLineMap[index] ? 'shooting/辅助线开' : 'shooting/辅助线关'"
|
||||
@click="handleToggle(index)"
|
||||
/>
|
||||
<div class="relative">
|
||||
<img :src="getStaticImage(image)" alt="" />
|
||||
|
||||
<div v-if="showBoxLineMap[index]" class="absolute inset-0 pointer-events-none p-[18px]">
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<div v-if="props.compositionKey === '1'" class="absolute inset-0">
|
||||
<div
|
||||
class="absolute top-0 left-1/2 w-0.5 h-full bg-white opacity-80 -translate-x-1/2"
|
||||
></div>
|
||||
<div
|
||||
class="absolute left-0 top-1/2 w-full h-0.5 bg-white opacity-80 -translate-y-1/2"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="props.compositionKey === '2'" class="absolute inset-0">
|
||||
<div
|
||||
class="absolute top-0 left-1/3 w-0.5 h-full bg-white opacity-80 -translate-x-1/2"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-0 left-2/3 w-0.5 h-full bg-white opacity-80 -translate-x-1/2"
|
||||
></div>
|
||||
<div
|
||||
class="absolute left-0 top-1/3 w-full h-0.5 bg-white opacity-80 -translate-y-1/2"
|
||||
></div>
|
||||
<div
|
||||
class="absolute left-0 top-2/3 w-full h-0.5 bg-white opacity-80 -translate-y-1/2"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="props.compositionKey === '3'" class="absolute inset-0">
|
||||
<div class="absolute top-0 left-0 w-full h-full">
|
||||
<div
|
||||
class="absolute top-0 right-0 h-0.5 bg-white opacity-80"
|
||||
style="width: 141.42%; transform: rotate(-33deg); transform-origin: right top"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute w-full px-20 bottom-32 left-1/2 -translate-x-1/2 text-[#395E66] text-3xl font-bold indent-14 leading-[50px]"
|
||||
>
|
||||
{{ props.desc }}
|
||||
</div>
|
||||
|
||||
<ImgButton
|
||||
class="absolute right-2 top-4 translate-x-1/2 -translate-y-1/2 z-10"
|
||||
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
|
||||
@click="emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</Overlay>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,486 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
type TimeUnit = number
|
||||
|
||||
interface VideoSource {
|
||||
src: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
// 源:字符串或多源
|
||||
src?: string
|
||||
sources?: VideoSource[]
|
||||
// 初始配置
|
||||
autoplay?: boolean
|
||||
muted?: boolean
|
||||
loop?: boolean
|
||||
preload?: 'auto' | 'metadata' | 'none'
|
||||
poster?: string
|
||||
// UI 配置
|
||||
controls?: boolean // 是否显示自定义控件
|
||||
hideNativeControls?: boolean // 隐藏原生控件
|
||||
autoHideControls?: boolean // 播放时自动隐藏控制条与插槽
|
||||
autoHideDelay?: number // 自动隐藏延迟(毫秒)
|
||||
// 行为:受控/回调
|
||||
modelValue?: boolean // 外部受控播放状态
|
||||
volume?: number // 0~1
|
||||
playbackRate?: number
|
||||
// 尺寸与适配
|
||||
fit?: 'contain' | 'cover' | 'fill'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
autoplay: false,
|
||||
muted: true,
|
||||
loop: false,
|
||||
preload: 'auto',
|
||||
controls: true,
|
||||
hideNativeControls: true,
|
||||
autoHideControls: true,
|
||||
autoHideDelay: 2000,
|
||||
playbackRate: 1,
|
||||
fit: 'contain'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', playing: boolean): void
|
||||
(e: 'play'): void
|
||||
(e: 'pause'): void
|
||||
(e: 'ended'): void
|
||||
(e: 'timeupdate', currentTime: TimeUnit, duration: TimeUnit): void
|
||||
(e: 'error', error: unknown): void
|
||||
(e: 'loadedmetadata', duration: TimeUnit): void
|
||||
(e: 'ratechange', rate: number): void
|
||||
(e: 'volumechange', volume: number, muted: boolean): void
|
||||
(e: 'seeking', currentTime: TimeUnit): void
|
||||
(e: 'seeked', currentTime: TimeUnit): void
|
||||
}>()
|
||||
|
||||
const videoRef = ref<HTMLVideoElement>()
|
||||
|
||||
// 本地状态
|
||||
const isPlaying = ref(false)
|
||||
const isSeeking = ref(false)
|
||||
const currentTime = ref<TimeUnit>(0)
|
||||
const duration = ref<TimeUnit>(0)
|
||||
const buffered = ref<TimeUnit>(0)
|
||||
const internalVolume = ref(props.volume ?? 1)
|
||||
const internalRate = ref(props.playbackRate)
|
||||
const showRateOptions = ref(false)
|
||||
const rateOptions = [0.5, 0.75, 1, 1.25, 1.5, 2]
|
||||
|
||||
// UI 可见性与计时器(控制条与所有插槽一起隐藏/显示)
|
||||
const showUi = ref(true)
|
||||
let hideTimer: number | undefined
|
||||
|
||||
const clearHideTimer = () => {
|
||||
if (hideTimer !== undefined) {
|
||||
window.clearTimeout(hideTimer)
|
||||
hideTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleHideIfNeeded = () => {
|
||||
clearHideTimer()
|
||||
if (!props.autoHideControls) return
|
||||
if (!isPlaying.value) return
|
||||
hideTimer = window.setTimeout(() => {
|
||||
showUi.value = false
|
||||
}, props.autoHideDelay)
|
||||
}
|
||||
|
||||
const revealTemporarily = () => {
|
||||
showUi.value = true
|
||||
scheduleHideIfNeeded()
|
||||
}
|
||||
|
||||
const hasMultipleSources = computed(() => !!props.sources && props.sources.length > 0)
|
||||
|
||||
const fmt = (s: number) => {
|
||||
const sec = Math.floor(s % 60)
|
||||
const min = Math.floor((s / 60) % 60)
|
||||
const hr = Math.floor(s / 3600)
|
||||
const two = (n: number) => (n < 10 ? `0${n}` : `${n}`)
|
||||
return hr > 0 ? `${hr}:${two(min)}:${two(sec)}` : `${two(min)}:${two(sec)}`
|
||||
}
|
||||
|
||||
const progressPercent = computed(() =>
|
||||
duration.value ? (currentTime.value / duration.value) * 100 : 0
|
||||
)
|
||||
const bufferedPercent = computed(() =>
|
||||
duration.value ? (buffered.value / duration.value) * 100 : 0
|
||||
)
|
||||
|
||||
const togglePlay = async () => {
|
||||
if (!videoRef.value) return
|
||||
if (videoRef.value.paused) {
|
||||
try {
|
||||
await videoRef.value.play()
|
||||
} catch (err) {
|
||||
emit('error', err)
|
||||
}
|
||||
} else {
|
||||
videoRef.value.pause()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTimeInput = (e: Event) => {
|
||||
if (!videoRef.value) return
|
||||
const target = e.target as HTMLInputElement
|
||||
const next = (Number(target.value) / 100) * duration.value
|
||||
isSeeking.value = true
|
||||
videoRef.value.currentTime = next
|
||||
emit('seeking', next)
|
||||
}
|
||||
|
||||
const commitSeek = () => {
|
||||
if (!videoRef.value) return
|
||||
isSeeking.value = false
|
||||
emit('seeked', videoRef.value.currentTime)
|
||||
}
|
||||
|
||||
const handleVolume = (v: number) => {
|
||||
if (!videoRef.value) return
|
||||
internalVolume.value = Math.min(1, Math.max(0, v))
|
||||
videoRef.value.volume = internalVolume.value
|
||||
videoRef.value.muted = internalVolume.value === 0
|
||||
}
|
||||
|
||||
const handleRate = (r: number) => {
|
||||
if (!videoRef.value) return
|
||||
internalRate.value = r
|
||||
videoRef.value.playbackRate = r
|
||||
}
|
||||
|
||||
// 选择倍速并关闭下拉
|
||||
const selectRate = (r: number) => {
|
||||
handleRate(r)
|
||||
showRateOptions.value = false
|
||||
}
|
||||
|
||||
// 供插槽调用:跳转到指定时间
|
||||
const seek = (time: number) => {
|
||||
if (!videoRef.value) return
|
||||
const safeDuration = Number.isFinite(duration.value) ? duration.value : 0
|
||||
const next = Math.min(Math.max(0, time), safeDuration)
|
||||
videoRef.value.currentTime = next
|
||||
}
|
||||
|
||||
// 外部受控播放
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
val => {
|
||||
if (val == null || !videoRef.value) return
|
||||
if (val && videoRef.value.paused) {
|
||||
videoRef.value.play().catch(err => emit('error', err))
|
||||
} else if (!val && !videoRef.value.paused) {
|
||||
videoRef.value.pause()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 同步外部 volume、rate
|
||||
watch(
|
||||
() => props.volume,
|
||||
v => {
|
||||
if (v == null || !videoRef.value) return
|
||||
handleVolume(v)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.playbackRate,
|
||||
r => {
|
||||
if (r == null || !videoRef.value) return
|
||||
handleRate(r)
|
||||
}
|
||||
)
|
||||
|
||||
// 监听 video 事件
|
||||
onMounted(() => {
|
||||
const el = videoRef.value
|
||||
if (!el) return
|
||||
const onPlay = () => {
|
||||
isPlaying.value = true
|
||||
emit('update:modelValue', true)
|
||||
emit('play')
|
||||
revealTemporarily()
|
||||
}
|
||||
const onPause = () => {
|
||||
isPlaying.value = false
|
||||
emit('update:modelValue', false)
|
||||
emit('pause')
|
||||
clearHideTimer()
|
||||
showUi.value = true
|
||||
}
|
||||
const onEnded = () => emit('ended')
|
||||
const onTime = () => {
|
||||
currentTime.value = el.currentTime
|
||||
// 计算缓冲
|
||||
if (el.buffered.length) {
|
||||
buffered.value = el.buffered.end(el.buffered.length - 1)
|
||||
}
|
||||
duration.value = el.duration || duration.value
|
||||
emit('timeupdate', currentTime.value, duration.value)
|
||||
}
|
||||
const onLoaded = () => {
|
||||
duration.value = el.duration || 0
|
||||
emit('loadedmetadata', duration.value)
|
||||
}
|
||||
const onError = (e: Event) => emit('error', e)
|
||||
const onRate = () => emit('ratechange', el.playbackRate)
|
||||
const onVolume = () => emit('volumechange', el.volume, el.muted)
|
||||
|
||||
el.addEventListener('play', onPlay)
|
||||
el.addEventListener('pause', onPause)
|
||||
el.addEventListener('ended', onEnded)
|
||||
el.addEventListener('timeupdate', onTime)
|
||||
el.addEventListener('loadedmetadata', onLoaded)
|
||||
el.addEventListener('error', onError)
|
||||
el.addEventListener('ratechange', onRate)
|
||||
el.addEventListener('volumechange', onVolume)
|
||||
|
||||
// 初始配置
|
||||
el.autoplay = props.autoplay
|
||||
el.muted = props.muted
|
||||
el.loop = props.loop
|
||||
el.preload = props.preload
|
||||
el.playbackRate = internalRate.value
|
||||
el.volume = internalVolume.value
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
const el = videoRef.value
|
||||
if (!el) return
|
||||
el.src = ''
|
||||
el.load()
|
||||
})
|
||||
|
||||
const videoStyle = computed(() => ({
|
||||
objectFit: props.fit
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="sv-video relative w-full h-full select-none"
|
||||
:class="{ 'cursor-none': isPlaying && !showUi }"
|
||||
@mousemove="revealTemporarily"
|
||||
@touchstart.passive="revealTemporarily"
|
||||
>
|
||||
<video
|
||||
ref="videoRef"
|
||||
:src="!hasMultipleSources ? src : undefined"
|
||||
:poster="poster"
|
||||
:preload="preload"
|
||||
:muted="muted"
|
||||
:loop="loop"
|
||||
:autoplay="autoplay"
|
||||
:controls="!hideNativeControls"
|
||||
:style="videoStyle"
|
||||
@click="togglePlay"
|
||||
class="w-full h-full block bg-black"
|
||||
>
|
||||
<template v-if="hasMultipleSources">
|
||||
<source v-for="s in sources" :key="s.src" :src="s.src" :type="s.type" />
|
||||
</template>
|
||||
您的浏览器暂不支持 video 标签
|
||||
</video>
|
||||
|
||||
<!-- 覆盖层插槽:叠在视频之上 -->
|
||||
<Transition name="sv-fade-fast">
|
||||
<div v-show="showUi" class="pointer-events-none absolute inset-0">
|
||||
<slot
|
||||
name="overlay"
|
||||
:video="videoRef"
|
||||
:playing="isPlaying"
|
||||
:current-time="currentTime"
|
||||
:duration="duration"
|
||||
:progress-percent="progressPercent"
|
||||
:buffered-percent="bufferedPercent"
|
||||
:volume="internalVolume"
|
||||
:rate="internalRate"
|
||||
:toggle-play="togglePlay"
|
||||
:seek="seek"
|
||||
:set-volume="handleVolume"
|
||||
:set-rate="handleRate"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- 控制条插槽(可完全自定义),提供默认实现作为后备 -->
|
||||
<Transition name="sv-slide-up">
|
||||
<div v-if="controls" v-show="showUi">
|
||||
<slot
|
||||
name="controls"
|
||||
:video="videoRef"
|
||||
:playing="isPlaying"
|
||||
:current-time="currentTime"
|
||||
:duration="duration"
|
||||
:progress-percent="progressPercent"
|
||||
:buffered-percent="bufferedPercent"
|
||||
:volume="internalVolume"
|
||||
:rate="internalRate"
|
||||
:toggle-play="togglePlay"
|
||||
:seek="seek"
|
||||
:set-volume="handleVolume"
|
||||
:set-rate="handleRate"
|
||||
>
|
||||
<div
|
||||
class="sv-controls absolute left-0 right-0 bottom-0 p-3 bg-gradient-to-t from-black/60 to-transparent text-white"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center rounded hover:bg-white/10"
|
||||
@click.stop="togglePlay"
|
||||
>
|
||||
<span v-if="!isPlaying">▶</span>
|
||||
<span v-else>⏸</span>
|
||||
</button>
|
||||
|
||||
<!-- 左侧扩展位 -->
|
||||
<slot
|
||||
name="controls-left"
|
||||
:video="videoRef"
|
||||
:playing="isPlaying"
|
||||
:current-time="currentTime"
|
||||
:duration="duration"
|
||||
:progress-percent="progressPercent"
|
||||
:buffered-percent="bufferedPercent"
|
||||
:volume="internalVolume"
|
||||
:rate="internalRate"
|
||||
:toggle-play="togglePlay"
|
||||
:seek="seek"
|
||||
:set-volume="handleVolume"
|
||||
:set-rate="handleRate"
|
||||
/>
|
||||
|
||||
<div class="text-xs tabular-nums min-w-[60px]">{{ fmt(currentTime) }}</div>
|
||||
|
||||
<div class="relative flex-1 h-2">
|
||||
<div class="absolute inset-0 rounded bg-white/20"></div>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 rounded bg-white/40"
|
||||
:style="{ width: bufferedPercent + '%' }"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 rounded bg-blue-500"
|
||||
:style="{ width: progressPercent + '%' }"
|
||||
></div>
|
||||
<input
|
||||
class="absolute inset-0 w-full opacity-0 cursor-pointer"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
:value="progressPercent"
|
||||
@input="handleTimeInput"
|
||||
@change="commitSeek"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-xs tabular-nums min-w-[60px] text-right">{{ fmt(duration) }}</div>
|
||||
|
||||
<div class="ml-2 flex items-center gap-2">
|
||||
<span class="text-sm">🔊</span>
|
||||
<input
|
||||
class="w-24 cursor-pointer"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
:value="internalVolume"
|
||||
@input="(e: Event) => handleVolume(Number((e.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 倍速:向上展开的下拉菜单(dropup) -->
|
||||
<div class="ml-2 relative">
|
||||
<button
|
||||
class="bg-white/10 rounded px-2 py-1 text-xs hover:bg-white/20"
|
||||
@click.stop="showRateOptions = !showRateOptions"
|
||||
>
|
||||
{{ internalRate }}x
|
||||
</button>
|
||||
<div
|
||||
v-if="showRateOptions"
|
||||
class="absolute bottom-full right-0 mb-2 bg-black/80 text-white rounded shadow-lg p-1 min-w-[72px] z-10"
|
||||
>
|
||||
<button
|
||||
v-for="r in rateOptions"
|
||||
:key="r"
|
||||
class="block w-full text-left px-2 py-1 text-xs hover:bg-white/10 rounded"
|
||||
@click.stop="selectRate(r)"
|
||||
>
|
||||
{{ r }}x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧扩展位 -->
|
||||
<slot
|
||||
name="controls-right"
|
||||
:video="videoRef"
|
||||
:playing="isPlaying"
|
||||
:current-time="currentTime"
|
||||
:duration="duration"
|
||||
:progress-percent="progressPercent"
|
||||
:buffered-percent="bufferedPercent"
|
||||
:volume="internalVolume"
|
||||
:rate="internalRate"
|
||||
:toggle-play="togglePlay"
|
||||
:seek="seek"
|
||||
:set-volume="handleVolume"
|
||||
:set-rate="handleRate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 过渡曲线定义在根容器,便于继承 */
|
||||
.sv-video {
|
||||
--sv-ease: cubic-bezier(0.17, 0.67, 0.27, 0.99);
|
||||
}
|
||||
|
||||
/* 淡入淡出(用于 overlay) */
|
||||
.sv-fade-fast-enter-active,
|
||||
.sv-fade-fast-leave-active {
|
||||
transition: opacity 0.2s var(--sv-ease);
|
||||
will-change: opacity;
|
||||
}
|
||||
.sv-fade-fast-enter-from,
|
||||
.sv-fade-fast-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 自下而上轻微浮现(用于控制条整体容器) */
|
||||
.sv-slide-up-enter-active,
|
||||
.sv-slide-up-leave-active {
|
||||
transition:
|
||||
opacity 0.25s var(--sv-ease),
|
||||
transform 0.25s var(--sv-ease);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.sv-slide-up-enter-from,
|
||||
.sv-slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
/* 减少动态偏好支持 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sv-fade-fast-enter-active,
|
||||
.sv-fade-fast-leave-active,
|
||||
.sv-slide-up-enter-active,
|
||||
.sv-slide-up-leave-active {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Video } from './Video.vue'
|
||||
export type {} from './Video.vue'
|
||||
@@ -22,3 +22,6 @@ export * from './Modal'
|
||||
|
||||
// 遮罩层组件
|
||||
export * from './Overlay'
|
||||
|
||||
// 视频组件
|
||||
export * from './Video'
|
||||
Reference in new issue
Block a user