|
|
|
@@ -258,17 +258,32 @@ let currentTaskId = null;
|
|
|
|
|
|
|
|
|
|
// ==================== 短剧列表 ====================
|
|
|
|
|
|
|
|
|
|
var dramaPage = 1;
|
|
|
|
|
var dramaTotal = 0;
|
|
|
|
|
var dramaPageSize = 20;
|
|
|
|
|
|
|
|
|
|
async function loadList() {
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch('/drama/list');
|
|
|
|
|
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">加载失败: ' + err.message + '</div>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPageInfo(total, page, pageSize, onPrev, onNext) {
|
|
|
|
|
const totalPages = Math.ceil(total / pageSize) || 1;
|
|
|
|
|
if (total <= pageSize) return '';
|
|
|
|
|
return '<div style="display:flex;justify-content:center;align-items:center;gap:10px;padding:12px 0 4px;font-size:13px;color:#888;">'
|
|
|
|
|
+ '<button class="btn btn-sm btn-outline" onclick="' + onPrev + '" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>'
|
|
|
|
|
+ '<span>第 ' + page + ' / ' + totalPages + ' 页(共 ' + total + ' 条)</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');
|
|
|
|
@@ -296,9 +311,12 @@ function renderList(list) {
|
|
|
|
|
<button class="btn btn-sm btn-danger" onclick="confirmDelete('${d.id}','${escHtml(d.title)}')">删除</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
}).join('') + renderPageInfo(dramaTotal, dramaPage, dramaPageSize, 'dramaPagePrev()', 'dramaPageNext()');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dramaPagePrev() { if (dramaPage > 1) { dramaPage--; loadList(); } }
|
|
|
|
|
function dramaPageNext() { var tp = Math.ceil(dramaTotal / dramaPageSize) || 1; if (dramaPage < tp) { dramaPage++; loadList(); } }
|
|
|
|
|
|
|
|
|
|
// ==================== 短剧 CRUD ====================
|
|
|
|
|
|
|
|
|
|
function showDramaModal() {
|
|
|
|
@@ -459,11 +477,16 @@ document.getElementById('confirmBtn').addEventListener('click', async function()
|
|
|
|
|
|
|
|
|
|
// ==================== 剧集管理 ====================
|
|
|
|
|
|
|
|
|
|
var episodePage = 1;
|
|
|
|
|
var episodeTotal = 0;
|
|
|
|
|
var episodePageSize = 10;
|
|
|
|
|
|
|
|
|
|
function openEpisodeModal(id) {
|
|
|
|
|
currentDramaId = id;
|
|
|
|
|
document.getElementById('episodeModal').classList.add('show');
|
|
|
|
|
document.getElementById('epModalDramaTitle').textContent = '加载中...';
|
|
|
|
|
document.getElementById('epModalDramaMeta').textContent = '';
|
|
|
|
|
episodePage = 1;
|
|
|
|
|
loadEpisodeDrama(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -476,29 +499,33 @@ function hideEpisodeModal() {
|
|
|
|
|
|
|
|
|
|
async function loadEpisodeDrama(id) {
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetch('/drama/get?id=' + id);
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
if (data.code !== 0) throw new Error(data.message);
|
|
|
|
|
currentDrama = data.data;
|
|
|
|
|
const [dramaResp, epResp] = await Promise.all([
|
|
|
|
|
fetch('/drama/get?id=' + id),
|
|
|
|
|
fetch('/drama/episode/list?dramaId=' + id + '&page=' + episodePage + '&pageSize=' + episodePageSize)
|
|
|
|
|
]);
|
|
|
|
|
const dramaData = await dramaResp.json();
|
|
|
|
|
const epData = await epResp.json();
|
|
|
|
|
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;
|
|
|
|
|
document.getElementById('epModalDramaTitle').textContent = currentDrama.title || '未命名短剧';
|
|
|
|
|
document.getElementById('epModalDramaMeta').textContent = (currentDrama.style || '') + ' · ' + (currentDrama.episodeDuration || '-') + 's/集';
|
|
|
|
|
renderEps();
|
|
|
|
|
renderEps(epData.data.list);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
document.getElementById('epModalDramaTitle').textContent = '加载失败';
|
|
|
|
|
alert('加载剧集失败: ' + err.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderEps() {
|
|
|
|
|
function renderEps(eps) {
|
|
|
|
|
const list = document.getElementById('epList');
|
|
|
|
|
const eps = currentDrama.episodes || [];
|
|
|
|
|
if (eps.length === 0) {
|
|
|
|
|
if (!eps || eps.length === 0) {
|
|
|
|
|
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无剧集,点击下方添加</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var taskMap = {};
|
|
|
|
|
if (currentDrama.tasks) { currentDrama.tasks.forEach(function(t) { taskMap[t.episodeId] = t; }); }
|
|
|
|
|
eps.sort(function(a, b) { return (a.index || 0) - (b.index || 0); });
|
|
|
|
|
list.innerHTML = eps.map(function(ep) {
|
|
|
|
|
const status = ep.status || 'pending';
|
|
|
|
|
const statusLabels = { pending:'待生成', generating:'生成中...', review:'待审核', completed:'已完成', failed:'生成失败' };
|
|
|
|
@@ -516,18 +543,21 @@ function renderEps() {
|
|
|
|
|
+ '<div class="status ' + status + '">'
|
|
|
|
|
+ (status === 'generating' ? '<span class="spinner-sm"></span>' : '')
|
|
|
|
|
+ (statusLabels[status] || status)
|
|
|
|
|
+ (ep.videoUrl ? ' · <button class="btn btn-sm btn-outline" onclick="previewEpisode(\'' + ep.id + '\')">预览</button>' : '')
|
|
|
|
|
+ (ep.videoUrl ? ' · <button class="btn btn-sm btn-outline" onclick="previewEpisode(' + ep.id + ')">预览</button>' : '')
|
|
|
|
|
+ segHtml
|
|
|
|
|
+ '</div></div>'
|
|
|
|
|
+ '<div class="actions">'
|
|
|
|
|
+ (status === 'pending' || status === 'failed' ? '<button class="btn btn-sm btn-success" onclick="generateEp(\'' + ep.id + '\')">生成视频</button>' : '')
|
|
|
|
|
+ (status === 'review' ? '<button class="btn btn-sm btn-primary" onclick="openReviewPanel(\'' + ep.id + '\')">审核</button>' : '')
|
|
|
|
|
+ '<button class="btn btn-sm btn-outline" onclick="editEp(\'' + ep.id + '\')">编辑</button>'
|
|
|
|
|
+ '<button class="btn btn-sm btn-danger" onclick="deleteEp(\'' + ep.id + '\',\'' + escAttr(ep.title) + '\')">删除</button>'
|
|
|
|
|
+ (status === 'pending' || status === 'failed' ? '<button class="btn btn-sm btn-success" onclick="generateEp(' + ep.id + ')">生成视频</button>' : '')
|
|
|
|
|
+ (status === 'review' ? '<button class="btn btn-sm btn-primary" onclick="openReviewPanel(' + ep.id + ')">审核</button>' : '')
|
|
|
|
|
+ '<button class="btn btn-sm btn-outline" onclick="editEp(' + ep.id + ')">编辑</button>'
|
|
|
|
|
+ '<button class="btn btn-sm btn-danger" onclick="deleteEp(' + ep.id + ',' + escAttr(ep.title) + ')">删除</button>'
|
|
|
|
|
+ '</div></div>';
|
|
|
|
|
}).join('');
|
|
|
|
|
}).join('') + renderPageInfo(episodeTotal, episodePage, episodePageSize, 'epPagePrev()', 'epPageNext()');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function epPagePrev() { if (episodePage > 1) { episodePage--; loadEpisodeDrama(currentDramaId); } }
|
|
|
|
|
function epPageNext() { var tp = Math.ceil(episodeTotal / episodePageSize) || 1; if (episodePage < tp) { episodePage++; loadEpisodeDrama(currentDramaId); } }
|
|
|
|
|
|
|
|
|
|
function segmentPreviews(task, epId) {
|
|
|
|
|
var sd; try { sd = JSON.parse(task.stepsData); } catch(e) { return ""; }
|
|
|
|
|
var segs = sd.segments || [];
|
|
|
|
@@ -821,26 +851,28 @@ async function savePrompt() {
|
|
|
|
|
|
|
|
|
|
<!-- 模型配置弹窗 -->
|
|
|
|
|
<div class="modal-overlay" id="configModal">
|
|
|
|
|
<div class="modal modal-wide" style="max-width:560px;">
|
|
|
|
|
<h3>模型配置</h3>
|
|
|
|
|
<div class="form-group"><label>对话 API Key</label><input type="password" id="cfg_chatApiKey" placeholder="sk-..."><div class="help-text">对话模型 API 密钥</div></div>
|
|
|
|
|
<h4 style="font-size:13px;margin:14px 0 8px;color:#1a1a2e;border-left:3px solid #4a6cf7;padding-left:8px;">对话模型(Agent 文字推理)</h4>
|
|
|
|
|
<div class="form-group"><label>API 地址</label><input type="text" id="cfg_chatBaseURL" placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"></div>
|
|
|
|
|
<div class="form-group"><label>对话模型</label><input type="text" id="cfg_chatModelName" placeholder="qwen3.7-plus, gpt-4o, deepseek-chat 等"></div>
|
|
|
|
|
<h4 style="font-size:13px;margin:14px 0 8px;color:#1a1a2e;border-left:3px solid #4a6cf7;padding-left:8px;">视频生成模型</h4>
|
|
|
|
|
<div class="form-group"><label>视频 API Key</label><input type="password" id="cfg_videoApiKey" placeholder="sk-..."><div class="help-text">视频生成 API 密钥,可与对话模型不同</div></div>
|
|
|
|
|
<div class="form-group"><label>视频 API 地址</label><input type="text" id="cfg_videoBaseURL" placeholder="https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis"></div>
|
|
|
|
|
<div class="form-group"><label>视频模型</label><input type="text" id="cfg_videoModelName" placeholder="wan2.7-t2v"></div>
|
|
|
|
|
<div class="form-group"><label>视频查询地址</label><input type="text" id="cfg_videoQueryURL" placeholder="https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"><div class="help-text">用 {task_id} 占位,轮询时自动替换</div></div>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="form-group"><label>Max Tokens</label><input type="number" id="cfg_maxTokens" value="4096" min="256" max="32768"></div>
|
|
|
|
|
<div class="form-group"><label>Temperature</label><input type="number" id="cfg_temperature" value="0.8" min="0" max="2" step="0.1"></div>
|
|
|
|
|
<div class="modal modal-wide" style="max-width:680px;padding:20px 24px;">
|
|
|
|
|
<h3 style="margin-bottom:14px;">模型配置</h3>
|
|
|
|
|
<div class="row" style="margin-bottom:10px;">
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>对话 API Key</label><input type="password" id="cfg_chatApiKey" placeholder="sk-..."><div class="help-text">对话模型 API 密钥</div></div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>视频 API Key</label><input type="password" id="cfg_videoApiKey" placeholder="sk-..."><div class="help-text">视频生成 API 密钥</div></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="form-group"><label>单段最小时长(秒)</label><input type="number" id="cfg_minSingleDuration" value="2" min="1" max="60"><div class="help-text">不足时从前面的段借</div></div>
|
|
|
|
|
<div class="form-group"><label>单段最大时长(秒)</label><input type="number" id="cfg_maxSingleDuration" value="30" min="5" max="300"><div class="help-text">超出会自动分段生成</div></div>
|
|
|
|
|
<div class="row" style="margin-bottom:10px;">
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>对话 API 地址</label><input type="text" id="cfg_chatBaseURL" placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"></div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>视频 API 地址</label><input type="text" id="cfg_videoBaseURL" placeholder="https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-top:16px;display:flex;gap:10px;justify-content:center;align-items:center;">
|
|
|
|
|
<div class="row" style="margin-bottom:10px;">
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>对话模型</label><input type="text" id="cfg_chatModelName" placeholder="qwen3.7-plus, gpt-4o, deepseek-chat 等"></div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>视频模型</label><input type="text" id="cfg_videoModelName" placeholder="wan2.7-t2v"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:10px;"><label>视频查询地址</label><input type="text" id="cfg_videoQueryURL" placeholder="https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"><div class="help-text">用 {task_id} 占位,轮询时自动替换</div></div>
|
|
|
|
|
<div class="row" style="margin-bottom:10px;">
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>Max Tokens</label><input type="number" id="cfg_maxTokens" value="4096" min="256" max="32768"></div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>Temperature</label><input type="number" id="cfg_temperature" value="0.8" min="0" max="2" step="0.1"></div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>单段最小时长(秒)</label><input type="number" id="cfg_minSingleDuration" value="2" min="1" max="60"><div class="help-text">不足时从前面的段借</div></div>
|
|
|
|
|
<div class="form-group" style="margin-bottom:0;"><label>单段最大时长(秒)</label><input type="number" id="cfg_maxSingleDuration" value="30" min="5" max="300"><div class="help-text">超出会自动分段生成</div></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-top:14px;display:flex;gap:10px;justify-content:center;align-items:center;">
|
|
|
|
|
<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>
|
|
|
|
|