631 lines
30 KiB
HTML
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, ''').replace(/"/g, '"'); }
|
|
|
|
</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>
|