Files
2026-07-19 21:57:40 +08:00

592 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, 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>