Files
video-factory/shortdrama/view/index.html
T
2026-07-03 11:38:30 +08:00

631 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>短剧管理</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:#f0f2f5; color:#333; padding:20px; }
.container { max-width:1000px; margin:0 auto; }
.header { display:flex; justify-content:space-between; align-items:center; margin-bottom:24px; }
.header h1 { font-size:24px; color:#1a1a2e; }
.header-actions { display:flex; gap:10px; }
.btn { display:inline-flex; align-items:center; gap:6px; padding:10px 20px; border-radius:8px; font-size:14px; font-weight:600; cursor:pointer; text-decoration:none; border:none; transition:all .2s; }
.btn-primary { background:#4a6cf7; color:#fff; }
.btn-primary:hover { background:#3a5ce5; }
.btn-chat { background:#0891b2; color:#fff; }
.btn-chat:hover { background:#067394; }
.btn-video { background:#7c3aed; color:#fff; }
.btn-video:hover { background:#6d28d9; }
.btn-success { background:#e8f5e9; color:#2e7d32; }
.btn-success:hover { background:#c8e6c9; }
.btn-danger { background:#fee; color:#e44; }
.btn-danger:hover { background:#fdd; }
.btn-sm { padding:4px 10px; font-size:11px; }
.btn-outline { background:transparent; border:1px solid #d9d9d9; color:#555; }
.btn-outline:hover { border-color:#4a6cf7; color:#4a6cf7; }
.btn:disabled { opacity:0.5; cursor:not-allowed; }
.card { background:#fff; border-radius:12px; padding:20px; margin-bottom:16px; box-shadow:0 2px 8px rgba(0,0,0,0.06); display:flex; justify-content:space-between; align-items:center; }
.card.clickable { cursor:pointer; transition:box-shadow .2s, transform .2s; }
.card.clickable:hover { box-shadow:0 4px 16px rgba(0,0,0,0.1); transform:translateY(-1px); }
.card-info h3 { font-size:16px; color:#1a1a2e; margin-bottom:6px; }
.card-info .meta { font-size:13px; color:#888; display:flex; gap:16px; flex-wrap:wrap; }
.card-info .meta span { display:inline-flex; align-items:center; gap:4px; }
.card-actions { display:flex; gap:8px; flex-shrink:0; }
.tag { display:inline-block; padding:2px 10px; border-radius:20px; font-size:11px; font-weight:600; }
.tag-style { background:#eef1ff; color:#4a6cf7; }
.empty { text-align:center; padding:60px 20px; color:#999; }
.empty p { font-size:14px; margin-bottom:16px; }
/* 弹窗通用 */
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.4); z-index:1000; justify-content:center; align-items:center; }
#confirmModal { z-index:1100; }
.modal-overlay.show { display:flex; }
.modal { background:#fff; border-radius:12px; padding:24px; max-width:400px; width:90%; box-shadow:0 20px 60px rgba(0,0,0,0.2); max-height:90vh; overflow-y:auto; }
.modal-wide { max-width:480px; }
.modal h3 { font-size:16px; margin-bottom:16px; }
.modal h4 { font-size:14px; margin-bottom:10px; color:#1a1a2e; }
.modal p { font-size:14px; color:#666; margin-bottom:20px; }
.modal-actions { display:flex; gap:10px; justify-content:flex-end; }
.form-group { margin-bottom:14px; }
.form-group:last-child { margin-bottom:0; }
.form-group label { display:block; font-size:12px; font-weight:600; color:#555; margin-bottom:4px; }
.form-group input, .form-group select { width:100%; padding:8px 10px; border:1px solid #d9d9d9; border-radius:6px; font-size:13px; outline:none; }
.form-group input:focus, .form-group select:focus { border-color:#4a6cf7; box-shadow:0 0 0 2px rgba(74,108,247,0.1); }
.row { display:flex; gap:12px; }
.row > * { flex:1; }
.help-text { font-size:12px; color:#999; margin-top:4px; }
.success-msg { display:none; text-align:center; padding:8px; background:#e8f5e9; border-radius:6px; color:#2e7d32; font-size:12px; margin-top:10px; }
.success-msg.show { display:block; }
.spinner { width:18px; height:18px; border:3px solid #e0e0e0; border-top-color:#4a6cf7; border-radius:50%; animation:spin .6s linear infinite; display:inline-block; }
@keyframes spin { to { transform:rotate(360deg); } }
.add-link { display:inline-flex; align-items:center; gap:4px; color:#4a6cf7; font-size:13px; cursor:pointer; margin-top:6px; }
.add-link:hover { text-decoration:underline; }
/* 剧集 */
.ep-list { display:grid; gap:10px; }
.ep-item { display:flex; gap:12px; align-items:flex-start; padding:14px 16px; background:#fafafa; border-radius:8px; border:1px solid #eee; }
.ep-item .index { width:28px; height:28px; border-radius:50%; background:#4a6cf7; color:#fff; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:600; flex-shrink:0; }
.ep-item .info { flex:1; }
.ep-item .info .title { font-size:14px; font-weight:600; }
.ep-item .info .script { font-size:12px; color:#888; margin-top:4px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ep-item .info .script.expanded { -webkit-line-clamp:unset; }
.ep-item .info .script-preview { font-size:12px; color:#888; margin-top:4px; cursor:pointer; }
.ep-item .info .script-preview:hover { color:#4a6cf7; }
.ep-item .info .error-msg { font-size:12px; color:#e44; margin-top:4px; display:none; word-break:break-all; }
.ep-item .info .status { font-size:11px; margin-top:4px; display:inline-flex; align-items:center; gap:4px; }
.ep-item .info .status.pending { color:#999; }
.ep-item .info .status.generating { color:#f90; }
.ep-item .info .status.completed { color:#090; }
.ep-item .info .status.failed { color:#e44; }
.ep-item .actions { display:flex; gap:6px; flex-shrink:0; flex-wrap:wrap; }
@media (max-width:640px) { .ep-item { flex-direction:column; } .ep-item .actions { align-self:flex-end; } }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>短剧管理</h1>
<div class="header-actions">
<button class="btn btn-primary" onclick="showConfigModal()">模型管理</button>
<button class="btn btn-primary" onclick="showDramaModal()">+ 新建短剧</button>
</div>
</div>
<div id="dramaList"></div>
<div class="empty" id="emptyState" style="display:none">
<p>暂无短剧,点击上方按钮创建第一个短剧</p>
</div>
<template id="epTmpl">
<div class="ep-item">
<div class="index"></div>
<div class="info">
<div class="title"></div>
<div class="script-preview" onclick="toggleScript(this)" style="display:none"></div>
<div class="error-msg"></div>
</div>
<div class="actions">
<button class="btn btn-sm btn-success generate-btn" style="display:none">生成视频</button>
<button class="btn btn-sm btn-primary review-btn" style="display:none">审核</button>
<button class="btn btn-sm btn-success preview-btn" style="display:none">预览</button>
<button class="btn btn-sm btn-outline edit-btn">编辑</button>
<button class="btn btn-sm btn-danger delete-btn">删除</button>
</div>
</div>
</template>
<script>
let deleteId = null;
let editDramaId = null;
let confirmAction = null;
let currentDramaId = null;
let currentDrama = null;
let currentCharDramaId = null;
var currentChars = [];
let pollingInterval = null;
let currentPollEpId = null;
let currentTaskId = null;
// videoUrl 将本地文件路径转为可访问的 URL,远程 URL 保持原样
function toVideoUrl(path) {
if (!path) return '';
if (path.indexOf('://') > 0) return path;
if (path.startsWith('workspace/')) return '/' + path;
// 兜底:处理仍存着绝对路径的历史数据
var wsIdx = path.indexOf('/workspace/');
if (wsIdx >= 0) return path.substring(wsIdx);
return path;
}
var dramaPage = 1;
var dramaTotal = 0;
var dramaPageSize = 20;
async function loadList() {
try {
const resp = await fetch('/drama/list?page=' + dramaPage + '&pageSize=' + dramaPageSize);
const data = await resp.json();
if (data.code !== 0) throw new Error(data.message);
dramaTotal = data.data.total;
renderList(data.data.list);
} catch (err) {
document.getElementById('dramaList').innerHTML = '<div class="card" style="color:red;cursor:default">\u52a0\u8f7d\u5931\u8d25: ' + err.message + '</div>';
}
}
function renderPageInfo(total, page, pageSize, onPrev, onNext, onChangeSize) {
const totalPages = Math.ceil(total / pageSize) || 1;
if (total === 0) return '';
const sizeOptions = [10, 20, 50, 100];
return '<div style="display:flex;justify-content:center;align-items:center;gap:12px;padding:12px 0 4px;font-size:13px;color:#888;flex-wrap:wrap;">'
+ '<span>共 ' + total + ' 条</span>'
+ '<span>每页 <select onchange="' + onChangeSize + '(this.value)" style="padding:2px 4px;border:1px solid #d9d9d9;border-radius:4px;font-size:12px;outline:none;">'
+ sizeOptions.map(function(s) { return '<option value="' + s + '"' + (s === pageSize ? ' selected' : '') + '>' + s + '</option>'; }).join('')
+ '</select> 条</span>'
+ '<button class="btn btn-sm btn-outline" onclick="' + onPrev + '" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>'
+ '<span>第 ' + page + ' / ' + totalPages + ' 页</span>'
+ '<button class="btn btn-sm btn-outline" onclick="' + onNext + '" ' + (page >= totalPages ? 'disabled' : '') + '>下一页</button>'
+ '</div>';
}
function renderList(list) {
const container = document.getElementById('dramaList');
const empty = document.getElementById('emptyState');
if (!list || list.length === 0) {
container.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
container.innerHTML = list.map(d => {
const epCount = d.epCount || 0;
const epDuration = d.episodeDuration || '-';
return `<div class="card">
<div class="card-info">
<h3>${escHtml(d.title)}</h3>
<div class="meta">
<span class="tag tag-style">${escHtml(d.style)}</span>
<span>${epCount} 集</span>
<span>每集 ${epDuration}s</span>
</div>
</div>
<div class="card-actions" onclick="event.stopPropagation()">
<button class="btn btn-sm btn-primary" onclick="openEpisodeModal('${d.id}','${escHtml(d.title)}')">剧集</button>
<button class="btn btn-sm btn-outline" onclick="openDramaEditModal('${d.id}', 'basic')">编辑</button>
<button class="btn btn-sm btn-danger" onclick="confirmDelete('${d.id}','${escHtml(d.title)}')">删除</button>
</div>
</div>`;
}).join('') + renderPageInfo(dramaTotal, dramaPage, dramaPageSize, 'dramaPagePrev()', 'dramaPageNext()', 'dramaChangeSize');
}
function dramaPagePrev() { if (dramaPage > 1) { dramaPage--; loadList(); } }
function dramaPageNext() { var tp = Math.ceil(dramaTotal / dramaPageSize) || 1; if (dramaPage < tp) { dramaPage++; loadList(); } }
function dramaChangeSize(val) { dramaPageSize = parseInt(val); dramaPage = 1; loadList(); }
// ==================== 删除短剧 ====================
function confirmDelete(id, title) {
deleteId = id;
confirmAction = null;
document.getElementById('confirmTitle').textContent = '确认删除';
document.getElementById('confirmMsg').textContent = '确定要删除《' + title + '》吗?此操作不可恢复。';
document.getElementById('confirmBtn').className = 'btn btn-danger';
document.getElementById('confirmModal').classList.add('show');
}
function closeModal() {
document.getElementById('confirmModal').classList.remove('show');
deleteId = null;
confirmAction = null;
}
// 确认按钮的点击处理(在 loadModals 中绑定)
async function handleConfirmClick() {
if (confirmAction) { await confirmAction(); closeModal(); return; }
if (!deleteId) return;
try {
await fetch('/drama/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: parseInt(deleteId) }) });
closeModal(); loadList();
} catch (err) { alert('删除失败: ' + err.message); closeModal(); }
}
// ==================== 剧集管理 ====================
var episodePage = 1;
var episodeTotal = 0;
var episodePageSize = 10;
function toggleScript(el) {
if (el.classList.contains('expanded')) { el.classList.remove('expanded'); el.textContent = el.textContent.substring(0, 60) + '...'; }
else { el.classList.add('expanded'); el.textContent = el.getAttribute('data-full') || el.textContent; }
}
// ==================== 剧集 CRUD ====================
function showEpModal() {
document.getElementById('epModalTitle').textContent = '添加剧集';
document.getElementById('editEpId').value = '';
document.getElementById('epTitle').value = '';
document.getElementById('epScript').value = '';
document.getElementById('epModal').classList.add('show');
}
function hideEpModal() { document.getElementById('epModal').classList.remove('show'); }
function editEp(epId) {
const ep = (currentDrama.episodes || []).find(x => x.id == epId);
if (!ep) return;
document.getElementById('epModalTitle').textContent = '编辑剧集';
document.getElementById('editEpId').value = epId;
document.getElementById('epTitle').value = ep.title || '';
document.getElementById('epScript').value = ep.script || '';
document.getElementById('epModal').classList.add('show');
}
async function saveEp() {
const title = document.getElementById('epTitle').value.trim();
if (!title) { alert('请输入剧集标题'); return; }
const editEpId = document.getElementById('editEpId').value;
const body = { dramaId: parseInt(currentDramaId), title, script: document.getElementById('epScript').value.trim() };
if (editEpId) body.epId = parseInt(editEpId);
const btn = document.getElementById('saveEpBtn'); btn.disabled = true;
try {
const url = editEpId ? '/drama/episode/update' : '/drama/episode/add';
const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const data = await resp.json();
if (data.code !== 0) throw new Error(data.message);
hideEpModal(); fetchDramaAndEps();
} catch (err) { alert('保存剧集失败: ' + err.message); }
finally { btn.disabled = false; }
}
function deleteEp(epId) {
const ep = (currentDrama.episodes || []).find(x => x.id == epId);
const title = ep ? ep.title : '';
confirmAction = async function() {
try {
const resp = await fetch('/drama/episode/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dramaId: parseInt(currentDramaId), epId: parseInt(epId) }) });
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
fetchDramaAndEps();
} catch (err) { alert('删除剧集失败: ' + err.message); }
};
document.getElementById('confirmTitle').textContent = '删除剧集';
document.getElementById('confirmMsg').textContent = '确定要删除剧集「' + title + '」吗?';
document.getElementById('confirmBtn').className = 'btn btn-danger';
document.getElementById('confirmModal').classList.add('show');
}
// ==================== 独立剧集管理弹窗 ====================
function openEpisodeModal(id, title) {
currentDramaId = id;
document.getElementById("epModalDramaTitle").textContent = escHtml(title) + " - 剧集管理";
document.getElementById("episodeModal").classList.add("show");
Promise.all([
fetch("/drama/get?id=" + id),
fetch("/drama/episode/list?dramaId=" + id + "&page=1&pageSize=100")
]).then(function(results) {
return Promise.all(results.map(function(r) { return r.json(); }));
}).then(function(results) {
var dramaData = results[0], epData = results[1];
if (dramaData.code === 0) {
var d = dramaData.data;
currentDrama = d;
document.getElementById("epModalDramaMeta").textContent = (d.style || "") + " . " + (d.episodeDuration || "-") + "s/集";
}
if (epData.code === 0) {
renderEpisodeModalList(epData.data.list || []);
}
}).catch(function(err) {
document.getElementById("episodeEpList").innerHTML = "<div style='color:red;font-size:13px;padding:16px 0;text-align:center;'>加载失败: " + err.message + "</div>";
});
}
function hideEpisodeModal() {
document.getElementById("episodeModal").classList.remove("show");
currentDramaId = null;
currentDrama = null;
loadList();
}
function renderEpisodeModalList(eps) {
var list = document.getElementById("episodeEpList");
if (!list) return;
if (!eps || eps.length === 0) {
list.innerHTML = "<div style='color:#999;font-size:13px;padding:16px 0;'>暂无剧集,点击下方“添加剧集”按钮创建</div>";
return;
}
var taskMap = {};
if (currentDrama && currentDrama.tasks) {
currentDrama.tasks.forEach(function(t) { if (!taskMap[t.episodeId]) taskMap[t.episodeId] = []; taskMap[t.episodeId].push(t); });
}
var tmpl = document.getElementById("epTmpl");
var frag = document.createDocumentFragment();
eps.forEach(function(ep) {
var status = ep.status || "pending";
var node = tmpl.content.cloneNode(true);
node.querySelector(".index").textContent = ep.index || "-";
node.querySelector(".title").textContent = escHtml(ep.title || "未命名");
var sp = node.querySelector(".script-preview");
if (ep.script) {
sp.style.display = "";
sp.textContent = ep.script.length > 60 ? ep.script.substring(0,60) + "..." : ep.script;
sp.dataset.full = ep.script;
} else { sp.style.display = "none"; }
var gb = node.querySelector(".generate-btn");
if (status === "pending" || status === "failed") { gb.style.display = ""; gb.onclick = function(e) { e.stopPropagation(); generateEp(ep.id); }; }
var rb = node.querySelector(".review-btn");
var pb = node.querySelector(".preview-btn");
if (status === "completed") {
rb.style.display = "none";
pb.style.display = "";
pb.onclick = function(e) { e.stopPropagation(); playMergedVideo(ep); };
} else if (status !== "pending" && status !== "failed" && taskMap[ep.id] && taskMap[ep.id].length > 0) {
rb.style.display = "";
rb.onclick = function(e) { e.stopPropagation(); openReviewPanel(ep.id); };
pb.style.display = "none";
} else {
pb.style.display = "none";
}
node.querySelector(".edit-btn").onclick = function(e) { e.stopPropagation(); editEp(ep.id); };
node.querySelector(".delete-btn").onclick = function(e) { e.stopPropagation(); deleteEp(ep.id); };
frag.appendChild(node);
});
list.innerHTML = "";
list.appendChild(frag);
}
// ==================== 生成与轮询 ====================
async function generateEp(epId) {
const btn = event.target; btn.disabled = true; btn.textContent = '生成中...';
try {
const resp = await fetch('/drama/episode/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dramaId: parseInt(currentDramaId), epId: parseInt(epId) }) });
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
startPolling(epId);
} catch (err) { alert('生成失败: ' + err.message); btn.disabled = false; btn.textContent = '生成视频'; }
}
function startPolling(epId) {
currentPollEpId = epId; pollEpisode();
pollingInterval = setInterval(pollEpisode, 15000);
}
function stopPolling() {
if (pollingInterval) { clearInterval(pollingInterval); pollingInterval = null; }
currentPollEpId = null;
}
var lastPollStatus = {};
async function pollEpisode() {
if (!currentPollEpId) return;
try {
const resp = await fetch('/drama/episode/poll?epId=' + currentPollEpId);
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
var ps = data.data;
if (ps.status === 'generating' && lastPollStatus[currentPollEpId] !== 'generating') {
showReviewPanel(ps.tasks, ps.currentTaskId);
fetchDramaAndEps();
} else if (ps.status === 'review' && lastPollStatus[currentPollEpId] !== 'review') {
showReviewPanel(ps.tasks, ps.currentTaskId);
fetchDramaAndEps();
} else if (ps.status === 'completed' && lastPollStatus[currentPollEpId] !== 'completed') {
stopPolling(); showReviewPanel(null); fetchDramaAndEps();
} else if (ps.status === 'failed' && lastPollStatus[currentPollEpId] !== 'failed') {
showReviewPanel(ps.tasks, ps.currentTaskId);
fetchDramaAndEps();
}
lastPollStatus[currentPollEpId] = ps.status;
} catch (err) { console.error('轮询失败:', err); }
}
// ==================== 预览审核 ====================
function showReviewPanel(tasks, currentTaskIdVal) {
const modal = document.getElementById('reviewModal');
if (!tasks || tasks.length === 0) { modal.classList.remove('show'); return; }
tasks.sort(function(a, b) { return a.segmentIdx - b.segmentIdx; });
var html = '';
for (var i = 0; i < tasks.length; i++) {
var t = tasks[i];
var statusText = t.status === 'completed' ? '已通过' : (t.status === 'failed' ? '失败' : (t.status === 'generating' ? '生成中' : '待审核'));
var statusColor = t.status === 'completed' ? '#090' : (t.status === 'failed' ? '#e44' : (t.status === 'generating' ? '#4a6cf7' : '#f90'));
var errHtml = '';
if (t.status === 'failed' && t.errorMessage) { errHtml = '<div style="font-size:12px;color:#e44;margin-top:4px;">' + escHtml(t.errorMessage) + '</div>'; }
var actionsHtml = '';
if (t.status === 'review' && t.videoUrl) {
actionsHtml = '<button class="btn btn-sm btn-primary" onclick="confirmOneSegment(' + t.id + ',' + t.episodeId + ')" style="white-space:nowrap;">通过</button>'
+ '<button class="btn btn-sm btn-outline" onclick="rejectOneSegment(' + t.id + ')" style="white-space:nowrap;">重做</button>';
}
html += '<div style="margin-bottom:12px;border-bottom:1px solid #eee;padding-bottom:12px;">'
+ '<div style="font-size:13px;font-weight:600;color:#555;margin-bottom:6px;">第' + (t.segmentIdx + 1) + '段 <span style="font-size:11px;color:' + statusColor + ';">' + statusText + '</span></div>'
+ (errHtml ? '<div style="margin-bottom:4px;">' + errHtml + '</div>' : '')
+ '<div style="display:flex;gap:10px;align-items:flex-start;">'
+ (t.videoUrl
? '<video src="' + toVideoUrl(t.videoUrl) + '" controls style="max-width:360px;max-height:240px;border-radius:8px;flex-shrink:0;"></video>'
: '<div style="color:#888;font-size:13px;padding:20px;flex-shrink:0;">无视频</div>')
+ '<div style="display:flex;flex-direction:column;gap:6px;padding-top:4px;">' + actionsHtml + '</div>'
+ '</div></div>';
}
document.getElementById('reviewSegments').innerHTML = html;
document.getElementById('reviewModalTitle').textContent = '预览审核';
document.getElementById('reviewStatus').textContent = tasks.length + '段 · ' + tasks.filter(function(x){return x.status==="completed";}).length + '已通过';
document.getElementById('reviewSegments').style.display = 'block';
modal.classList.add('show');
currentTaskId = currentTaskIdVal;
}
function hideReviewPanel() { document.getElementById('reviewModal').classList.remove('show'); }
function openReviewPanel(epId) {
fetch('/drama/episode/task?epId=' + epId).then(r => r.json()).then(d => {
if (d.code !== 0) { alert(d.message); return; }
showReviewPanel(d.data.tasks, d.data.currentTaskId);
}).catch(err => alert('加载审核数据失败: ' + err.message));
}
function playMergedVideo(ep) {
if (!ep.videoUrl) { alert('暂无合并后的视频'); return; }
var modal = document.getElementById('reviewModal');
modal.classList.add('show');
document.getElementById('reviewModalTitle').textContent = '预览 - ' + escHtml(ep.title || '');
document.getElementById('reviewSegments').innerHTML = '<div style="text-align:center;padding:20px;">'
+ '<video src="' + toVideoUrl(ep.videoUrl) + '" controls style="max-width:100%;max-height:70vh;border-radius:8px;"></video>'
+ '</div>';
document.getElementById('reviewStatus').textContent = '';
document.getElementById('reviewSegments').style.display = 'block';
}
function previewEpisode(epId, segIdx) {
segIdx = segIdx !== undefined ? segIdx : -1;
fetch('/drama/episode/task?epId=' + epId).then(r => r.json()).then(d => {
if (d.code !== 0) { alert(d.message); return; }
var tasks = d.data.tasks || [];
tasks.sort(function(a, b) { return a.segmentIdx - b.segmentIdx; });
var html = '';
for (var i = 0; i < tasks.length; i++) {
var t = tasks[i];
var isFocused = (segIdx >= 0 && t.segmentIdx === segIdx);
var style = isFocused ? 'margin-bottom:16px;border:2px solid #4a6cf7;border-radius:8px;padding:16px;background:#f8f9ff;' : 'margin-bottom:16px;border-bottom:1px solid #eee;padding-bottom:16px;';
html += '<div style="' + style + '"><div style="font-size:13px;font-weight:600;color:#555;margin-bottom:8px;">第' + (t.segmentIdx + 1) + '段' + (isFocused ? ' <span style="color:#4a6cf7;">← 当前</span>' : '') + '</div>';
html += t.videoUrl ? '<video src="' + toVideoUrl(t.videoUrl) + '" controls style="max-width:100%;max-height:400px;border-radius:8px;"></video>' : '<div style="color:#888;font-size:13px;">无视频</div>';
html += '</div>';
}
document.getElementById('reviewSegments').innerHTML = html;
document.getElementById('reviewSegments').style.display = 'block';
document.getElementById('reviewBody').style.display = 'none';
document.getElementById('reviewLoading').style.display = 'none';
document.getElementById('reviewModalTitle').textContent = '视频预览';
document.getElementById('reviewStatus').textContent = '';
document.getElementById('reviewModal').classList.add('show');
}).catch(err => alert('加载预览失败: ' + err.message));
}
async function continueSegment() {
if (!currentTaskId) return;
const btn = document.getElementById('continueBtn'); btn.disabled = true; btn.textContent = '处理中...';
try {
const resp = await fetch('/drama/segment/continue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: currentTaskId }) });
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
hideReviewPanel(); document.getElementById('reviewStatus').textContent = '已确认,继续生成...';
} catch (err) { alert('操作失败: ' + err.message); }
finally { btn.disabled = false; btn.textContent = '继续下一段'; }
}
async function feedbackSegment() {
if (!currentTaskId) return;
const feedback = document.getElementById('feedbackInput').value.trim();
if (!feedback) { alert('请输入反馈意见'); return; }
const btn = document.getElementById('feedbackBtn'); btn.disabled = true; btn.textContent = '处理中...';
try {
const resp = await fetch('/drama/segment/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: currentTaskId, feedback }) });
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
hideReviewPanel(); document.getElementById('reviewStatus').textContent = '已提交反馈,重新生成...';
} catch (err) { alert('操作失败: ' + err.message); }
finally { btn.disabled = false; btn.textContent = '重新生成'; }
}
async function confirmOneSegment(taskId, epId) {
try {
const resp = await fetch('/drama/segment/continue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: taskId }) });
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
// 重新加载审核面板
fetch('/drama/episode/task?epId=' + epId).then(r => r.json()).then(d => {
if (d.code === 0) showReviewPanel(d.data.tasks, d.data.currentTaskId);
});
} catch (err) { alert('确认失败: ' + err.message); }
}
async function rejectOneSegment(taskId) {
var reason = prompt('请输入重做原因(将作为重新生成的提示词):');
if (reason === null) return;
try {
const resp = await fetch('/drama/segment/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: taskId, feedback: reason }) });
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
alert('已提交,将根据反馈重新生成该段');
// 重新加载剧集列表
if (currentDramaId) { fetchDramaAndEps(); }
hideReviewPanel();
} catch (err) { alert('操作失败: ' + err.message); }
}
// ==================== 工具函数 ====================
function escHtml(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function escAttr(s) { if (!s) return ''; return s.replace(/'/g, '&#39;').replace(/"/g, '&quot;'); }
</script>
<div id="modalContainer"></div>
<script>
// ==================== 动态加载弹窗 ====================
const MODAL_FILES = [
'modal-confirm.html',
'modal-drama.html',
'modal-char-form.html',
'modal-scene-form.html',
'modal-prop-form.html',
'modal-ep-form.html',
'modal-episode.html',
'modal-review.html',
'modal-config.html'
];
async function loadModals() {
const container = document.getElementById('modalContainer');
await Promise.all(MODAL_FILES.map(async (file) => {
try {
const resp = await fetch('/views/' + file);
if (!resp.ok) { console.error('加载弹窗失败:', file, resp.status); return; }
const text = await resp.text();
// 提取样式
const styles = [];
const noStyle = text.replace(/<style>([\s\S]*?)<\/style>/g, function(_, css) { styles.push(css); return ''; });
// 提取脚本
const scripts = [];
const html = noStyle.replace(/<script>([\s\S]*?)<\/script>/g, function(_, code) { scripts.push(code); return ''; });
container.insertAdjacentHTML('beforeend', html);
styles.forEach(function(css) { var s = document.createElement('style'); s.textContent = css; document.head.appendChild(s); });
scripts.forEach(function(code) { var s = document.createElement('script'); s.textContent = code.replace(/<\/script/gi, '<\\/script'); document.body.appendChild(s); });
} catch (err) {
console.error('加载弹窗失败:', file, err);
}
}));
document.getElementById('confirmBtn').addEventListener('click', handleConfirmClick);
// 验证子表单弹窗是否加载成功
['modalCharModal','modalSceneModal','modalPropModal'].forEach(function(id) {
if (!document.getElementById(id)) console.warn('弹窗未加载: ' + id);
});
// 剧集视图:在模态窗口完成 CRUD 后自动刷新剧集列表
var origFetch = window.fetchDramaAndEps;
if (origFetch) {
window.fetchDramaAndEps = function() {
origFetch();
if (document.getElementById('episodeModal').classList.contains('show')) {
// 刷新独立剧集弹窗中的列表
var eps = document.getElementById('episodeEpList');
if (eps) {
fetch('/drama/episode/list?dramaId=' + currentDramaId + '&page=1&pageSize=100').then(function(r){return r.json();}).then(function(d){
if (d.code === 0) renderEpisodeModalList(d.data.list || []);
});
}
}
};
}
loadList();
}
loadModals();
</script>
</body>
</html>