361 lines
11 KiB
JavaScript
361 lines
11 KiB
JavaScript
// Author: fruit.com.cn
|
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import {
|
|
addEpisodeApi,
|
|
attachAssetApi,
|
|
deleteEpisodeApi,
|
|
getMyDramaApi,
|
|
listEpisodesApi,
|
|
listSubmissionsApi,
|
|
sortEpisodesApi,
|
|
submitDramaApi,
|
|
updateDramaApi,
|
|
updateEpisodeApi,
|
|
withdrawSubmissionApi
|
|
} from '@/api/admin/creatorCenter'
|
|
import { dateCell } from '@/utils/format'
|
|
import { probeVideoDuration, resumeUpload, uploadFile } from '@/utils/uploader'
|
|
import {
|
|
DRAMA_COVER_PORTRAIT,
|
|
EPISODE_COVER,
|
|
compressForPreset
|
|
} from '@/views/contentManagement/upload/imagePresets'
|
|
import { submissionStatusMeta, submissionTypeText, submitOptions } from './useCreatorWorkspace'
|
|
import { useCreatorAccess } from './useCreatorAccess'
|
|
|
|
/** 中断的上传会话按「作品 + 文件名 + 大小」记住,刷新后还能接着传 */
|
|
const RESUME_KEY = 'creator-upload-sessions'
|
|
|
|
function readSessions() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(RESUME_KEY) || '{}')
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function writeSession(key, value) {
|
|
const all = readSessions()
|
|
if (value) all[key] = value
|
|
else delete all[key]
|
|
localStorage.setItem(RESUME_KEY, JSON.stringify(all))
|
|
}
|
|
|
|
export function useDramaEdit() {
|
|
const { ensureAccess, impersonating } = useCreatorAccess()
|
|
const route = useRoute()
|
|
// 作品 id 是字符串(雪花 id 过 Number 会被四舍五入),按字符串比对
|
|
const dramaId = route.params.id ? String(route.params.id) : ''
|
|
|
|
const loading = ref(false)
|
|
const drama = ref(null)
|
|
const episodes = ref([])
|
|
const submissions = ref([])
|
|
const savingInfo = ref(false)
|
|
const infoForm = reactive({
|
|
title: '',
|
|
subtitle: '',
|
|
genre: '',
|
|
description: '',
|
|
cover: '',
|
|
trailerUrl: '',
|
|
language: 'zh-CN'
|
|
})
|
|
|
|
/** 只有草稿与已下线能改(与后端 requireEditable 一致),其余状态页面转只读 */
|
|
const editable = computed(() => {
|
|
// 代看只读:整页禁用,与后端 requireWritable 的边界保持一致
|
|
if (impersonating.value) return false
|
|
return drama.value?.status === 'draft' || drama.value?.status === 'offline'
|
|
})
|
|
const readonlyReason = computed(() => {
|
|
if (impersonating.value) return '代看模式只读:可以查看作品与分集,但不能编辑、上传或提交'
|
|
switch (drama.value?.status) {
|
|
case 'published':
|
|
return '已上线的作品只读,要改内容请先提交「申请下线」'
|
|
case 'reviewing':
|
|
return '提交正在审核中,撤回后即可继续编辑'
|
|
case 'approved':
|
|
return '作品已通过审核待发布,暂不可编辑'
|
|
default:
|
|
return ''
|
|
}
|
|
})
|
|
|
|
async function loadAll() {
|
|
loading.value = true
|
|
try {
|
|
const [detail, episodeList, submissionList] = await Promise.all([
|
|
getMyDramaApi(dramaId),
|
|
listEpisodesApi(dramaId),
|
|
listSubmissionsApi(dramaId)
|
|
])
|
|
drama.value = detail.data || null
|
|
episodes.value = episodeList.data || []
|
|
submissions.value = submissionList.data || []
|
|
Object.assign(infoForm, {
|
|
title: drama.value?.title || '',
|
|
subtitle: drama.value?.subtitle || '',
|
|
genre: drama.value?.genre || '',
|
|
description: drama.value?.description || '',
|
|
cover: drama.value?.cover || '',
|
|
trailerUrl: drama.value?.trailerUrl || '',
|
|
language: drama.value?.language || 'zh-CN'
|
|
})
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadEpisodes() {
|
|
episodes.value = (await listEpisodesApi(dramaId)).data || []
|
|
const detail = await getMyDramaApi(dramaId)
|
|
drama.value = detail.data || drama.value
|
|
}
|
|
|
|
async function saveInfo() {
|
|
if (!infoForm.title.trim()) return ElMessage.warning('请填写作品名称')
|
|
savingInfo.value = true
|
|
try {
|
|
await updateDramaApi(dramaId, {
|
|
title: infoForm.title.trim(),
|
|
subtitle: infoForm.subtitle,
|
|
genre: infoForm.genre,
|
|
description: infoForm.description,
|
|
cover: infoForm.cover,
|
|
trailerUrl: infoForm.trailerUrl,
|
|
language: infoForm.language
|
|
})
|
|
ElMessage.success('作品信息已保存')
|
|
await loadAll()
|
|
} finally {
|
|
savingInfo.value = false
|
|
}
|
|
}
|
|
|
|
// ---------- 分集 ----------
|
|
const episodeVisible = ref(false)
|
|
const episodeSaving = ref(false)
|
|
const episodeEditingId = ref(null)
|
|
const episodeForm = reactive({ title: '', durationSec: 0, cover: '', epNo: undefined })
|
|
|
|
function openEpisode(row) {
|
|
episodeEditingId.value = row?.id ?? null
|
|
Object.assign(episodeForm, {
|
|
title: row?.title ?? '',
|
|
durationSec: row?.durationSec ?? 0,
|
|
cover: row?.cover ?? '',
|
|
epNo: row?.number ?? undefined
|
|
})
|
|
episodeVisible.value = true
|
|
}
|
|
|
|
async function saveEpisode() {
|
|
episodeSaving.value = true
|
|
try {
|
|
if (episodeEditingId.value) {
|
|
await updateEpisodeApi(dramaId, episodeEditingId.value, {
|
|
title: episodeForm.title.trim(),
|
|
durationSec: episodeForm.durationSec || 0,
|
|
cover: episodeForm.cover,
|
|
epNo: episodeForm.epNo
|
|
})
|
|
} else {
|
|
// 新增不传集号:后端按当前最大集号往后排
|
|
await addEpisodeApi(dramaId, {
|
|
title: episodeForm.title.trim(),
|
|
durationSec: episodeForm.durationSec || 0,
|
|
cover: episodeForm.cover
|
|
})
|
|
}
|
|
ElMessage.success('分集已保存')
|
|
episodeVisible.value = false
|
|
await loadEpisodes()
|
|
} finally {
|
|
episodeSaving.value = false
|
|
}
|
|
}
|
|
|
|
async function removeEpisode(row) {
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
`删除第 ${row.number} 集「${row.title || '未命名'}」?`,
|
|
'删除分集',
|
|
{
|
|
confirmButtonText: '确认删除',
|
|
cancelButtonText: '取消',
|
|
type: 'warning',
|
|
confirmButtonClass: 'el-button--danger'
|
|
}
|
|
)
|
|
} catch {
|
|
return
|
|
}
|
|
await deleteEpisodeApi(dramaId, row.id)
|
|
ElMessage.success('已删除')
|
|
await loadEpisodes()
|
|
}
|
|
|
|
/** 上移 / 下移:排序接口收的是"排好序的完整 id 列表",本地换位后整体提交 */
|
|
async function moveEpisode(row, offset) {
|
|
const list = [...episodes.value]
|
|
const index = list.findIndex((item) => item.id === row.id)
|
|
const target = index + offset
|
|
if (index < 0 || target < 0 || target >= list.length) return
|
|
const [moved] = list.splice(index, 1)
|
|
list.splice(target, 0, moved)
|
|
episodes.value = list
|
|
await sortEpisodesApi(
|
|
dramaId,
|
|
list.map((item) => item.id)
|
|
)
|
|
ElMessage.success('顺序已调整')
|
|
await loadEpisodes()
|
|
}
|
|
|
|
// ---------- 上传(复用分片上传,支持断点续传) ----------
|
|
const uploadingId = ref(null)
|
|
const uploadPercent = ref(0)
|
|
const resumeHint = ref('')
|
|
|
|
function sessionKey(file) {
|
|
return `${dramaId}:${file.name}:${file.size}`
|
|
}
|
|
|
|
function findResumeSession(file) {
|
|
return readSessions()[sessionKey(file)]?.uploadId || ''
|
|
}
|
|
|
|
function hasResumeSession(file) {
|
|
return Boolean(findResumeSession(file))
|
|
}
|
|
|
|
/** 上传视频并挂到该分集:有中断会话就接着传,否则新起一个会话 */
|
|
async function uploadVideo(row, file) {
|
|
uploadingId.value = row.id
|
|
uploadPercent.value = 0
|
|
const key = sessionKey(file)
|
|
const uploadId = findResumeSession(file)
|
|
try {
|
|
const durationSec = await probeVideoDuration(file)
|
|
const asset = uploadId
|
|
? await resumeUpload(file, uploadId, {
|
|
durationSec,
|
|
onProgress: (percent) => (uploadPercent.value = percent)
|
|
})
|
|
: await uploadFile(file, {
|
|
bizType: 'EPISODE',
|
|
durationSec,
|
|
onProgress: (percent) => (uploadPercent.value = percent),
|
|
// 会话一建立就记下来:中途断网 / 关页面后还能接着传
|
|
onSession: (id) => writeSession(key, { uploadId: id })
|
|
})
|
|
writeSession(key, null)
|
|
await attachAssetApi(dramaId, row.id, asset.id)
|
|
resumeHint.value = ''
|
|
ElMessage.success('视频已上传并挂到该分集')
|
|
await loadEpisodes()
|
|
} catch {
|
|
resumeHint.value = `「${file.name}」上传中断,进度已记住;重新选择同一个文件即可续传`
|
|
} finally {
|
|
uploadingId.value = null
|
|
uploadPercent.value = 0
|
|
}
|
|
}
|
|
|
|
/** 上传封面(作品封面或分集封面):图片走同一个分片上传接口,bizType=DRAMA_COVER */
|
|
async function uploadCover(row, file) {
|
|
uploadingId.value = row ? row.id : -1
|
|
uploadPercent.value = 0
|
|
try {
|
|
const preset = row ? EPISODE_COVER : DRAMA_COVER_PORTRAIT
|
|
const compressed = await compressForPreset(file, preset)
|
|
const asset = await uploadFile(compressed, {
|
|
bizType: 'DRAMA_COVER',
|
|
onProgress: (percent) => (uploadPercent.value = percent)
|
|
})
|
|
if (row) {
|
|
await updateEpisodeApi(dramaId, row.id, { cover: asset.url })
|
|
ElMessage.success('分集封面已更新')
|
|
await loadEpisodes()
|
|
} else {
|
|
infoForm.cover = asset.url
|
|
ElMessage.success('封面已上传,保存作品信息后生效')
|
|
}
|
|
} finally {
|
|
uploadingId.value = null
|
|
uploadPercent.value = 0
|
|
}
|
|
}
|
|
|
|
// ---------- 提交审核 ----------
|
|
async function submit(type) {
|
|
if (type === 'OFFLINE') {
|
|
try {
|
|
await ElMessageBox.confirm(
|
|
'申请下线后由后台审核,通过后内容才下线。确认提交?',
|
|
'申请下线',
|
|
{
|
|
confirmButtonText: '确认提交',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}
|
|
)
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
await submitDramaApi(dramaId, type)
|
|
ElMessage.success('已提交,等待后台审核')
|
|
await loadAll()
|
|
}
|
|
|
|
async function withdraw(submissionId) {
|
|
await withdrawSubmissionApi(submissionId)
|
|
ElMessage.success('已撤回,作品回到草稿')
|
|
await loadAll()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!(await ensureAccess())) return
|
|
await loadAll()
|
|
})
|
|
|
|
return {
|
|
dramaId,
|
|
loading,
|
|
drama,
|
|
episodes,
|
|
submissions,
|
|
editable,
|
|
readonlyReason,
|
|
infoForm,
|
|
savingInfo,
|
|
episodeVisible,
|
|
episodeSaving,
|
|
episodeEditingId,
|
|
episodeForm,
|
|
uploadingId,
|
|
uploadPercent,
|
|
resumeHint,
|
|
submitOptions,
|
|
submissionStatusMeta,
|
|
submissionTypeText,
|
|
dateCell,
|
|
loadAll,
|
|
saveInfo,
|
|
openEpisode,
|
|
saveEpisode,
|
|
removeEpisode,
|
|
moveEpisode,
|
|
uploadVideo,
|
|
uploadCover,
|
|
hasResumeSession,
|
|
submit,
|
|
withdraw
|
|
}
|
|
}
|