@@ -10,29 +10,56 @@
< div class = "video-selector" >
<!-- 搜索栏 -- >
< div class = "search-bar" >
< div class = "search-input-wrapper" >
< SearchOutlined class = "search-icon" / >
< a-input-search
v -model :value = "searchKeyword"
placeholder = "搜索 视频名称"
placeholder = "输入 视频名称进行搜索... "
allow -clear
@search ="handleSearch"
class = "search-input"
/ >
< / div >
< / div >
<!-- 视频网格 -- >
< div class = "video-grid" v-loading = "loading" >
< div class = "video-grid" >
<!-- 骨架屏加载状态 -- >
< a-skeleton
v-if = "loading"
v-for = "n in 6"
:key = "n"
:loading = "true"
:active = "true"
:avatar = "false"
:title = "false"
class = "video-card-skeleton"
>
< template # default >
< div class = "skeleton-thumbnail" > < / div >
< div class = "skeleton-content" >
< a-skeleton-title class = "skeleton-title" / >
< a-skeleton-paragraph class = "skeleton-meta" :rows = "1" / >
< / div >
< / template >
< / a-skeleton >
<!-- 视频列表 -- >
< div
v-for = "video in videoList"
:key = "video.id"
class = "video-card"
: class = "{ selected: selectedVideoId === video.id }"
@click ="selectVideo(video)"
role = "button"
: aria -label = " ` 选择视频 : $ { video.fileName } ` "
>
< div class = "video-thumbnail" >
< img
: src = "getVideoPreviewUrl(video) || defaultCover"
:alt = "video.fileName"
@error ="handleImageError"
loading = "lazy"
/ >
< div class = "video-duration" > { { formatDuration ( video . duration ) } } < / div >
< div class = "video-selected-mark" v-if = "selectedVideoId === video.id" >
@@ -40,7 +67,9 @@
< / div >
< / div >
< div class = "video-info" >
< div class = "video-title" :title = "video.fileName" > { { video . fileName } } < / div >
< div class = "video-title" :title = "video.fileName" >
< span class = "title-text" > { { video . fileName } } < / span >
< / div >
< div class = "video-meta" >
< span class = "meta-item" >
< VideoCameraOutlined / >
@@ -56,8 +85,22 @@
<!-- 空状态 -- >
< div v-if = "!loading && videoList.length === 0" class="empty-state" >
< div class = "empty-illustration" >
< div class = "empty-icon-wrapper" >
< PictureOutlined class = "empty-icon" / >
< p > { { searchKeyword ? '未找到匹配的视频' : '暂无视频,请先上传视频' } } < / p >
< / div >
< div class = "empty-text" >
< h3 class = "empty-title" >
{ { searchKeyword ? '未找到匹配的视频' : '暂无视频' } }
< / h3 >
< p class = "empty-description" >
{ { searchKeyword ? '尝试使用不同的关键词搜索' : '请先上传视频文件' } }
< / p >
< a-button v-if = "searchKeyword" type="link" @click="clearSearch" class="clear-search-btn" >
< CloseOutlined / > 清除搜索条件
< / a-button >
< / div >
< / div >
< / div >
< / div >
@@ -89,7 +132,14 @@
< script setup >
import { ref , computed , watch } from 'vue'
import { message } from 'ant-design-vue'
import { CheckOutlined , PictureOutlined , VideoCameraOutlined , ClockCircleOutlined } from '@ant-design/icons-vue'
import {
CheckOutlined ,
PictureOutlined ,
VideoCameraOutlined ,
ClockCircleOutlined ,
SearchOutlined ,
CloseOutlined
} from '@ant-design/icons-vue'
import { MaterialService } from '@/api/material'
const props = defineProps ( {
@@ -101,7 +151,6 @@ const props = defineProps({
const emit = defineEmits ( [ 'update:open' , 'select' ] )
// 状态管理
const visible = computed ( {
get : ( ) => props . open ,
set : ( val ) => emit ( 'update:open' , val )
@@ -116,10 +165,8 @@ const currentPage = ref(1)
const pageSize = ref ( 20 )
const total = ref ( 0 )
// 默认封面
const defaultCover = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjExMCIgdmlld0JveD0iMCAwIDIwMCAxMTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTEwIiBmaWxsPSIjMzc0MTUxIi8+CjxwYXRoIGQ9Ik04NSA0NUwxMTUgNjVMMTA1IDg1TDc1IDc1TDg1IDQ1WiIgZmlsbD0iIzU3MjY1MSIvPgo8L3N2Zz4K'
// 模态框标题
const modalTitle = '选择视频'
// 获取视频列表
@@ -148,13 +195,17 @@ const fetchVideoList = async () => {
}
}
// 搜索
const handleSearch = ( ) => {
currentPage . value = 1
fetchVideoList ( )
}
// 分页变化
const clearSearch = ( ) => {
searchKeyword . value = ''
currentPage . value = 1
fetchVideoList ( )
}
const handlePageChange = ( page , size ) => {
currentPage . value = page
if ( size ) {
@@ -163,26 +214,21 @@ const handlePageChange = (page, size) => {
fetchVideoList ( )
}
// 每页数量变化
const handlePageSizeChange = ( _current , size ) => {
// _current 参数未使用,但需要保留以匹配事件处理器签名
currentPage . value = 1
pageSize . value = size
fetchVideoList ( )
}
// 选择视频
const selectVideo = ( video ) => {
selectedVideoId . value = video . id
selectedVideo . value = video
}
// 图片加载错误处理
const handleImageError = ( event ) => {
event . target . src = defaultCover
}
// 格式化时长
const formatDuration = ( seconds ) => {
if ( ! seconds ) return '--:--'
const minutes = Math . floor ( seconds / 60 )
@@ -190,7 +236,6 @@ const formatDuration = (seconds) => {
return ` ${ String ( minutes ) . padStart ( 2 , '0' ) } : ${ String ( remainingSeconds ) . padStart ( 2 , '0' ) } `
}
// 格式化文件大小
const formatFileSize = ( bytes ) => {
if ( ! bytes ) return '0 B'
const units = [ 'B' , 'KB' , 'MB' , 'GB' ]
@@ -203,32 +248,25 @@ const formatFileSize = (bytes) => {
return ` ${ size . toFixed ( 1 ) } ${ units [ unitIndex ] } `
}
// 获取视频预览URL( 优先使用base64, 然后是URL)
const getVideoPreviewUrl = ( video ) => {
// 优先使用 coverBase64( 如果存在)
if ( video . coverBase64 ) {
// 确保 base64 有正确的前缀
if ( ! video . coverBase64 . startsWith ( 'data:' ) ) {
return ` data:image/jpeg;base64, ${ video . coverBase64 } `
}
return video . coverBase64
}
// 其次使用 previewUrl
if ( video . previewUrl ) {
return video . previewUrl
}
// 最后使用 coverUrl
if ( video . coverUrl ) {
return video . coverUrl
}
// 返回默认封面
return defaultCover
}
// 取消
const handleCancel = ( ) => {
visible . value = false
selectedVideoId . value = null
@@ -236,7 +274,6 @@ const handleCancel = () => {
searchKeyword . value = ''
}
// 确认
const handleConfirm = ( ) => {
if ( ! selectedVideo . value ) {
message . warning ( '请选择一个视频' )
@@ -247,9 +284,10 @@ const handleConfirm = () => {
handleCancel ( )
}
// 监听visible变化
watch ( ( ) => props . open , ( newVal ) => {
if ( newVal ) {
// 监听modal打开
watch ( ( ) => props . open , ( isOpen ) => {
if ( isOpen ) {
videoList . value = [ ]
selectedVideoId . value = null
selectedVideo . value = null
currentPage . value = 1
@@ -265,16 +303,58 @@ watch(() => props.open, (newVal) => {
gap : 16 px ;
}
/* 搜索栏样式 */
. search - bar {
padding : 16 px ;
background : rgba ( 0 , 0 , 0 , 0.2 ) ;
background : # f9fafb ;
border - radius : 8 px ;
}
. search - input {
width : 100 % ;
. search - input - wrapper {
position : relative ;
display : flex ;
align - items : center ;
}
. search - icon {
position : absolute ;
left : 12 px ;
z - index : 100 ;
color : # 9 ca3af ;
font - size : 16 px ;
}
. search - input : deep ( . ant - input ) {
padding - left : 36 px ;
padding - right : 12 px ;
height : 40 px ;
background : # ffffff ;
border - radius : 6 px ;
color : # 111827 ;
font - size : 14 px ;
}
. search - input : deep ( . ant - input - group ) ,
. search - input : deep ( . ant - input - group - wrapper ) ,
. search - input : deep ( . ant - input - group - addon ) {
display : block ;
}
. search - input : deep ( . ant - input - group - addon ) {
display : none ;
}
. search - input : deep ( . ant - input ) : focus ,
. search - input : deep ( . ant - input - affix - wrapper - focused ) {
border - color : # 3 b82f6 ;
box - shadow : 0 0 0 2 px rgba ( 59 , 130 , 246 , 0.2 ) ;
}
. search - input : deep ( . ant - input : : placeholder ) {
color : # 9 ca3af ;
}
/* 视频网格样式 */
. video - grid {
display : grid ;
grid - template - columns : repeat ( auto - fill , minmax ( 200 px , 1 fr ) ) ;
@@ -284,33 +364,57 @@ watch(() => props.open, (newVal) => {
padding : 4 px ;
}
. video - grid : : - webkit - scrollbar {
width : 6 px ;
}
. video - grid : : - webkit - scrollbar - track {
background : # f3f4f6 ;
border - radius : 3 px ;
}
. video - grid : : - webkit - scrollbar - thumb {
background : # d1d5db ;
border - radius : 3 px ;
}
. video - grid : : - webkit - scrollbar - thumb : hover {
background : # 9 ca3af ;
}
/* 视频卡片样式 */
. video - card {
background : rgba ( 0 , 0 , 0 , 0.3 ) ;
border : 2 px solid rgba ( 59 , 130 , 246 , 0.2 ) ;
border - radius : 12 px ;
background : # ffffff ;
border : 3 px solid transparent ;
border - radius : 8 px ;
overflow : hidden ;
cursor : pointer ;
transition : all 0.3 s ;
transition : all 0.2 s ;
box - shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. video - card : hover {
border - color : rgba ( 59 , 130 , 246 , 0.5 ) ;
transform : translateY ( - 2 px ) ;
box - shadow : 0 4 px 12 px rgba ( 59 , 130 , 246 , 0.3 ) ;
border - color : # 3 b82f6 ;
box - shadow : 0 4 px 12 px rgba ( 59 , 130 , 246 , 0.15 ) ;
}
. video - card . selected {
border - color : # 3 B82F6 ;
background : rgba ( 59 , 130 , 246 , 0.1 ) ;
box - shadow : 0 0 0 3 px rgba ( 59 , 130 , 246 , 0.2 ) ;
box - shadow : 0 0 0 2 px rgba ( 59 , 130 , 246 , 0.25 ) ;
}
. video - card : focus - visible {
border - color : # 3 B82F6 ;
box - shadow : 0 0 0 2 px rgba ( 59 , 130 , 246 , 0.2 ) ;
}
/* 缩略图样式 */
. video - thumbnail {
position : relative ;
width : 100 % ;
height : 112 px ;
overflow : hidden ;
background : # 374151 ;
background : # f3f4f6 ;
}
. video - thumbnail img {
@@ -325,8 +429,8 @@ watch(() => props.open, (newVal) => {
right : 8 px ;
background : rgba ( 0 , 0 , 0 , 0.8 ) ;
color : # fff ;
padding : 2 px 6 px ;
border - radius : 4 px ;
padding : 4 px 8 px ;
border - radius : 6 px ;
font - size : 12 px ;
font - weight : 600 ;
}
@@ -346,15 +450,16 @@ watch(() => props.open, (newVal) => {
font - size : 14 px ;
}
/* 信息区域样式 */
. video - info {
padding : 12 px ;
padding : 14 px ;
}
. video - title {
font - size : 14 px ;
font - weight : 600 ;
color : # fff ;
margin - bottom : 8 px ;
color : # 111827 ;
margin - bottom : 10 px ;
overflow : hidden ;
text - overflow : ellipsis ;
white - space : nowrap ;
@@ -364,7 +469,7 @@ watch(() => props.open, (newVal) => {
display : flex ;
gap : 12 px ;
font - size : 12 px ;
color : # 94 a3b8 ;
color : # 6 b7280 ;
}
. meta - item {
@@ -373,6 +478,7 @@ watch(() => props.open, (newVal) => {
gap : 4 px ;
}
/* 空状态样式 */
. empty - state {
grid - column : 1 / - 1 ;
display : flex ;
@@ -380,27 +486,185 @@ watch(() => props.open, (newVal) => {
align - items : center ;
justify - content : center ;
padding : 60 px 20 px ;
color : # 94 a3b8 ;
}
. empty - icon {
font - size : 48 px ;
margin - bottom : 16 px ;
color : # 6 b7280 ;
}
. empty - illustration {
text - align : center ;
}
. empty - icon - wrapper {
width : 80 px ;
height : 80 px ;
margin : 0 auto 16 px ;
background : # f3f4f6 ;
border - radius : 50 % ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. empty - icon {
font - size : 36 px ;
color : # 9 ca3af ;
}
. empty - text {
max - width : 400 px ;
}
. empty - title {
font - size : 16 px ;
font - weight : 600 ;
color : # 111827 ;
margin : 0 0 8 px 0 ;
}
. empty - description {
font - size : 14 px ;
color : # 6 b7280 ;
margin : 0 ;
}
. clear - search - btn {
margin - top : 12 px ;
color : # 3 b82f6 ;
font - weight : 500 ;
}
. clear - search - btn : hover {
color : # 60 a5fa ;
}
/* 分页样式 */
. pagination - wrapper {
display : flex ;
justify - content : center ;
padding : 16 px 0 ;
border - top : 1 px solid rgba ( 59 , 130 , 246 , 0.1 ) ;
border - top : 1 px solid # e5e7eb ;
background : # f9fafb ;
border - radius : 8 px ;
margin - top : 8 px ;
}
/* 底部操作栏样式 */
. modal - footer {
display : flex ;
justify - content : flex - end ;
gap : 12 px ;
padding - top : 16 px ;
border - top : 1 px solid rgba ( 59 , 130 , 246 , 0.1 ) ;
padding : 16 px 20 px ;
border - top : 1 px solid # e5e7eb ;
background : # f9fafb ;
border - radius : 8 px ;
margin - top : 8 px ;
}
/* 响应式设计 */
@ media ( max - width : 768 px ) {
. video - grid {
grid - template - columns : repeat ( auto - fill , minmax ( 160 px , 1 fr ) ) ;
gap : 12 px ;
}
. video - thumbnail {
height : 100 px ;
}
. video - info {
padding : 10 px ;
}
. video - title {
font - size : 13 px ;
}
. video - meta {
gap : 8 px ;
font - size : 11 px ;
}
. search - bar {
padding : 12 px ;
}
. modal - footer {
padding : 12 px 0 ;
}
}
@ media ( max - width : 480 px ) {
. video - grid {
grid - template - columns : repeat ( auto - fill , minmax ( 140 px , 1 fr ) ) ;
gap : 10 px ;
}
. video - thumbnail {
height : 90 px ;
}
. video - info {
padding : 8 px ;
}
. search - input : deep ( . ant - input ) {
height : 36 px ;
font - size : 13 px ;
}
. modal - footer . ant - btn {
height : 32 px ;
padding : 0 12 px ;
font - size : 13 px ;
}
}
/* 骨架屏样式 */
. video - card - skeleton {
background : # ffffff ;
border : 2 px solid transparent ;
border - radius : 8 px ;
overflow : hidden ;
}
. skeleton - thumbnail {
width : 100 % ;
height : 112 px ;
background : # f3f4f6 ;
}
. skeleton - content {
padding : 14 px ;
}
. skeleton - title {
margin - bottom : 10 px ;
}
. skeleton - meta : deep ( . ant - skeleton - paragraph > li ) {
height : 16 px ;
width : 60 px ;
background : # f3f4f6 ;
border - radius : 4 px ;
}
@ media ( max - width : 768 px ) {
. skeleton - thumbnail {
height : 100 px ;
}
. skeleton - content {
padding : 10 px ;
}
}
@ media ( max - width : 480 px ) {
. skeleton - thumbnail {
height : 90 px ;
}
. skeleton - content {
padding : 8 px ;
}
. skeleton - meta : deep ( . ant - skeleton - paragraph > li ) {
width : 50 px ;
}
}
< / style >