console
<!DOCTYPE html>
<html>
<head>
<title>管理系统</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/xlsx.full.min.js"></script>
<style>
:root {
--card-width: 180px;
--card-height: 300px;
}
body {
margin: 0;
font-family: Arial;
display: grid;
grid-template-columns: 200px 1fr;
min-height: 100vh;
}
.nav {
background: #f5f5f5;
padding: 20px;
border-right: 1px solid #ddd;
position: relative;
}
.category-list {
list-style: none;
padding: 0;
margin-top: 20px;
}
.category-item {
padding: 10px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.category-item.active {
background: #2196F3;
color: white;
}
.main {
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, var(--card-width));
gap: 20px;
justify-content: center;
align-content: start;
}
.card {
width: var(--card-width);
height: var(--card-height);
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
position: relative;
transition: 0.3s;
background: white;
}
.upload-card {
width: var(--card-width);
height: var(--card-height);
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: 0.3s;
}
.card {
width: var(--card-width);
height: var(--card-height);
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
position: relative;
transition: 0.3s;
background: white;
display: flex;
flex-direction: column;
}
.card-body {
padding: 10px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.card-title {
font-weight: bold;
font-size: 16px;
width: 100%;
height: 70px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-actions {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 10px 5px;
display: flex;
justify-content: space-around;
background: rgba(255,255,255,0.95);
border-top: 1px solid #eee;
height: 10px;
align-items: center;
}
.card-actions button {
width: 28px;
height: 28px;
flex-shrink: 0;
}
.card-actions button i {
font-size: 18px;
}
.card:hover .card-actions {
opacity: 1;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
width: 90%;
max-width: 500px;
}
.modal-input-group {
margin: 10px 0;
}
.modal-input-group input {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
#cropper-container {
width: 100%;
height: 400px;
margin: 10px 0;
}
button {
padding: 0;
border: none;
border-radius: 4px;
cursor: pointer;
background: transparent;
margin: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
button i {
font-size: 16px;
color: #666;
}
button:hover i {
color: #2196F3;
}
.card.dragging {
opacity: 0.5;
transform: rotate(3deg);
transition: none;
}
.card.ghost {
background: #f5f5f5;
border: 2px dashed #2196F3;
}
.confirm-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
z-index: 2000;
}
.confirm-buttons {
margin-top: 15px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.modal-actions {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.modal-btn {
flex: 1;
max-width: 150px;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
border: none;
cursor: pointer;
}
.primary {
background: #2196F3;
color: white;
}
.secondary {
background: #666;
color: white;
}
</style>
</style>
</head>
<body>
<div class="nav">
<button onclick="showAddCategoryModal()" style="width:auto;padding:8px 16px;background:#f88888;color:white">
+ 添加类目
</button>
<button onclick="exportCards()" style="margin-top:10px;width:auto;padding:8px 16px;background:#f88888;color:white">
⇩ 导出全部
</button>
<ul class="category-list" id="categoryList"></ul>
</div>
<div class="main" id="mainContent"></div>
<div class="modal" id="cropModal">
<div class="modal-content">
<div id="cropper-container"></div>
<div class="modal-input-group">
<input type="text" id="cardName" placeholder="卡片名称" required>
</div>
<div class="modal-input-group">
<input type="text" id="cardPath" placeholder="本地路径">
</div>
<div class="modal-input-group">
<input type="url" id="cardUrl" placeholder="网址 (可选)">
</div>
<div class="modal-actions">
<button onclick="confirmCrop()" class="modal-btn primary">确认创建</button>
<button onclick="closeModal()" class="modal-btn secondary">取消</button>
</div>
</div>
<div class="modal" id="editModal">
<div class="modal-content">
<div id="edit-cropper-container"></div>
<div class="modal-input-group">
<input type="text" id="editCardName" placeholder="卡片名称" required>
</div>
<div class="modal-input-group">
<input type="text" id="editCardPath" placeholder="本地路径">
</div>
<div class="modal-input-group">
<input type="url" id="editCardUrl" placeholder="网址 (可选)">
</div>
<button onclick="confirmEdit()" style="width:auto;padding:8px 16px;background:#2196F3;color:white">保存修改</button>
<button onclick="closeModal()" style="background: #666;width:auto;padding:8px 16px;color:white">取消</button>
</div>
</div>
<div class="modal" id="categoryModal">
<div class="modal-content">
<div class="modal-input-group">
<input type="text" id="newCategoryName" placeholder="分类名称" required>
</div>
<div class="modal-actions">
<button onclick="addCategory()" class="modal-btn primary">添加分类</button>
<button onclick="closeModal()" class="modal-btn secondary">取消</button>
</div>
</div>
<input type="file" id="fileInput" hidden accept="image/*">
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
<script>
let data = {
categories: [],
currentCategoryId: null
};
function loadData() {
const savedData = localStorage.getItem('cardData');
if (savedData) {
try {
data = JSON.parse(savedData);
if (!data.currentCategoryId && data.categories.length > 0) {
data.currentCategoryId = data.categories[0].id;
}
} catch (e) {
initDefaultData();
}
} else {
initDefaultData();
}
}
function initDefaultData() {
const allCategory = {
id: 0,
name: '全部',
cards: [],
isDefault: true
};
const defaultCategory = {
id: Date.now(),
name: 'S级',
cards: []
};
data.categories = [allCategory, defaultCategory];
data.currentCategoryId = allCategory.id;
saveData();
}
function saveData() {
localStorage.setItem('cardData', JSON.stringify(data));
}
function renderCategories() {
const list = document.getElementById('categoryList');
list.innerHTML = data.categories.map(cat => `
<li class="category-item ${cat.id === data.currentCategoryId ? 'active' : ''}"
onclick="switchCategory(${cat.id})">
<span>${cat.name}</span>
${!cat.isDefault ? `<button onclick="event.stopPropagation();deleteCategory(${cat.id})"
style="padding:2px 5px;font-size:0.8em">×</button>` : ''}
</li>
`).join('');
}
function switchCategory(id) {
data.currentCategoryId = id;
saveData();
renderCategories();
renderCards();
}
function showAddCategoryModal() {
document.getElementById('categoryModal').style.display = 'flex';
}
function addCategory() {
const nameInput = document.getElementById('newCategoryName');
const newCategory = {
id: Date.now(),
name: nameInput.value,
cards: []
};
data.categories.push(newCategory);
saveData();
nameInput.value = '';
renderCategories();
closeModal();
}
function deleteCategory(id) {
if (!confirm(`确定要删除该分类吗?\n(分类内的卡片不会被删除)`)) return;
data.categories = data.categories.filter(cat => cat.id !== id);
if (data.currentCategoryId === id) {
data.currentCategoryId = data.categories[0]?.id || null;
}
saveData();
renderCategories();
renderCards();
}
function renderCards() {
const main = document.getElementById('mainContent');
const currentCategory = data.categories.find(c => c.id === data.currentCategoryId);
main.innerHTML = `
${currentCategory.isDefault ? '' : `
<div class="upload-card" onclick="document.getElementById('fileInput').click()">
+ 上传新卡片
</div>`}
${currentCategory.cards.map(card => `
<div class="card" data-id="${card.id}">
<img src="${card.image}" class="card-image">
<div class="card-body">
<div class="card-title">${card.name}</div>
</div>
<div class="card-actions">
${card.path ? `<button onclick="openFolder('${escapePath(card.path)}')">
<i class="fas fa-folder-open"></i>
</button>` : ''}
${card.url ? `<button onclick="window.open('${card.url}', '_blank')">
<i class="fas fa-external-link-alt"></i>
</button>` : ''}
<!-- 分离编辑按钮 -->
<button onclick="openEditModal(${card.id})">
<i class="fas fa-edit"></i>
</button>
<button onclick="deleteCard(${card.id})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('')}
`;
initSortable();
}
let sortable = null;
let sortableInstances = [];
function initSortable() {
sortableInstances.forEach(instance => instance.destroy());
sortableInstances = [];
const container = document.getElementById('mainContent');
const currentCategoryId = data.currentCategoryId;
const sortable = new Sortable(container, {
animation: 150,
handle: '.card',
ghostClass: 'ghost',
chosenClass: 'dragging',
delay: 300,
forceFallback: true,
onEnd: function(evt) {
const currentCategory = data.categories.find(c => c.id === currentCategoryId);
const [removed] = currentCategory.cards.splice(evt.oldIndex, 1);
currentCategory.cards.splice(evt.newIndex, 0, removed);
saveData();
}
});
sortableInstances.push(sortable);
}
function escapePath(path) {
const escaped = path.replace(/\\/g, '\\\\');
return JSON.stringify(escaped).replace(/^"|"$/g, '');
}
function openFolder(path) {
const decodedPath = path.replace(/\\\\/g, '\\');
try {
navigator.clipboard.writeText(decodedPath).then(() => {
alert(`✅ 正确路径已复制:\n${decodedPath}`);
}).catch(() => {
copyViaFallback(decodedPath);
});
} catch (e) {
copyViaFallback(decodedPath);
}
function copyViaFallback(path) {
const textarea = document.createElement('textarea');
textarea.value = path;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert(`✅ 路径已复制:\n${path}`);
}
}
document.getElementById('fileInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
initCropper(e.target.result);
}
reader.readAsDataURL(file);
}
});
let cropper = null;
function initCropper(imageSrc) {
document.getElementById('cropModal').style.display = 'flex';
const container = document.getElementById('cropper-container');
container.innerHTML = `<img src="${imageSrc}" style="max-width: 100%">`;
cropper = new Cropper(container.querySelector('img'), {
aspectRatio: 3/4,
viewMode: 1,
guides: false
});
}
function confirmCrop() {
const canvas = cropper.getCroppedCanvas({
width: 180,
height: 240
});
const newCard = {
id: Date.now(),
image: canvas.toDataURL(),
name: document.getElementById('cardName').value,
path: document.getElementById('cardPath').value,
url: document.getElementById('cardUrl').value
};
const currentCategory = data.categories.find(c => c.id === data.currentCategoryId);
const allCategory = data.categories.find(c => c.isDefault);
if (currentCategory) currentCategory.cards.push(newCard);
if (allCategory) allCategory.cards.push(newCard);
saveData();
renderCards();
closeModal();
document.getElementById('cardName').value = '';
document.getElementById('cardPath').value = '';
document.getElementById('cardUrl').value = '';
}
function getAllCards() {
return data.categories.flatMap(c => c.cards);
}
function deleteCard(cardId) {
if (!confirm('确定要永久删除该卡片吗?')) return;
const currentCategory = data.categories.find(c => c.id === data.currentCategoryId);
currentCategory.cards = currentCategory.cards.filter(c => c.id !== cardId);
data.categories.forEach(cat => {
if (!cat.isDefault) {
cat.cards = cat.cards.filter(c => c.id !== cardId);
}
});
saveData();
renderCards();
}
function showCropModal() {
document.getElementById('cropModal').style.display = 'flex';
initModalEvents();
}
function editCard(cardId) {
editingCardId = cardId;
const card = getAllCards().find(c => c.id === cardId);
document.getElementById('cropModal').style.display = 'flex';
document.getElementById('cardName').value = card.name;
document.getElementById('cardPath').value = card.path || '';
document.getElementById('cardUrl').value = card.url || '';
const container = document.getElementById('cropper-container');
container.innerHTML = `<img src="${card.image}" style="max-width: 100%">`;
cropper = new Cropper(container.querySelector('img'), {
aspectRatio: 3/4,
viewMode: 1
});
}
function closeModal() {
document.querySelectorAll('.modal').forEach(m => m.style.display = 'none');
if (cropper) {
cropper.destroy();
cropper = null;
}
if (editCropper) {
editCropper.destroy();
editCropper = null;
}
editingCardId = null;
}
let editCropper = null;
let editingCardId = null;
function openEditModal(cardId) {
editingCardId = cardId;
const card = data.categories.flatMap(c => c.cards).find(c => c.id === cardId);
document.getElementById('editModal').style.display = 'flex';
document.getElementById('editCardName').value = card.name;
document.getElementById('editCardPath').value = card.path || '';
document.getElementById('editCardUrl').value = card.url || '';
const container = document.getElementById('edit-cropper-container');
container.innerHTML = `<img src="${card.image}" style="max-width: 100%">`;
editCropper = new Cropper(container.querySelector('img'), {
aspectRatio: 3/4,
viewMode: 1
});
}
function confirmEdit() {
const canvas = editCropper.getCroppedCanvas({
width: 180,
height: 240
});
const updatedData = {
id: editingCardId,
image: canvas.toDataURL(),
name: document.getElementById('editCardName').value,
path: document.getElementById('editCardPath').value,
url: document.getElementById('editCardUrl').value
};
data.categories.forEach(category => {
const index = category.cards.findIndex(c => c.id === editingCardId);
if (index !== -1) {
category.cards[index] = updatedData;
}
});
saveData();
renderCards();
closeModal();
}
loadData();
renderCategories();
renderCards();
</script>
</body>
</html>