20260703173210

This commit is contained in:
oneao committed 2026-07-03 17:32:11 +08:00
1 parent d0f7f0d3d8
commit 3556d5e8c1
15 files changed
+853 -143

No files matched your search

+536
View File
@@ -0,0 +1,536 @@
<!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>