初始提交
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
<!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 href="/vendor/bootstrap/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/vendor/cropperjs/cropper.min.css" rel="stylesheet">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', sans-serif; background: #fff; }
|
||||
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
||||
|
||||
.top-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; }
|
||||
|
||||
.feature-status { display: flex; align-items: center; gap: 15px; }
|
||||
.feature-status span { font-size: 16px; font-weight: 500; }
|
||||
|
||||
.toggle { position: relative; display: inline-block; width: 50px; height: 26px; }
|
||||
.toggle input { opacity: 0; width: 0; height: 0; }
|
||||
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; transition: .3s; border-radius: 26px; }
|
||||
.slider:before { position: absolute; content: ""; height: 22px; width: 22px; left: 2px; bottom: 2px; background: #fff; transition: .3s; border-radius: 50%; }
|
||||
input:checked + .slider { background: #1890ff; }
|
||||
input:checked + .slider:before { transform: translateX(24px); }
|
||||
|
||||
.group { margin-bottom: 40px; border: 1px solid #e8e8e8; padding: 20px; }
|
||||
|
||||
.group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
|
||||
.group-info { display: flex; align-items: center; gap: 20px; }
|
||||
.group-name { font-size: 18px; font-weight: 500; }
|
||||
.group-time { font-size: 14px; color: #666; }
|
||||
|
||||
.group-actions { display: flex; gap: 20px; }
|
||||
.group-actions a { color: #1890ff; text-decoration: none; font-size: 14px; cursor: pointer; }
|
||||
.group-actions a.delete { color: #ff4d4f; }
|
||||
|
||||
.image-list { display: flex; flex-wrap: wrap; gap: 15px; }
|
||||
|
||||
.image-item { position: relative; width: 140px; height: 140px; background: #e8e8e8; cursor: pointer; }
|
||||
.image-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
|
||||
.image-item.disabled::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.action-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 24px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8e8e8;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
z-index: 100;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.action-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-menu a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-menu a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.action-menu a.enable { color: #52c41a; }
|
||||
.action-menu a.disable { color: #1890ff; }
|
||||
.action-menu a.delete { color: #ff4d4f; }
|
||||
|
||||
.time-input { display: flex; align-items: center; gap: 10px; justify-content: center; }
|
||||
.time-input input { width: 60px; height: 36px; border: 1px solid #d9d9d9; text-align: center; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="top-bar">
|
||||
<div class="feature-status">
|
||||
<span>功能状态</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="featureToggle">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/" class="btn btn-outline-secondary">首页</a>
|
||||
<button class="btn btn-dark" id="addGroupBtn">添加分组</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="groupsContainer"></div>
|
||||
</div>
|
||||
|
||||
<!-- 删除分组弹窗 -->
|
||||
<div class="modal fade" id="deleteGroupModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body text-center py-4">
|
||||
<h5 class="mb-4">确认删除该分组?</h5>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<button type="button" class="btn btn-outline-secondary px-4" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-danger px-4" id="confirmDeleteGroup">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设置时间弹窗 -->
|
||||
<div class="modal fade" id="setTimeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body text-center py-4">
|
||||
<h5 class="mb-4">设置时间</h5>
|
||||
<div class="time-input mb-4">
|
||||
<input type="number" id="timeMinutes" min="0" value="0">
|
||||
<span>分</span>
|
||||
<input type="number" id="timeSeconds" min="0" value="30">
|
||||
<span>秒</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<button type="button" class="btn btn-outline-secondary px-4" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-dark px-4" id="confirmSetTime">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除图片弹窗 -->
|
||||
<div class="modal fade" id="deleteImageModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body text-center py-4">
|
||||
<h5 class="mb-4">确认删除该图片?</h5>
|
||||
<div class="d-flex justify-content-center gap-3">
|
||||
<button type="button" class="btn btn-outline-secondary px-4" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-danger px-4" id="confirmDeleteImage">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 裁剪弹窗 -->
|
||||
<div class="modal fade" id="cropModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">裁剪图片</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="max-height: 400px; overflow: hidden;">
|
||||
<img id="cropImage" style="max-width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmCrop">确认上传</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script src="/vendor/cropperjs/cropper.min.js"></script>
|
||||
<script>
|
||||
let cropper = null;
|
||||
let selectedFile = null;
|
||||
let currentGroupId = null;
|
||||
let deleteGroupId = null;
|
||||
let deleteImageId = null;
|
||||
let setTimeGroupId = null;
|
||||
let currentImageData = null;
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) window.location.href = '/login';
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
};
|
||||
|
||||
let deleteGroupModal, setTimeModal, deleteImageModal, cropModal;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
deleteGroupModal = new bootstrap.Modal(document.getElementById('deleteGroupModal'));
|
||||
setTimeModal = new bootstrap.Modal(document.getElementById('setTimeModal'));
|
||||
deleteImageModal = new bootstrap.Modal(document.getElementById('deleteImageModal'));
|
||||
cropModal = new bootstrap.Modal(document.getElementById('cropModal'));
|
||||
|
||||
loadConfig();
|
||||
loadGroups();
|
||||
|
||||
document.getElementById('featureToggle').onchange = toggleFeature;
|
||||
document.getElementById('addGroupBtn').onclick = addGroup;
|
||||
document.getElementById('confirmDeleteGroup').onclick = doDeleteGroup;
|
||||
document.getElementById('confirmSetTime').onclick = doSetTime;
|
||||
document.getElementById('confirmDeleteImage').onclick = doDeleteImage;
|
||||
document.getElementById('confirmCrop').onclick = doCrop;
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.image-item')) {
|
||||
document.querySelectorAll('.action-menu').forEach(m => m.classList.remove('show'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function loadConfig() {
|
||||
const res = await fetch('/api/config');
|
||||
const config = await res.json();
|
||||
document.getElementById('featureToggle').checked = config.feature_enabled === 'true';
|
||||
}
|
||||
|
||||
async function toggleFeature() {
|
||||
const enabled = document.getElementById('featureToggle').checked;
|
||||
await fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ key: 'feature_enabled', value: enabled.toString() })
|
||||
});
|
||||
}
|
||||
|
||||
let countdownInterval = null;
|
||||
let groupsData = [];
|
||||
|
||||
async function loadGroups() {
|
||||
const res = await fetch('/api/groups');
|
||||
groupsData = await res.json();
|
||||
renderGroups();
|
||||
}
|
||||
|
||||
function renderGroups() {
|
||||
const container = document.getElementById('groupsContainer');
|
||||
container.innerHTML = groupsData.map(g => {
|
||||
const min = Math.floor(g.display_seconds / 60);
|
||||
const sec = g.display_seconds % 60;
|
||||
const timeStr = min + '分' + sec + '秒';
|
||||
|
||||
let countdownStr = '';
|
||||
if (g.update_time > 0 && g.display_seconds > 0) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const remaining = Math.max(0, g.update_time + g.display_seconds - now);
|
||||
countdownStr = ' | 剩余:' + remaining + '秒';
|
||||
} else if (g.display_seconds === 0) {
|
||||
countdownStr = ' | 持续展示';
|
||||
} else {
|
||||
countdownStr = ' | 未开始';
|
||||
}
|
||||
|
||||
return '<div class="group" data-id="' + g.id + '">' +
|
||||
'<div class="group-header">' +
|
||||
'<div class="group-info">' +
|
||||
'<span class="group-name">' + g.name + '</span>' +
|
||||
'<span class="group-time">展示时间:' + timeStr + '<span id="countdown-' + g.id + '" style="color:#1890ff;">' + countdownStr + '</span></span>' +
|
||||
'</div>' +
|
||||
'<div class="group-actions">' +
|
||||
'<a onclick="addImage(' + g.id + ')">添加图片</a>' +
|
||||
'<a onclick="setTime(' + g.id + ', ' + g.display_seconds + ')">设置时间</a>' +
|
||||
'<a class="delete" onclick="deleteGroup(' + g.id + ')">删除</a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="image-list" id="images-' + g.id + '"></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
groupsData.forEach(g => loadImages(g.id));
|
||||
|
||||
// 启动倒计时
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
countdownInterval = setInterval(updateCountdowns, 1000);
|
||||
}
|
||||
|
||||
function updateCountdowns() {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
groupsData.forEach(g => {
|
||||
const el = document.getElementById('countdown-' + g.id);
|
||||
if (!el) return;
|
||||
|
||||
if (g.update_time > 0 && g.display_seconds > 0) {
|
||||
const remaining = Math.max(0, g.update_time + g.display_seconds - now);
|
||||
el.textContent = ' | 剩余:' + remaining + '秒';
|
||||
el.style.color = remaining <= 10 ? '#ff4d4f' : '#1890ff';
|
||||
} else if (g.display_seconds === 0) {
|
||||
el.textContent = ' | 持续展示';
|
||||
} else {
|
||||
el.textContent = ' | 未开始';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadImages(groupId) {
|
||||
const res = await fetch('/api/images?group_id=' + groupId);
|
||||
const images = await res.json();
|
||||
const container = document.getElementById('images-' + groupId);
|
||||
|
||||
container.innerHTML = images.map(img => {
|
||||
const disabledClass = img.enabled ? '' : ' disabled';
|
||||
const toggleText = img.enabled ? '下架' : '上架';
|
||||
const toggleClass = img.enabled ? 'disable' : 'enable';
|
||||
return '<div class="image-item' + disabledClass + '">' +
|
||||
'<img src="/uploads/' + img.filename + '">' +
|
||||
'<div class="delete-btn" onclick="toggleActionMenu(this)">-</div>' +
|
||||
'<div class="action-menu">' +
|
||||
'<a class="' + toggleClass + '" onclick="toggleImage(' + img.id + ', ' + (img.enabled ? 0 : 1) + ')">' + toggleText + '</a>' +
|
||||
'<a class="delete" onclick="deleteImage(' + img.id + ')">删除</a>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function addGroup() {
|
||||
const groupsRes = await fetch('/api/groups');
|
||||
const groups = await groupsRes.json();
|
||||
const nextNum = groups.length + 1;
|
||||
|
||||
const res = await fetch('/api/groups', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ name: '分组' + nextNum, display_seconds: 30 })
|
||||
});
|
||||
if (res.ok) loadGroups();
|
||||
}
|
||||
|
||||
function deleteGroup(id) {
|
||||
deleteGroupId = id;
|
||||
deleteGroupModal.show();
|
||||
}
|
||||
|
||||
async function doDeleteGroup() {
|
||||
await fetch('/api/groups/' + deleteGroupId, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
deleteGroupModal.hide();
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
function setTime(groupId, seconds) {
|
||||
setTimeGroupId = groupId;
|
||||
const min = Math.floor(seconds / 60);
|
||||
const sec = seconds % 60;
|
||||
document.getElementById('timeMinutes').value = min;
|
||||
document.getElementById('timeSeconds').value = sec;
|
||||
setTimeModal.show();
|
||||
}
|
||||
|
||||
async function doSetTime() {
|
||||
const min = parseInt(document.getElementById('timeMinutes').value) || 0;
|
||||
const sec = parseInt(document.getElementById('timeSeconds').value) || 0;
|
||||
const total = min * 60 + sec;
|
||||
|
||||
await fetch('/api/groups/' + setTimeGroupId + '/settime', {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify({ display_seconds: total })
|
||||
});
|
||||
setTimeModal.hide();
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
function addImage(groupId) {
|
||||
currentGroupId = groupId;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
selectedFile = file;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('cropImage').src = e.target.result;
|
||||
cropModal.show();
|
||||
|
||||
setTimeout(() => {
|
||||
if (cropper) cropper.destroy();
|
||||
cropper = new Cropper(document.getElementById('cropImage'), {
|
||||
aspectRatio: 1,
|
||||
viewMode: 1,
|
||||
autoCropArea: 1
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
async function doCrop() {
|
||||
if (!cropper || !selectedFile) return;
|
||||
|
||||
const canvas = cropper.getCroppedCanvas({ width: 280, height: 280 });
|
||||
const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.7));
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', blob, selectedFile.name);
|
||||
formData.append('group_id', currentGroupId);
|
||||
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
cropModal.hide();
|
||||
loadImages(currentGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleActionMenu(btn) {
|
||||
event.stopPropagation();
|
||||
const menu = btn.nextElementSibling;
|
||||
const wasShow = menu.classList.contains('show');
|
||||
document.querySelectorAll('.action-menu').forEach(m => m.classList.remove('show'));
|
||||
if (!wasShow) menu.classList.add('show');
|
||||
}
|
||||
|
||||
async function toggleImage(id, enable) {
|
||||
event.stopPropagation();
|
||||
const endpoint = enable ? 'enable' : 'disable';
|
||||
await fetch('/api/images/' + id + '/' + endpoint, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
document.querySelectorAll('.action-menu').forEach(m => m.classList.remove('show'));
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
function deleteImage(id) {
|
||||
event.stopPropagation();
|
||||
deleteImageId = id;
|
||||
document.querySelectorAll('.action-menu').forEach(m => m.classList.remove('show'));
|
||||
deleteImageModal.show();
|
||||
}
|
||||
|
||||
async function doDeleteImage() {
|
||||
await fetch('/api/images/' + deleteImageId, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
deleteImageModal.hide();
|
||||
loadGroups();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,260 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 15px 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
header .container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
header nav a {
|
||||
margin-left: 20px;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
header nav a:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 30px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-item.disabled::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.group-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.group-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.image-card {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-card img {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image-card.disabled::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.image-card .actions {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
max-width: 400px;
|
||||
margin: 50px auto;
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.login-form h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.toggle input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.image-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>图片资源展示</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #fff; }
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.user-icon:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
background: #e8e8e8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 80px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
首页
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="image-grid" id="imageGrid"></div>
|
||||
<div class="empty" id="empty">暂无图片</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentGroupIndex = 0;
|
||||
let groups = [];
|
||||
let timer = null;
|
||||
|
||||
function checkLogin() {
|
||||
if (!localStorage.getItem('token')) {
|
||||
window.location.href = '/login';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', loadDisplay);
|
||||
|
||||
async function loadDisplay() {
|
||||
try {
|
||||
const res = await fetch('/api/display');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.enabled || data.groups.length === 0) {
|
||||
document.getElementById('empty').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('empty').style.display = 'none';
|
||||
groups = data.groups;
|
||||
showGroup(0);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function showGroup(index) {
|
||||
if (index >= groups.length) index = 0;
|
||||
currentGroupIndex = index;
|
||||
|
||||
const group = groups[index];
|
||||
const grid = document.getElementById('imageGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
group.images.forEach(img => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'image-item';
|
||||
|
||||
const imgEl = document.createElement('img');
|
||||
imgEl.src = '/uploads/' + img.filename;
|
||||
imgEl.loading = 'lazy';
|
||||
|
||||
let pressTimer;
|
||||
imgEl.addEventListener('touchstart', function() {
|
||||
pressTimer = setTimeout(function() {
|
||||
saveImage('/uploads/' + img.filename, img.original_name);
|
||||
}, 1000);
|
||||
});
|
||||
imgEl.addEventListener('touchend', function() { clearTimeout(pressTimer); });
|
||||
imgEl.addEventListener('touchmove', function() { clearTimeout(pressTimer); });
|
||||
|
||||
div.appendChild(imgEl);
|
||||
grid.appendChild(div);
|
||||
});
|
||||
|
||||
startTimer(group.display_seconds);
|
||||
}
|
||||
|
||||
function startTimer(seconds) {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(function() {
|
||||
showGroup(currentGroupIndex + 1);
|
||||
}, seconds * 1000);
|
||||
}
|
||||
|
||||
function saveImage(url, name) {
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name || 'image.jpg';
|
||||
a.click();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,321 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkAuth();
|
||||
loadConfig();
|
||||
loadGroups();
|
||||
|
||||
document.getElementById('logout').addEventListener('click', logout);
|
||||
document.getElementById('featureToggle').addEventListener('change', toggleFeature);
|
||||
document.getElementById('addGroupBtn').addEventListener('click', addGroup);
|
||||
document.getElementById('uploadBtn').addEventListener('click', () => document.getElementById('fileInput').click());
|
||||
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
|
||||
document.getElementById('cropCancel').addEventListener('click', closeCropModal);
|
||||
document.getElementById('cropConfirm').addEventListener('click', cropAndUpload);
|
||||
});
|
||||
|
||||
let cropper = null;
|
||||
let selectedFile = null;
|
||||
|
||||
function checkAuth() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
// 验证token有效性(可选)
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
const config = await response.json();
|
||||
document.getElementById('featureToggle').checked = config.feature_enabled === 'true';
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleFeature() {
|
||||
const enabled = document.getElementById('featureToggle').checked;
|
||||
try {
|
||||
await fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({ key: 'feature_enabled', value: enabled.toString() })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新配置失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const response = await fetch('/api/groups');
|
||||
const groups = await response.json();
|
||||
renderGroups(groups);
|
||||
updateGroupSelect(groups);
|
||||
} catch (error) {
|
||||
console.error('加载分组失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderGroups(groups) {
|
||||
const container = document.getElementById('groupsList');
|
||||
container.innerHTML = '';
|
||||
|
||||
groups.forEach(group => {
|
||||
const groupItem = document.createElement('div');
|
||||
groupItem.className = 'group-item';
|
||||
groupItem.innerHTML = `
|
||||
<div>
|
||||
<strong>${group.name}</strong>
|
||||
<span style="margin-left: 10px; color: #666;">展示时间: ${group.display_seconds}秒</span>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-danger" onclick="deleteGroup(${group.id})">删除</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(groupItem);
|
||||
});
|
||||
}
|
||||
|
||||
function updateGroupSelect(groups) {
|
||||
const select = document.getElementById('groupSelect');
|
||||
select.innerHTML = '<option value="">请选择分组</option>';
|
||||
|
||||
groups.forEach(group => {
|
||||
const option = document.createElement('option');
|
||||
option.value = group.id;
|
||||
option.textContent = group.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function addGroup() {
|
||||
const name = prompt('请输入分组名称:');
|
||||
if (!name) return;
|
||||
|
||||
const displaySeconds = prompt('请输入展示时间(秒):', '30');
|
||||
if (!displaySeconds) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/groups', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ name, display_seconds: parseInt(displaySeconds) })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadGroups();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加分组失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteGroup(id) {
|
||||
if (!confirm('确定要删除这个分组吗?')) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`/api/groups/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
loadGroups();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除分组失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileSelect(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
selectedFile = file;
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
const cropImage = document.getElementById('cropImage');
|
||||
cropImage.src = e.target.result;
|
||||
|
||||
document.getElementById('cropModal').style.display = 'block';
|
||||
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
}
|
||||
|
||||
cropper = new Cropper(cropImage, {
|
||||
aspectRatio: 1,
|
||||
viewMode: 1,
|
||||
autoCropArea: 1,
|
||||
ready: function() {
|
||||
// 裁剪器就绪
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
function closeCropModal() {
|
||||
document.getElementById('cropModal').style.display = 'none';
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
cropper = null;
|
||||
}
|
||||
selectedFile = null;
|
||||
}
|
||||
|
||||
async function cropAndUpload() {
|
||||
if (!cropper || !selectedFile) return;
|
||||
|
||||
const groupId = document.getElementById('groupSelect').value;
|
||||
if (!groupId) {
|
||||
alert('请先选择分组');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取裁剪后的canvas
|
||||
const canvas = cropper.getCroppedCanvas({
|
||||
width: 280,
|
||||
height: 280
|
||||
});
|
||||
|
||||
// 压缩图片
|
||||
const compressedBlob = await compressImage(canvas, 0.7);
|
||||
|
||||
// 创建FormData
|
||||
const formData = new FormData();
|
||||
formData.append('image', compressedBlob, selectedFile.name);
|
||||
formData.append('group_id', groupId);
|
||||
|
||||
// 上传
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
closeCropModal();
|
||||
loadImages(groupId);
|
||||
alert('上传成功');
|
||||
} else {
|
||||
alert('上传失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
alert('上传失败');
|
||||
}
|
||||
}
|
||||
|
||||
function compressImage(canvas, quality) {
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
}, 'image/jpeg', quality);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadImages(groupId) {
|
||||
if (!groupId) {
|
||||
document.getElementById('imageList').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/images?group_id=${groupId}`);
|
||||
const images = await response.json();
|
||||
renderImages(images);
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImages(images) {
|
||||
const container = document.getElementById('imageList');
|
||||
container.innerHTML = '';
|
||||
|
||||
images.forEach(image => {
|
||||
const imageCard = document.createElement('div');
|
||||
imageCard.className = `image-card ${image.enabled ? '' : 'disabled'}`;
|
||||
imageCard.innerHTML = `
|
||||
<img src="/uploads/${image.filename}" alt="${image.original_name}">
|
||||
<div class="actions">
|
||||
<button class="btn btn-success" onclick="toggleImage(${image.id}, ${image.enabled ? 0 : 1})">
|
||||
${image.enabled ? '下架' : '上架'}
|
||||
</button>
|
||||
<button class="btn btn-danger" onclick="deleteImage(${image.id})">删除</button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(imageCard);
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleImage(id, enable) {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const endpoint = enable ? 'enable' : 'disable';
|
||||
const response = await fetch(`/api/images/${id}/${endpoint}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const groupId = document.getElementById('groupSelect').value;
|
||||
loadImages(groupId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新图片状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteImage(id) {
|
||||
if (!confirm('确定要删除这张图片吗?')) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
const response = await fetch(`/api/images/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const groupId = document.getElementById('groupSelect').value;
|
||||
loadImages(groupId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除图片失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 分组选择变化时加载图片
|
||||
document.getElementById('groupSelect').addEventListener('change', function() {
|
||||
loadImages(this.value);
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadDisplayData();
|
||||
});
|
||||
|
||||
let currentGroupIndex = 0;
|
||||
let groups = [];
|
||||
let displayTimer = null;
|
||||
|
||||
async function loadDisplayData() {
|
||||
try {
|
||||
const response = await fetch('/api/display');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.enabled) {
|
||||
document.getElementById('feature-disabled').style.display = 'block';
|
||||
document.getElementById('image-display').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('feature-disabled').style.display = 'none';
|
||||
document.getElementById('image-display').style.display = 'block';
|
||||
|
||||
groups = data.groups;
|
||||
if (groups.length === 0) {
|
||||
document.getElementById('group-name').textContent = '暂无分组';
|
||||
return;
|
||||
}
|
||||
|
||||
showGroup(0);
|
||||
startRotation();
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showGroup(index) {
|
||||
if (index >= groups.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
currentGroupIndex = index;
|
||||
const group = groups[index];
|
||||
|
||||
document.getElementById('group-name').textContent = group.name;
|
||||
|
||||
const grid = document.getElementById('image-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
group.images.forEach(image => {
|
||||
const imageItem = document.createElement('div');
|
||||
imageItem.className = 'image-item';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = `/uploads/${image.filename}`;
|
||||
img.alt = image.original_name;
|
||||
img.loading = 'lazy';
|
||||
|
||||
// 长按保存功能
|
||||
let pressTimer;
|
||||
img.addEventListener('touchstart', function(e) {
|
||||
pressTimer = setTimeout(function() {
|
||||
saveImage(`/uploads/${image.filename}`, image.original_name);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
img.addEventListener('touchend', function() {
|
||||
clearTimeout(pressTimer);
|
||||
});
|
||||
|
||||
img.addEventListener('touchmove', function() {
|
||||
clearTimeout(pressTimer);
|
||||
});
|
||||
|
||||
imageItem.appendChild(img);
|
||||
grid.appendChild(imageItem);
|
||||
});
|
||||
|
||||
// 更新倒计时
|
||||
updateTimer(group.display_seconds);
|
||||
}
|
||||
|
||||
function updateTimer(seconds) {
|
||||
const timerElement = document.getElementById('group-timer');
|
||||
let remaining = seconds;
|
||||
|
||||
if (displayTimer) {
|
||||
clearInterval(displayTimer);
|
||||
}
|
||||
|
||||
timerElement.textContent = `剩余时间: ${remaining}秒`;
|
||||
|
||||
displayTimer = setInterval(function() {
|
||||
remaining--;
|
||||
if (remaining <= 0) {
|
||||
clearInterval(displayTimer);
|
||||
showGroup(currentGroupIndex + 1);
|
||||
return;
|
||||
}
|
||||
timerElement.textContent = `剩余时间: ${remaining}秒`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function startRotation() {
|
||||
if (groups.length <= 1) return;
|
||||
|
||||
// 自动轮播已在updateTimer中处理
|
||||
}
|
||||
|
||||
function saveImage(url, filename) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename || 'image.jpg';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const errorDiv = document.getElementById('error');
|
||||
|
||||
loginForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
window.location.href = '/admin';
|
||||
} else {
|
||||
errorDiv.textContent = data.error;
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
errorDiv.textContent = '登录失败,请重试';
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
<!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>
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>图片资源管理</h1>
|
||||
<nav>
|
||||
<a href="/">首页</a>
|
||||
<a href="/login">登录</a>
|
||||
<a href="/admin">管理</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<div class="login-form">
|
||||
<h2>管理员登录</h2>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn" style="width: 100%;">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
+9
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* Cropper.js v1.5.13
|
||||
* https://fengyuanchen.github.io/cropperjs
|
||||
*
|
||||
* Copyright 2015-present Chen Fengyuan
|
||||
* Released under the MIT license
|
||||
*
|
||||
* Date: 2022-11-20T05:30:43.444Z
|
||||
*/.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}
|
||||
+10
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user