refactor(admin): 拆分剧集转码产物面板组件
This commit is contained in:
1 parent
52b10d3040
commit
e2ca3bfa81
8 files changed
+860
-608
No files matched your search
@@ -0,0 +1,315 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
<!-- 剧集转码逐帧、雪碧图与分页预览 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type {
|
||||
MediaThumbnailFrameItem,
|
||||
MediaThumbnailResult,
|
||||
MediaThumbnailSpriteInfo
|
||||
} from '@/api/admin/types'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { formatTimeMs, THUMBNAIL_PAGE_SIZES } from './episodeTranscodeUi'
|
||||
|
||||
interface FramesView {
|
||||
data: MediaThumbnailResult | null
|
||||
loading: boolean
|
||||
errorMessage: string
|
||||
pageIndex: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
view: FramesView
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
pageChange: [value: number]
|
||||
pageSizeChange: [value: number]
|
||||
retry: []
|
||||
}>()
|
||||
|
||||
const FRAME_CARD_WIDTH = 128
|
||||
const PREVIEW_MAX_WIDTH = 520
|
||||
const PREVIEW_MAX_HEIGHT = 360
|
||||
|
||||
const { t } = useI18n()
|
||||
const frameView = ref<'frames' | 'sprites'>('frames')
|
||||
const previewVisible = ref(false)
|
||||
const previewFrame = ref<MediaThumbnailFrameItem | null>(null)
|
||||
const frames = computed(() => props.view.data?.frames?.list || [])
|
||||
const sprite = computed(() => props.view.data?.sprite || null)
|
||||
const spriteUrls = computed(() => sprite.value?.spriteUrls || [])
|
||||
const framesTotal = computed(() => Number(props.view.data?.frames?.total || 0))
|
||||
const previewStyle = computed(() => {
|
||||
const frame = previewFrame.value
|
||||
const info = sprite.value
|
||||
if (!frame || !info) return {}
|
||||
const scale = Math.min(
|
||||
PREVIEW_MAX_WIDTH / Math.max(1, frame.width),
|
||||
PREVIEW_MAX_HEIGHT / Math.max(1, frame.height)
|
||||
)
|
||||
return frameStyle(frame, info, scale)
|
||||
})
|
||||
|
||||
function openFramePreview(frame: MediaThumbnailFrameItem) {
|
||||
previewFrame.value = frame
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
function frameStyle(frame: MediaThumbnailFrameItem, info: MediaThumbnailSpriteInfo, scale: number) {
|
||||
return {
|
||||
width: `${Math.round(frame.width * scale)}px`,
|
||||
height: `${Math.round(frame.height * scale)}px`,
|
||||
backgroundImage: `url("${frame.spriteUrl || frame.imageUrl}")`,
|
||||
backgroundPosition: `-${Math.round(frame.x * scale)}px -${Math.round(frame.y * scale)}px`,
|
||||
backgroundSize: `${Math.round((info.columns || 1) * frame.width * scale)}px ${Math.round(
|
||||
(info.rows || 1) * frame.height * scale
|
||||
)}px`
|
||||
}
|
||||
}
|
||||
|
||||
function frameCardStyle(frame: MediaThumbnailFrameItem) {
|
||||
if (!sprite.value) return {}
|
||||
return frameStyle(frame, sprite.value, FRAME_CARD_WIDTH / Math.max(1, frame.width))
|
||||
}
|
||||
|
||||
function spriteFrameCount(index: number) {
|
||||
const info = sprite.value
|
||||
if (!info) return 0
|
||||
const capacity = Math.max(1, info.columns * info.rows)
|
||||
return Math.max(0, Math.min(capacity, info.totalFrames - index * capacity))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="episode-transcode-frames__heading">
|
||||
<h4>{{ t('episodeTranscode.frameTitle') }}</h4>
|
||||
<div class="episode-transcode-frames__tools">
|
||||
<span>{{ t('episodeTranscode.frameCount', { total: framesTotal }) }}</span>
|
||||
<el-radio-group v-model="frameView" size="small">
|
||||
<el-radio-button label="frames">{{ t('episodeTranscode.frameView') }}</el-radio-button>
|
||||
<el-radio-button label="sprites">{{ t('episodeTranscode.spriteView') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert
|
||||
v-if="view.errorMessage"
|
||||
type="error"
|
||||
:title="view.errorMessage"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #default>
|
||||
<el-button size="small" text @click="emit('retry')">
|
||||
{{ t('episodeTranscode.retryLoad') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
<div v-loading="view.loading">
|
||||
<template v-if="frameView === 'frames'">
|
||||
<div v-if="frames.length" class="episode-transcode-frames__grid">
|
||||
<button
|
||||
v-for="frame in frames"
|
||||
:key="frame.frameIndex"
|
||||
class="episode-transcode-frames__card"
|
||||
type="button"
|
||||
:aria-label="t('episodeTranscode.previewFrame', { time: formatTimeMs(frame.timeMs) })"
|
||||
@click="openFramePreview(frame)"
|
||||
>
|
||||
<span class="episode-transcode-frames__image" :style="frameCardStyle(frame)"></span>
|
||||
<span>{{ formatTimeMs(frame.timeMs) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<el-empty v-else :description="t('episodeTranscode.noFrames')" :image-size="58" />
|
||||
<el-pagination
|
||||
v-if="framesTotal"
|
||||
class="episode-transcode-frames__pagination"
|
||||
background
|
||||
layout="sizes, prev, pager, next"
|
||||
:current-page="view.pageIndex"
|
||||
:page-size="view.pageSize"
|
||||
:page-sizes="THUMBNAIL_PAGE_SIZES"
|
||||
:total="framesTotal"
|
||||
@current-change="emit('pageChange', $event)"
|
||||
@size-change="emit('pageSizeChange', $event)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else-if="spriteUrls.length && sprite" class="episode-transcode-frames__sprites">
|
||||
<figure
|
||||
v-for="(url, index) in spriteUrls"
|
||||
:key="url"
|
||||
class="episode-transcode-frames__sprite"
|
||||
>
|
||||
<div
|
||||
class="episode-transcode-frames__sprite-frame"
|
||||
:style="{
|
||||
aspectRatio: `${sprite.columns * sprite.thumbWidth} / ${
|
||||
sprite.rows * sprite.thumbHeight
|
||||
}`
|
||||
}"
|
||||
>
|
||||
<el-image
|
||||
:src="url"
|
||||
:preview-src-list="spriteUrls"
|
||||
:initial-index="index"
|
||||
fit="contain"
|
||||
preview-teleported
|
||||
hide-on-click-modal
|
||||
/>
|
||||
<span
|
||||
class="episode-transcode-frames__sprite-grid"
|
||||
:style="{
|
||||
backgroundSize: `calc(100% / ${sprite.columns}) calc(100% / ${sprite.rows})`
|
||||
}"
|
||||
></span>
|
||||
</div>
|
||||
<figcaption>
|
||||
<b>{{ t('episodeTranscode.spriteSheet', { index: index + 1 }) }}</b>
|
||||
<span>{{
|
||||
t('episodeTranscode.spriteSheetFrames', { count: spriteFrameCount(index) })
|
||||
}}</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<el-empty v-else :description="t('episodeTranscode.noSprites')" :image-size="58" />
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
:title="t('episodeTranscode.zoomTitle')"
|
||||
width="min(92vw, 600px)"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div class="episode-transcode-frames__preview-shell">
|
||||
<div class="episode-transcode-frames__preview-image" :style="previewStyle"></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.episode-transcode-frames__heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 18px 0 10px;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__heading h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__tools {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__grid {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(4, 128px);
|
||||
}
|
||||
|
||||
.episode-transcode-frames__card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: zoom-in;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__image {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-fill-color-dark);
|
||||
background-repeat: no-repeat;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__card:hover .episode-transcode-frames__image {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.episode-transcode-frames__pagination {
|
||||
margin-top: 14px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__sprites {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
}
|
||||
|
||||
.episode-transcode-frames__sprite {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__sprite-frame {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--el-fill-color-dark);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__sprite-frame :deep(.el-image) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__sprite-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(to right, rgb(255 255 255 / 34%) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgb(255 255 255 / 34%) 1px, transparent 1px);
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__sprite figcaption {
|
||||
display: flex;
|
||||
padding-top: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__preview-shell {
|
||||
display: flex;
|
||||
min-height: 240px;
|
||||
background: var(--el-fill-color-dark);
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.episode-transcode-frames__preview-image {
|
||||
background-repeat: no-repeat;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.episode-transcode-frames__grid {
|
||||
grid-template-columns: repeat(2, 128px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user