fix: 超管作品详情分页与配音页、配音 API pageNum 对齐
- content-review 详情 VO 增加 pages;作品管理/作品审核抽屉展示与配音预览 - DubbingView:乐读派 voice 返回 0 基与本库 1 基 pageNum 映射,单页请求传 idx Made-with: Cursor
This commit is contained in:
parent
5ae9233afc
commit
41ecbd216f
@ -13,9 +13,11 @@ import com.lesingle.modules.sys.entity.SysUser;
|
||||
import com.lesingle.modules.sys.mapper.SysUserMapper;
|
||||
import com.lesingle.modules.ugc.entity.UgcReviewLog;
|
||||
import com.lesingle.modules.ugc.entity.UgcWork;
|
||||
import com.lesingle.modules.ugc.entity.UgcWorkPage;
|
||||
import com.lesingle.modules.ugc.enums.WorkPublishStatus;
|
||||
import com.lesingle.modules.ugc.mapper.UgcReviewLogMapper;
|
||||
import com.lesingle.modules.ugc.mapper.UgcWorkMapper;
|
||||
import com.lesingle.modules.ugc.mapper.UgcWorkPageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -34,6 +36,7 @@ import java.util.stream.Stream;
|
||||
public class PublicContentReviewService {
|
||||
|
||||
private final UgcWorkMapper ugcWorkMapper;
|
||||
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||
private final UgcReviewLogMapper ugcReviewLogMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
@ -442,6 +445,13 @@ public class PublicContentReviewService {
|
||||
vo.put("creator", userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
List<UgcWorkPage> pages = ugcWorkPageMapper.selectList(
|
||||
new LambdaQueryWrapper<UgcWorkPage>()
|
||||
.eq(UgcWorkPage::getWorkId, work.getId())
|
||||
.orderByAsc(UgcWorkPage::getPageNo));
|
||||
vo.put("pages", pages);
|
||||
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,10 +124,14 @@
|
||||
<a-descriptions-item label="发布时间">{{ formatDate(detailData.publishTime) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 作品描述 -->
|
||||
<div v-if="detailData.description" class="detail-section">
|
||||
<h4>作品简介</h4>
|
||||
<p class="detail-desc">{{ detailData.description }}</p>
|
||||
<!-- 提交/展示时间(与租户端作品详情一致) -->
|
||||
<div class="detail-submit-time">{{ formatDetailDateTime(detailData) }}</div>
|
||||
|
||||
<!-- 作品介绍 -->
|
||||
<div class="detail-section">
|
||||
<h4 class="section-title">作品介绍</h4>
|
||||
<div v-if="detailData.description" class="detail-desc">{{ detailData.description }}</div>
|
||||
<span v-else class="detail-empty">暂无介绍</span>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
@ -138,13 +142,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绘本翻页预览 -->
|
||||
<!-- 作品详情:分页图 + 文案 + 配音 -->
|
||||
<div v-if="detailData.pages?.length" class="preview-section">
|
||||
<h4>绘本内容预览</h4>
|
||||
<h4 class="section-title">作品详情</h4>
|
||||
<div class="page-preview">
|
||||
<img v-if="detailData.pages[previewPage]?.imageUrl" :src="detailData.pages[previewPage].imageUrl" class="preview-img" />
|
||||
<p v-if="detailData.pages[previewPage]?.text" class="preview-text">{{ detailData.pages[previewPage].text }}</p>
|
||||
<div class="preview-nav">
|
||||
<div v-if="detailData.pages[previewPage]?.audioUrl" class="preview-audio-wrap">
|
||||
<audio :src="detailData.pages[previewPage].audioUrl" controls preload="metadata" class="preview-audio" />
|
||||
</div>
|
||||
<div v-if="detailData.pages.length > 1" class="preview-nav">
|
||||
<a-button :disabled="previewPage === 0" size="small" @click="previewPage--">上一页</a-button>
|
||||
<span>{{ previewPage + 1 }} / {{ detailData.pages.length }}</span>
|
||||
<a-button :disabled="previewPage === detailData.pages.length - 1" size="small" @click="previewPage++">下一页</a-button>
|
||||
@ -268,6 +275,13 @@ const columns = [
|
||||
|
||||
const formatDate = (d: string) => d ? dayjs(d).format('YYYY-MM-DD HH:mm') : '-'
|
||||
|
||||
/** 详情抽屉顶部时间:优先提交审核时间,与审核台一致 */
|
||||
function formatDetailDateTime(d: Record<string, unknown>) {
|
||||
const raw = (d.submitReviewTime ?? d.createTime) as string | undefined
|
||||
if (!raw) return '-'
|
||||
return dayjs(raw).format('YYYY.MM.DD HH:mm')
|
||||
}
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
statsRaw.value = await request.get('/content-review/management/stats') as any
|
||||
@ -444,17 +458,27 @@ $primary: #6366f1;
|
||||
.cover-thumb { width: 48px; height: 64px; object-fit: cover; border-radius: 6px; }
|
||||
.cover-empty { width: 48px; height: 64px; background: #f3f4f6; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #d1d5db; }
|
||||
|
||||
.detail-submit-time {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 16px 0 20px;
|
||||
}
|
||||
|
||||
// 详情区块
|
||||
.detail-section {
|
||||
margin-top: 16px;
|
||||
h4 { font-size: 13px; font-weight: 600; color: #374151; margin: 0 0 8px; }
|
||||
.detail-desc { font-size: 13px; color: #6b7280; line-height: 1.6; margin: 0; }
|
||||
.section-title { font-size: 14px; font-weight: 600; color: #1e1b4b; margin: 0 0 10px; padding-bottom: 8px; border-bottom: 1px solid #f0ecf9; }
|
||||
.detail-desc { font-size: 13px; color: #6b7280; line-height: 1.6; margin: 0; white-space: pre-wrap; }
|
||||
.detail-empty { font-size: 13px; color: #9ca3af; }
|
||||
}
|
||||
|
||||
.preview-section { margin-top: 20px; h4 { font-size: 14px; font-weight: 600; color: #1e1b4b; margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid #f0ecf9; } }
|
||||
.page-preview {
|
||||
.preview-img { width: 100%; max-height: 300px; object-fit: contain; border-radius: 8px; margin-bottom: 8px; }
|
||||
.preview-text { font-size: 13px; color: #374151; line-height: 1.6; padding: 8px 0; }
|
||||
.preview-audio-wrap { margin: 8px 0 12px; }
|
||||
.preview-audio { width: 100%; max-height: 40px; }
|
||||
.preview-nav { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; span { font-size: 12px; color: #6b7280; } }
|
||||
}
|
||||
|
||||
|
||||
@ -158,13 +158,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 绘本翻页预览 -->
|
||||
<!-- 绘本翻页预览(与作品管理详情一致:含配音预览) -->
|
||||
<div v-if="detailData.pages?.length" class="preview-section">
|
||||
<h4>绘本内容预览</h4>
|
||||
<div class="page-preview">
|
||||
<img v-if="detailData.pages[previewPage]?.imageUrl" :src="detailData.pages[previewPage].imageUrl" class="preview-img" />
|
||||
<p v-if="detailData.pages[previewPage]?.text" class="preview-text">{{ detailData.pages[previewPage].text }}</p>
|
||||
<div class="preview-nav">
|
||||
<div v-if="detailData.pages[previewPage]?.audioUrl" class="preview-audio-wrap">
|
||||
<audio :key="`${previewPage}-${detailData.pages[previewPage].audioUrl}`" :src="detailData.pages[previewPage].audioUrl" controls preload="metadata" class="preview-audio" />
|
||||
</div>
|
||||
<div v-if="detailData.pages.length > 1" class="preview-nav">
|
||||
<a-button :disabled="previewPage === 0" size="small" @click="previewPage--">上一页</a-button>
|
||||
<span>{{ previewPage + 1 }} / {{ detailData.pages.length }}</span>
|
||||
<a-button :disabled="previewPage === detailData.pages.length - 1" size="small" @click="previewPage++">下一页</a-button>
|
||||
@ -565,6 +568,8 @@ $primary: #6366f1;
|
||||
.page-preview {
|
||||
.preview-img { width: 100%; max-height: 300px; object-fit: contain; border-radius: 8px; margin-bottom: 8px; }
|
||||
.preview-text { font-size: 13px; color: #374151; line-height: 1.6; padding: 8px 0; }
|
||||
.preview-audio-wrap { margin: 8px 0 12px; }
|
||||
.preview-audio { width: 100%; max-height: 40px; }
|
||||
.preview-nav { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; span { font-size: 12px; color: #6b7280; } }
|
||||
}
|
||||
.review-actions { margin-top: 20px; padding-top: 16px; border-top: 1px solid #f0ecf9;
|
||||
|
||||
@ -15,7 +15,7 @@ export default { name: 'DubbingView' }
|
||||
<!-- 当前页展示 -->
|
||||
<div class="page-display">
|
||||
<div class="page-image-wrap">
|
||||
<div class="page-badge-pill">{{ currentPage.pageNum === 0 ? '封面' : 'P' + currentPage.pageNum }}</div>
|
||||
<div class="page-badge-pill">{{ idx === 0 ? '封面' : 'P' + currentPage.pageNum }}</div>
|
||||
<img v-if="currentPage.imageUrl" :src="currentPage.imageUrl" class="page-image" />
|
||||
<div v-else class="page-image placeholder">
|
||||
<picture-outlined />
|
||||
@ -222,6 +222,39 @@ function showToast(msg: string) {
|
||||
setTimeout(() => { toast.value = '' }, 2500)
|
||||
}
|
||||
|
||||
/**
|
||||
* 乐读派 voicedPages:pageNum 从 0(封面)起。
|
||||
* 本库 pageList:pageNum 与 t_ugc_work_page.page_no 从 1 起。
|
||||
* 对每个返回项:换算目标 pageNum → 在 pages 中查找同 pageNum 的分页 → 写入 audioUrl。
|
||||
*/
|
||||
function applyVoicedPagesFromApi(voicedPages: { pageNum: number; audioUrl: string }[]) {
|
||||
voicedPages.forEach(vp => {
|
||||
const index = pages.value.findIndex(p => (p.pageNum - 1) === vp.pageNum)
|
||||
if (index !== -1) {
|
||||
pages.value[index].audioUrl = vp.audioUrl
|
||||
pages.value[index].localBlob = null
|
||||
pages.value[index].isAiVoice = true
|
||||
}
|
||||
})
|
||||
|
||||
// for (const vp of voicedPages) {
|
||||
// const apiPageNum = Number(vp.pageNum)
|
||||
// if (Number.isNaN(apiPageNum) || !vp.audioUrl) continue
|
||||
|
||||
// const targetPageNum = apiPageNum + 1
|
||||
// let item = pages.value.find(p => p.pageNum === targetPageNum)
|
||||
// if (!item) {
|
||||
// item = pages.value.find(p => p.pageNum === apiPageNum)
|
||||
// }
|
||||
|
||||
// if (item) {
|
||||
// item.audioUrl = vp.audioUrl
|
||||
// item.localBlob = null
|
||||
// item.isAiVoice = true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// -- 确认弹窗 --
|
||||
const confirmVisible = ref(false)
|
||||
const confirmTitle = ref('')
|
||||
@ -387,17 +420,10 @@ function autoAdvance() {
|
||||
async function voiceSingle() {
|
||||
voicingSingle.value = true
|
||||
try {
|
||||
const res = await voicePage({ workId: workId.value, voiceAll: false, pageNum: currentPage.value.pageNum })
|
||||
const res = await voicePage({ workId: workId.value, voiceAll: false, pageNum: idx.value })
|
||||
const data = res
|
||||
if (data.voicedPages?.length) {
|
||||
for (const vp of data.voicedPages) {
|
||||
const p = pages.value.find(x => x.pageNum === vp.pageNum)
|
||||
if (p) {
|
||||
p.audioUrl = vp.audioUrl
|
||||
p.localBlob = null
|
||||
p.isAiVoice = true
|
||||
}
|
||||
}
|
||||
applyVoicedPagesFromApi(data.voicedPages)
|
||||
showToast('AI 配音成功')
|
||||
} else {
|
||||
showToast('配音失败,请重试')
|
||||
@ -422,14 +448,7 @@ async function voiceAllConfirm() {
|
||||
const res = await voicePage({ workId: workId.value, voiceAll: true })
|
||||
const data = res
|
||||
if (data.voicedPages) {
|
||||
for (const vp of data.voicedPages) {
|
||||
const p = pages.value.find(x => x.pageNum === vp.pageNum)
|
||||
if (p) {
|
||||
p.audioUrl = vp.audioUrl
|
||||
p.localBlob = null
|
||||
p.isAiVoice = true
|
||||
}
|
||||
}
|
||||
applyVoicedPagesFromApi(data.voicedPages)
|
||||
}
|
||||
const failed = data.failedPages?.length || 0
|
||||
showToast(failed > 0 ? `${data.totalSucceeded} 页成功,${failed} 页失败` : '全部 AI 配音完成')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user