1110 lines
36 KiB
JavaScript
1110 lines
36 KiB
JavaScript
// API Base URL
|
|
const API_BASE = '/api';
|
|
|
|
// State Management
|
|
const state = {
|
|
currentPage: 'partner-list',
|
|
isAuthenticated: false,
|
|
currentUser: null,
|
|
partners: []
|
|
};
|
|
|
|
// DOM Elements
|
|
const loginPage = document.getElementById('loginPage');
|
|
const adminDashboard = document.getElementById('adminDashboard');
|
|
const loginForm = document.getElementById('loginForm');
|
|
const loginError = document.getElementById('loginError');
|
|
const logoutBtn = document.getElementById('logoutBtn');
|
|
const navLinks = document.querySelectorAll('.nav-link');
|
|
const pageTitle = document.getElementById('pageTitle');
|
|
const currentUserSpan = document.getElementById('currentUser');
|
|
const partnerModal = document.getElementById('partnerModal');
|
|
const partnerForm = document.getElementById('partnerForm');
|
|
const toast = document.getElementById('toast');
|
|
|
|
// Initialize App
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
checkAuthStatus();
|
|
setupEventListeners();
|
|
});
|
|
|
|
// Check Authentication Status
|
|
async function checkAuthStatus() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/status`, {
|
|
credentials: 'include'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.authenticated) {
|
|
state.isAuthenticated = true;
|
|
state.currentUser = data.user;
|
|
showDashboard();
|
|
} else {
|
|
showLogin();
|
|
}
|
|
} catch (error) {
|
|
console.error('Auth check failed:', error);
|
|
showLogin();
|
|
}
|
|
}
|
|
|
|
// Show Login Page
|
|
function showLogin() {
|
|
loginPage.style.display = 'flex';
|
|
adminDashboard.style.display = 'none';
|
|
}
|
|
|
|
// Show Dashboard
|
|
function showDashboard() {
|
|
loginPage.style.display = 'none';
|
|
adminDashboard.style.display = 'flex';
|
|
currentUserSpan.textContent = state.currentUser.username;
|
|
navigateToPage('partner-list');
|
|
}
|
|
|
|
// Setup Event Listeners
|
|
function setupEventListeners() {
|
|
// Login Form
|
|
loginForm.addEventListener('submit', handleLogin);
|
|
|
|
// Logout Button
|
|
logoutBtn.addEventListener('click', handleLogout);
|
|
|
|
// Navigation Links
|
|
navLinks.forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const page = link.getAttribute('data-page');
|
|
navigateToPage(page);
|
|
});
|
|
});
|
|
|
|
// Partner Modal
|
|
document.querySelectorAll('.modal-close').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
partnerModal.classList.remove('show');
|
|
});
|
|
});
|
|
|
|
// Click outside modal to close
|
|
partnerModal.addEventListener('click', (e) => {
|
|
if (e.target === partnerModal) {
|
|
partnerModal.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Partner Form Submit
|
|
partnerForm.addEventListener('submit', handlePartnerSubmit);
|
|
|
|
// Add Partner Button (from nav link)
|
|
const addPartnerLink = document.querySelector('[data-page="add-partner"]');
|
|
addPartnerLink.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
openPartnerModal();
|
|
});
|
|
|
|
// Storage Configuration Form
|
|
const storageConfigForm = document.getElementById('storageConfigForm');
|
|
if (storageConfigForm) {
|
|
storageConfigForm.addEventListener('submit', handleStorageConfigSubmit);
|
|
}
|
|
|
|
// Create Directory Button
|
|
const createDirBtn = document.getElementById('createDirBtn');
|
|
if (createDirBtn) {
|
|
createDirBtn.addEventListener('click', handleCreateDirectory);
|
|
}
|
|
|
|
// VOD Config Form
|
|
const vodConfigForm = document.getElementById('vodConfigForm');
|
|
if (vodConfigForm) {
|
|
vodConfigForm.addEventListener('submit', handleVodConfigSubmit);
|
|
}
|
|
|
|
// Add Video Form
|
|
const addVideoForm = document.getElementById('addVideoForm');
|
|
if (addVideoForm) {
|
|
addVideoForm.addEventListener('submit', handleVideoUpload);
|
|
}
|
|
|
|
// Video Edit Form
|
|
const videoEditForm = document.getElementById('videoEditForm');
|
|
if (videoEditForm) {
|
|
videoEditForm.addEventListener('submit', handleVideoEditSubmit);
|
|
}
|
|
|
|
// Cover Preview
|
|
const videoCover = document.getElementById('videoCover');
|
|
if (videoCover) {
|
|
videoCover.addEventListener('change', handleCoverPreview);
|
|
}
|
|
|
|
// Change Password Form
|
|
const changePasswordForm = document.getElementById('changePasswordForm');
|
|
if (changePasswordForm) {
|
|
changePasswordForm.addEventListener('submit', handleChangePassword);
|
|
}
|
|
}
|
|
|
|
// Handle Login
|
|
async function handleLogin(e) {
|
|
e.preventDefault();
|
|
loginError.style.display = 'none';
|
|
|
|
const formData = new FormData(loginForm);
|
|
const data = {
|
|
username: formData.get('username'),
|
|
password: formData.get('password')
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
state.isAuthenticated = true;
|
|
state.currentUser = result.user;
|
|
showDashboard();
|
|
} else {
|
|
loginError.textContent = result.message;
|
|
loginError.style.display = 'block';
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
loginError.textContent = 'Login failed. Please try again.';
|
|
loginError.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Handle Logout
|
|
async function handleLogout() {
|
|
try {
|
|
await fetch(`${API_BASE}/auth/logout`, {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
});
|
|
|
|
state.isAuthenticated = false;
|
|
state.currentUser = null;
|
|
showLogin();
|
|
showToast('Logged out successfully', 'success');
|
|
} catch (error) {
|
|
console.error('Logout error:', error);
|
|
showToast('Logout failed', 'error');
|
|
}
|
|
}
|
|
|
|
// Navigate to Page
|
|
function navigateToPage(page) {
|
|
state.currentPage = page;
|
|
|
|
// Update active nav link
|
|
navLinks.forEach(link => {
|
|
if (link.getAttribute('data-page') === page) {
|
|
link.classList.add('active');
|
|
} else {
|
|
link.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// Hide all pages
|
|
document.querySelectorAll('.page-content').forEach(p => {
|
|
p.style.display = 'none';
|
|
});
|
|
|
|
// Show selected page
|
|
switch (page) {
|
|
case 'storage-config':
|
|
pageTitle.textContent = '存储配置';
|
|
document.getElementById('storageConfigPage').style.display = 'block';
|
|
loadStorageConfig();
|
|
break;
|
|
case 'add-partner':
|
|
openPartnerModal();
|
|
break;
|
|
case 'partner-list':
|
|
pageTitle.textContent = '合作伙伴列表';
|
|
document.getElementById('partnerListPage').style.display = 'block';
|
|
loadPartners();
|
|
break;
|
|
case 'video-list':
|
|
pageTitle.textContent = '媒体列表';
|
|
document.getElementById('videoListPage').style.display = 'block';
|
|
loadVideos();
|
|
break;
|
|
case 'video-config':
|
|
pageTitle.textContent = '媒体配置';
|
|
document.getElementById('videoConfigPage').style.display = 'block';
|
|
loadVodConfig();
|
|
break;
|
|
case 'add-video':
|
|
pageTitle.textContent = '新增媒体';
|
|
document.getElementById('addVideoPage').style.display = 'block';
|
|
resetVideoUploadForm();
|
|
break;
|
|
case 'system-settings':
|
|
pageTitle.textContent = '系统设置';
|
|
document.getElementById('systemSettingsPage').style.display = 'block';
|
|
loadSystemSettings();
|
|
break;
|
|
case 'change-password':
|
|
pageTitle.textContent = '修改密码';
|
|
document.getElementById('changePasswordPage').style.display = 'block';
|
|
document.getElementById('changePasswordForm').reset();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Load Storage Configuration
|
|
async function loadStorageConfig() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/config/storage`, {
|
|
credentials: 'include'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const { config, dirStatus } = result.data;
|
|
|
|
document.getElementById('uploadDir').value = config.logo_upload_dir || '';
|
|
document.getElementById('maxSize').value = config.logo_max_size || '';
|
|
document.getElementById('allowedExt').value = config.allowed_extensions || '';
|
|
|
|
// Update directory status
|
|
const existsIndicator = document.getElementById('dirExists');
|
|
const writableIndicator = document.getElementById('dirWritable');
|
|
|
|
if (dirStatus.exists) {
|
|
existsIndicator.classList.add('success');
|
|
existsIndicator.classList.remove('error');
|
|
} else {
|
|
existsIndicator.classList.add('error');
|
|
existsIndicator.classList.remove('success');
|
|
}
|
|
|
|
if (dirStatus.writable) {
|
|
writableIndicator.classList.add('success');
|
|
writableIndicator.classList.remove('error');
|
|
} else {
|
|
writableIndicator.classList.add('error');
|
|
writableIndicator.classList.remove('success');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Load storage config error:', error);
|
|
showToast('Failed to load storage configuration', 'error');
|
|
}
|
|
}
|
|
|
|
// Handle Storage Config Submit
|
|
async function handleStorageConfigSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(e.target);
|
|
const data = {
|
|
logo_upload_dir: formData.get('logo_upload_dir'),
|
|
logo_max_size: formData.get('logo_max_size'),
|
|
allowed_extensions: formData.get('allowed_extensions')
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/config/storage`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('Storage configuration updated successfully', 'success');
|
|
loadStorageConfig();
|
|
} else {
|
|
showToast(result.message || 'Failed to update configuration', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Update storage config error:', error);
|
|
showToast('Failed to update configuration', 'error');
|
|
}
|
|
}
|
|
|
|
// Handle Create Directory
|
|
async function handleCreateDirectory() {
|
|
const dirPath = document.getElementById('uploadDir').value;
|
|
|
|
if (!dirPath) {
|
|
showToast('Please enter a directory path', 'warning');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/config/storage/create-dir`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({ dirPath })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('Directory created successfully', 'success');
|
|
loadStorageConfig();
|
|
} else {
|
|
showToast(result.message || 'Failed to create directory', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Create directory error:', error);
|
|
showToast('Failed to create directory', 'error');
|
|
}
|
|
}
|
|
|
|
// Load Partners
|
|
async function loadPartners() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/partners`, {
|
|
credentials: 'include'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
state.partners = result.data;
|
|
renderPartners(result.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Load partners error:', error);
|
|
showToast('Failed to load partners', 'error');
|
|
}
|
|
}
|
|
|
|
// Render Partners Table
|
|
function renderPartners(partners) {
|
|
const tbody = document.getElementById('partnersTableBody');
|
|
|
|
if (partners.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center">暂无合作伙伴</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = partners.map(partner => `
|
|
<tr>
|
|
<td>${partner.id}</td>
|
|
<td><img src="/${partner.logo}" alt="${partner.name}"></td>
|
|
<td>${partner.name}</td>
|
|
<td>${partner.url || '-'}</td>
|
|
<td>${partner.sort_order}</td>
|
|
<td>
|
|
<span class="badge ${partner.status === 1 ? 'badge-success' : 'badge-danger'}">
|
|
${partner.status === 1 ? '启用' : '禁用'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm btn-primary" onclick="editPartner(${partner.id})">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deletePartner(${partner.id})">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// Open Partner Modal
|
|
function openPartnerModal(partnerId = null) {
|
|
const modal = document.getElementById('partnerModal');
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
const form = document.getElementById('partnerForm');
|
|
|
|
form.reset();
|
|
document.getElementById('partnerId').value = '';
|
|
document.getElementById('currentLogoPreview').style.display = 'none';
|
|
|
|
if (partnerId) {
|
|
modalTitle.textContent = '编辑合作伙伴';
|
|
loadPartnerData(partnerId);
|
|
} else {
|
|
modalTitle.textContent = '新增合作伙伴';
|
|
document.getElementById('partnerLogo').required = true;
|
|
}
|
|
|
|
modal.classList.add('show');
|
|
}
|
|
|
|
// Load Partner Data for Editing
|
|
async function loadPartnerData(partnerId) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/partners/${partnerId}`, {
|
|
credentials: 'include'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const partner = result.data;
|
|
document.getElementById('partnerId').value = partner.id;
|
|
document.getElementById('partnerName').value = partner.name;
|
|
document.getElementById('partnerUrl').value = partner.url || '';
|
|
document.getElementById('partnerStatus').value = partner.status;
|
|
document.getElementById('partnerSortOrder').value = partner.sort_order;
|
|
|
|
// Show current logo
|
|
document.getElementById('currentLogo').src = '/' + partner.logo;
|
|
document.getElementById('currentLogoPreview').style.display = 'block';
|
|
document.getElementById('partnerLogo').required = false;
|
|
}
|
|
} catch (error) {
|
|
console.error('Load partner error:', error);
|
|
showToast('Failed to load partner data', 'error');
|
|
}
|
|
}
|
|
|
|
// Handle Partner Form Submit
|
|
async function handlePartnerSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(partnerForm);
|
|
const partnerId = formData.get('partnerId');
|
|
|
|
const url = partnerId
|
|
? `${API_BASE}/partners/${partnerId}`
|
|
: `${API_BASE}/partners`;
|
|
|
|
const method = partnerId ? 'PUT' : 'POST';
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
credentials: 'include',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast(result.message, 'success');
|
|
partnerModal.classList.remove('show');
|
|
loadPartners();
|
|
} else {
|
|
showToast(result.message || 'Operation failed', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Partner submit error:', error);
|
|
showToast('Operation failed', 'error');
|
|
}
|
|
}
|
|
|
|
// Edit Partner (Global function for onclick)
|
|
window.editPartner = function(partnerId) {
|
|
openPartnerModal(partnerId);
|
|
};
|
|
|
|
// Delete Partner (Global function for onclick)
|
|
window.deletePartner = async function(partnerId) {
|
|
if (!confirm('Are you sure you want to delete this partner?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/partners/${partnerId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('Partner deleted successfully', 'success');
|
|
loadPartners();
|
|
} else {
|
|
showToast(result.message || 'Failed to delete partner', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete partner error:', error);
|
|
showToast('Failed to delete partner', 'error');
|
|
}
|
|
};
|
|
|
|
// Load System Settings
|
|
async function loadSystemSettings() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/config/settings`, {
|
|
credentials: 'include'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
renderSystemSettings(result.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Load system settings error:', error);
|
|
showToast('Failed to load system settings', 'error');
|
|
}
|
|
}
|
|
|
|
// Render System Settings
|
|
function renderSystemSettings(settings) {
|
|
const container = document.getElementById('settingsTableContainer');
|
|
|
|
if (settings.length === 0) {
|
|
container.innerHTML = '<p class="text-muted">No settings found</p>';
|
|
return;
|
|
}
|
|
|
|
const html = `
|
|
<div class="table-responsive">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Setting Key</th>
|
|
<th>Value</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${settings.map(setting => `
|
|
<tr>
|
|
<td><strong>${setting.setting_key}</strong></td>
|
|
<td>${setting.setting_value}</td>
|
|
<td class="text-muted">${setting.description || '-'}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// Show Toast Notification
|
|
function showToast(message, type = 'success') {
|
|
toast.textContent = message;
|
|
toast.className = `toast ${type} show`;
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
// ==================== 视频管理功能 ====================
|
|
|
|
// 加载视频列表
|
|
async function loadVideos() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/videos`, {
|
|
credentials: 'include'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
renderVideos(result.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Load videos error:', error);
|
|
showToast('Failed to load videos', 'error');
|
|
}
|
|
}
|
|
|
|
// 渲染视频列表
|
|
function renderVideos(videos) {
|
|
const tbody = document.getElementById('videosTableBody');
|
|
|
|
if (videos.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="text-center">暂无视频</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = videos.map(video => {
|
|
const duration = video.duration ? formatDuration(video.duration) : '-';
|
|
const uploadStatusText = getUploadStatusText(video.upload_status);
|
|
const statusBadge = video.status === 1 ? 'badge-success' : video.status === 2 ? 'badge-warning' : 'badge-danger';
|
|
const statusText = video.status === 1 ? '启用' : video.status === 2 ? '处理中' : '禁用';
|
|
|
|
// 判断封面URL是完整URL还是相对路径
|
|
const coverSrc = video.cover_url ?
|
|
(video.cover_url.startsWith('http') ? video.cover_url : `/${video.cover_url}`) :
|
|
'';
|
|
|
|
return `
|
|
<tr>
|
|
<td>${video.id}</td>
|
|
<td>
|
|
${coverSrc ? `<img src="${coverSrc}" alt="${video.title_cn}">` : '-'}
|
|
</td>
|
|
<td>${video.title_cn}</td>
|
|
<td>${video.title_en}</td>
|
|
<td>${duration}</td>
|
|
<td><span class="badge ${getUploadStatusBadge(video.upload_status)}">${uploadStatusText}</span></td>
|
|
<td><span class="badge ${statusBadge}">${statusText}</span></td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm btn-primary" onclick="editVideo(${video.id})">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteVideo(${video.id})">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// 格式化时长
|
|
function formatDuration(seconds) {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = seconds % 60;
|
|
|
|
if (hours > 0) {
|
|
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
}
|
|
return `${minutes}:${String(secs).padStart(2, '0')}`;
|
|
}
|
|
|
|
// 获取上传状态文本
|
|
function getUploadStatusText(status) {
|
|
const statusMap = {
|
|
'pending': '待上传',
|
|
'uploading': '上传中',
|
|
'success': '成功',
|
|
'failed': '失败'
|
|
};
|
|
return statusMap[status] || status;
|
|
}
|
|
|
|
// 获取上传状态徽章样式
|
|
function getUploadStatusBadge(status) {
|
|
const badgeMap = {
|
|
'pending': 'badge-secondary',
|
|
'uploading': 'badge-warning',
|
|
'success': 'badge-success',
|
|
'failed': 'badge-danger'
|
|
};
|
|
return badgeMap[status] || 'badge-secondary';
|
|
}
|
|
|
|
// 编辑视频
|
|
window.editVideo = async function(videoId) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/videos/${videoId}`, {
|
|
credentials: 'include'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const video = result.data;
|
|
document.getElementById('editVideoId').value = video.id;
|
|
document.getElementById('editTitleCn').value = video.title_cn;
|
|
document.getElementById('editTitleEn').value = video.title_en;
|
|
document.getElementById('editDescCn').value = video.desc_cn || '';
|
|
document.getElementById('editDescEn').value = video.desc_en || '';
|
|
document.getElementById('editStatus').value = video.status;
|
|
document.getElementById('editSortOrder').value = video.sort_order;
|
|
|
|
document.getElementById('videoModal').classList.add('show');
|
|
}
|
|
} catch (error) {
|
|
console.error('Load video error:', error);
|
|
showToast('Failed to load video data', 'error');
|
|
}
|
|
};
|
|
|
|
// 处理视频编辑表单提交
|
|
async function handleVideoEditSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const videoId = document.getElementById('editVideoId').value;
|
|
const formData = new FormData();
|
|
|
|
formData.append('title_cn', document.getElementById('editTitleCn').value);
|
|
formData.append('title_en', document.getElementById('editTitleEn').value);
|
|
formData.append('desc_cn', document.getElementById('editDescCn').value);
|
|
formData.append('desc_en', document.getElementById('editDescEn').value);
|
|
formData.append('status', document.getElementById('editStatus').value);
|
|
formData.append('sort_order', document.getElementById('editSortOrder').value);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/videos/${videoId}`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('Video updated successfully', 'success');
|
|
document.getElementById('videoModal').classList.remove('show');
|
|
loadVideos();
|
|
} else {
|
|
showToast(result.message || 'Failed to update video', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Update video error:', error);
|
|
showToast('Failed to update video', 'error');
|
|
}
|
|
}
|
|
|
|
// 删除视频
|
|
window.deleteVideo = async function(videoId) {
|
|
if (!confirm('确定要删除这个视频吗?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/videos/${videoId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('Video deleted successfully', 'success');
|
|
loadVideos();
|
|
} else {
|
|
showToast(result.message || 'Failed to delete video', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete video error:', error);
|
|
showToast('Failed to delete video', 'error');
|
|
}
|
|
};
|
|
|
|
// ==================== VOD配置 ====================
|
|
|
|
// 加载VOD配置
|
|
async function loadVodConfig() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/videos/vod-config`, {
|
|
credentials: 'include'
|
|
});
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const config = result.data;
|
|
document.getElementById('secretId').value = config.secret_id || '';
|
|
document.getElementById('secretKey').value = config.secret_key || '';
|
|
document.getElementById('vodRegion').value = config.region || 'ap-guangzhou';
|
|
document.getElementById('subAppId').value = config.sub_app_id || '';
|
|
document.getElementById('vodProcedure').value = config.procedure || '';
|
|
document.getElementById('storageRegion').value = config.storage_region || '';
|
|
|
|
// COS配置
|
|
document.getElementById('cosBucket').value = config.cos_bucket || '';
|
|
document.getElementById('cosRegion').value = config.cos_region || 'ap-guangzhou';
|
|
document.getElementById('cosPath').value = config.cos_path || 'video-covers/';
|
|
|
|
// 防盗链配置
|
|
document.getElementById('signKey').value = config.sign_key || '';
|
|
}
|
|
} catch (error) {
|
|
console.error('Load VOD config error:', error);
|
|
showToast('加载VOD配置失败', 'error');
|
|
}
|
|
}
|
|
|
|
// 处理VOD配置提交
|
|
async function handleVodConfigSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(e.target);
|
|
const data = {
|
|
secret_id: formData.get('secret_id'),
|
|
secret_key: formData.get('secret_key'),
|
|
region: formData.get('region'),
|
|
sub_app_id: formData.get('sub_app_id'),
|
|
procedure: formData.get('procedure'),
|
|
storage_region: formData.get('storage_region'),
|
|
// COS配置
|
|
cos_bucket: formData.get('cos_bucket'),
|
|
cos_region: formData.get('cos_region'),
|
|
cos_path: formData.get('cos_path'),
|
|
// 防盗链配置
|
|
sign_key: formData.get('sign_key')
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/videos/vod-config`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('VOD配置更新成功', 'success');
|
|
} else {
|
|
showToast(result.message || '配置更新失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Update VOD config error:', error);
|
|
showToast('配置更新失败', 'error');
|
|
}
|
|
}
|
|
|
|
// ==================== 视频上传 ====================
|
|
|
|
let currentUploadTask = null;
|
|
|
|
// 重置上传表单
|
|
function resetVideoUploadForm() {
|
|
document.getElementById('addVideoForm').reset();
|
|
document.getElementById('coverPreview').style.display = 'none';
|
|
document.getElementById('uploadProgress').style.display = 'none';
|
|
document.getElementById('uploadVideoBtn').style.display = 'inline-flex';
|
|
document.getElementById('cancelUploadBtn').style.display = 'none';
|
|
}
|
|
|
|
// 封面预览
|
|
function handleCoverPreview(e) {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(event) {
|
|
document.getElementById('coverPreviewImg').src = event.target.result;
|
|
document.getElementById('coverPreview').style.display = 'block';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
}
|
|
|
|
// 处理视频上传
|
|
async function handleVideoUpload(e) {
|
|
e.preventDefault();
|
|
|
|
const titleCn = document.getElementById('videoTitleCn').value;
|
|
const titleEn = document.getElementById('videoTitleEn').value;
|
|
const descCn = document.getElementById('videoDescCn').value;
|
|
const descEn = document.getElementById('videoDescEn').value;
|
|
const coverFile = document.getElementById('videoCover').files[0];
|
|
const videoFile = document.getElementById('videoFile').files[0];
|
|
|
|
if (!videoFile) {
|
|
showToast('请选择视频文件', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 1. 获取上传签名
|
|
showToast('正在获取上传凭证...', 'info');
|
|
const sigResponse = await fetch(`${API_BASE}/videos/upload-signature`, {
|
|
method: 'POST',
|
|
credentials: 'include'
|
|
});
|
|
const sigResult = await sigResponse.json();
|
|
|
|
if (!sigResult.success) {
|
|
showToast(sigResult.message || '获取上传凭证失败', 'error');
|
|
return;
|
|
}
|
|
|
|
// 2. 先创建视频记录(可选上传封面)
|
|
const formData = new FormData();
|
|
formData.append('title_cn', titleCn);
|
|
formData.append('title_en', titleEn);
|
|
formData.append('desc_cn', descCn);
|
|
formData.append('desc_en', descEn);
|
|
if (coverFile) {
|
|
formData.append('cover', coverFile);
|
|
}
|
|
formData.append('status', 2); // Processing
|
|
|
|
const videoRecordResponse = await fetch(`${API_BASE}/videos`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
body: formData
|
|
});
|
|
|
|
const videoRecordResult = await videoRecordResponse.json();
|
|
|
|
if (!videoRecordResult.success) {
|
|
showToast(videoRecordResult.message || '创建视频记录失败', 'error');
|
|
return;
|
|
}
|
|
|
|
const videoId = videoRecordResult.data.id;
|
|
|
|
// 3. 使用腾讯云VOD SDK上传视频
|
|
showUploadProgress(videoFile.name);
|
|
|
|
const tcVod = new TcVod.default({
|
|
getSignature: () => sigResult.data.signature
|
|
});
|
|
|
|
const uploader = tcVod.upload({
|
|
mediaFile: videoFile,
|
|
});
|
|
|
|
// 监听上传进度
|
|
uploader.on('media_progress', (info) => {
|
|
updateUploadProgress(info.percent * 100, info.speed);
|
|
});
|
|
|
|
// 上传完成
|
|
uploader.done().then(async (doneResult) => {
|
|
console.log('Upload complete:', doneResult);
|
|
|
|
// 更新视频记录
|
|
const updateData = new FormData();
|
|
updateData.append('file_id', doneResult.fileId);
|
|
updateData.append('video_url', doneResult.video.url);
|
|
updateData.append('upload_status', 'success');
|
|
updateData.append('status', 1); // Active
|
|
|
|
await fetch(`${API_BASE}/videos/${videoId}`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
body: updateData
|
|
});
|
|
|
|
showToast('视频上传成功!', 'success');
|
|
setTimeout(() => {
|
|
navigateToPage('video-list');
|
|
}, 1500);
|
|
}).catch(async (error) => {
|
|
console.error('Upload failed:', error);
|
|
|
|
// 更新失败状态
|
|
const updateData = new FormData();
|
|
updateData.append('upload_status', 'failed');
|
|
|
|
await fetch(`${API_BASE}/videos/${videoId}`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
body: updateData
|
|
});
|
|
|
|
showToast('视频上传失败: ' + error.message, 'error');
|
|
document.getElementById('uploadVideoBtn').style.display = 'inline-flex';
|
|
document.getElementById('cancelUploadBtn').style.display = 'none';
|
|
});
|
|
|
|
currentUploadTask = uploader;
|
|
document.getElementById('uploadVideoBtn').style.display = 'none';
|
|
document.getElementById('cancelUploadBtn').style.display = 'inline-flex';
|
|
|
|
// 取消上传按钮
|
|
document.getElementById('cancelUploadBtn').onclick = () => {
|
|
if (currentUploadTask) {
|
|
currentUploadTask.cancel();
|
|
showToast('上传已取消', 'warning');
|
|
resetVideoUploadForm();
|
|
}
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('Video upload error:', error);
|
|
showToast('上传失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// 显示上传进度
|
|
function showUploadProgress(fileName) {
|
|
document.getElementById('uploadProgress').style.display = 'block';
|
|
document.getElementById('uploadFileName').textContent = fileName;
|
|
document.getElementById('uploadPercent').textContent = '0%';
|
|
document.getElementById('progressFill').style.width = '0%';
|
|
document.getElementById('uploadStatus').textContent = '正在上传...';
|
|
document.getElementById('uploadSpeed').textContent = '';
|
|
}
|
|
|
|
// 更新上传进度
|
|
function updateUploadProgress(percent, speed) {
|
|
const percentInt = Math.floor(percent);
|
|
document.getElementById('uploadPercent').textContent = percentInt + '%';
|
|
document.getElementById('progressFill').style.width = percentInt + '%';
|
|
|
|
if (speed) {
|
|
const speedText = formatSpeed(speed);
|
|
document.getElementById('uploadSpeed').textContent = speedText;
|
|
}
|
|
}
|
|
|
|
// 格式化速度
|
|
function formatSpeed(bytesPerSecond) {
|
|
if (bytesPerSecond < 1024) {
|
|
return bytesPerSecond.toFixed(2) + ' B/s';
|
|
} else if (bytesPerSecond < 1024 * 1024) {
|
|
return (bytesPerSecond / 1024).toFixed(2) + ' KB/s';
|
|
} else {
|
|
return (bytesPerSecond / (1024 * 1024)).toFixed(2) + ' MB/s';
|
|
}
|
|
}
|
|
|
|
// ==================== 修改密码功能 ====================
|
|
|
|
// 处理修改密码
|
|
async function handleChangePassword(e) {
|
|
e.preventDefault();
|
|
|
|
const currentPassword = document.getElementById('currentPassword').value;
|
|
const newPassword = document.getElementById('newPassword').value;
|
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
|
|
|
// 验证新密码
|
|
if (newPassword.length < 6) {
|
|
showToast('新密码长度至少6位', 'error');
|
|
return;
|
|
}
|
|
|
|
// 验证两次密码是否一致
|
|
if (newPassword !== confirmPassword) {
|
|
showToast('两次输入的新密码不一致', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/change-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
current_password: currentPassword,
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('密码修改成功,请重新登录', 'success');
|
|
document.getElementById('changePasswordForm').reset();
|
|
|
|
// 2秒后自动退出登录
|
|
setTimeout(() => {
|
|
handleLogout();
|
|
}, 2000);
|
|
} else {
|
|
showToast(result.message || '密码修改失败', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Change password error:', error);
|
|
showToast('密码修改失败', 'error');
|
|
}
|
|
}
|