536 lines
15 KiB
HTML
536 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>分类与标签</title>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<style>
|
|
:root {
|
|
--primary-color: #8b5cf6;
|
|
--primary-light: #ede9fe;
|
|
--bg-color: #f4f5f7;
|
|
--card-color: rgba(255, 255, 255, 0.85);
|
|
--text-primary: #1f2937;
|
|
--text-secondary: #6b7280;
|
|
--danger-color: #ef4444;
|
|
--radius-lg: 20px;
|
|
--radius-md: 14px;
|
|
--shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-primary);
|
|
padding-bottom: 100px;
|
|
}
|
|
|
|
.app-container {
|
|
max-width: 480px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
padding: 60px 20px 20px;
|
|
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
|
color: white;
|
|
border-radius: 0 0 30px 30px;
|
|
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.3);
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.header p {
|
|
font-size: 15px;
|
|
opacity: 0.9;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Content */
|
|
.content {
|
|
padding: 20px;
|
|
}
|
|
|
|
/* Section Divider */
|
|
.section-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin: 10px 0 10px 5px;
|
|
}
|
|
|
|
/* Card */
|
|
.card {
|
|
background: var(--card-color);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: var(--radius-lg);
|
|
padding: 8px 16px;
|
|
margin-bottom: 16px;
|
|
box-shadow: var(--shadow);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
/* List Item */
|
|
.list-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 16px 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.list-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.list-item .icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 14px;
|
|
font-size: 18px;
|
|
background: var(--primary-light);
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.list-item .info {
|
|
flex: 1;
|
|
}
|
|
|
|
.list-item .title {
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.list-item .subtitle {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.list-item .actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 10px;
|
|
border: none;
|
|
background: #f3f4f6;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-icon:active {
|
|
transform: scale(0.95);
|
|
background: #e5e7eb;
|
|
}
|
|
|
|
.btn-icon.delete {
|
|
color: var(--danger-color);
|
|
}
|
|
|
|
/* Tree View */
|
|
.tree-item {
|
|
padding: 14px 0;
|
|
}
|
|
|
|
.tree-parent {
|
|
display: flex;
|
|
align-items: center;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.tree-children {
|
|
padding-left: 30px;
|
|
margin-top: 8px;
|
|
border-left: 2px dashed #e0e0e0;
|
|
}
|
|
|
|
.tree-child {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 0;
|
|
font-size: 15px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.tree-child::before {
|
|
content: "";
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #d1d5db;
|
|
margin-right: 12px;
|
|
}
|
|
|
|
/* FAB */
|
|
.fab {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 20px;
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
|
color: white;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.4);
|
|
transition: all 0.3s;
|
|
z-index: 50;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.fab:hover {
|
|
transform: scale(1.05) rotate(90deg);
|
|
}
|
|
|
|
/* Modal */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(5px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 200;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.modal-overlay.show {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.modal {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
padding: 24px;
|
|
width: 90%;
|
|
max-width: 360px;
|
|
box-shadow: var(--shadow);
|
|
transform: translateY(20px);
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.modal-overlay.show .modal {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.modal-header {
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.form-control {
|
|
width: 100%;
|
|
padding: 14px 16px;
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: var(--radius-md);
|
|
font-size: 16px;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.form-control:focus {
|
|
outline: none;
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.btn {
|
|
flex: 1;
|
|
padding: 14px;
|
|
border-radius: var(--radius-md);
|
|
border: none;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #f3f4f6;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 30px 20px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state i {
|
|
font-size: 40px;
|
|
margin-bottom: 12px;
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="app-container">
|
|
<header class="header">
|
|
<h1>分类与标签</h1>
|
|
<p>统一管理您的内容组织方式</p>
|
|
</header>
|
|
|
|
<main class="content">
|
|
<!-- 分类管理模块 -->
|
|
<div class="section-title">分类管理</div>
|
|
<div class="card">
|
|
<div id="categoryList"></div>
|
|
</div>
|
|
|
|
<!-- 标签管理模块 -->
|
|
<div class="section-title">标签管理 (支持父子级)</div>
|
|
<div class="card">
|
|
<div id="tagTree"></div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- FAB -->
|
|
<button class="fab" id="fabBtn"><i class="fas fa-plus"></i></button>
|
|
|
|
<!-- Modal -->
|
|
<div class="modal-overlay" id="modalOverlay">
|
|
<div class="modal">
|
|
<div class="modal-header" id="modalTitle">添加</div>
|
|
<div class="form-group">
|
|
<label for="inputName">名称</label>
|
|
<input type="text" id="inputName" class="form-control" placeholder="请输入名称">
|
|
</div>
|
|
<div class="form-group" id="parentGroup" style="display: none;">
|
|
<label for="selectParent">父级标签</label>
|
|
<select id="selectParent" class="form-control">
|
|
<option value="">无 (设为顶级)</option>
|
|
</select>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" id="cancelBtn">取消</button>
|
|
<button class="btn btn-primary" id="saveBtn">保存</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let categories = [{ id: 1, name: "💼 工作" }, { id: 2, name: "🏠 生活" }, { id: 3, name: "📚 学习" }];
|
|
let tags = [
|
|
{ id: 1, name: "🚀 项目", parentId: null },
|
|
{ id: 2, name: "前端", parentId: 1 },
|
|
{ id: 3, name: "后端", parentId: 1 },
|
|
{ id: 4, name: "🍎 健康", parentId: null },
|
|
{ id: 5, name: "健身", parentId: 4 }
|
|
];
|
|
|
|
let currentMode = 'category';
|
|
let editId = null;
|
|
|
|
const elements = {
|
|
categoryList: document.getElementById('categoryList'),
|
|
tagTree: document.getElementById('tagTree'),
|
|
fabBtn: document.getElementById('fabBtn'),
|
|
modal: document.getElementById('modalOverlay'),
|
|
modalTitle: document.getElementById('modalTitle'),
|
|
inputName: document.getElementById('inputName'),
|
|
parentGroup: document.getElementById('parentGroup'),
|
|
selectParent: document.getElementById('selectParent'),
|
|
cancelBtn: document.getElementById('cancelBtn'),
|
|
saveBtn: document.getElementById('saveBtn')
|
|
};
|
|
|
|
function renderCategories() {
|
|
if (categories.length === 0) {
|
|
elements.categoryList.innerHTML = `<div class="empty-state"><i class="far fa-folder-open"></i><p>暂无分类</p></div>`;
|
|
return;
|
|
}
|
|
elements.categoryList.innerHTML = categories.map(cat => `
|
|
<div class="list-item">
|
|
<div class="icon"><i class="fas fa-folder"></i></div>
|
|
<div class="info"><div class="title">${cat.name}</div></div>
|
|
<div class="actions">
|
|
<button class="btn-icon" onclick="editCategory(${cat.id})"><i class="fas fa-pen"></i></button>
|
|
<button class="btn-icon delete" onclick="deleteCategory(${cat.id})"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderTags() {
|
|
const parents = tags.filter(t => t.parentId === null);
|
|
if (parents.length === 0) {
|
|
elements.tagTree.innerHTML = `<div class="empty-state"><i class="fas fa-tag"></i><p>暂无标签</p></div>`;
|
|
return;
|
|
}
|
|
elements.tagTree.innerHTML = parents.map(parent => {
|
|
const children = tags.filter(t => t.parentId === parent.id);
|
|
return `
|
|
<div class="tree-item">
|
|
<div class="tree-parent list-item">
|
|
<div class="icon"><i class="fas fa-tag"></i></div>
|
|
<div class="info"><div class="title">${parent.name}</div></div>
|
|
<div class="actions">
|
|
<button class="btn-icon" onclick="editTag(${parent.id})"><i class="fas fa-pen"></i></button>
|
|
<button class="btn-icon delete" onclick="deleteTag(${parent.id})"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
${children.map(child => `
|
|
<div class="tree-children">
|
|
<div class="tree-child list-item">
|
|
<div class="info"><div class="title">${child.name}</div></div>
|
|
<div class="actions">
|
|
<button class="btn-icon" onclick="editTag(${child.id})"><i class="fas fa-pen"></i></button>
|
|
<button class="btn-icon delete" onclick="deleteTag(${child.id})"><i class="fas fa-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function updateParentOptions() {
|
|
elements.selectParent.innerHTML = '<option value="">无 (设为顶级)</option>' +
|
|
tags.filter(t => t.parentId === null)
|
|
.map(t => `<option value="${t.id}">${t.name}</option>`).join('');
|
|
}
|
|
|
|
function openModal(mode, id = null) {
|
|
currentMode = mode;
|
|
editId = id;
|
|
elements.inputName.value = '';
|
|
elements.parentGroup.style.display = 'none';
|
|
|
|
if (mode === 'category') {
|
|
elements.modalTitle.textContent = id ? '编辑分类' : '新建分类';
|
|
if (id) { const cat = categories.find(c => c.id === id); if (cat) elements.inputName.value = cat.name; }
|
|
} else {
|
|
elements.modalTitle.textContent = id ? '编辑标签' : '新建标签';
|
|
elements.parentGroup.style.display = 'block';
|
|
updateParentOptions();
|
|
if (id) {
|
|
const tag = tags.find(t => t.id === id);
|
|
if (tag) {
|
|
elements.inputName.value = tag.name;
|
|
elements.selectParent.value = tag.parentId || '';
|
|
}
|
|
}
|
|
}
|
|
elements.modal.classList.add('show');
|
|
elements.inputName.focus();
|
|
}
|
|
|
|
function closeModal() { elements.modal.classList.remove('show'); }
|
|
|
|
function saveData() {
|
|
const name = elements.inputName.value.trim();
|
|
if (!name) { alert('名称不能为空'); return; }
|
|
|
|
if (currentMode === 'category') {
|
|
if (editId) { const cat = categories.find(c => c.id === editId); if (cat) cat.name = name; }
|
|
else { categories.push({ id: Date.now(), name }); }
|
|
renderCategories();
|
|
} else {
|
|
const parentId = elements.selectParent.value ? parseInt(elements.selectParent.value) : null;
|
|
if (editId) { const tag = tags.find(t => t.id === editId); if (tag) { tag.name = name; tag.parentId = parentId; } }
|
|
else { tags.push({ id: Date.now(), name, parentId }); }
|
|
renderTags();
|
|
}
|
|
closeModal();
|
|
}
|
|
|
|
// Global handlers
|
|
window.editCategory = (id) => openModal('category', id);
|
|
window.deleteCategory = (id) => {
|
|
if (confirm('确定删除?')) { categories = categories.filter(c => c.id !== id); renderCategories(); }
|
|
};
|
|
window.editTag = (id) => openModal('tag', id);
|
|
window.deleteTag = (id) => {
|
|
if (confirm('确定删除此标签及子标签?')) {
|
|
const idsToDelete = [id];
|
|
function findChildren(pid) { tags.filter(t => t.parentId === pid).forEach(child => { idsToDelete.push(child.id); findChildren(child.id); }); }
|
|
findChildren(id);
|
|
tags = tags.filter(t => !idsToDelete.includes(t.id));
|
|
renderTags();
|
|
}
|
|
};
|
|
|
|
// Events
|
|
elements.fabBtn.addEventListener('click', () => {
|
|
// 智能判断:如果分类为空,默认添加分类;否则弹出选择器或默认添加标签
|
|
// 为了简单直观,这里我们直接让FAB默认打开“添加标签”的窗口,因为标签更复杂
|
|
// 或者您可以改为打开一个Action Sheet
|
|
openModal('tag');
|
|
});
|
|
|
|
// 改进:长按FAB添加分类(可选交互)
|
|
let pressTimer;
|
|
elements.fabBtn.addEventListener('touchstart', () => {
|
|
pressTimer = setTimeout(() => openModal('category'), 500);
|
|
});
|
|
elements.fabBtn.addEventListener('touchend', () => clearTimeout(pressTimer));
|
|
|
|
elements.cancelBtn.addEventListener('click', closeModal);
|
|
elements.saveBtn.addEventListener('click', saveData);
|
|
elements.modal.addEventListener('click', (e) => { if (e.target === elements.modal) closeModal(); });
|
|
|
|
// Init
|
|
renderCategories();
|
|
renderTags();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |