Initial commit: Momentry Studio v0.1.0
This commit is contained in:
199
src/components/VideoPlayer.vue
Normal file
199
src/components/VideoPlayer.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div v-if="visible" class="video-player-modal" @click.self="close">
|
||||
<div class="video-player-box">
|
||||
<button class="close-btn" @click="close">×</button>
|
||||
|
||||
<video ref="videoEl" class="video" controls autoplay @loadedmetadata="onLoaded" @timeupdate="onTimeUpdate">
|
||||
<source :src="videoUrl" type="video/mp4" />
|
||||
</video>
|
||||
|
||||
<div class="video-info">
|
||||
<h3>{{ title }}</h3>
|
||||
<div class="time-display">
|
||||
<span>{{ formatTime(currentTime) }}</span>
|
||||
<span class="separator">/</span>
|
||||
<span>{{ formatTime(duration) }}</span>
|
||||
<span v-if="hasRange" class="range">({{ formatTime(rangeStart) }} - {{ formatTime(rangeEnd) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasRange" class="segment-controls">
|
||||
<button @click="seekToStart" class="seg-btn">⏮ 跳到起點</button>
|
||||
<button @click="togglePlay" class="seg-btn">{{ isPlaying ? '⏸ 暫停' : '▶ 播放' }}</button>
|
||||
<button @click="seekToEnd" class="seg-btn">跳到結尾 ⏭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
fileUuid: string
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
title?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const videoEl = ref<HTMLVideoElement | null>(null)
|
||||
const visible = ref(true)
|
||||
const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
const isPlaying = ref(false)
|
||||
|
||||
const CORE_API = 'http://localhost:3002'
|
||||
const API_KEY = 'muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69'
|
||||
|
||||
const videoUrl = computed(() => {
|
||||
const start = props.startTime ?? 0
|
||||
const end = props.endTime ?? 99999
|
||||
return `${CORE_API}/api/v1/file/${props.fileUuid}/video?api_key=${API_KEY}&start_time=${start}&end_time=${end}`
|
||||
})
|
||||
|
||||
const hasRange = computed(() => {
|
||||
return props.startTime !== undefined && props.endTime !== undefined
|
||||
})
|
||||
|
||||
const rangeStart = computed(() => props.startTime ?? 0)
|
||||
const rangeEnd = computed(() => props.endTime ?? 0)
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function onLoaded() {
|
||||
if (videoEl.value && props.startTime) {
|
||||
videoEl.value.currentTime = props.startTime
|
||||
videoEl.value.play()
|
||||
isPlaying.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function onTimeUpdate() {
|
||||
if (videoEl.value) {
|
||||
currentTime.value = videoEl.value.currentTime
|
||||
duration.value = videoEl.value.duration || 0
|
||||
|
||||
// 如果播放到 range end,暫停
|
||||
if (hasRange.value && videoEl.value.currentTime >= (props.endTime ?? 0)) {
|
||||
videoEl.value.pause()
|
||||
isPlaying.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function seekToStart() {
|
||||
if (videoEl.value && props.startTime) {
|
||||
videoEl.value.currentTime = props.startTime
|
||||
videoEl.value.play()
|
||||
isPlaying.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function seekToEnd() {
|
||||
if (videoEl.value && props.endTime) {
|
||||
videoEl.value.currentTime = props.endTime - 1
|
||||
videoEl.value.play()
|
||||
isPlaying.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (videoEl.value) {
|
||||
if (videoEl.value.paused) {
|
||||
videoEl.value.play()
|
||||
isPlaying.value = true
|
||||
} else {
|
||||
videoEl.value.pause()
|
||||
isPlaying.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(sec: number): string {
|
||||
if (!sec || isNaN(sec)) return '0:00'
|
||||
const m = Math.floor(sec / 60)
|
||||
const s = Math.floor(sec % 60)
|
||||
return `${m}:${s.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 鍵盤快捷鍵
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (!visible.value) return
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
close()
|
||||
break
|
||||
case ' ':
|
||||
e.preventDefault()
|
||||
togglePlay()
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
if (videoEl.value) videoEl.value.currentTime = Math.max(0, videoEl.value.currentTime - 5)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (videoEl.value) videoEl.value.currentTime = Math.min(duration.value, videoEl.value.currentTime + 5)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', onKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-player-modal {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.85);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 2000;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
.video-player-box {
|
||||
background: #111;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
max-width: 90vw;
|
||||
position: relative;
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
.close-btn {
|
||||
position: absolute; top: -12px; right: -12px;
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
background: #fff; border: none; font-size: 1.5rem;
|
||||
cursor: pointer; z-index: 10;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
.close-btn:hover { background: #f3f4f6; }
|
||||
.video {
|
||||
max-width: 80vw; max-height: 60vh;
|
||||
border-radius: 8px; display: block;
|
||||
}
|
||||
.video-info {
|
||||
color: #fff; margin-top: 16px; text-align: center;
|
||||
}
|
||||
.video-info h3 { font-size: 1rem; margin-bottom: 8px; font-weight: 500; }
|
||||
.time-display { font-size: 0.85rem; color: #9ca3af; display: flex; align-items: center; justify-content: center; gap: 6px; }
|
||||
.range { color: #4f46e5; }
|
||||
.segment-controls {
|
||||
display: flex; justify-content: center; gap: 12px; margin-top: 12px;
|
||||
}
|
||||
.seg-btn {
|
||||
padding: 8px 16px; background: #1f2937; color: #fff;
|
||||
border: 1px solid #374151; border-radius: 8px;
|
||||
cursor: pointer; font-size: 0.85rem; transition: background 0.15s;
|
||||
}
|
||||
.seg-btn:hover { background: #374151; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user