392 lines
17 KiB
HTML
392 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>后台管理 - 短视频平台</title>
|
|
<link rel="stylesheet" href="/css/style.css">
|
|
</head>
|
|
<body class="admin-page">
|
|
<!-- 后台顶部栏 -->
|
|
<header class="admin-header">
|
|
<div class="admin-header-content">
|
|
<div class="logo">
|
|
<svg viewBox="0 0 24 24" width="32" height="32">
|
|
<path fill="#ff2442" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
</svg>
|
|
<span>后台管理</span>
|
|
</div>
|
|
<div class="admin-user">
|
|
<span id="adminUsername">管理员</span>
|
|
<button onclick="logout()" class="logout-btn">退出</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="admin-container">
|
|
<!-- 侧边栏 -->
|
|
<aside class="admin-sidebar">
|
|
<nav class="admin-nav">
|
|
<a href="#" class="nav-item active" data-tab="videos">
|
|
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
<path fill="currentColor" d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z"/>
|
|
</svg>
|
|
<span>视频管理</span>
|
|
</a>
|
|
<a href="#" class="nav-item" data-tab="stats">
|
|
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
<path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/>
|
|
</svg>
|
|
<span>数据统计</span>
|
|
</a>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- 主内容区 -->
|
|
<main class="admin-main">
|
|
<!-- 视频管理 -->
|
|
<div id="videosTab" class="admin-tab active">
|
|
<div class="tab-header">
|
|
<h2>视频管理</h2>
|
|
<button class="btn-primary" onclick="showAddModal()">添加视频</button>
|
|
</div>
|
|
|
|
<div class="video-table-wrapper">
|
|
<table class="video-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>封面</th>
|
|
<th>标题</th>
|
|
<th>分类</th>
|
|
<th>作者</th>
|
|
<th>播放量</th>
|
|
<th>点赞</th>
|
|
<th>上传时间</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="videoTableBody">
|
|
<!-- 动态加载 -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 数据统计 -->
|
|
<div id="statsTab" class="admin-tab">
|
|
<div class="tab-header">
|
|
<h2>数据统计</h2>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-icon">
|
|
<svg viewBox="0 0 24 24" width="32" height="32">
|
|
<path fill="currentColor" d="M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-8 12.5v-9l6 4.5-6 4.5z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="stat-info">
|
|
<div class="stat-value" id="totalVideos">0</div>
|
|
<div class="stat-label">视频总数</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon views-icon">
|
|
<svg viewBox="0 0 24 24" width="32" height="32">
|
|
<path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="stat-info">
|
|
<div class="stat-value" id="totalViews">0</div>
|
|
<div class="stat-label">总播放量</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon likes-icon">
|
|
<svg viewBox="0 0 24 24" width="32" height="32">
|
|
<path fill="currentColor" d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="stat-info">
|
|
<div class="stat-value" id="totalLikes">0</div>
|
|
<div class="stat-label">总点赞数</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-placeholder">
|
|
<p>📊 数据可视化图表(可扩展)</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- 添加/编辑视频模态框 -->
|
|
<div class="modal" id="videoModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3 id="modalTitle">添加视频</h3>
|
|
<button class="modal-close" onclick="closeModal()">×</button>
|
|
</div>
|
|
<form id="videoForm" class="video-form">
|
|
<input type="hidden" id="videoId">
|
|
|
|
<div class="form-group">
|
|
<label for="videoTitle">视频标题 *</label>
|
|
<input type="text" id="videoTitleInput" name="title" required placeholder="请输入视频标题">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="videoUrl">视频地址 *</label>
|
|
<input type="url" id="videoUrl" name="url" required placeholder="视频文件URL地址">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="videoCover">封面地址</label>
|
|
<input type="url" id="videoCover" name="cover" placeholder="封面图片URL地址">
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="videoCategory">分类 *</label>
|
|
<select id="videoCategory" name="category" required>
|
|
<option value="推荐">推荐</option>
|
|
<option value="热门">热门</option>
|
|
<option value="萌宠">萌宠</option>
|
|
<option value="美食">美食</option>
|
|
<option value="旅行">旅行</option>
|
|
<option value="科技">科技</option>
|
|
<option value="风景">风景</option>
|
|
<option value="其他">其他</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="videoDuration">时长(秒)</label>
|
|
<input type="number" id="videoDuration" name="duration" placeholder="视频时长">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="videoAuthor">作者</label>
|
|
<input type="text" id="videoAuthor" name="author" placeholder="视频作者">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="videoDescription">描述</label>
|
|
<textarea id="videoDescription" name="description" rows="3" placeholder="视频描述"></textarea>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn-secondary" onclick="closeModal()">取消</button>
|
|
<button type="submit" class="btn-primary">保存</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 检查登录状态
|
|
async function checkAuth() {
|
|
try {
|
|
const response = await fetch('/api/admin/check');
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
window.location.href = '/admin';
|
|
} else {
|
|
document.getElementById('adminUsername').textContent = data.user.username;
|
|
loadVideos();
|
|
}
|
|
} catch (error) {
|
|
window.location.href = '/admin';
|
|
}
|
|
}
|
|
|
|
// 登出
|
|
async function logout() {
|
|
try {
|
|
await fetch('/api/admin/logout', { method: 'POST' });
|
|
window.location.href = '/admin';
|
|
} catch (error) {
|
|
console.error('登出失败:', error);
|
|
}
|
|
}
|
|
|
|
// 加载视频列表
|
|
async function loadVideos() {
|
|
try {
|
|
const response = await fetch('/api/videos');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
renderVideoTable(data.data);
|
|
updateStats(data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('加载视频失败:', error);
|
|
}
|
|
}
|
|
|
|
// 渲染视频表格
|
|
function renderVideoTable(videos) {
|
|
const tbody = document.getElementById('videoTableBody');
|
|
tbody.innerHTML = videos.map(video => `
|
|
<tr>
|
|
<td>${video.id}</td>
|
|
<td><img src="${video.cover || 'https://via.placeholder.com/80x45'}" alt="${video.title}" class="table-cover"></td>
|
|
<td class="video-title-cell">${video.title}</td>
|
|
<td><span class="category-tag">${video.category}</span></td>
|
|
<td>${video.author || '未知'}</td>
|
|
<td>${formatNumber(video.views)}</td>
|
|
<td>${formatNumber(video.likes)}</td>
|
|
<td>${video.createTime}</td>
|
|
<td class="action-cell">
|
|
<button onclick="editVideo(${video.id})" class="btn-edit">编辑</button>
|
|
<button onclick="deleteVideo(${video.id})" class="btn-delete">删除</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// 更新统计数据
|
|
function updateStats(videos) {
|
|
const totalVideos = videos.length;
|
|
const totalViews = videos.reduce((sum, v) => sum + (v.views || 0), 0);
|
|
const totalLikes = videos.reduce((sum, v) => sum + (v.likes || 0), 0);
|
|
|
|
document.getElementById('totalVideos').textContent = totalVideos;
|
|
document.getElementById('totalViews').textContent = formatNumber(totalViews);
|
|
document.getElementById('totalLikes').textContent = formatNumber(totalLikes);
|
|
}
|
|
|
|
// 格式化数字
|
|
function formatNumber(num) {
|
|
if (num >= 10000) {
|
|
return (num / 10000).toFixed(1) + '万';
|
|
}
|
|
return num || 0;
|
|
}
|
|
|
|
// 显示添加模态框
|
|
function showAddModal() {
|
|
document.getElementById('modalTitle').textContent = '添加视频';
|
|
document.getElementById('videoForm').reset();
|
|
document.getElementById('videoId').value = '';
|
|
document.getElementById('videoModal').classList.add('active');
|
|
}
|
|
|
|
// 编辑视频
|
|
async function editVideo(id) {
|
|
try {
|
|
const response = await fetch(`/api/videos/${id}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const video = data.data;
|
|
document.getElementById('modalTitle').textContent = '编辑视频';
|
|
document.getElementById('videoId').value = video.id;
|
|
document.getElementById('videoTitleInput').value = video.title;
|
|
document.getElementById('videoUrl').value = video.url;
|
|
document.getElementById('videoCover').value = video.cover || '';
|
|
document.getElementById('videoCategory').value = video.category;
|
|
document.getElementById('videoDuration').value = video.duration || '';
|
|
document.getElementById('videoAuthor').value = video.author || '';
|
|
document.getElementById('videoDescription').value = video.description || '';
|
|
document.getElementById('videoModal').classList.add('active');
|
|
}
|
|
} catch (error) {
|
|
console.error('加载视频失败:', error);
|
|
}
|
|
}
|
|
|
|
// 删除视频
|
|
async function deleteVideo(id) {
|
|
if (!confirm('确定要删除这个视频吗?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/videos/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
loadVideos();
|
|
} else {
|
|
alert('删除失败: ' + data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('删除视频失败:', error);
|
|
}
|
|
}
|
|
|
|
// 关闭模态框
|
|
function closeModal() {
|
|
document.getElementById('videoModal').classList.remove('active');
|
|
}
|
|
|
|
// 提交表单
|
|
document.getElementById('videoForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const formData = {
|
|
title: document.getElementById('videoTitleInput').value,
|
|
url: document.getElementById('videoUrl').value,
|
|
cover: document.getElementById('videoCover').value,
|
|
category: document.getElementById('videoCategory').value,
|
|
duration: parseInt(document.getElementById('videoDuration').value) || 10,
|
|
author: document.getElementById('videoAuthor').value || '管理员',
|
|
description: document.getElementById('videoDescription').value
|
|
};
|
|
|
|
const videoId = document.getElementById('videoId').value;
|
|
const isEdit = !!videoId;
|
|
|
|
try {
|
|
const url = isEdit ? `/api/admin/videos/${videoId}` : '/api/admin/videos';
|
|
const method = isEdit ? 'PUT' : 'POST';
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
closeModal();
|
|
loadVideos();
|
|
} else {
|
|
alert('保存失败: ' + data.message);
|
|
}
|
|
} catch (error) {
|
|
console.error('保存视频失败:', error);
|
|
alert('保存失败,请重试');
|
|
}
|
|
});
|
|
|
|
// 切换标签
|
|
document.querySelectorAll('.admin-nav .nav-item').forEach(item => {
|
|
item.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const tab = item.dataset.tab;
|
|
|
|
document.querySelectorAll('.admin-nav .nav-item').forEach(i => i.classList.remove('active'));
|
|
document.querySelectorAll('.admin-tab').forEach(t => t.classList.remove('active'));
|
|
|
|
item.classList.add('active');
|
|
document.getElementById(tab + 'Tab').classList.add('active');
|
|
});
|
|
});
|
|
|
|
// 初始化
|
|
document.addEventListener('DOMContentLoaded', checkAuth);
|
|
</script>
|
|
</body>
|
|
</html> |