592 lines
17 KiB
HTML
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>
|