视频任务管理更新

This commit is contained in:
super
2026-07-19 22:23:26 +08:00
parent 69784b8d32
commit cbde22ec79
62 changed files with 2164 additions and 280 deletions

View File

@@ -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();
});