@@ -6,7 +6,12 @@ import { useRoute } from 'vue-router'
import { ElMessage , ElMessageBox } from 'element-plus'
import { AppIcons } from '@/constants/icons'
import { pageDramasApi } from '@/api/admin/drama'
import { deleteEpisodeApi , listEpisodesApi , sortEpisodesApi } from '@/api/admin/episode'
import {
deleteEpisodeApi ,
listEpisodesApi ,
parseEpisodePage ,
sortEpisodesApi
} from '@/api/admin/episode'
import { useRbac } from '@/stores/rbac'
import { formatCount } from '@/utils/format'
import { statusClass } from '@/utils/status'
@@ -15,6 +20,8 @@ import BatchUploadDialog from './upload/BatchUploadDialog.vue'
import EpisodeFormDialog from './upload/EpisodeFormDialog.vue'
import { listPendingSessions } from './upload/uploadSessions'
/** 排序/批量建集需要全集快照,单次请求不超过接口 pageSize 上限。 */
const SNAPSHOT _PAGE _SIZE = 200
const DEFAULT _PAGE _SIZE = 100
const PAGE _SIZE _OPTIONS = [ 50 , 100 , 200 ]
@@ -25,30 +32,30 @@ const { t } = useI18n()
const loading = ref ( false )
const dramas = ref ( [ ] )
const episodes = ref ( [ ] )
const total = ref ( 0 )
const selectedDramaId = ref ( undefined )
const statusFilter = ref ( undefined )
const pageIndex = ref ( 1 )
const pageSize = ref ( DEFAULT _PAGE _SIZE )
const batchUploadVisible = ref ( false )
const previewVisible = ref ( false )
const previewEpisode = ref ( null )
const formDialogRef = ref ( )
/** 上移/下移与批量建集用的全集快照。 */
const snapshotEpisodes = ref ( [ ] )
const batchNextNumber = computed ( ( ) => nextNumber ( ) )
const selectedDrama = computed ( ( ) => dramas . value . find ( ( item ) => item . id === selectedDramaId . value ) )
const existingEpisodeNumbers = computed ( ( ) =>
episodes . value . map ( ( item ) => Number ( item . number ) ) . filter ( ( n ) => Number . isFinite ( n ) )
snapshotEpisodes . value . map ( ( item ) => Number ( item . number ) ) . filter ( ( n ) => Number . isFinite ( n ) )
)
const pagedEpisodes = computed ( ( ) => {
const start = ( pageIndex . value - 1 ) * pageSize . value
return episodes . value . slice ( start , start + pageSize . value )
const showPagination = computed ( ( ) => {
if ( total . value <= 0 ) return false
return Math . ceil ( total . value / pageSize . value ) > 1
} )
const showPagination = computed ( ( ) => {
const total = episodes . value . length
if ( total <= 0 ) return false
return Math . ceil ( total / pageSize . value ) > 1
} )
const showEmpty = computed ( ( ) => ! loading . value && episodes . value . length === 0 )
async function loadDramas ( ) {
const res = await pageDramasApi ( { pageIndex : 1 , pageSize : 200 } )
@@ -65,31 +72,75 @@ async function loadDramas() {
async function loadEpisodes ( ) {
if ( ! selectedDramaId . value ) {
episodes . value = [ ]
total . value = 0
snapshotEpisodes . value = [ ]
return
}
loading . value = true
try {
const res = await listEpisodesApi ( selectedDramaId . value )
episodes . value = res . data || [ ]
clampPageIndex ( )
const res = await listEpisodesApi ( {
dramaId : selectedDramaId . value ,
status : statusFilter . value || undefined ,
pageIndex : pageIndex . value ,
pageSize : pageSize . value
} )
// 按 OpenAPI PageResult{total,list} 解包,禁止再把 data 当数组切片
const page = parseEpisodePage ( res . data )
episodes . value = page . list
total . value = page . total
await clampOrRetreatPage ( )
} finally {
loading . value = false
}
}
/** 刷新后若当前页越界(如删除末页最后一条),夹到最后一页 。 */
function clampPageIndex ( ) {
const total = episodes . value . length
const maxPage = Math . max ( 1 , Math . ceil ( total / pageSize . value ) || 1 )
if ( pageIndex . value > maxPage ) pageIndex . value = maxPage
/** 拉当前剧全集快照,供排序与批量建集集号计算。不过滤 status,按页取完 。 */
async function refreshSnapshot ( ) {
if ( ! selectedDramaId . value ) {
snapshotEpisodes . value = [ ]
return
}
const collected = [ ]
let page = 1
let totalCount = 0
do {
const res = await listEpisodesApi ( {
dramaId : selectedDramaId . value ,
pageIndex : page ,
pageSize : SNAPSHOT _PAGE _SIZE
} )
const parsed = parseEpisodePage ( res . data )
totalCount = parsed . total
collected . push ( ... parsed . list )
if ( ! parsed . list . length ) break
page += 1
} while ( collected . length < totalCount )
snapshotEpisodes . value = collected
}
/**
* 删除后若当前页空且仍有数据,回退上一页再拉;否则夹页。
*/
async function clampOrRetreatPage ( ) {
const maxPage = Math . max ( 1 , Math . ceil ( total . value / pageSize . value ) || 1 )
if ( episodes . value . length === 0 && total . value > 0 && pageIndex . value > 1 ) {
pageIndex . value = Math . min ( pageIndex . value - 1 , maxPage )
await loadEpisodes ( )
return
}
if ( pageIndex . value > maxPage ) {
pageIndex . value = maxPage
await loadEpisodes ( )
}
}
function nextNumber ( ) {
return episodes . value . reduce ( ( max , item ) => Math . max ( max , item . number || 0 ) , 0 ) + 1
return snapshotEpisodes . value . reduce ( ( max , item ) => Math . max ( max , item . number || 0 ) , 0 ) + 1
}
function openBatchUpload ( ) {
async function openBatchUpload ( ) {
if ( ! selectedDramaId . value ) return
await refreshSnapshot ( )
batchUploadVisible . value = true
}
@@ -97,7 +148,8 @@ async function onBatchEpisodesCreated() {
await loadEpisodes ( )
}
function openCreate ( ) {
async function openCreate ( ) {
await refreshSnapshot ( )
formDialogRef . value ? . openCreate ( )
}
@@ -118,6 +170,9 @@ async function confirmDelete(row) {
}
await deleteEpisodeApi ( row . id )
ElMessage . success ( '剧集已删除' )
if ( episodes . value . length === 1 && pageIndex . value > 1 ) {
pageIndex . value -= 1
}
await loadEpisodes ( )
}
@@ -146,25 +201,32 @@ function stopThumb(event) {
}
}
function episodeIndexInFullList ( row ) {
function pageLocalIndex ( row ) {
return episodes . value . findIndex ( ( item ) => item . id === row . id )
}
function globalIndex ( row ) {
const local = pageLocalIndex ( row )
if ( local < 0 ) return - 1
return ( pageIndex . value - 1 ) * pageSize . value + local
}
function canMoveUp ( row ) {
return episodeIndexInFullList ( row ) > 0
return globalIndex ( row ) > 0
}
function canMoveDown ( row ) {
const index = episodeIndexInFullList ( row )
return index >= 0 && index < episodes . value . length - 1
const index = globalIndex ( row )
return index >= 0 && index < total . value - 1
}
/** 上移/下移后把新顺序整表提交,由后端重排 sort。 */
/** 上移/下移后把快照 新顺序整表提交,由后端重排 sort。 */
async function move ( row , offset ) {
const index = episodeIndexInFullList ( row )
await refreshSnapshot ( )
const index = snapshotEpisodes . value . findIndex ( ( item ) => item . id === row . id )
const target = index + offset
if ( index < 0 || target < 0 || target >= episodes . value . length ) return
const next = [ ... episodes . value ]
if ( index < 0 || target < 0 || target >= snapshotEpisodes . value . length ) return
const next = [ ... snapshotEpisodes . value ]
const [ moved ] = next . splice ( index , 1 )
next . splice ( target , 0 , moved )
await sortEpisodesApi ( next . map ( ( item ) => item . id ) )
@@ -178,8 +240,18 @@ async function onDramaChange() {
openBatchIfPendingSessions ( )
}
function onStatusChange ( ) {
pageIndex . value = 1
loadEpisodes ( )
}
function handleSizeChange ( ) {
pageIndex . value = 1
loadEpisodes ( )
}
function handlePageChange ( ) {
loadEpisodes ( )
}
/** 进入页面或切换短剧时,若有 24h 内未完成上传则打开批量上传弹窗以提示续传。 */
@@ -220,7 +292,7 @@ onMounted(async () => {
< span class = "episode-context-label" > 当前短剧 < / span >
< b class = "episode-context-title" > { { selectedDrama ? . title || '暂无短剧' } } < / b >
< small class = "episode-context-count"
> 共 { { selectedDrama ? . episodes ? ? episodes . length } } 集 · 收藏
> 共 { { selectedDrama ? . episodes ? ? total } } 集 · 收藏
{ { formatCount ( selectedDrama ? . favoriteCount ? ? 0 ) } } · 热度
{ { formatCount ( selectedDrama ? . views ? ? 0 ) } } < / s m a l l
>
@@ -240,22 +312,40 @@ onMounted(async () => {
:value = "item.id"
/ >
< / el-select >
< span class = "episode-context-label" > 状态 < / span >
< el-select
v-model = "statusFilter"
clearable
placeholder = "全部状态"
class = "status-select"
@change ="onStatusChange"
>
< el-option label = "草稿" value = "draft" / >
< el-option label = "已上线" value = "published" / >
< el-option label = "已下线" value = "offline" / >
< / el-select >
< / div >
< / div >
< div class = "episode-toolbar"
> < div
> < h2 > 剧集列表 ( { { episodes . length } } ) < / h2 > < p > 按列表顺序播放 < / p > < / d i v
> < / div >
> < h2 > 剧集列表 ( { { total } } ) < / h2 > < p > 按列表顺序播放 < / p > < / d i v
> < / d i v
>
< el-table
v-loading = "loading"
:data = "pagedE pisodes"
:data = "e pisodes"
row -key = " id "
empty -text = " 该短剧暂无剧集 "
class = "drama-el-table episode-el-table"
>
< template # empty >
< el-empty
v-if = "showEmpty"
: description = "selectedDramaId ? '该短剧暂无剧集' : '请先选择短剧'"
/ >
< / template >
< el-table-column label = "集数" width = "90"
> < template # default = "{ row }" > { {
String ( row . number ) . padStart ( 2 , '0' )
String ( row . number ? ? 0 ) . padStart ( 2 , '0' )
} } < / template > < / e l - t a b l e - c o l u m n
>
< el-table-column label = "剧集名称" min -width = " 240 "
@@ -356,11 +446,12 @@ onMounted(async () => {
v -model :current-page = "pageIndex"
v -model :page-size = "pageSize"
:page-sizes = "PAGE_SIZE_OPTIONS"
:total = "episodes.length "
:total = "total "
:pager-count = "7"
layout = "total, sizes, prev, pager, next, jumper"
background
@ size -change = " handleSizeChange "
@ current -change = " handlePageChange "
/ > < / div >
< / section >
< EpisodeFormDialog