From 092fe18ae7d4aa1ee2a46967d493edd93ce8d62f Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Wed, 21 Oct 2026 23:20:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=89=A7=E9=9B=86=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=94=B9=E4=B8=BA=E6=9C=8D=E5=8A=A1=E7=AB=AF=E5=88=86?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/episode.ts | 13 +- src/styles/business/style.css | 9 +- .../EpisodeManagementView.vue | 144 ++++++++++++++---- 3 files changed, 127 insertions(+), 39 deletions(-) diff --git a/src/api/admin/episode.ts b/src/api/admin/episode.ts index 6488c53..2c17d69 100644 --- a/src/api/admin/episode.ts +++ b/src/api/admin/episode.ts @@ -1,10 +1,17 @@ // Author: fruit.com.cn import request from '@/axios' -import type { EpisodeItem, EpisodeSaveData } from './types' +import type { EpisodeItem, EpisodeSaveData, PageResult } from './types' -export const listEpisodesApi = (dramaId: number, status?: string) => { - return request.get({ url: '/admin/episodes', params: { dramaId, status } }) +export interface EpisodePageParams { + dramaId: number + status?: string + pageIndex?: number + pageSize?: number +} + +export const listEpisodesApi = (params: EpisodePageParams) => { + return request.get>({ url: '/admin/episodes', params }) } export const createEpisodeApi = (data: EpisodeSaveData) => { diff --git a/src/styles/business/style.css b/src/styles/business/style.css index d08deb0..5ef09b3 100644 --- a/src/styles/business/style.css +++ b/src/styles/business/style.css @@ -560,13 +560,16 @@ body { gap: 6px; } -.drama-select { +.drama-select, +.status-select { width: 100%; } -.drama-select .el-input__wrapper { +.drama-select .el-input__wrapper, +.status-select .el-input__wrapper { box-shadow: 0 0 0 1px #efedf3 inset !important; } -.drama-select .el-input__inner { +.drama-select .el-input__inner, +.status-select .el-input__inner { font-size: 13px; } diff --git a/src/views/contentManagement/EpisodeManagementView.vue b/src/views/contentManagement/EpisodeManagementView.vue index b2cad80..4abd935 100644 --- a/src/views/contentManagement/EpisodeManagementView.vue +++ b/src/views/contentManagement/EpisodeManagementView.vue @@ -15,6 +15,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,29 +27,27 @@ 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(() => { - const total = episodes.value.length - if (total <= 0) return false - return Math.ceil(total / pageSize.value) > 1 + if (total.value <= 0) return false + return Math.ceil(total.value / pageSize.value) > 1 }) async function loadDramas() { @@ -65,31 +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 || [] - clampPageIndex() + 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 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 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 +} + +/** + * 删除后若当前页空且仍有数据,回退上一页再拉;否则夹页。 + */ +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 +139,8 @@ async function onBatchEpisodesCreated() { await loadEpisodes() } -function openCreate() { +async function openCreate() { + await refreshSnapshot() formDialogRef.value?.openCreate() } @@ -118,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() } @@ -146,25 +192,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)) @@ -174,12 +227,23 @@ async function move(row, 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 内未完成上传则打开批量上传弹窗以提示续传。 */ @@ -220,7 +284,7 @@ onMounted(async () => { 当前短剧 {{ selectedDrama?.title || '暂无短剧' }} 共 {{ selectedDrama?.episodes ?? episodes.length }} 集 · 收藏 + >共 {{ selectedDrama?.episodes ?? total }} 集 · 收藏 {{ formatCount(selectedDrama?.favoriteCount ?? 0) }} · 热度 {{ formatCount(selectedDrama?.views ?? 0) }} @@ -240,15 +304,28 @@ onMounted(async () => { :value="item.id" /> + 状态 + + + + +

剧集列表({{ episodes.length }})

按列表顺序播放

+ >

剧集列表({{ total }})

按列表顺序播放

{ 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" />