@@ -15,6 +15,11 @@ 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 ]
const route = useRoute ( )
const { can } = useRbac ( )
const { t } = useI18n ( )
@@ -22,18 +27,29 @@ 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 showPagination = computed ( ( ) => {
if ( total . value <= 0 ) return false
return Math . ceil ( total . value / pageSize . value ) > 1
} )
async function loadDramas ( ) {
const res = await pageDramasApi ( { pageIndex : 1 , pageSize : 200 } )
dramas . value = res . data ? . list || [ ]
@@ -49,23 +65,73 @@ 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 || [ ]
const res = await listEpisodesApi ( {
dramaId : selectedDramaId . value ,
status : statusFilter . value || undefined ,
pageIndex : pageIndex . value ,
pageSize : pageSize . value
} )
episodes . value = res . data ? . list || [ ]
total . value = res . data ? . total || 0
await clampOrRetreatPage ( )
} finally {
loading . value = false
}
}
function nextNumber ( ) {
return episodes . value . reduce ( ( max , item ) => Math . max ( max , item . number || 0 ) , 0 ) + 1
/** 拉当前剧全集快照,供排序与批量建集集号计算。不过滤 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 list = res . data ? . list || [ ]
totalCount = Number ( res . data ? . total || 0 )
collected . push ( ... list )
if ( ! list . length ) break
page += 1
} while ( collected . length < totalCount )
snapshotEpisodes . value = collected
}
function openBatchUpload ( ) {
/**
* 删除后若当前页空且仍有数据,回退上一页再拉;否则夹页。
*/
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 snapshotEpisodes . value . reduce ( ( max , item ) => Math . max ( max , item . number || 0 ) , 0 ) + 1
}
async function openBatchUpload ( ) {
if ( ! selectedDramaId . value ) return
await refreshSnapshot ( )
batchUploadVisible . value = true
}
@@ -73,7 +139,8 @@ async function onBatchEpisodesCreated() {
await loadEpisodes ( )
}
function openCreate ( ) {
async function openCreate ( ) {
await refreshSnapshot ( )
formDialogRef . value ? . openCreate ( )
}
@@ -94,6 +161,9 @@ async function confirmDelete(row) {
}
await deleteEpisodeApi ( row . id )
ElMessage . success ( '剧集已删除' )
if ( episodes . value . length === 1 && pageIndex . value > 1 ) {
pageIndex . value -= 1
}
await loadEpisodes ( )
}
@@ -122,11 +192,32 @@ function stopThumb(event) {
}
}
/** 上移/下移后把新顺序整表提交,由后端重排 sort。 */
async function move ( index , offset ) {
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 globalIndex ( row ) > 0
}
function canMoveDown ( row ) {
const index = globalIndex ( row )
return index >= 0 && index < total . value - 1
}
/** 上移/下移后把快照新顺序整表提交,由后端重排 sort。 */
async function move ( row , offset ) {
await refreshSnapshot ( )
const index = snapshotEpisodes . value . findIndex ( ( item ) => item . id === row . id )
const target = index + offset
if ( 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 ) )
@@ -135,10 +226,26 @@ async function move(index, offset) {
}
async function onDramaChange ( ) {
pageIndex . value = 1
statusFilter . value = undefined
await loadEpisodes ( )
openBatchIfPendingSessions ( )
}
function onStatusChange ( ) {
pageIndex . value = 1
loadEpisodes ( )
}
function handleSizeChange ( ) {
pageIndex . value = 1
loadEpisodes ( )
}
function handlePageChange ( ) {
loadEpisodes ( )
}
/** 进入页面或切换短剧时,若有 24h 内未完成上传则打开批量上传弹窗以提示续传。 */
function openBatchIfPendingSessions ( ) {
if ( ! selectedDramaId . value || ! can ( 'episode:upload' ) ) return
@@ -177,7 +284,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
>
@@ -197,11 +304,23 @@ 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
> < h2 > 剧集列表 ( { { total } } ) < / h2 > < p > 按列表顺序播放 < / p > < / d i v
> < / d i v
>
< el-table
@@ -278,7 +397,7 @@ onMounted(async () => {
> < / e l - t a b l e - c o l u m n
>
< el-table-column label = "操作" width = "250" fixed = "right"
> < template # default = "{ row, $index }"
> < template # default = "{ row }"
> < div class = "table-actions"
> < el-button
v-if = "can('episode:upload')"
@@ -289,14 +408,14 @@ onMounted(async () => {
> < el-button
v-if = "can('episode:sort')"
size = "small"
: disabled = "$index === 0 "
@click ="move($index , -1)"
:disabled = "!canMoveUp(row) "
@click ="move(row , -1)"
> 上移 < / e l - b u t t o n
> < el-button
v-if = "can('episode:sort')"
size = "small"
: disabled = "$index === episodes.length - 1 "
@click ="move($index , 1)"
:disabled = "!canMoveDown(row) "
@click ="move(row , 1)"
> 下移 < / e l - b u t t o n
> < el-button
v-if = "can('episode:upload')"
@@ -309,6 +428,18 @@ onMounted(async () => {
> < / e l - t a b l e - c o l u m n
>
< / el-table >
< div v-if = "showPagination" class="pagination"
> < el-pagination
v -model :current-page = "pageIndex"
v -model :page-size = "pageSize"
:page-sizes = "PAGE_SIZE_OPTIONS"
:total = "total"
:pager-count = "7"
layout = "total, sizes, prev, pager, next, jumper"
background
@ size -change = " handleSizeChange "
@ current -change = " handlePageChange "
/ > < / div >
< / section >
< EpisodeFormDialog
ref = "formDialogRef"