视频任务管理更新
This commit is contained in:
@@ -380,11 +380,30 @@
|
||||
}
|
||||
var allColumnsList = [];
|
||||
function loadColumnsForPermission() {
|
||||
fetch('/api/admin/columns')
|
||||
.then(function (r) { return r.json(); })
|
||||
var ownedKeys = {};
|
||||
var ownedRoutes = {};
|
||||
var ownPermissionRequest = (currentUserRole === 'admin' && currentUserId)
|
||||
? fetch('/api/admin/user/' + currentUserId + '/column-permissions')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
(res.items || []).forEach(function (item) {
|
||||
if (item.column_key) ownedKeys[String(item.column_key).trim()] = true;
|
||||
if (item.route_path) ownedRoutes[String(item.route_path).trim()] = true;
|
||||
});
|
||||
})
|
||||
.catch(function () { })
|
||||
: Promise.resolve();
|
||||
ownPermissionRequest.then(function () {
|
||||
return fetch('/api/admin/columns');
|
||||
}).then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (!res.success) return;
|
||||
allColumnsList = res.items || [];
|
||||
allColumnsList = (res.items || []).filter(function (item) {
|
||||
if (item.column_key === 'admin_image_video_task_data') return false;
|
||||
if (currentUserRole !== 'admin') return true;
|
||||
return !!ownedKeys[String(item.column_key || '').trim()] ||
|
||||
!!ownedRoutes[String(item.route_path || '').trim()];
|
||||
});
|
||||
renderColumnPermissionWrap('createColumnPermissionWrap');
|
||||
renderColumnPermissionWrap('editColumnPermissionWrap');
|
||||
var selCreate = document.getElementById('createColumnPermissionWrap');
|
||||
@@ -617,17 +636,161 @@
|
||||
document.getElementById('editUserModal').classList.remove('show');
|
||||
};
|
||||
|
||||
// ========== 生成记录 ==========
|
||||
// ========== 视频任务管理 ==========
|
||||
var imageVideoTaskPage = 1, imageVideoTaskPageSize = 20;
|
||||
var imageVideoCards = [];
|
||||
var selectedImageVideoKeys = new Set();
|
||||
var imageVideoDownloadInProgress = false;
|
||||
var imageVideoPermissionUsers = [];
|
||||
var imageVideoPermissionInitialUserIds = new Set();
|
||||
var selectedImageVideoPermissionUserIds = new Set();
|
||||
var imageVideoPermissionView = 'granted';
|
||||
function updateImageVideoPermissionAccess() {
|
||||
var button = document.getElementById('btnOpenImageVideoPermissions');
|
||||
if (button) button.style.display = currentUserRole === 'super_admin' ? 'inline-flex' : 'none';
|
||||
}
|
||||
function imageVideoPermissionUsersForView() {
|
||||
if (imageVideoPermissionView === 'granted') {
|
||||
return imageVideoPermissionUsers.filter(function (user) {
|
||||
return imageVideoPermissionInitialUserIds.has(Number(user.id));
|
||||
});
|
||||
}
|
||||
return imageVideoPermissionUsers;
|
||||
}
|
||||
function syncImageVideoPermissionTabs() {
|
||||
document.getElementById('imageVideoPermissionGrantedCount').textContent = '(' + imageVideoPermissionInitialUserIds.size + ')';
|
||||
document.getElementById('imageVideoPermissionAllCount').textContent = '(' + imageVideoPermissionUsers.length + ')';
|
||||
document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) {
|
||||
var active = tab.dataset.imageVideoPermissionView === imageVideoPermissionView;
|
||||
tab.classList.toggle('active', active);
|
||||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
function filteredImageVideoPermissionUsers() {
|
||||
var users = imageVideoPermissionUsersForView();
|
||||
var keyword = (document.getElementById('imageVideoPermissionSearch').value || '').trim().toLowerCase();
|
||||
if (!keyword) return users;
|
||||
return users.filter(function (user) {
|
||||
return String(user.username || '').toLowerCase().indexOf(keyword) >= 0;
|
||||
});
|
||||
}
|
||||
function syncImageVideoPermissionSelectAll() {
|
||||
var visibleUsers = filteredImageVideoPermissionUsers();
|
||||
var selectedCount = visibleUsers.filter(function (user) {
|
||||
return selectedImageVideoPermissionUserIds.has(Number(user.id));
|
||||
}).length;
|
||||
var selectAll = document.getElementById('imageVideoPermissionSelectAll');
|
||||
selectAll.checked = visibleUsers.length > 0 && selectedCount === visibleUsers.length;
|
||||
selectAll.indeterminate = selectedCount > 0 && selectedCount < visibleUsers.length;
|
||||
selectAll.disabled = visibleUsers.length === 0;
|
||||
}
|
||||
function renderImageVideoPermissionUsers() {
|
||||
var list = document.getElementById('imageVideoPermissionList');
|
||||
var summary = document.getElementById('imageVideoPermissionSummary');
|
||||
var visibleUsers = filteredImageVideoPermissionUsers();
|
||||
syncImageVideoPermissionTabs();
|
||||
var pendingCount = imageVideoPermissionUsers.filter(function (user) {
|
||||
return imageVideoPermissionInitialUserIds.has(Number(user.id)) !== selectedImageVideoPermissionUserIds.has(Number(user.id));
|
||||
}).length;
|
||||
summary.textContent = imageVideoPermissionView === 'granted'
|
||||
? '当前显示已保存分配用户,共 ' + visibleUsers.length + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '')
|
||||
: '当前显示全部用户,已保存分配 ' + imageVideoPermissionInitialUserIds.size + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '');
|
||||
list.innerHTML = visibleUsers.length ? visibleUsers.map(function (user) {
|
||||
var userId = Number(user.id);
|
||||
var persistedGranted = imageVideoPermissionInitialUserIds.has(userId);
|
||||
var pendingGranted = selectedImageVideoPermissionUserIds.has(userId);
|
||||
var pendingChanged = persistedGranted !== pendingGranted;
|
||||
var actionText = pendingChanged
|
||||
? (pendingGranted ? '待保存分配' : '待保存取消')
|
||||
: (persistedGranted ? '取消分配' : '分配');
|
||||
return '<div class="image-video-permission-row">' +
|
||||
'<input type="checkbox" aria-label="' + escapeHtml(user.username || '-') + (persistedGranted ? ' 已保存分配' : ' 未分配') + '" data-image-video-permission-user="' + userId + '"' +
|
||||
(pendingGranted ? ' checked' : '') + '>' +
|
||||
'<span class="image-video-permission-user">' +
|
||||
'<span class="image-video-permission-name" title="' + escapeHtml(user.username || '') + '">' + escapeHtml(user.username || '-') + '</span>' +
|
||||
'<span class="image-video-permission-role">' + escapeHtml(roleLabel(user.role)) + '</span>' +
|
||||
'</span>' +
|
||||
'<button class="image-video-permission-state' + (pendingChanged ? ' pending' : (persistedGranted ? ' granted' : '')) + '" type="button" data-image-video-permission-toggle="' + userId + '">' +
|
||||
actionText +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}).join('') : '<div class="image-video-permission-empty">暂无匹配用户</div>';
|
||||
syncImageVideoPermissionSelectAll();
|
||||
}
|
||||
function setImageVideoPermissionLoading(loading) {
|
||||
document.getElementById('btnSaveImageVideoPermissions').disabled = loading;
|
||||
document.getElementById('imageVideoPermissionSearch').disabled = loading;
|
||||
if (loading) document.getElementById('imageVideoPermissionSelectAll').disabled = true;
|
||||
}
|
||||
function openImageVideoPermissions() {
|
||||
if (currentUserRole !== 'super_admin') return;
|
||||
var modal = document.getElementById('imageVideoPermissionModal');
|
||||
var message = document.getElementById('imageVideoPermissionMessage');
|
||||
modal.classList.add('show');
|
||||
message.textContent = '';
|
||||
message.className = 'msg';
|
||||
imageVideoPermissionView = 'granted';
|
||||
syncImageVideoPermissionTabs();
|
||||
document.getElementById('imageVideoPermissionSearch').value = '';
|
||||
document.getElementById('imageVideoPermissionList').innerHTML = '<div class="image-video-permission-empty">加载中...</div>';
|
||||
setImageVideoPermissionLoading(true);
|
||||
fetch('/api/admin/image-video-task-permissions')
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (res) {
|
||||
if (!res.success) throw new Error(res.error || '权限加载失败');
|
||||
imageVideoPermissionUsers = res.items || [];
|
||||
imageVideoPermissionInitialUserIds = new Set(imageVideoPermissionUsers.filter(function (user) {
|
||||
return !!user.granted;
|
||||
}).map(function (user) { return Number(user.id); }));
|
||||
selectedImageVideoPermissionUserIds = new Set(imageVideoPermissionInitialUserIds);
|
||||
renderImageVideoPermissionUsers();
|
||||
})
|
||||
.catch(function (error) {
|
||||
imageVideoPermissionUsers = [];
|
||||
selectedImageVideoPermissionUserIds.clear();
|
||||
document.getElementById('imageVideoPermissionList').innerHTML = '<div class="image-video-permission-empty">' + escapeHtml(error.message || '权限加载失败') + '</div>';
|
||||
})
|
||||
.finally(function () { setImageVideoPermissionLoading(false); });
|
||||
}
|
||||
function closeImageVideoPermissions() {
|
||||
document.getElementById('imageVideoPermissionModal').classList.remove('show');
|
||||
}
|
||||
function saveImageVideoPermissions() {
|
||||
var message = document.getElementById('imageVideoPermissionMessage');
|
||||
message.textContent = '保存中...';
|
||||
message.className = 'msg';
|
||||
setImageVideoPermissionLoading(true);
|
||||
fetch('/api/admin/image-video-task-permissions', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_ids: Array.from(selectedImageVideoPermissionUserIds).sort(function (a, b) { return a - b; }) })
|
||||
})
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (res) {
|
||||
if (!res.success) throw new Error(res.error || '权限保存失败');
|
||||
imageVideoPermissionUsers.forEach(function (user) {
|
||||
user.granted = selectedImageVideoPermissionUserIds.has(Number(user.id));
|
||||
});
|
||||
imageVideoPermissionInitialUserIds = new Set(selectedImageVideoPermissionUserIds);
|
||||
renderImageVideoPermissionUsers();
|
||||
message.textContent = res.msg || '保存成功';
|
||||
message.className = 'msg ok';
|
||||
})
|
||||
.catch(function (error) {
|
||||
message.textContent = error.message || '权限保存失败';
|
||||
message.className = 'msg err';
|
||||
})
|
||||
.finally(function () {
|
||||
setImageVideoPermissionLoading(false);
|
||||
syncImageVideoPermissionSelectAll();
|
||||
});
|
||||
}
|
||||
function buildImageVideoTaskQuery(page) {
|
||||
var params = new URLSearchParams();
|
||||
params.set('page', String(page || 1));
|
||||
params.set('page_size', String(imageVideoTaskPageSize));
|
||||
var values = {
|
||||
user_id: document.getElementById('imageVideoFilterUserId').value,
|
||||
username: document.getElementById('imageVideoFilterUsername').value.trim(),
|
||||
status: document.getElementById('imageVideoFilterStatus').value,
|
||||
coze_execute_id: document.getElementById('imageVideoFilterExecuteId').value.trim(),
|
||||
submitted_from: document.getElementById('imageVideoFilterFrom').value,
|
||||
submitted_to: document.getElementById('imageVideoFilterTo').value
|
||||
};
|
||||
@@ -642,107 +805,353 @@
|
||||
var url = String(value || '').trim();
|
||||
return /^https?:\/\//i.test(url) ? url : '';
|
||||
}
|
||||
function imageVideoDownloadIcon() {
|
||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"></path></svg>';
|
||||
}
|
||||
function imageVideoUnavailableHtml(message) {
|
||||
return '<div class="image-video-unavailable">' +
|
||||
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 10 4.5-2.5v9L15 14"></path><rect x="3" y="6" width="12" height="12" rx="2"></rect><path d="m4 4 16 16"></path></svg>' +
|
||||
'<strong>' + escapeHtml(message || '暂无可用视频') + '</strong>' +
|
||||
'</div>';
|
||||
}
|
||||
function imageVideoCardKey(taskId, videoIndex) {
|
||||
return String(taskId) + ':' + String(videoIndex);
|
||||
}
|
||||
function flattenImageVideoTasks(items) {
|
||||
var cards = [];
|
||||
(items || []).forEach(function (task) {
|
||||
var videos = Array.isArray(task.videos) ? task.videos : [];
|
||||
if (!videos.length) {
|
||||
cards.push({ key: imageVideoCardKey(task.task_id, 'empty'), task: task, video: null, videoIndex: 0 });
|
||||
return;
|
||||
}
|
||||
videos.forEach(function (video, index) {
|
||||
cards.push({
|
||||
key: imageVideoCardKey(task.task_id, index),
|
||||
task: task,
|
||||
video: {
|
||||
sourceUrl: safeAdminUrl(video.source_url),
|
||||
archivedUrl: safeAdminUrl(video.archived_url),
|
||||
displayUrl: safeAdminUrl(video.display_url),
|
||||
objectKey: String(video.object_key || ''),
|
||||
archiveStatus: String(video.archive_status || ''),
|
||||
archiveError: String(video.archive_error || '')
|
||||
},
|
||||
videoIndex: index
|
||||
});
|
||||
});
|
||||
});
|
||||
return cards;
|
||||
}
|
||||
function renderImageVideoCard(card) {
|
||||
var task = card.task;
|
||||
var video = card.video;
|
||||
var hasVideo = !!(video && video.displayUrl);
|
||||
var debugUrl = safeAdminUrl(task.debug_url);
|
||||
var generatedAt = task.completed_at || task.submitted_at || '-';
|
||||
var mediaHtml = hasVideo
|
||||
? '<input class="image-video-card-check" type="checkbox" data-image-video-select="' + escapeHtml(card.key) + '" aria-label="选择任务 ' + escapeHtml(task.task_id) + ' 的视频 ' + (card.videoIndex + 1) + '">' +
|
||||
'<video src="' + escapeHtml(video.displayUrl) + '" controls playsinline preload="metadata"></video>'
|
||||
: imageVideoUnavailableHtml(task.status === 'FAILED' ? '任务失败,未生成视频' : '视频生成中或暂无结果');
|
||||
return '<article class="image-video-card" data-image-video-card="' + escapeHtml(card.key) + '">' +
|
||||
'<div class="image-video-media">' + mediaHtml + '</div>' +
|
||||
'<div class="image-video-card-body">' +
|
||||
'<div class="image-video-card-head">' +
|
||||
'<div class="image-video-card-title" title="任务 ' + escapeHtml(task.task_id) + '">任务 #' + escapeHtml(task.task_id) + (video ? ' · 视频 ' + (card.videoIndex + 1) : '') + '</div>' +
|
||||
renderImageVideoStatus(task.status) +
|
||||
'</div>' +
|
||||
'<div class="image-video-card-info">' +
|
||||
'<div class="image-video-info-row"><label>用户名</label><span>' + escapeHtml(task.username || '-') + '</span></div>' +
|
||||
'<div class="image-video-info-row"><label>所属分组</label><span>' + escapeHtml(task.group_name || '-') + '</span></div>' +
|
||||
'<div class="image-video-info-row"><label>任务模式</label><span>' + escapeHtml(task.mode || '-') + '</span></div>' +
|
||||
'<div class="image-video-info-row"><label>生成时间</label><span>' + escapeHtml(generatedAt) + '</span></div>' +
|
||||
'<div class="image-video-info-row"><label>调试链接</label>' + (debugUrl
|
||||
? '<button class="image-video-copy-link" type="button" data-image-video-copy-debug="' + escapeHtml(card.key) + '">复制链接</button>'
|
||||
: '<span>-</span>') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="image-video-card-actions">' +
|
||||
'<button class="image-video-card-action" type="button" data-image-video-download="' + escapeHtml(card.key) + '"' + (hasVideo ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</article>';
|
||||
}
|
||||
function syncImageVideoSelectionUi() {
|
||||
var selectable = imageVideoCards.filter(function (card) { return !!(card.video && card.video.displayUrl); });
|
||||
document.querySelectorAll('[data-image-video-card]').forEach(function (cardEl) {
|
||||
var selected = selectedImageVideoKeys.has(cardEl.dataset.imageVideoCard);
|
||||
cardEl.classList.toggle('selected', selected);
|
||||
var checkbox = cardEl.querySelector('[data-image-video-select]');
|
||||
if (checkbox) checkbox.checked = selected;
|
||||
});
|
||||
var selectedCount = selectable.filter(function (card) { return selectedImageVideoKeys.has(card.key); }).length;
|
||||
var selectAll = document.getElementById('imageVideoSelectAll');
|
||||
selectAll.checked = selectable.length > 0 && selectedCount === selectable.length;
|
||||
selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length;
|
||||
selectAll.disabled = imageVideoDownloadInProgress || selectable.length === 0;
|
||||
var batchButton = document.getElementById('btnBatchDownloadImageVideos');
|
||||
batchButton.disabled = imageVideoDownloadInProgress || selectedCount === 0;
|
||||
batchButton.innerHTML = imageVideoDownloadIcon() + (imageVideoDownloadInProgress ? '处理中' : '批量下载' + (selectedCount ? ' (' + selectedCount + ')' : ''));
|
||||
document.querySelectorAll('[data-image-video-download]').forEach(function (button) {
|
||||
var card = imageVideoCards.find(function (item) { return item.key === button.dataset.imageVideoDownload; });
|
||||
button.disabled = imageVideoDownloadInProgress || !card || !card.video || !card.video.displayUrl;
|
||||
});
|
||||
}
|
||||
function renderImageVideoCards() {
|
||||
var grid = document.getElementById('imageVideoTaskGrid');
|
||||
grid.innerHTML = imageVideoCards.length
|
||||
? imageVideoCards.map(renderImageVideoCard).join('')
|
||||
: '<div class="image-video-empty">暂无符合条件的视频任务</div>';
|
||||
grid.querySelectorAll('video').forEach(function (video) {
|
||||
video.addEventListener('error', function () {
|
||||
var media = video.closest('.image-video-media');
|
||||
if (!media || media.querySelector('.image-video-unavailable')) return;
|
||||
video.remove();
|
||||
media.insertAdjacentHTML('beforeend', imageVideoUnavailableHtml('视频加载失败,可尝试下载'));
|
||||
}, { once: true });
|
||||
});
|
||||
syncImageVideoSelectionUi();
|
||||
}
|
||||
function resetImageVideoSelection() {
|
||||
selectedImageVideoKeys.clear();
|
||||
document.getElementById('imageVideoDownloadProgress').textContent = '';
|
||||
syncImageVideoSelectionUi();
|
||||
}
|
||||
function loadImageVideoTasks(page) {
|
||||
imageVideoTaskPage = page || 1;
|
||||
var tbody = document.getElementById('imageVideoTaskListBody');
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">加载中...</td></tr>';
|
||||
selectedImageVideoKeys.clear();
|
||||
var grid = document.getElementById('imageVideoTaskGrid');
|
||||
grid.innerHTML = '<div class="image-video-empty">加载中...</div>';
|
||||
document.getElementById('imageVideoDownloadProgress').textContent = '';
|
||||
fetch('/api/admin/image-video-tasks?' + buildImageVideoTaskQuery(imageVideoTaskPage))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (!res.success) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">加载失败:' + escapeHtml(res.error || '') + '</td></tr>';
|
||||
imageVideoCards = [];
|
||||
grid.innerHTML = '<div class="image-video-empty">加载失败:' + escapeHtml(res.error || '') + '</div>';
|
||||
document.getElementById('imageVideoTaskTotal').textContent = '';
|
||||
syncImageVideoSelectionUi();
|
||||
return;
|
||||
}
|
||||
var items = res.items || [];
|
||||
document.getElementById('imageVideoTaskTotal').textContent = '共 ' + (res.total || 0) + ' 条';
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">暂无视频任务</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(function (item) {
|
||||
var videoUrl = safeAdminUrl(item.video_url);
|
||||
var videoLink = videoUrl ? '<a href="' + escapeHtml(videoUrl) + '" target="_blank" rel="noreferrer">查看视频</a>' : '-';
|
||||
return '<tr>' +
|
||||
'<td>' + escapeHtml(item.task_id) + '</td>' +
|
||||
'<td>' + escapeHtml(item.username || '-') + '<div style="color:#888;font-size:12px;">ID ' + escapeHtml(item.user_id) + '</div></td>' +
|
||||
'<td>' + escapeHtml(item.mode || '-') + '</td>' +
|
||||
'<td>' + renderImageVideoStatus(item.status) + '</td>' +
|
||||
'<td>' + escapeHtml(item.coze_status || '-') + '</td>' +
|
||||
'<td title="' + escapeHtml(item.coze_execute_id || '') + '">' + escapeHtml(item.coze_execute_id || '-').slice(0, 18) + '</td>' +
|
||||
'<td>' + videoLink + '</td>' +
|
||||
'<td>' + escapeHtml(item.archive_status || '-') + '</td>' +
|
||||
'<td>' + escapeHtml(item.submitted_at || '-') + '</td>' +
|
||||
'<td><button class="btn btn-sm" data-image-video-detail="' + escapeHtml(item.task_id) + '">详情</button></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
}
|
||||
imageVideoCards = flattenImageVideoTasks(items);
|
||||
var videoCount = imageVideoCards.filter(function (card) { return !!card.video; }).length;
|
||||
document.getElementById('imageVideoTaskTotal').textContent = '共 ' + (res.total || 0) + ' 个任务 · 本页 ' + videoCount + ' 个视频';
|
||||
renderImageVideoCards();
|
||||
renderPagination('imageVideoTaskPagination', res.total, res.page, res.page_size, loadImageVideoTasks);
|
||||
})
|
||||
.catch(function () {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">请求失败</td></tr>';
|
||||
imageVideoCards = [];
|
||||
grid.innerHTML = '<div class="image-video-empty">请求失败,请稍后重试</div>';
|
||||
document.getElementById('imageVideoTaskTotal').textContent = '';
|
||||
syncImageVideoSelectionUi();
|
||||
});
|
||||
}
|
||||
function formatImageVideoJson(value) {
|
||||
try { return JSON.stringify(value == null ? {} : value, null, 2); }
|
||||
catch (error) { return String(value || ''); }
|
||||
function imageVideoFilename(card) {
|
||||
var url = card.video.displayUrl || '';
|
||||
var path = url.split(/[?#]/)[0];
|
||||
var match = path.match(/\.([a-z0-9]{2,5})$/i);
|
||||
return 'task-' + card.task.task_id + '-video-' + (card.videoIndex + 1) + '.' + (match ? match[1].toLowerCase() : 'mp4');
|
||||
}
|
||||
function renderImageVideoTaskDetail(item) {
|
||||
var videos = item.videos || [];
|
||||
var videoHtml = videos.length ? videos.map(function (video, index) {
|
||||
var displayUrl = safeAdminUrl(video.display_url);
|
||||
var sourceUrl = safeAdminUrl(video.source_url);
|
||||
var archivedUrl = safeAdminUrl(video.archived_url);
|
||||
return '<div class="image-video-video-item">' +
|
||||
'<strong>视频 ' + (index + 1) + '</strong>' +
|
||||
(displayUrl ? '<video src="' + escapeHtml(displayUrl) + '" controls playsinline preload="metadata"></video>' : '<p class="empty-tip">无可用视频地址</p>') +
|
||||
'<div style="margin-top:8px;display:flex;gap:10px;flex-wrap:wrap;">' +
|
||||
(archivedUrl ? '<a href="' + escapeHtml(archivedUrl) + '" target="_blank" rel="noreferrer">OSS 视频</a>' : '') +
|
||||
(sourceUrl ? '<a href="' + escapeHtml(sourceUrl) + '" target="_blank" rel="noreferrer">Coze 原始视频</a>' : '') +
|
||||
'</div><div style="font-size:12px;color:#777;margin-top:6px;">归档:' + escapeHtml(video.archive_status || '-') +
|
||||
(video.archive_error ? ' · ' + escapeHtml(video.archive_error) : '') + '</div></div>';
|
||||
}).join('') : '<p class="empty-tip">当前没有解析到视频地址</p>';
|
||||
var debugUrl = safeAdminUrl(item.debug_url);
|
||||
return '<div class="image-video-detail-meta">' +
|
||||
'<div><span>任务 ID</span>' + escapeHtml(item.task_id) + '</div>' +
|
||||
'<div><span>用户</span>' + escapeHtml(item.username || '-') + '(' + escapeHtml(item.user_id) + ')</div>' +
|
||||
'<div><span>任务模式</span>' + escapeHtml(item.mode || '-') + '</div>' +
|
||||
'<div><span>状态</span>' + escapeHtml(item.status || '-') + ' / ' + escapeHtml(item.coze_status || '-') + '</div>' +
|
||||
'<div><span>Coze 执行 ID</span>' + escapeHtml(item.coze_execute_id || '-') + '</div>' +
|
||||
'<div><span>提交时间</span>' + escapeHtml(item.submitted_at || '-') + '</div>' +
|
||||
'<div><span>完成时间</span>' + escapeHtml(item.completed_at || '-') + '</div>' +
|
||||
'<div><span>归档状态</span>' + escapeHtml(item.archive_status || '-') + '</div>' +
|
||||
'</div>' +
|
||||
(debugUrl ? '<p><strong>调试链接:</strong><a href="' + escapeHtml(debugUrl) + '" target="_blank" rel="noreferrer">' + escapeHtml(debugUrl) + '</a></p>' : '') +
|
||||
(item.error_message ? '<p class="msg err">任务错误:' + escapeHtml(item.error_message) + '</p>' : '') +
|
||||
(item.archive_error ? '<p class="msg err">归档错误:' + escapeHtml(item.archive_error) + '</p>' : '') +
|
||||
'<div class="image-video-video-grid">' + videoHtml + '</div>' +
|
||||
'<details class="image-video-json"><summary>请求参数(密钥已脱敏)</summary><pre>' + escapeHtml(formatImageVideoJson(item.request)) + '</pre></details>' +
|
||||
'<details class="image-video-json"><summary>Coze 首次提交响应</summary><pre>' + escapeHtml(formatImageVideoJson(item.submit_response)) + '</pre></details>' +
|
||||
'<details class="image-video-json"><summary>Coze 最终响应</summary><pre>' + escapeHtml(formatImageVideoJson(item.result)) + '</pre></details>';
|
||||
function triggerImageVideoLink(url, filename, newWindow) {
|
||||
var link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
if (newWindow) {
|
||||
link.target = '_blank';
|
||||
link.rel = 'noreferrer';
|
||||
}
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
function openImageVideoTaskDetail(taskId) {
|
||||
var content = document.getElementById('imageVideoTaskDetailContent');
|
||||
content.innerHTML = '<p class="empty-tip">加载中...</p>';
|
||||
document.getElementById('imageVideoTaskDetailModal').classList.add('show');
|
||||
fetch('/api/admin/image-video-tasks/' + encodeURIComponent(taskId))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
content.innerHTML = res.success ? renderImageVideoTaskDetail(res.item || {}) : '<p class="msg err">' + escapeHtml(res.error || '加载失败') + '</p>';
|
||||
function downloadImageVideo(card) {
|
||||
var url = card && card.video ? card.video.displayUrl : '';
|
||||
if (!url) return Promise.resolve({ fallback: false, skipped: true });
|
||||
var filename = imageVideoFilename(card);
|
||||
return fetch(url, { __skipLoading: true })
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.blob();
|
||||
})
|
||||
.catch(function () { content.innerHTML = '<p class="msg err">请求失败</p>'; });
|
||||
.then(function (blob) {
|
||||
var objectUrl = URL.createObjectURL(blob);
|
||||
triggerImageVideoLink(objectUrl, filename, false);
|
||||
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
|
||||
return { fallback: false, skipped: false };
|
||||
})
|
||||
.catch(function () {
|
||||
triggerImageVideoLink(url, filename, true);
|
||||
return { fallback: true, skipped: false };
|
||||
});
|
||||
}
|
||||
function runImageVideoDownloads(cards) {
|
||||
if (imageVideoDownloadInProgress || !cards.length) return;
|
||||
imageVideoDownloadInProgress = true;
|
||||
syncImageVideoSelectionUi();
|
||||
var progress = document.getElementById('imageVideoDownloadProgress');
|
||||
progress.textContent = '正在下载';
|
||||
downloadImageVideo(cards[0]).then(function (result) {
|
||||
progress.textContent = result.fallback ? '已在新窗口打开视频' : '下载已开始';
|
||||
}).finally(function () {
|
||||
imageVideoDownloadInProgress = false;
|
||||
syncImageVideoSelectionUi();
|
||||
});
|
||||
}
|
||||
function imageVideoZipFilename(response) {
|
||||
var disposition = response.headers.get('Content-Disposition') || '';
|
||||
var encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
if (encoded) {
|
||||
try { return decodeURIComponent(encoded[1]); } catch (error) { }
|
||||
}
|
||||
var plain = disposition.match(/filename="?([^";]+)"?/i);
|
||||
return plain ? plain[1] : 'video-tasks.zip';
|
||||
}
|
||||
function downloadImageVideoZip(cards) {
|
||||
if (imageVideoDownloadInProgress || !cards.length) return;
|
||||
imageVideoDownloadInProgress = true;
|
||||
syncImageVideoSelectionUi();
|
||||
var progress = document.getElementById('imageVideoDownloadProgress');
|
||||
progress.textContent = '正在打包 ' + cards.length + ' 个视频...';
|
||||
fetch('/api/admin/image-video-tasks/download-zip', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
items: cards.map(function (card) {
|
||||
return { task_id: Number(card.task.task_id), video_index: card.videoIndex };
|
||||
})
|
||||
}),
|
||||
__skipLoading: true
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return response.json().catch(function () { return {}; }).then(function (data) {
|
||||
throw new Error(data.error || '压缩包生成失败');
|
||||
});
|
||||
}
|
||||
var filename = imageVideoZipFilename(response);
|
||||
var errorCount = Number(response.headers.get('X-Archive-Error-Count') || 0);
|
||||
return response.blob().then(function (blob) {
|
||||
return { blob: blob, filename: filename, errorCount: errorCount };
|
||||
});
|
||||
}).then(function (result) {
|
||||
var objectUrl = URL.createObjectURL(result.blob);
|
||||
triggerImageVideoLink(objectUrl, result.filename, false);
|
||||
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
|
||||
progress.textContent = result.errorCount
|
||||
? '压缩包已下载,' + result.errorCount + ' 个视频失败,详见包内错误清单'
|
||||
: '压缩包下载已开始';
|
||||
}).catch(function (error) {
|
||||
progress.textContent = error.message || '压缩包下载失败';
|
||||
}).finally(function () {
|
||||
imageVideoDownloadInProgress = false;
|
||||
syncImageVideoSelectionUi();
|
||||
});
|
||||
}
|
||||
function copyImageVideoDebugUrl(card, button) {
|
||||
var url = card ? safeAdminUrl(card.task.debug_url) : '';
|
||||
if (!url) return;
|
||||
var copyPromise;
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
copyPromise = navigator.clipboard.writeText(url);
|
||||
} else {
|
||||
copyPromise = new Promise(function (resolve, reject) {
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.value = url;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
var copied = document.execCommand('copy');
|
||||
textarea.remove();
|
||||
if (copied) resolve();
|
||||
else reject(new Error('copy failed'));
|
||||
});
|
||||
}
|
||||
copyPromise.then(function () {
|
||||
button.textContent = '已复制';
|
||||
button.classList.add('copied');
|
||||
setTimeout(function () {
|
||||
button.textContent = '复制链接';
|
||||
button.classList.remove('copied');
|
||||
}, 1600);
|
||||
}).catch(function () {
|
||||
button.textContent = '复制失败';
|
||||
setTimeout(function () { button.textContent = '复制链接'; }, 1600);
|
||||
});
|
||||
}
|
||||
document.getElementById('btnFilterImageVideoTasks').onclick = function () { loadImageVideoTasks(1); };
|
||||
document.getElementById('btnOpenImageVideoPermissions').onclick = openImageVideoPermissions;
|
||||
document.getElementById('btnCloseImageVideoPermissions').onclick = closeImageVideoPermissions;
|
||||
document.getElementById('btnCancelImageVideoPermissions').onclick = closeImageVideoPermissions;
|
||||
document.getElementById('btnSaveImageVideoPermissions').onclick = saveImageVideoPermissions;
|
||||
document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) {
|
||||
tab.onclick = function () {
|
||||
imageVideoPermissionView = tab.dataset.imageVideoPermissionView || 'granted';
|
||||
document.getElementById('imageVideoPermissionSearch').value = '';
|
||||
renderImageVideoPermissionUsers();
|
||||
};
|
||||
});
|
||||
document.getElementById('imageVideoPermissionSearch').oninput = renderImageVideoPermissionUsers;
|
||||
document.getElementById('imageVideoPermissionSelectAll').onchange = function (event) {
|
||||
filteredImageVideoPermissionUsers().forEach(function (user) {
|
||||
var userId = Number(user.id);
|
||||
if (event.target.checked) selectedImageVideoPermissionUserIds.add(userId);
|
||||
else selectedImageVideoPermissionUserIds.delete(userId);
|
||||
});
|
||||
renderImageVideoPermissionUsers();
|
||||
};
|
||||
document.getElementById('imageVideoPermissionList').onclick = function (event) {
|
||||
var toggle = event.target.closest('[data-image-video-permission-toggle]');
|
||||
if (!toggle) return;
|
||||
var userId = Number(toggle.dataset.imageVideoPermissionToggle);
|
||||
if (selectedImageVideoPermissionUserIds.has(userId)) selectedImageVideoPermissionUserIds.delete(userId);
|
||||
else selectedImageVideoPermissionUserIds.add(userId);
|
||||
renderImageVideoPermissionUsers();
|
||||
};
|
||||
document.getElementById('imageVideoPermissionList').onchange = function (event) {
|
||||
var checkbox = event.target.closest('[data-image-video-permission-user]');
|
||||
if (!checkbox) return;
|
||||
var userId = Number(checkbox.dataset.imageVideoPermissionUser);
|
||||
if (checkbox.checked) selectedImageVideoPermissionUserIds.add(userId);
|
||||
else selectedImageVideoPermissionUserIds.delete(userId);
|
||||
renderImageVideoPermissionUsers();
|
||||
};
|
||||
document.getElementById('imageVideoPermissionModal').onclick = function (event) {
|
||||
if (event.target === event.currentTarget) closeImageVideoPermissions();
|
||||
};
|
||||
document.getElementById('btnResetImageVideoTasks').onclick = function () {
|
||||
['imageVideoFilterUserId', 'imageVideoFilterUsername', 'imageVideoFilterStatus', 'imageVideoFilterExecuteId', 'imageVideoFilterFrom', 'imageVideoFilterTo']
|
||||
['imageVideoFilterUsername', 'imageVideoFilterFrom', 'imageVideoFilterTo']
|
||||
.forEach(function (id) { document.getElementById(id).value = ''; });
|
||||
loadImageVideoTasks(1);
|
||||
};
|
||||
document.getElementById('imageVideoTaskListBody').onclick = function (event) {
|
||||
var button = event.target.closest('[data-image-video-detail]');
|
||||
if (button) openImageVideoTaskDetail(button.dataset.imageVideoDetail);
|
||||
document.getElementById('imageVideoSelectAll').onchange = function (event) {
|
||||
imageVideoCards.forEach(function (card) {
|
||||
if (!card.video || !card.video.displayUrl) return;
|
||||
if (event.target.checked) selectedImageVideoKeys.add(card.key);
|
||||
else selectedImageVideoKeys.delete(card.key);
|
||||
});
|
||||
syncImageVideoSelectionUi();
|
||||
};
|
||||
document.getElementById('btnCloseImageVideoTaskDetail').onclick = function () {
|
||||
document.getElementById('imageVideoTaskDetailModal').classList.remove('show');
|
||||
document.getElementById('imageVideoTaskDetailContent').innerHTML = '';
|
||||
document.getElementById('btnBatchDownloadImageVideos').onclick = function () {
|
||||
downloadImageVideoZip(imageVideoCards.filter(function (card) { return selectedImageVideoKeys.has(card.key); }));
|
||||
};
|
||||
document.getElementById('imageVideoTaskGrid').onchange = function (event) {
|
||||
var checkbox = event.target.closest('[data-image-video-select]');
|
||||
if (!checkbox) return;
|
||||
if (checkbox.checked) selectedImageVideoKeys.add(checkbox.dataset.imageVideoSelect);
|
||||
else selectedImageVideoKeys.delete(checkbox.dataset.imageVideoSelect);
|
||||
syncImageVideoSelectionUi();
|
||||
};
|
||||
document.getElementById('imageVideoTaskGrid').onclick = function (event) {
|
||||
var copyButton = event.target.closest('[data-image-video-copy-debug]');
|
||||
if (copyButton) {
|
||||
var copyCard = imageVideoCards.find(function (item) { return item.key === copyButton.dataset.imageVideoCopyDebug; });
|
||||
if (copyCard) copyImageVideoDebugUrl(copyCard, copyButton);
|
||||
}
|
||||
var downloadButton = event.target.closest('[data-image-video-download]');
|
||||
if (downloadButton) {
|
||||
var card = imageVideoCards.find(function (item) { return item.key === downloadButton.dataset.imageVideoDownload; });
|
||||
if (card) runImageVideoDownloads([card]);
|
||||
}
|
||||
};
|
||||
|
||||
var historyPage = 1, historyPageSize = 15;
|
||||
@@ -4348,6 +4757,7 @@
|
||||
roleEl.style.display = 'none';
|
||||
}
|
||||
updateShopManageGroupButtonsAccess();
|
||||
updateImageVideoPermissionAccess();
|
||||
})
|
||||
.catch(function () {
|
||||
return fetch('/api/auth/check')
|
||||
@@ -4356,11 +4766,13 @@
|
||||
document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录';
|
||||
document.getElementById('adminCurrentUserRole').style.display = 'none';
|
||||
updateShopManageGroupButtonsAccess();
|
||||
updateImageVideoPermissionAccess();
|
||||
})
|
||||
.catch(function () {
|
||||
document.getElementById('adminCurrentUsername').textContent = '当前用户';
|
||||
document.getElementById('adminCurrentUserRole').style.display = 'none';
|
||||
updateShopManageGroupButtonsAccess();
|
||||
updateImageVideoPermissionAccess();
|
||||
});
|
||||
})
|
||||
.finally(function () {
|
||||
@@ -4398,7 +4810,6 @@
|
||||
|
||||
loadAdminCurrentUser().finally(function () {
|
||||
loadUserOptions();
|
||||
loadColumnsForPermission();
|
||||
loadShopManageGroups();
|
||||
loadAdminMenus();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user