1
This commit is contained in:
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
commonHttp "video-factory/common/http"
|
||||
"video-factory/shortdrama/controller"
|
||||
"video-factory/shortdrama/service"
|
||||
@@ -10,36 +9,7 @@ import (
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
)
|
||||
|
||||
//go:embed shortdrama/view/*.html
|
||||
var viewFS embed.FS
|
||||
|
||||
func main() {
|
||||
//// ==================== HTML 页面路由 ====================
|
||||
//servePage := func(name string) ghttp.HandlerFunc {
|
||||
// return func(r *ghttp.Request) {
|
||||
// // 优先读本地文件(通过 volume 挂载可热更新),否则用 embed
|
||||
// data, err := os.ReadFile("shortdrama/view/" + name)
|
||||
// if err != nil {
|
||||
// data, err = viewFS.ReadFile("shortdrama/view/" + name)
|
||||
// if err != nil {
|
||||
// r.Response.WriteStatus(http.StatusNotFound)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// r.Response.Write(data)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//commonHttp.Httpserver.BindHandler("/", servePage("index.html"))
|
||||
//commonHttp.Httpserver.BindHandler("/views/*", func(r *ghttp.Request) {
|
||||
// name := strings.TrimPrefix(r.URL.Path, "/views/")
|
||||
// if name == "" || !strings.HasSuffix(name, ".html") {
|
||||
// r.Response.WriteStatus(http.StatusNotFound)
|
||||
// return
|
||||
// }
|
||||
// servePage(name)(r)
|
||||
//})
|
||||
|
||||
// ==================== API 路由(通过 RouteRegister 自动注册,遵循 ai-agent 规范) ====================
|
||||
commonHttp.RouteRegister([]interface{}{
|
||||
controller.Drama,
|
||||
|
||||
@@ -1,630 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,127 +0,0 @@
|
||||
<script>
|
||||
// ==================== 演员管理 ====================
|
||||
|
||||
function openActorModal(id) {
|
||||
currentCharDramaId = id;
|
||||
document.getElementById('actorModal').classList.add('show');
|
||||
document.getElementById('actorModalDramaTitle').textContent = '加载中...';
|
||||
document.getElementById('actorModalDramaMeta').textContent = '';
|
||||
document.getElementById('actorList').innerHTML = '<div style="color:#999;font-size:13px;text-align:center;padding:20px;">加载中...</div>';
|
||||
fetch('/drama/get?id=' + id).then(r => r.json()).then(data => {
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
const d = data.data;
|
||||
document.getElementById('actorModalDramaTitle').textContent = d.title || '未命名短剧';
|
||||
document.getElementById('actorModalDramaMeta').textContent = (d.style || '') + ' · 演员';
|
||||
renderActorList(d.characters || []);
|
||||
}).catch(err => {
|
||||
document.getElementById('actorList').innerHTML = '<div style="color:red;font-size:13px;text-align:center;padding:20px;">加载失败: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function hideActorModal() {
|
||||
document.getElementById('actorModal').classList.remove('show');
|
||||
currentCharDramaId = null;
|
||||
}
|
||||
|
||||
function renderActorList(chars) {
|
||||
currentChars = chars;
|
||||
const list = document.getElementById('actorList');
|
||||
if (!chars || chars.length === 0) {
|
||||
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无演员,点击下方添加</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = chars.map(function(c, i) {
|
||||
var initial = c.name ? c.name.charAt(0) : '?';
|
||||
var portraitUrl = c.portraitPath ? '/' + c.portraitPath : '';
|
||||
var voiceName = c.voicePath ? c.voicePath.split('/').pop() : '';
|
||||
return '<div style="display:flex;gap:8px;align-items:center;padding:8px 10px;background:#fafafa;border-radius:8px;border:1px solid #eee;margin-bottom:6px;">'
|
||||
+ (portraitUrl ? '<img src="' + portraitUrl + '" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0;">' : '<span style="width:36px;height:36px;border-radius:50%;background:#e0e0e0;display:inline-flex;align-items:center;justify-content:center;font-size:15px;font-weight:bold;color:#666;flex-shrink:0;">' + initial + '</span>')
|
||||
+ '<div style="flex:1;font-size:13px;"><strong>' + escHtml(c.name) + '</strong>' + (voiceName ? ' · ' + escHtml(voiceName) : '') + (c.description ? '<br><span style="color:#888;font-size:11px;">' + escHtml(c.description) + '</span>' : '') + '</div>'
|
||||
+ '<div style="display:flex;gap:4px;flex-shrink:0;">'
|
||||
+ '<button class="btn btn-sm btn-outline" onclick="editChar(' + i + ')" style="padding:2px 8px;font-size:11px;">编辑</button>'
|
||||
+ '<button class="btn btn-sm btn-danger" onclick="deleteChar(' + i + ')" style="padding:2px 8px;font-size:11px;">删除</button></div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showCharForm() {
|
||||
document.getElementById('modalCharTitle').textContent = '添加演员';
|
||||
document.getElementById('editModalCharIdx').value = '-1';
|
||||
document.getElementById('modalCharName').value = '';
|
||||
document.getElementById('modalCharDesc').value = '';
|
||||
document.getElementById('modalCharVoice').value = '';
|
||||
document.getElementById('modalCharPortrait').value = '';
|
||||
document.getElementById('modalCharModal').classList.add('show');
|
||||
}
|
||||
|
||||
function hideModalCharForm() { document.getElementById('modalCharModal').classList.remove('show'); }
|
||||
|
||||
function editChar(idx) {
|
||||
var c = currentChars[idx]; if (!c) return;
|
||||
document.getElementById('modalCharTitle').textContent = '编辑演员';
|
||||
document.getElementById('editModalCharIdx').value = c.id;
|
||||
document.getElementById('modalCharName').value = c.name || '';
|
||||
document.getElementById('modalCharDesc').value = c.description || '';
|
||||
document.getElementById('modalCharVoice').value = '';
|
||||
document.getElementById('modalCharPortrait').value = '';
|
||||
document.getElementById('modalCharModal').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveChar() {
|
||||
var name = document.getElementById('modalCharName').value.trim();
|
||||
if (!name) { alert('请输入演员名'); return; }
|
||||
var charId = parseInt(document.getElementById('editModalCharIdx').value);
|
||||
const btn = event.target; btn.disabled = true;
|
||||
try {
|
||||
var formData = new FormData();
|
||||
formData.append('dramaId', currentCharDramaId);
|
||||
formData.append('name', name);
|
||||
formData.append('description', document.getElementById('modalCharDesc').value.trim());
|
||||
var voiceInput = document.getElementById('modalCharVoice');
|
||||
if (voiceInput.files && voiceInput.files[0]) {
|
||||
formData.append('voiceFile', voiceInput.files[0]);
|
||||
}
|
||||
var portraitInput = document.getElementById('modalCharPortrait');
|
||||
if (portraitInput.files && portraitInput.files[0]) {
|
||||
formData.append('portraitFile', portraitInput.files[0]);
|
||||
}
|
||||
if (charId >= 0) formData.append('charId', charId);
|
||||
const url = charId >= 0 ? '/drama/character/update' : '/drama/character/add';
|
||||
const resp = await fetch(url, { method: 'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
hideModalCharForm();
|
||||
openActorModal(currentCharDramaId);
|
||||
} catch (err) { alert('保存演员失败: ' + err.message); }
|
||||
finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function deleteChar(idx) {
|
||||
var c = currentChars[idx]; if (!c) return;
|
||||
confirmAction = async function() {
|
||||
try {
|
||||
const resp = await fetch('/drama/character/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dramaId: parseInt(currentCharDramaId), charId: parseInt(c.id) }) });
|
||||
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
|
||||
openActorModal(currentCharDramaId);
|
||||
} catch (err) { alert('删除演员失败: ' + err.message); }
|
||||
};
|
||||
document.getElementById('confirmTitle').textContent = '删除演员';
|
||||
document.getElementById('confirmMsg').textContent = '确定要删除演员「' + c.name + '」吗?';
|
||||
document.getElementById('confirmBtn').className = 'btn btn-danger';
|
||||
document.getElementById('confirmModal').classList.add('show');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 演员管理弹窗 -->
|
||||
<div class="modal-overlay" id="actorModal">
|
||||
<div class="modal" style="width:80vw;height:80vh;max-width:1100px;padding:24px 28px;display:flex;flex-direction:column;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-shrink:0;">
|
||||
<div>
|
||||
<h3 id="actorModalDramaTitle" style="margin-bottom:4px;">演员管理</h3>
|
||||
<span id="actorModalDramaMeta" style="font-size:12px;color:#888;"></span>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="hideActorModal()">关闭</button>
|
||||
</div>
|
||||
<div id="actorList" style="flex:1;overflow-y:auto;padding-right:8px;min-height:60px;"></div>
|
||||
<div class="add-link" onclick="showCharForm()" style="margin-top:8px;flex-shrink:0;">+ 添加演员</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
<style>
|
||||
#modalCharModal { z-index:1050; }
|
||||
</style>
|
||||
|
||||
<!-- 演员编辑弹窗 -->
|
||||
<div class="modal-overlay" id="modalCharModal">
|
||||
<div class="modal">
|
||||
<h3 id="modalCharTitle">添加演员</h3>
|
||||
<div class="form-group">
|
||||
<label>演员名 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="modalCharName" placeholder="请输入演员名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>演员描述 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="modalCharDesc" placeholder="请输入演员描述">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>声音文件 <span style="color:#e44;">*</span></label>
|
||||
<input type="file" id="modalCharVoice" accept="audio/*">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>人物形象 <span style="color:#e44;">*</span></label>
|
||||
<input type="file" id="modalCharPortrait" accept="image/*">
|
||||
</div>
|
||||
<input type="hidden" id="editModalCharIdx" value="-1">
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-outline" onclick="hideModalCharForm()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveChar(this)">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,293 +0,0 @@
|
||||
<style>
|
||||
.config-tab-bar { display:flex; gap:4px; border-bottom:2px solid #eee; flex-shrink:0; }
|
||||
.config-tab { padding:8px 16px; font-size:13px; font-weight:600; color:#888; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all .2s; user-select:none; }
|
||||
.config-tab:hover { color:#4a6cf7; }
|
||||
.config-tab.active { color:#4a6cf7; border-bottom-color:#4a6cf7; }
|
||||
.config-tab-content { display:none; }
|
||||
.config-tab-content.active { display:block; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let curConfigTab = 'chat';
|
||||
|
||||
function showConfigModal() {
|
||||
curConfigTab = 'chat';
|
||||
document.getElementById('configModal').classList.add('show');
|
||||
switchConfigTab('chat');
|
||||
loadConfigForm();
|
||||
}
|
||||
|
||||
function hideConfigModal() {
|
||||
document.getElementById('configModal').classList.remove('show');
|
||||
}
|
||||
|
||||
function switchConfigTab(tab) {
|
||||
curConfigTab = tab;
|
||||
document.querySelectorAll('.config-tab').forEach(function(el) {
|
||||
el.classList.toggle('active', el.dataset.tab === tab);
|
||||
});
|
||||
document.querySelectorAll('.config-tab-content').forEach(function(el) {
|
||||
el.classList.toggle('active', el.id === 'cfg_tab_' + tab);
|
||||
});
|
||||
}
|
||||
|
||||
function switchVideoCategory() {
|
||||
var sel = gid('cfg_videoModelCategory');
|
||||
var refSections = gid('cfg_video_ref_sections');
|
||||
if (!sel || !refSections) return;
|
||||
refSections.style.display = sel.value === 'i2v' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
function gid(id) { return document.getElementById(id); }
|
||||
function sv(id, val) { const el = gid(id); if (el) el.value = val; }
|
||||
|
||||
// ==================== 加载配置 ====================
|
||||
|
||||
async function loadConfigForm() {
|
||||
try {
|
||||
const resp = await fetch('/config/model');
|
||||
const data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
var cfg = data.data || {};
|
||||
|
||||
sv('cfg_chatModelName', cfg.chatModelName || '');
|
||||
sv('cfg_chatApiKey', cfg.chatApiKey || '');
|
||||
sv('cfg_chat_submit_url', cfg.chatBaseUrl || '');
|
||||
if (cfg.maxTokens) sv('cfg_maxTokens', cfg.maxTokens);
|
||||
if (cfg.temperature) sv('cfg_temperature', cfg.temperature);
|
||||
|
||||
sv('cfg_videoModelName', cfg.videoModelName || '');
|
||||
sv('cfg_videoApiKey', cfg.videoApiKey || '');
|
||||
sv('cfg_video_submit_url', cfg.videoBaseUrl || '');
|
||||
sv('cfg_video_query_url', cfg.videoQueryUrl || '');
|
||||
sv('cfg_videoModelCategory', cfg.videoModelCategory || 't2v');
|
||||
if (cfg.minSingleDuration) sv('cfg_minSingleDuration', cfg.minSingleDuration);
|
||||
if (cfg.maxSingleDuration) sv('cfg_maxSingleDuration', cfg.maxSingleDuration);
|
||||
if (cfg.maxReferenceImages) sv('cfg_maxReferenceImages', cfg.maxReferenceImages);
|
||||
if (cfg.maxRefVideoCount) sv('cfg_maxRefVideoCount', cfg.maxRefVideoCount);
|
||||
if (cfg.maxRefVideoFileSize) sv('cfg_maxRefVideoFileSize', cfg.maxRefVideoFileSize);
|
||||
if (cfg.maxRefVideoDuration) sv('cfg_maxRefVideoDuration', cfg.maxRefVideoDuration);
|
||||
if (cfg.refVideoFormats) sv('cfg_refVideoFormats', cfg.refVideoFormats);
|
||||
if (cfg.maxRefAudioCount) sv('cfg_maxRefAudioCount', cfg.maxRefAudioCount);
|
||||
if (cfg.maxRefAudioFileSize) sv('cfg_maxRefAudioFileSize', cfg.maxRefAudioFileSize);
|
||||
if (cfg.maxRefAudioDuration) sv('cfg_maxRefAudioDuration', cfg.maxRefAudioDuration);
|
||||
if (cfg.refAudioFormats) sv('cfg_refAudioFormats', cfg.refAudioFormats);
|
||||
if (cfg.maxRefImageFileSize) sv('cfg_maxRefImageFileSize', cfg.maxRefImageFileSize);
|
||||
if (cfg.refImageFormats) sv('cfg_refImageFormats', cfg.refImageFormats);
|
||||
switchVideoCategory();
|
||||
} catch (err) { console.error('加载配置失败:', err); }
|
||||
}
|
||||
|
||||
// ==================== 保存 ====================
|
||||
|
||||
async function saveConfig() {
|
||||
var chatModelName = gid('cfg_chatModelName').value.trim();
|
||||
var chatApiUrl = gid('cfg_chat_submit_url')?.value?.trim() || '';
|
||||
var chatApiKey = gid('cfg_chatApiKey').value.trim();
|
||||
|
||||
if (!chatModelName) { alert('请输入对话模型名称'); return; }
|
||||
if (!chatApiUrl) { alert('请输入对话模型请求地址'); return; }
|
||||
if (!chatApiKey) { alert('请输入对话模型 API Key'); return; }
|
||||
|
||||
var maxTokens = parseInt(gid('cfg_maxTokens')?.value);
|
||||
var temperature = parseFloat(gid('cfg_temperature')?.value);
|
||||
if (!maxTokens || maxTokens < 1) { alert('请输入对话模型最大 Token 数'); return; }
|
||||
if (temperature === undefined || isNaN(temperature) || temperature < 0) { alert('请输入对话模型 Temperature'); return; }
|
||||
|
||||
var videoModelName = gid('cfg_videoModelName').value.trim();
|
||||
var videoApiUrl = gid('cfg_video_submit_url')?.value?.trim() || '';
|
||||
var videoApiKey = gid('cfg_videoApiKey').value.trim();
|
||||
var videoQueryUrl = gid('cfg_video_query_url')?.value?.trim() || '';
|
||||
var videoModelCategory = gid('cfg_videoModelCategory')?.value || 't2v';
|
||||
|
||||
if (!videoModelName) { alert('请输入视频生成模型名称'); return; }
|
||||
if (!videoApiUrl) { alert('请输入视频生成模型请求地址'); return; }
|
||||
if (!videoApiKey) { alert('请输入视频生成模型 API Key'); return; }
|
||||
if (!videoQueryUrl) { alert('请输入视频生成结果拉取地址'); return; }
|
||||
|
||||
var minSingleDuration = parseInt(gid('cfg_minSingleDuration')?.value);
|
||||
var maxSingleDuration = parseInt(gid('cfg_maxSingleDuration')?.value);
|
||||
if (!minSingleDuration || minSingleDuration < 1) { alert('请输入单次生成最小时长'); return; }
|
||||
if (!maxSingleDuration || maxSingleDuration < 1) { alert('请输入单次生成最大时长'); return; }
|
||||
if (minSingleDuration > maxSingleDuration) { alert('单次生成最小时长不能大于最大时长'); return; }
|
||||
|
||||
// 图生图模式:校验参考字段
|
||||
if (videoModelCategory === 'i2v') {
|
||||
if (!parseInt(gid('cfg_maxRefVideoCount')?.value) > 0) { alert('图生图模式下,参考视频数量必须大于0'); return; }
|
||||
if (!parseInt(gid('cfg_maxRefVideoFileSize')?.value) > 0) { alert('图生图模式下,单个参考视频最大大小必须填写'); return; }
|
||||
if (!parseInt(gid('cfg_maxRefVideoDuration')?.value) > 0) { alert('图生图模式下,单个参考视频最大时长必须填写'); return; }
|
||||
if (!gid('cfg_refVideoFormats')?.value?.trim()) { alert('图生图模式下,允许的视频格式不能为空'); return; }
|
||||
if (!parseInt(gid('cfg_maxRefAudioCount')?.value) > 0) { alert('图生图模式下,参考音频数量必须大于0'); return; }
|
||||
if (!parseInt(gid('cfg_maxRefAudioFileSize')?.value) > 0) { alert('图生图模式下,单个参考音频最大大小必须填写'); return; }
|
||||
if (!parseInt(gid('cfg_maxRefAudioDuration')?.value) > 0) { alert('图生图模式下,单个参考音频最大时长必须填写'); return; }
|
||||
if (!gid('cfg_refAudioFormats')?.value?.trim()) { alert('图生图模式下,允许的音频格式不能为空'); return; }
|
||||
if (parseInt(gid('cfg_maxReferenceImages')?.value) < 2) { alert('图生图模式下,单次生成最大参考图数量至少为2'); return; }
|
||||
if (!parseInt(gid('cfg_maxRefImageFileSize')?.value) > 0) { alert('图生图模式下,单个参考图片最大大小必须填写'); return; }
|
||||
if (!gid('cfg_refImageFormats')?.value?.trim()) { alert('图生图模式下,允许的图片格式不能为空'); return; }
|
||||
}
|
||||
|
||||
const btn = gid('cfg_saveBtn');
|
||||
const loading = gid('cfg_loading');
|
||||
const success = gid('cfg_successMsg');
|
||||
btn.disabled = true; loading.classList.add('show'); success.classList.remove('show');
|
||||
|
||||
try {
|
||||
const body = {
|
||||
chatApiKey: chatApiKey,
|
||||
chatBaseUrl: chatApiUrl,
|
||||
chatModelName: chatModelName,
|
||||
maxTokens: maxTokens,
|
||||
temperature: temperature,
|
||||
videoApiKey: videoApiKey,
|
||||
videoBaseUrl: videoApiUrl,
|
||||
videoModelName: videoModelName,
|
||||
videoModelCategory: videoModelCategory,
|
||||
videoQueryUrl: gid('cfg_video_query_url')?.value?.trim() || '',
|
||||
minSingleDuration: parseInt(gid('cfg_minSingleDuration')?.value) || 2,
|
||||
maxSingleDuration: parseInt(gid('cfg_maxSingleDuration')?.value) || 15,
|
||||
maxReferenceImages: parseInt(gid('cfg_maxReferenceImages')?.value) || 9,
|
||||
maxRefVideoCount: parseInt(gid('cfg_maxRefVideoCount')?.value) || 0,
|
||||
maxRefVideoFileSize: parseInt(gid('cfg_maxRefVideoFileSize')?.value) || 0,
|
||||
maxRefVideoDuration: parseInt(gid('cfg_maxRefVideoDuration')?.value) || 0,
|
||||
refVideoFormats: gid('cfg_refVideoFormats')?.value || '',
|
||||
maxRefAudioCount: parseInt(gid('cfg_maxRefAudioCount')?.value) || 0,
|
||||
maxRefAudioFileSize: parseInt(gid('cfg_maxRefAudioFileSize')?.value) || 0,
|
||||
maxRefAudioDuration: parseInt(gid('cfg_maxRefAudioDuration')?.value) || 0,
|
||||
refAudioFormats: gid('cfg_refAudioFormats')?.value || '',
|
||||
maxRefImageFileSize: parseInt(gid('cfg_maxRefImageFileSize')?.value) || 0,
|
||||
refImageFormats: gid('cfg_refImageFormats')?.value || '',
|
||||
};
|
||||
const resp = await fetch('/config/model', {
|
||||
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);
|
||||
success.classList.add('show');
|
||||
setTimeout(function() { success.classList.remove('show'); }, 3000);
|
||||
} catch (err) {
|
||||
alert('保存失败: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
loading.classList.remove('show');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 模型管理弹窗 -->
|
||||
<div class="modal-overlay" id="configModal">
|
||||
<div class="modal" style="width:80vw;height:80vh;max-width:1100px;padding:24px 28px;display:flex;flex-direction:column;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-shrink:0;">
|
||||
<h3 style="margin:0;">模型管理</h3>
|
||||
<button class="btn btn-outline btn-sm" onclick="hideConfigModal()">关闭</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 栏 -->
|
||||
<div class="config-tab-bar" style="display:flex;">
|
||||
<div class="config-tab active" data-tab="chat" onclick="switchConfigTab('chat')">对话模型</div>
|
||||
<div class="config-tab" data-tab="video" onclick="switchConfigTab('video')">视频生成模型</div>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;overflow-y:auto;padding-right:8px;margin-top:16px;">
|
||||
|
||||
<!-- ========== 对话模型 ========== -->
|
||||
<div class="config-tab-content active" id="cfg_tab_chat">
|
||||
<div style="margin-bottom:16px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">基础配置</span>
|
||||
<span style="font-size:11px;color:#fff;background:#4a6cf7;border-radius:3px;padding:1px 6px;">必填</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group" style="margin-bottom:0;"><label>模型名称 <span style="color:#e44;">*</span></label><input type="text" id="cfg_chatModelName" placeholder="qwen-max" style="width:100%;"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>API Key <span style="color:#e44;">*</span></label><input type="password" id="cfg_chatApiKey" placeholder="sk-..." style="width:100%;"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>请求地址 <span style="color:#e44;">*</span></label><input type="text" id="cfg_chat_submit_url" placeholder="https://..." style="width:100%;"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>请求方式 <span style="color:#e44;">*</span></label><select id="cfg_chat_submit_method"><option value="POST">POST</option><option value="GET">GET</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group" style="margin-bottom:0;"><label>最大 Token 数 <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxTokens" value="4096" min="256" max="131072"><div class="help-text">超过长度自动截断</div></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>Temperature <span style="color:#e44;">*</span></label><input type="number" id="cfg_temperature" value="0.8" min="0" max="2" step="0.1"><div class="help-text">随机值(0-2)</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== 视频生成模型 ========== -->
|
||||
<div class="config-tab-content" id="cfg_tab_video">
|
||||
<div style="margin-bottom:16px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">基础配置</span>
|
||||
<span style="font-size:11px;color:#fff;background:#4a6cf7;border-radius:3px;padding:1px 6px;">必填</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group" style="margin-bottom:0;"><label>模型名称 <span style="color:#e44;">*</span></label><input type="text" id="cfg_videoModelName" placeholder="wanx2.1-t2v-turbo" style="width:100%;"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>API Key <span style="color:#e44;">*</span></label><input type="password" id="cfg_videoApiKey" placeholder="sk-..." style="width:100%;"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>请求地址 <span style="color:#e44;">*</span></label><input type="text" id="cfg_video_submit_url" placeholder="https://..." style="width:100%;"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>请求方式 <span style="color:#e44;">*</span></label><select id="cfg_video_submit_method"><option value="POST">POST</option><option value="GET">GET</option></select></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>结果拉取地址 <span style="color:#e44;">*</span></label><input type="text" id="cfg_video_query_url" placeholder="https://.../api/v1/tasks/{task_id}" style="width:100%;"><div class="help-text">用于查询视频生成任务状态和结果,{task_id} 会被自动替换</div></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>模型类型 <span style="color:#e44;">*</span></label><select id="cfg_videoModelCategory" onchange="switchVideoCategory()" style="width:100%;"><option value="t2v">文生图</option><option value="i2v">图生图</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group" style="margin-bottom:0;"><label>单次生成最小时长(秒) <span style="color:#e44;">*</span></label><input type="number" id="cfg_minSingleDuration" value="2" min="1" max="30"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>单次生成最大时长(秒) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxSingleDuration" value="15" min="1" max="60"></div>
|
||||
</div>
|
||||
<div id="cfg_video_ref_sections" style="display:none;">
|
||||
<!-- 参考视频约束 -->
|
||||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">参考视频</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group" style="margin-bottom:0;"><label>参考视频数量 <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefVideoCount" value="0" min="0" max="50"><div class="help-text">0=不限制</div></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>单个视频最大大小(MB) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefVideoFileSize" value="50" min="0" max="1024"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>单个视频最大时长(秒) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefVideoDuration" value="30" min="0" max="3600"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>允许的视频格式 <span style="color:#e44;">*</span></label><input type="text" id="cfg_refVideoFormats" value="mp4,avi,mov,mkv,webm" placeholder="逗号分隔"><div class="help-text">逗号分隔,如 mp4,avi,mov</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参考音频约束 -->
|
||||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">参考音频</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group" style="margin-bottom:0;"><label>参考音频数量 <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefAudioCount" value="0" min="0" max="50"><div class="help-text">0=不限制</div></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>单个音频最大大小(MB) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefAudioFileSize" value="10" min="0" max="1024"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>单个音频最大时长(秒) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefAudioDuration" value="30" min="0" max="3600"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>允许的音频格式 <span style="color:#e44;">*</span></label><input type="text" id="cfg_refAudioFormats" value="wav,mp3,aac,flac,ogg" placeholder="逗号分隔"><div class="help-text">逗号分隔,如 wav,mp3,aac</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 参考图片约束 -->
|
||||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
||||
<span style="font-weight:600;font-size:15px;color:#1a1a2e;">参考图片</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group" style="margin-bottom:0;grid-column:1/-1;">
|
||||
<label>单次生成最大参考图数量 <span style="color:#e44;">*</span></label>
|
||||
<input type="number" id="cfg_maxReferenceImages" value="9" min="1" max="50">
|
||||
<div class="help-text">模型API限制的单次最大参考图片数(含首帧)。实际使用时首帧占1个,演员+场景+道具参考图共占用除此以外的数量。例如填写9,则每次生成最多使用8张参考图</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>单个图片最大大小(MB) <span style="color:#e44;">*</span></label><input type="number" id="cfg_maxRefImageFileSize" value="10" min="0" max="100"></div>
|
||||
<div class="form-group" style="margin-bottom:0;"><label>允许的图片格式 <span style="color:#e44;">*</span></label><input type="text" id="cfg_refImageFormats" value="png,jpg,jpeg,webp,bmp" placeholder="逗号分隔"><div class="help-text">逗号分隔,如 png,jpg,jpeg,webp</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div style="flex-shrink:0;display:flex;gap:12px;justify-content:center;align-items:center;margin-top:16px;padding-top:14px;border-top:1px solid #eee;">
|
||||
<div class="loading" id="cfg_loading" style="display:none;align-items:center;gap:6px;color:#888;font-size:13px;"><div class="spinner"></div><span>保存中...</span></div>
|
||||
<button class="btn btn-primary" id="cfg_saveBtn" onclick="saveConfig()">保存配置</button>
|
||||
<button class="btn btn-outline" onclick="hideConfigModal()">取消</button>
|
||||
</div>
|
||||
<div class="success-msg" id="cfg_successMsg">配置已保存</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div class="modal-overlay" id="confirmModal">
|
||||
<div class="modal">
|
||||
<h3 id="confirmTitle">确认删除</h3>
|
||||
<p id="confirmMsg" style="font-size:14px;color:#666;margin-bottom:20px;">确定要删除这个短剧吗?此操作不可恢复。</p>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-outline" onclick="closeModal()">取消</button>
|
||||
<button class="btn btn-danger" id="confirmBtn">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,644 +0,0 @@
|
||||
<style>
|
||||
.drama-tab-bar { display:flex; gap:4px; border-bottom:2px solid #eee; flex-shrink:0; }
|
||||
.drama-tab { padding:8px 16px; font-size:13px; font-weight:600; color:#888; cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all .2s; user-select:none; }
|
||||
.drama-tab:hover { color:#4a6cf7; }
|
||||
.drama-tab.active { color:#4a6cf7; border-bottom-color:#4a6cf7; }
|
||||
.drama-tab-content { display:none; }
|
||||
.drama-tab-content.active { display:flex; flex-direction:column; flex:1; overflow:hidden; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// ==================== 弹窗入口 ====================
|
||||
|
||||
function showDramaModal() {
|
||||
// 新建短剧
|
||||
editDramaId = null;
|
||||
currentDramaId = null;
|
||||
dramaTabLoaded = {};
|
||||
document.getElementById('dramaModalTitle').textContent = '新建短剧';
|
||||
document.getElementById('modalTitle').value = '';
|
||||
document.getElementById('modalStyle').value = '现代都市';
|
||||
document.getElementById('modalEpisodeDuration').value = 60;
|
||||
document.getElementById('modalBasicForm').style.display = '';
|
||||
document.getElementById('dramaTabBar').style.display = 'none';
|
||||
document.querySelectorAll('.drama-tab-content').forEach(function(e) { e.classList.remove('active'); });
|
||||
document.getElementById('tab_basic').classList.add('active');
|
||||
document.getElementById('dramaModal').classList.add('show');
|
||||
}
|
||||
|
||||
function hideDramaModal() {
|
||||
document.getElementById('dramaModal').classList.remove('show');
|
||||
editDramaId = null;
|
||||
currentDramaId = null;
|
||||
}
|
||||
|
||||
function openDramaEditModal(id, tab) {
|
||||
editDramaId = id;
|
||||
currentDramaId = id;
|
||||
dramaTabLoaded = {};
|
||||
document.getElementById('dramaModalTitle').textContent = '短剧编辑';
|
||||
document.getElementById('modalBasicForm').style.display = '';
|
||||
document.getElementById('dramaTabBar').style.display = 'flex';
|
||||
document.getElementById('dramaModal').classList.add('show');
|
||||
// 取消所有 tab 的 active
|
||||
document.querySelectorAll('.drama-tab-content').forEach(function(e) { e.classList.remove('active'); });
|
||||
switchDramaTab(tab || 'basic');
|
||||
}
|
||||
|
||||
// ==================== Tab 切换 ====================
|
||||
|
||||
var dramaTabLoaded = {};
|
||||
|
||||
function switchDramaTab(tab) {
|
||||
document.querySelectorAll('.drama-tab').forEach(function(el) {
|
||||
el.classList.toggle('active', el.dataset.tab === tab);
|
||||
});
|
||||
document.querySelectorAll('.drama-tab-content').forEach(function(el) {
|
||||
el.classList.toggle('active', el.id === 'tab_' + tab);
|
||||
});
|
||||
// 按需加载数据
|
||||
if (!dramaTabLoaded[tab] && currentDramaId) {
|
||||
dramaTabLoaded[tab] = true;
|
||||
if (tab === 'basic') loadDramaBasic();
|
||||
else if (tab === 'actor') loadActorData();
|
||||
else if (tab === 'scene') loadSceneData();
|
||||
else if (tab === 'prop') loadPropData();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 基础信息 ====================
|
||||
|
||||
function loadDramaBasic() {
|
||||
if (!currentDramaId) return;
|
||||
fetch('/drama/get?id=' + currentDramaId).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
var d = data.data;
|
||||
document.getElementById('modalTitle').value = d.title || '';
|
||||
document.getElementById('modalStyle').value = d.style || '';
|
||||
document.getElementById('modalEpisodeDuration').value = d.episodeDuration || 60;
|
||||
}).catch(function(err) {
|
||||
alert('加载短剧信息失败: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveDrama() {
|
||||
var title = document.getElementById('modalTitle').value.trim();
|
||||
if (!title) { alert('请输入短剧标题'); return; }
|
||||
var style = document.getElementById('modalStyle').value;
|
||||
if (!style) { alert('请选择短剧风格'); return; }
|
||||
var episodeDuration = parseInt(document.getElementById('modalEpisodeDuration').value);
|
||||
if (!episodeDuration || episodeDuration < 1) { alert('请输入每集时长'); return; }
|
||||
var body = {
|
||||
title: title,
|
||||
style: style,
|
||||
episodeDuration: episodeDuration,
|
||||
};
|
||||
var btn = document.getElementById('saveDramaBtn');
|
||||
btn.disabled = true;
|
||||
try {
|
||||
var url;
|
||||
if (editDramaId) { url = '/drama/update'; body.id = parseInt(editDramaId); }
|
||||
else { url = '/drama/create'; }
|
||||
var resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
var data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
var successMsg = document.getElementById('saveSuccessMsg');
|
||||
if (!editDramaId && data.data && data.data.id) {
|
||||
// 新建后启用 tab 切换
|
||||
currentDramaId = data.data.id;
|
||||
editDramaId = data.data.id;
|
||||
document.getElementById('dramaModalTitle').textContent = '短剧编辑';
|
||||
document.getElementById('dramaTabBar').style.display = 'flex';
|
||||
dramaTabLoaded = { basic: true };
|
||||
successMsg.textContent = '短剧已创建';
|
||||
} else {
|
||||
successMsg.textContent = '已保存';
|
||||
}
|
||||
successMsg.style.display = 'block';
|
||||
setTimeout(function() { successMsg.style.display = 'none'; }, 3000);
|
||||
loadList();
|
||||
} catch (err) {
|
||||
alert('保存失败: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 剧集管理 ====================
|
||||
|
||||
var episodePage = 1;
|
||||
var episodeTotal = 0;
|
||||
var episodePageSize = 10;
|
||||
|
||||
function fetchDramaAndEps() {
|
||||
if (!currentDramaId) return;
|
||||
Promise.all([
|
||||
fetch('/drama/get?id=' + currentDramaId),
|
||||
fetch('/drama/episode/list?dramaId=' + currentDramaId + '&page=' + episodePage + '&pageSize=' + episodePageSize)
|
||||
]).then(function(responses) {
|
||||
return Promise.all(responses.map(function(r) { return r.json(); }));
|
||||
}).then(function(results) {
|
||||
var dramaData = results[0], epData = results[1];
|
||||
if (dramaData.code !== 0) throw new Error(dramaData.message);
|
||||
if (epData.code !== 0) throw new Error(epData.message);
|
||||
currentDrama = dramaData.data;
|
||||
episodeTotal = epData.data.total;
|
||||
var meta = document.getElementById('epMeta');
|
||||
if (meta) meta.textContent = (currentDrama.style || '') + ' · ' + (currentDrama.episodeDuration || '-') + 's/集';
|
||||
renderEpsTab(epData.data.list);
|
||||
}).catch(function(err) {
|
||||
var list = document.getElementById('epList');
|
||||
if (list) list.innerHTML = '<div style="color:red;font-size:13px;padding:8px 0">加载失败: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderEpsTab(eps) {
|
||||
var list = document.getElementById('epList');
|
||||
if (!list) return;
|
||||
if (!eps || eps.length === 0) {
|
||||
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无剧集,点击下方添加</div>' + epPageBar();
|
||||
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);
|
||||
list.insertAdjacentHTML('beforeend', epPageBar());
|
||||
}
|
||||
|
||||
function epPageBar() {
|
||||
var totalPages = Math.ceil(episodeTotal / episodePageSize) || 1;
|
||||
if (episodeTotal === 0) return '';
|
||||
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;">'
|
||||
+ '共 ' + episodeTotal + ' 条 每页 <select onchange="epChangeSize(this.value)" style="padding:2px 4px;border:1px solid #d9d9d9;border-radius:4px;font-size:12px;outline:none;">'
|
||||
+ [10,20,50,100].map(function(s) { return '<option value="' + s + '"' + (s === episodePageSize ? ' selected' : '') + '>' + s + '</option>'; }).join('')
|
||||
+ '</select>'
|
||||
+ ' <button class="btn btn-sm btn-outline" onclick="epPagePrev()" ' + (episodePage <= 1 ? 'disabled' : '') + '>上一页</button>'
|
||||
+ ' 第 ' + episodePage + '/' + totalPages + ' 页'
|
||||
+ ' <button class="btn btn-sm btn-outline" onclick="epPageNext()" ' + (episodePage >= totalPages ? 'disabled' : '') + '>下一页</button></div>';
|
||||
}
|
||||
|
||||
function epPagePrev() { if (episodePage > 1) { episodePage--; fetchDramaAndEps(); } }
|
||||
function epPageNext() { var tp = Math.ceil(episodeTotal / episodePageSize) || 1; if (episodePage < tp) { episodePage++; fetchDramaAndEps(); } }
|
||||
function epChangeSize(val) { episodePageSize = parseInt(val); episodePage = 1; fetchDramaAndEps(); }
|
||||
|
||||
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) {
|
||||
var ep = (currentDrama && currentDrama.episodes || []).find(function(x) { return 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() {
|
||||
var title = document.getElementById('epTitle').value.trim();
|
||||
if (!title) { alert('请输入剧集标题'); return; }
|
||||
var editEpId = document.getElementById('editEpId').value;
|
||||
var body = { dramaId: parseInt(currentDramaId), title: title, script: document.getElementById('epScript').value.trim() };
|
||||
if (editEpId) body.epId = parseInt(editEpId);
|
||||
var btn = document.getElementById('saveEpBtn'); btn.disabled = true;
|
||||
try {
|
||||
var url = editEpId ? '/drama/episode/update' : '/drama/episode/add';
|
||||
var resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
var 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) {
|
||||
var ep = (currentDrama && currentDrama.episodes || []).find(function(x) { return x.id == epId; });
|
||||
var title = ep ? ep.title : '';
|
||||
confirmAction = async function() {
|
||||
try {
|
||||
var resp = await fetch('/drama/episode/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dramaId: parseInt(currentDramaId), epId: parseInt(epId) }) });
|
||||
var 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');
|
||||
}
|
||||
|
||||
// ==================== 演员管理 ====================
|
||||
|
||||
var currentChars = [];
|
||||
|
||||
function loadActorData() {
|
||||
if (!currentDramaId) return;
|
||||
fetch('/drama/get?id=' + currentDramaId).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
renderActorTab(data.data.characters || []);
|
||||
}).catch(function(err) {
|
||||
var list = document.getElementById('actorList');
|
||||
if (list) list.innerHTML = '<div style="color:red;font-size:13px;padding:8px 0">加载失败: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderActorTab(chars) {
|
||||
currentChars = chars;
|
||||
var list = document.getElementById('actorList');
|
||||
if (!list) return;
|
||||
if (!chars || chars.length === 0) {
|
||||
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无演员,点击下方添加</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = chars.map(function(c, i) {
|
||||
var initial = c.name ? c.name.charAt(0) : '?';
|
||||
var portraitUrl = c.portraitPath ? '/' + c.portraitPath : '';
|
||||
var voiceName = c.voicePath ? c.voicePath.split('/').pop() : '';
|
||||
return '<div style="display:flex;gap:8px;align-items:center;padding:8px 10px;background:#fafafa;border-radius:8px;border:1px solid #eee;margin-bottom:6px;">'
|
||||
+ (portraitUrl ? '<img src="' + portraitUrl + '" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0;">' : '<span style="width:36px;height:36px;border-radius:50%;background:#e0e0e0;display:inline-flex;align-items:center;justify-content:center;font-size:15px;font-weight:bold;color:#666;flex-shrink:0;">' + initial + '</span>')
|
||||
+ '<div style="flex:1;font-size:13px;"><strong>' + escHtml(c.name) + '</strong>' + (voiceName ? ' · ' + escHtml(voiceName) : '') + (c.description ? '<br><span style="color:#888;font-size:11px;">' + escHtml(c.description) + '</span>' : '') + '</div>'
|
||||
+ '<div style="display:flex;gap:4px;flex-shrink:0;">'
|
||||
+ '<button class="btn btn-sm btn-outline" onclick="editChar(' + i + ')" style="padding:2px 8px;font-size:11px;">编辑</button>'
|
||||
+ '<button class="btn btn-sm btn-danger" onclick="deleteChar(' + i + ')" style="padding:2px 8px;font-size:11px;">删除</button></div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showCharForm() {
|
||||
try {
|
||||
if (!document.getElementById('modalCharModal')) {
|
||||
alert('演员表单弹窗未加载,请刷新页面重试'); return;
|
||||
}
|
||||
document.getElementById('modalCharTitle').textContent = '添加演员';
|
||||
document.getElementById('editModalCharIdx').value = '-1';
|
||||
document.getElementById('modalCharName').value = '';
|
||||
document.getElementById('modalCharDesc').value = '';
|
||||
document.getElementById('modalCharVoice').value = '';
|
||||
document.getElementById('modalCharPortrait').value = '';
|
||||
document.getElementById('modalCharModal').classList.add('show');
|
||||
} catch (e) { console.error('showCharForm error:', e); alert('演员表单弹窗异常: ' + e.message); }
|
||||
}
|
||||
function hideModalCharForm() { document.getElementById('modalCharModal').classList.remove('show'); }
|
||||
|
||||
function editChar(idx) {
|
||||
var c = currentChars[idx]; if (!c) return;
|
||||
document.getElementById('modalCharTitle').textContent = '编辑演员';
|
||||
document.getElementById('editModalCharIdx').value = c.id;
|
||||
document.getElementById('modalCharName').value = c.name || '';
|
||||
document.getElementById('modalCharDesc').value = c.description || '';
|
||||
document.getElementById('modalCharVoice').value = '';
|
||||
document.getElementById('modalCharPortrait').value = '';
|
||||
document.getElementById('modalCharModal').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveChar(btn) {
|
||||
var name = document.getElementById('modalCharName').value.trim();
|
||||
if (!name) { alert('请输入演员名'); return; }
|
||||
var charId = parseInt(document.getElementById('editModalCharIdx').value);
|
||||
btn = btn || document.getElementById('saveCharBtn'); btn.disabled = true;
|
||||
try {
|
||||
var formData = new FormData();
|
||||
formData.append('dramaId', currentDramaId);
|
||||
formData.append('name', name);
|
||||
formData.append('description', document.getElementById('modalCharDesc').value.trim());
|
||||
var voiceInput = document.getElementById('modalCharVoice');
|
||||
if (voiceInput.files && voiceInput.files[0]) formData.append('voiceFile', voiceInput.files[0]);
|
||||
var portraitInput = document.getElementById('modalCharPortrait');
|
||||
if (portraitInput.files && portraitInput.files[0]) formData.append('portraitFile', portraitInput.files[0]);
|
||||
if (charId >= 0) formData.append('charId', charId);
|
||||
var url = charId >= 0 ? '/drama/character/update' : '/drama/character/add';
|
||||
var resp = await fetch(url, { method: 'POST', body: formData });
|
||||
var data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
hideModalCharForm(); loadActorData();
|
||||
} catch (err) { alert('保存演员失败: ' + err.message); }
|
||||
finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function deleteChar(idx) {
|
||||
var c = currentChars[idx]; if (!c) return;
|
||||
confirmAction = async function() {
|
||||
try {
|
||||
var resp = await fetch('/drama/character/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ dramaId: parseInt(currentDramaId), charId: parseInt(c.id) }) });
|
||||
var data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
|
||||
loadActorData();
|
||||
} catch (err) { alert('删除演员失败: ' + err.message); }
|
||||
};
|
||||
document.getElementById('confirmTitle').textContent = '删除演员';
|
||||
document.getElementById('confirmMsg').textContent = '确定要删除演员「' + c.name + '」吗?';
|
||||
document.getElementById('confirmBtn').className = 'btn btn-danger';
|
||||
document.getElementById('confirmModal').classList.add('show');
|
||||
}
|
||||
|
||||
// ==================== 场景管理 ====================
|
||||
|
||||
var currentScenes = [];
|
||||
|
||||
function loadSceneData() {
|
||||
if (!currentDramaId) return;
|
||||
fetch('/drama/get?id=' + currentDramaId).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
renderSceneTab(data.data.scenes || []);
|
||||
}).catch(function(err) {
|
||||
var list = document.getElementById('sceneList');
|
||||
if (list) list.innerHTML = '<div style="color:red;font-size:13px;padding:8px 0">加载失败: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderSceneTab(scenes) {
|
||||
currentScenes = scenes;
|
||||
var list = document.getElementById('sceneList');
|
||||
if (!list) return;
|
||||
if (!scenes || scenes.length === 0) {
|
||||
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无场景,点击下方添加</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = scenes.map(function(s, i) {
|
||||
var initial = s.name ? s.name.charAt(0) : '?';
|
||||
var imgUrl = s.imagePath ? '/' + s.imagePath : '';
|
||||
return '<div style="display:flex;gap:8px;align-items:center;padding:8px 10px;background:#fafafa;border-radius:8px;border:1px solid #eee;margin-bottom:6px;">'
|
||||
+ (imgUrl ? '<img src="' + imgUrl + '" style="width:36px;height:36px;border-radius:6px;object-fit:cover;flex-shrink:0;">' : '<span style="width:36px;height:36px;border-radius:6px;background:#e0e0e0;display:inline-flex;align-items:center;justify-content:center;font-size:15px;font-weight:bold;color:#666;flex-shrink:0;">' + initial + '</span>')
|
||||
+ '<div style="flex:1;font-size:13px;"><strong>' + escHtml(s.name) + '</strong>' + (s.description ? '<br><span style="color:#888;font-size:11px;">' + escHtml(s.description) + '</span>' : '') + '</div>'
|
||||
+ '<div style="display:flex;gap:4px;flex-shrink:0;">'
|
||||
+ '<button class="btn btn-sm btn-outline" onclick="editScene(' + i + ')" style="padding:2px 8px;font-size:11px;">编辑</button>'
|
||||
+ '<button class="btn btn-sm btn-danger" onclick="deleteScene(' + i + ')" style="padding:2px 8px;font-size:11px;">删除</button></div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showSceneForm() {
|
||||
try {
|
||||
if (!document.getElementById('modalSceneModal')) {
|
||||
alert('场景表单弹窗未加载,请刷新页面重试'); return;
|
||||
}
|
||||
document.getElementById('modalSceneTitle').textContent = '添加场景';
|
||||
document.getElementById('editModalSceneIdx').value = '-1';
|
||||
document.getElementById('modalSceneName').value = '';
|
||||
document.getElementById('modalSceneDesc').value = '';
|
||||
document.getElementById('modalSceneImage').value = '';
|
||||
document.getElementById('modalSceneModal').classList.add('show');
|
||||
} catch (e) { console.error('showSceneForm error:', e); alert('场景表单弹窗异常: ' + e.message); }
|
||||
}
|
||||
function hideModalSceneForm() { document.getElementById('modalSceneModal').classList.remove('show'); }
|
||||
|
||||
function editScene(idx) {
|
||||
var s = currentScenes[idx]; if (!s) return;
|
||||
document.getElementById('modalSceneTitle').textContent = '编辑场景';
|
||||
document.getElementById('editModalSceneIdx').value = s.id;
|
||||
document.getElementById('modalSceneName').value = s.name || '';
|
||||
document.getElementById('modalSceneDesc').value = s.description || '';
|
||||
document.getElementById('modalSceneImage').value = '';
|
||||
document.getElementById('modalSceneModal').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveScene(btn) {
|
||||
var name = document.getElementById('modalSceneName').value.trim();
|
||||
if (!name) { alert('请输入场景名'); return; }
|
||||
var id = parseInt(document.getElementById('editModalSceneIdx').value);
|
||||
btn = btn || document.getElementById('saveSceneBtn'); btn.disabled = true;
|
||||
try {
|
||||
var formData = new FormData();
|
||||
formData.append('dramaId', currentDramaId);
|
||||
formData.append('name', name);
|
||||
formData.append('description', document.getElementById('modalSceneDesc').value.trim());
|
||||
var imgInput = document.getElementById('modalSceneImage');
|
||||
if (imgInput.files && imgInput.files[0]) formData.append('imageFile', imgInput.files[0]);
|
||||
if (id >= 0) formData.append('id', id);
|
||||
var url = id >= 0 ? '/drama/scene/update' : '/drama/scene/add';
|
||||
var resp = await fetch(url, { method: 'POST', body: formData });
|
||||
var data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
hideModalSceneForm(); loadSceneData();
|
||||
} catch (err) { alert('保存场景失败: ' + err.message); }
|
||||
finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function deleteScene(idx) {
|
||||
var s = currentScenes[idx]; if (!s) return;
|
||||
confirmAction = async function() {
|
||||
try {
|
||||
var resp = await fetch('/drama/scene/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: parseInt(s.id), dramaId: parseInt(currentDramaId) }) });
|
||||
var data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
|
||||
loadSceneData();
|
||||
} catch (err) { alert('删除场景失败: ' + err.message); }
|
||||
};
|
||||
document.getElementById('confirmTitle').textContent = '删除场景';
|
||||
document.getElementById('confirmMsg').textContent = '确定要删除场景「' + s.name + '」吗?';
|
||||
document.getElementById('confirmBtn').className = 'btn btn-danger';
|
||||
document.getElementById('confirmModal').classList.add('show');
|
||||
}
|
||||
|
||||
// ==================== 道具管理 ====================
|
||||
|
||||
var currentProps = [];
|
||||
|
||||
function loadPropData() {
|
||||
if (!currentDramaId) return;
|
||||
fetch('/drama/get?id=' + currentDramaId).then(function(r) { return r.json(); }).then(function(data) {
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
renderPropTab(data.data.props || []);
|
||||
}).catch(function(err) {
|
||||
var list = document.getElementById('propList');
|
||||
if (list) list.innerHTML = '<div style="color:red;font-size:13px;padding:8px 0">加载失败: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderPropTab(props) {
|
||||
currentProps = props;
|
||||
var list = document.getElementById('propList');
|
||||
if (!list) return;
|
||||
if (!props || props.length === 0) {
|
||||
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无道具,点击下方添加</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = props.map(function(p, i) {
|
||||
var initial = p.name ? p.name.charAt(0) : '?';
|
||||
var imgUrl = p.imagePath ? '/' + p.imagePath : '';
|
||||
return '<div style="display:flex;gap:8px;align-items:center;padding:8px 10px;background:#fafafa;border-radius:8px;border:1px solid #eee;margin-bottom:6px;">'
|
||||
+ (imgUrl ? '<img src="' + imgUrl + '" style="width:36px;height:36px;border-radius:6px;object-fit:cover;flex-shrink:0;">' : '<span style="width:36px;height:36px;border-radius:6px;background:#e0e0e0;display:inline-flex;align-items:center;justify-content:center;font-size:15px;font-weight:bold;color:#666;flex-shrink:0;">' + initial + '</span>')
|
||||
+ '<div style="flex:1;font-size:13px;"><strong>' + escHtml(p.name) + '</strong>' + (p.description ? '<br><span style="color:#888;font-size:11px;">' + escHtml(p.description) + '</span>' : '') + '</div>'
|
||||
+ '<div style="display:flex;gap:4px;flex-shrink:0;">'
|
||||
+ '<button class="btn btn-sm btn-outline" onclick="editProp(' + i + ')" style="padding:2px 8px;font-size:11px;">编辑</button>'
|
||||
+ '<button class="btn btn-sm btn-danger" onclick="deleteProp(' + i + ')" style="padding:2px 8px;font-size:11px;">删除</button></div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showPropForm() {
|
||||
try {
|
||||
if (!document.getElementById('modalPropModal')) {
|
||||
alert('道具表单弹窗未加载,请刷新页面重试'); return;
|
||||
}
|
||||
document.getElementById('modalPropTitle').textContent = '添加道具';
|
||||
document.getElementById('editModalPropIdx').value = '-1';
|
||||
document.getElementById('modalPropName').value = '';
|
||||
document.getElementById('modalPropDesc').value = '';
|
||||
document.getElementById('modalPropImage').value = '';
|
||||
document.getElementById('modalPropModal').classList.add('show');
|
||||
} catch (e) { console.error('showPropForm error:', e); alert('道具表单弹窗异常: ' + e.message); }
|
||||
}
|
||||
function hideModalPropForm() { document.getElementById('modalPropModal').classList.remove('show'); }
|
||||
|
||||
function editProp(idx) {
|
||||
var p = currentProps[idx]; if (!p) return;
|
||||
document.getElementById('modalPropTitle').textContent = '编辑道具';
|
||||
document.getElementById('editModalPropIdx').value = p.id;
|
||||
document.getElementById('modalPropName').value = p.name || '';
|
||||
document.getElementById('modalPropDesc').value = p.description || '';
|
||||
document.getElementById('modalPropImage').value = '';
|
||||
document.getElementById('modalPropModal').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveProp(btn) {
|
||||
var name = document.getElementById('modalPropName').value.trim();
|
||||
if (!name) { alert('请输入道具名'); return; }
|
||||
var id = parseInt(document.getElementById('editModalPropIdx').value);
|
||||
btn = btn || document.getElementById('savePropBtn'); btn.disabled = true;
|
||||
try {
|
||||
var formData = new FormData();
|
||||
formData.append('dramaId', currentDramaId);
|
||||
formData.append('name', name);
|
||||
formData.append('description', document.getElementById('modalPropDesc').value.trim());
|
||||
var imgInput = document.getElementById('modalPropImage');
|
||||
if (imgInput.files && imgInput.files[0]) formData.append('imageFile', imgInput.files[0]);
|
||||
if (id >= 0) formData.append('id', id);
|
||||
var url = id >= 0 ? '/drama/prop/update' : '/drama/prop/add';
|
||||
var resp = await fetch(url, { method: 'POST', body: formData });
|
||||
var data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
hideModalPropForm(); loadPropData();
|
||||
} catch (err) { alert('保存道具失败: ' + err.message); }
|
||||
finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function deleteProp(idx) {
|
||||
var p = currentProps[idx]; if (!p) return;
|
||||
confirmAction = async function() {
|
||||
try {
|
||||
var resp = await fetch('/drama/prop/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: parseInt(p.id), dramaId: parseInt(currentDramaId) }) });
|
||||
var data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
|
||||
loadPropData();
|
||||
} catch (err) { alert('删除道具失败: ' + err.message); }
|
||||
};
|
||||
document.getElementById('confirmTitle').textContent = '删除道具';
|
||||
document.getElementById('confirmMsg').textContent = '确定要删除道具「' + p.name + '」吗?';
|
||||
document.getElementById('confirmBtn').className = 'btn btn-danger';
|
||||
document.getElementById('confirmModal').classList.add('show');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ===== 主弹窗(含 5 个 Tab) ===== -->
|
||||
<div class="modal-overlay" id="dramaModal">
|
||||
<div class="modal" style="width:80vw;height:80vh;max-width:1100px;padding:24px 28px;display:flex;flex-direction:column;">
|
||||
<!-- 头部 -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-shrink:0;">
|
||||
<h3 id="dramaModalTitle" style="margin:0;">短剧编辑</h3>
|
||||
<button class="btn btn-outline btn-sm" onclick="hideDramaModal()">关闭</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 栏 -->
|
||||
<div class="drama-tab-bar" id="dramaTabBar" style="display:none;">
|
||||
<div class="drama-tab active" data-tab="basic" onclick="switchDramaTab('basic')">基础信息</div>
|
||||
<div class="drama-tab" data-tab="actor" onclick="switchDramaTab('actor')">演员</div>
|
||||
<div class="drama-tab" data-tab="scene" onclick="switchDramaTab('scene')">场景</div>
|
||||
<div class="drama-tab" data-tab="prop" onclick="switchDramaTab('prop')">道具</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 基础信息 ===== -->
|
||||
<div class="drama-tab-content active" id="tab_basic">
|
||||
<div id="modalBasicForm" style="flex:1;display:flex;align-items:center;justify-content:center;">
|
||||
<div style="width:100%;max-width:400px;">
|
||||
<div class="form-group" style="margin-bottom:20px;">
|
||||
<label>短剧标题 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="modalTitle" placeholder="请输入短剧标题">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:20px;">
|
||||
<label>风格 <span style="color:#e44;">*</span></label>
|
||||
<select id="modalStyle">
|
||||
<option value="现代都市">现代都市</option>
|
||||
<option value="古装仙侠">古装仙侠</option>
|
||||
<option value="科幻未来">科幻未来</option>
|
||||
<option value="悬疑推理">悬疑推理</option>
|
||||
<option value="古装">古装</option>
|
||||
<option value="现代">现代</option>
|
||||
<option value="科幻">科幻</option>
|
||||
<option value="奇幻">奇幻</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label>每集时长(秒) <span style="color:#e44;">*</span></label>
|
||||
<input type="number" id="modalEpisodeDuration" value="60" min="10" max="600">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:center;align-items:center;gap:12px;flex-shrink:0;padding-top:16px;border-top:1px solid #eee;">
|
||||
<button class="btn btn-primary" id="saveDramaBtn" onclick="saveDrama()">保存</button>
|
||||
<div class="success-msg" id="saveSuccessMsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 剧集 ===== -->
|
||||
|
||||
<!-- ===== 演员 ===== -->
|
||||
<div class="drama-tab-content" id="tab_actor">
|
||||
<div style="display:flex;justify-content:flex-end;flex-shrink:0;padding:8px 0 4px;">
|
||||
<div class="add-link" onclick="showCharForm()" style="margin:0;">+ 添加演员</div>
|
||||
</div>
|
||||
<div id="actorList" style="flex:1;overflow-y:auto;padding-right:4px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 场景 ===== -->
|
||||
<div class="drama-tab-content" id="tab_scene">
|
||||
<div style="display:flex;justify-content:flex-end;flex-shrink:0;padding:8px 0 4px;">
|
||||
<div class="add-link" onclick="showSceneForm()" style="margin:0;">+ 添加场景</div>
|
||||
</div>
|
||||
<div id="sceneList" style="flex:1;overflow-y:auto;padding-right:4px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 道具 ===== -->
|
||||
<div class="drama-tab-content" id="tab_prop">
|
||||
<div style="display:flex;justify-content:flex-end;flex-shrink:0;padding:8px 0 4px;">
|
||||
<div class="add-link" onclick="showPropForm()" style="margin:0;">+ 添加道具</div>
|
||||
</div>
|
||||
<div id="propList" style="flex:1;overflow-y:auto;padding-right:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,23 +0,0 @@
|
||||
<style>
|
||||
#epModal { z-index:1050; }
|
||||
</style>
|
||||
|
||||
<!-- 剧集编辑弹窗 -->
|
||||
<div class="modal-overlay" id="epModal">
|
||||
<div class="modal modal-wide">
|
||||
<h3 id="epModalTitle">添加剧集</h3>
|
||||
<div class="form-group">
|
||||
<label>剧集标题 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="epTitle" placeholder="请输入剧集标题">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>剧集描述</label>
|
||||
<textarea id="epScript" placeholder="请输入剧集描述" rows="4" style="width:100%;padding:8px 10px;border:1px solid #d9d9d9;border-radius:6px;font-size:13px;outline:none;font-family:inherit;resize:vertical"></textarea>
|
||||
</div>
|
||||
<input type="hidden" id="editEpId" value="">
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-outline" onclick="hideEpModal()">取消</button>
|
||||
<button class="btn btn-primary" id="saveEpBtn" onclick="saveEp()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
<!-- 剧集管理弹窗 -->
|
||||
<div class="modal-overlay" id="episodeModal">
|
||||
<div class="modal" style="width:80vw;height:80vh;max-width:1100px;padding:24px 28px;display:flex;flex-direction:column;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-shrink:0;">
|
||||
<div>
|
||||
<h3 id="epModalDramaTitle" style="margin-bottom:4px;">加载中...</h3>
|
||||
<span id="epModalDramaMeta" style="font-size:12px;color:#888;"></span>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="hideEpisodeModal()">关闭</button>
|
||||
</div>
|
||||
<div id="episodeEpList" class="ep-list" style="overflow-y:auto;"></div>
|
||||
<div class="add-link" onclick="showEpModal()" style="margin-top:8px;flex-shrink:0;">+ 添加剧集</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,27 +0,0 @@
|
||||
<style>
|
||||
#modalPropModal { z-index:1050; }
|
||||
</style>
|
||||
|
||||
<!-- 道具编辑弹窗 -->
|
||||
<div class="modal-overlay" id="modalPropModal">
|
||||
<div class="modal">
|
||||
<h3 id="modalPropTitle">添加道具</h3>
|
||||
<div class="form-group">
|
||||
<label>道具名 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="modalPropName" placeholder="请输入道具名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>道具描述 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="modalPropDesc" placeholder="请输入道具描述">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>道具图片 <span style="color:#e44;">*</span></label>
|
||||
<input type="file" id="modalPropImage" accept="image/*">
|
||||
</div>
|
||||
<input type="hidden" id="editModalPropIdx" value="-1">
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-outline" onclick="hideModalPropForm()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveProp(this)">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,120 +0,0 @@
|
||||
<script>
|
||||
// ==================== 道具管理 ====================
|
||||
|
||||
function openPropModal(id) {
|
||||
currentDramaId = id;
|
||||
document.getElementById('propModal').classList.add('show');
|
||||
document.getElementById('propModalDramaTitle').textContent = '加载中...';
|
||||
document.getElementById('propModalDramaMeta').textContent = '';
|
||||
document.getElementById('propList').innerHTML = '<div style="color:#999;font-size:13px;text-align:center;padding:20px;">加载中...</div>';
|
||||
fetch('/drama/get?id=' + id).then(r => r.json()).then(data => {
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
const d = data.data;
|
||||
document.getElementById('propModalDramaTitle').textContent = d.title || '未命名短剧';
|
||||
document.getElementById('propModalDramaMeta').textContent = (d.style || '') + ' · 道具';
|
||||
renderPropList(d.props || []);
|
||||
}).catch(err => {
|
||||
document.getElementById('propList').innerHTML = '<div style="color:red;font-size:13px;text-align:center;padding:20px;">加载失败: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function hidePropModal() {
|
||||
document.getElementById('propModal').classList.remove('show');
|
||||
currentDramaId = null;
|
||||
}
|
||||
|
||||
function renderPropList(props) {
|
||||
currentChars = props;
|
||||
const list = document.getElementById('propList');
|
||||
if (!props || props.length === 0) {
|
||||
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无道具,点击下方添加</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = props.map(function(p, i) {
|
||||
var initial = p.name ? p.name.charAt(0) : '?';
|
||||
var imgUrl = p.imagePath ? '/' + p.imagePath : '';
|
||||
return '<div style="display:flex;gap:8px;align-items:center;padding:8px 10px;background:#fafafa;border-radius:8px;border:1px solid #eee;margin-bottom:6px;">'
|
||||
+ (imgUrl ? '<img src="' + imgUrl + '" style="width:36px;height:36px;border-radius:6px;object-fit:cover;flex-shrink:0;">' : '<span style="width:36px;height:36px;border-radius:6px;background:#e0e0e0;display:inline-flex;align-items:center;justify-content:center;font-size:15px;font-weight:bold;color:#666;flex-shrink:0;">' + initial + '</span>')
|
||||
+ '<div style="flex:1;font-size:13px;"><strong>' + escHtml(p.name) + '</strong>' + (p.description ? '<br><span style="color:#888;font-size:11px;">' + escHtml(p.description) + '</span>' : '') + '</div>'
|
||||
+ '<div style="display:flex;gap:4px;flex-shrink:0;">'
|
||||
+ '<button class="btn btn-sm btn-outline" onclick="editProp(' + i + ')" style="padding:2px 8px;font-size:11px;">编辑</button>'
|
||||
+ '<button class="btn btn-sm btn-danger" onclick="deleteProp(' + i + ')" style="padding:2px 8px;font-size:11px;">删除</button></div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showPropForm() {
|
||||
document.getElementById('modalPropTitle').textContent = '添加道具';
|
||||
document.getElementById('editModalPropIdx').value = '-1';
|
||||
document.getElementById('modalPropName').value = '';
|
||||
document.getElementById('modalPropDesc').value = '';
|
||||
document.getElementById('modalPropImage').value = '';
|
||||
document.getElementById('modalPropModal').classList.add('show');
|
||||
}
|
||||
|
||||
function hideModalPropForm() { document.getElementById('modalPropModal').classList.remove('show'); }
|
||||
|
||||
function editProp(idx) {
|
||||
var p = currentChars[idx]; if (!p) return;
|
||||
document.getElementById('modalPropTitle').textContent = '编辑道具';
|
||||
document.getElementById('editModalPropIdx').value = p.id;
|
||||
document.getElementById('modalPropName').value = p.name || '';
|
||||
document.getElementById('modalPropDesc').value = p.description || '';
|
||||
document.getElementById('modalPropImage').value = '';
|
||||
document.getElementById('modalPropModal').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveProp() {
|
||||
var name = document.getElementById('modalPropName').value.trim();
|
||||
if (!name) { alert('请输入道具名'); return; }
|
||||
var id = parseInt(document.getElementById('editModalPropIdx').value);
|
||||
const btn = event.target; btn.disabled = true;
|
||||
try {
|
||||
var formData = new FormData();
|
||||
formData.append('dramaId', currentDramaId);
|
||||
formData.append('name', name);
|
||||
formData.append('description', document.getElementById('modalPropDesc').value.trim());
|
||||
var imgInput = document.getElementById('modalPropImage');
|
||||
if (imgInput.files && imgInput.files[0]) {
|
||||
formData.append('imageFile', imgInput.files[0]);
|
||||
}
|
||||
if (id >= 0) formData.append('id', id);
|
||||
const url = id >= 0 ? '/drama/prop/update' : '/drama/prop/add';
|
||||
const resp = await fetch(url, { method: 'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
hideModalPropForm();
|
||||
openPropModal(currentDramaId);
|
||||
} catch (err) { alert('保存道具失败: ' + err.message); }
|
||||
finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function deleteProp(idx) {
|
||||
var p = currentChars[idx]; if (!p) return;
|
||||
confirmAction = async function() {
|
||||
try {
|
||||
const resp = await fetch('/drama/prop/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: parseInt(p.id), dramaId: parseInt(currentDramaId) }) });
|
||||
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
|
||||
openPropModal(currentDramaId);
|
||||
} catch (err) { alert('删除道具失败: ' + err.message); }
|
||||
};
|
||||
document.getElementById('confirmTitle').textContent = '删除道具';
|
||||
document.getElementById('confirmMsg').textContent = '确定要删除道具「' + p.name + '」吗?';
|
||||
document.getElementById('confirmBtn').className = 'btn btn-danger';
|
||||
document.getElementById('confirmModal').classList.add('show');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 道具管理弹窗 -->
|
||||
<div class="modal-overlay" id="propModal">
|
||||
<div class="modal" style="width:80vw;height:80vh;max-width:1100px;padding:24px 28px;display:flex;flex-direction:column;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-shrink:0;">
|
||||
<div>
|
||||
<h3 id="propModalDramaTitle" style="margin-bottom:4px;">道具管理</h3>
|
||||
<span id="propModalDramaMeta" style="font-size:12px;color:#888;"></span>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="hidePropModal()">关闭</button>
|
||||
</div>
|
||||
<div id="propList" style="flex:1;overflow-y:auto;padding-right:8px;min-height:60px;"></div>
|
||||
<div class="add-link" onclick="showPropForm()" style="margin-top:8px;flex-shrink:0;">+ 添加道具</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- 预览审核弹窗 -->
|
||||
<div class="modal-overlay" id="reviewModal" style="z-index:1200;">
|
||||
<div class="modal modal-wide" style="max-width:720px;">
|
||||
<h3 id="reviewModalTitle">预览审核</h3>
|
||||
<div id="reviewSegments" style="max-height:65vh;overflow-y:auto;"></div>
|
||||
<div class="modal-actions" style="margin-top:12px;padding-top:12px;border-top:1px solid #eee;">
|
||||
<button class="btn btn-outline" onclick="hideReviewPanel()">关闭</button>
|
||||
<span id="reviewStatus" style="font-size:12px;color:#888;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,27 +0,0 @@
|
||||
<style>
|
||||
#modalSceneModal { z-index:1050; }
|
||||
</style>
|
||||
|
||||
<!-- 场景编辑弹窗 -->
|
||||
<div class="modal-overlay" id="modalSceneModal">
|
||||
<div class="modal">
|
||||
<h3 id="modalSceneTitle">添加场景</h3>
|
||||
<div class="form-group">
|
||||
<label>场景名 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="modalSceneName" placeholder="请输入场景名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>场景描述 <span style="color:#e44;">*</span></label>
|
||||
<input type="text" id="modalSceneDesc" placeholder="请输入场景描述">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>场景图片 <span style="color:#e44;">*</span></label>
|
||||
<input type="file" id="modalSceneImage" accept="image/*">
|
||||
</div>
|
||||
<input type="hidden" id="editModalSceneIdx" value="-1">
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-outline" onclick="hideModalSceneForm()">取消</button>
|
||||
<button class="btn btn-primary" onclick="saveScene(this)">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,120 +0,0 @@
|
||||
<script>
|
||||
// ==================== 场景管理 ====================
|
||||
|
||||
function openSceneModal(id) {
|
||||
currentDramaId = id;
|
||||
document.getElementById('sceneModal').classList.add('show');
|
||||
document.getElementById('sceneModalDramaTitle').textContent = '加载中...';
|
||||
document.getElementById('sceneModalDramaMeta').textContent = '';
|
||||
document.getElementById('sceneList').innerHTML = '<div style="color:#999;font-size:13px;text-align:center;padding:20px;">加载中...</div>';
|
||||
fetch('/drama/get?id=' + id).then(r => r.json()).then(data => {
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
const d = data.data;
|
||||
document.getElementById('sceneModalDramaTitle').textContent = d.title || '未命名短剧';
|
||||
document.getElementById('sceneModalDramaMeta').textContent = (d.style || '') + ' · 场景';
|
||||
renderSceneList(d.scenes || []);
|
||||
}).catch(err => {
|
||||
document.getElementById('sceneList').innerHTML = '<div style="color:red;font-size:13px;text-align:center;padding:20px;">加载失败: ' + err.message + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function hideSceneModal() {
|
||||
document.getElementById('sceneModal').classList.remove('show');
|
||||
currentDramaId = null;
|
||||
}
|
||||
|
||||
function renderSceneList(scenes) {
|
||||
currentChars = scenes;
|
||||
const list = document.getElementById('sceneList');
|
||||
if (!scenes || scenes.length === 0) {
|
||||
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无场景,点击下方添加</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = scenes.map(function(s, i) {
|
||||
var initial = s.name ? s.name.charAt(0) : '?';
|
||||
var imgUrl = s.imagePath ? '/' + s.imagePath : '';
|
||||
return '<div style="display:flex;gap:8px;align-items:center;padding:8px 10px;background:#fafafa;border-radius:8px;border:1px solid #eee;margin-bottom:6px;">'
|
||||
+ (imgUrl ? '<img src="' + imgUrl + '" style="width:36px;height:36px;border-radius:6px;object-fit:cover;flex-shrink:0;">' : '<span style="width:36px;height:36px;border-radius:6px;background:#e0e0e0;display:inline-flex;align-items:center;justify-content:center;font-size:15px;font-weight:bold;color:#666;flex-shrink:0;">' + initial + '</span>')
|
||||
+ '<div style="flex:1;font-size:13px;"><strong>' + escHtml(s.name) + '</strong>' + (s.description ? '<br><span style="color:#888;font-size:11px;">' + escHtml(s.description) + '</span>' : '') + '</div>'
|
||||
+ '<div style="display:flex;gap:4px;flex-shrink:0;">'
|
||||
+ '<button class="btn btn-sm btn-outline" onclick="editScene(' + i + ')" style="padding:2px 8px;font-size:11px;">编辑</button>'
|
||||
+ '<button class="btn btn-sm btn-danger" onclick="deleteScene(' + i + ')" style="padding:2px 8px;font-size:11px;">删除</button></div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showSceneForm() {
|
||||
document.getElementById('modalSceneTitle').textContent = '添加场景';
|
||||
document.getElementById('editModalSceneIdx').value = '-1';
|
||||
document.getElementById('modalSceneName').value = '';
|
||||
document.getElementById('modalSceneDesc').value = '';
|
||||
document.getElementById('modalSceneImage').value = '';
|
||||
document.getElementById('modalSceneModal').classList.add('show');
|
||||
}
|
||||
|
||||
function hideModalSceneForm() { document.getElementById('modalSceneModal').classList.remove('show'); }
|
||||
|
||||
function editScene(idx) {
|
||||
var s = currentChars[idx]; if (!s) return;
|
||||
document.getElementById('modalSceneTitle').textContent = '编辑场景';
|
||||
document.getElementById('editModalSceneIdx').value = s.id;
|
||||
document.getElementById('modalSceneName').value = s.name || '';
|
||||
document.getElementById('modalSceneDesc').value = s.description || '';
|
||||
document.getElementById('modalSceneImage').value = '';
|
||||
document.getElementById('modalSceneModal').classList.add('show');
|
||||
}
|
||||
|
||||
async function saveScene() {
|
||||
var name = document.getElementById('modalSceneName').value.trim();
|
||||
if (!name) { alert('请输入场景名'); return; }
|
||||
var id = parseInt(document.getElementById('editModalSceneIdx').value);
|
||||
const btn = event.target; btn.disabled = true;
|
||||
try {
|
||||
var formData = new FormData();
|
||||
formData.append('dramaId', currentDramaId);
|
||||
formData.append('name', name);
|
||||
formData.append('description', document.getElementById('modalSceneDesc').value.trim());
|
||||
var imgInput = document.getElementById('modalSceneImage');
|
||||
if (imgInput.files && imgInput.files[0]) {
|
||||
formData.append('imageFile', imgInput.files[0]);
|
||||
}
|
||||
if (id >= 0) formData.append('id', id);
|
||||
const url = id >= 0 ? '/drama/scene/update' : '/drama/scene/add';
|
||||
const resp = await fetch(url, { method: 'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
if (data.code !== 0) throw new Error(data.message);
|
||||
hideModalSceneForm();
|
||||
openSceneModal(currentDramaId);
|
||||
} catch (err) { alert('保存场景失败: ' + err.message); }
|
||||
finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
function deleteScene(idx) {
|
||||
var s = currentChars[idx]; if (!s) return;
|
||||
confirmAction = async function() {
|
||||
try {
|
||||
const resp = await fetch('/drama/scene/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: parseInt(s.id), dramaId: parseInt(currentDramaId) }) });
|
||||
const data = await resp.json(); if (data.code !== 0) throw new Error(data.message);
|
||||
openSceneModal(currentDramaId);
|
||||
} catch (err) { alert('删除场景失败: ' + err.message); }
|
||||
};
|
||||
document.getElementById('confirmTitle').textContent = '删除场景';
|
||||
document.getElementById('confirmMsg').textContent = '确定要删除场景「' + s.name + '」吗?';
|
||||
document.getElementById('confirmBtn').className = 'btn btn-danger';
|
||||
document.getElementById('confirmModal').classList.add('show');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 场景管理弹窗 -->
|
||||
<div class="modal-overlay" id="sceneModal">
|
||||
<div class="modal" style="width:80vw;height:80vh;max-width:1100px;padding:24px 28px;display:flex;flex-direction:column;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-shrink:0;">
|
||||
<div>
|
||||
<h3 id="sceneModalDramaTitle" style="margin-bottom:4px;">场景管理</h3>
|
||||
<span id="sceneModalDramaMeta" style="font-size:12px;color:#888;"></span>
|
||||
</div>
|
||||
<button class="btn btn-outline btn-sm" onclick="hideSceneModal()">关闭</button>
|
||||
</div>
|
||||
<div id="sceneList" style="flex:1;overflow-y:auto;padding-right:8px;min-height:60px;"></div>
|
||||
<div class="add-link" onclick="showSceneForm()" style="margin-top:8px;flex-shrink:0;">+ 添加场景</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user