feat: 所有页面支持1到4层级的嵌套结构
This commit is contained in:
199
assets/style.css
199
assets/style.css
@@ -892,6 +892,205 @@ body .content.expanded {
|
|||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 多层级嵌套样式 */
|
||||||
|
/* 层级2: 子分类 */
|
||||||
|
.category-level-2 {
|
||||||
|
margin: 1.5rem 0 1.5rem 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: linear-gradient(145deg,
|
||||||
|
rgba(var(--card-bg-rgb), 0.8),
|
||||||
|
rgba(var(--card-bg-rgb), 0.6));
|
||||||
|
border-left: 3px solid var(--accent-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: calc(100% - 3.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-level-2 .category-header h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-bright);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-level-2 .category-header h3 i {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 层级3: 分组 */
|
||||||
|
.group-level-3, .category-level-3 {
|
||||||
|
margin: 1rem 0 1rem 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(var(--card-bg-rgb), 0.4);
|
||||||
|
border-left: 2px solid var(--secondary-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: calc(100% - 2.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-level-3 .group-header h4,
|
||||||
|
.category-level-3 .category-header h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
color: var(--text-bright);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-level-3 .group-header h4 i,
|
||||||
|
.category-level-3 .category-header h4 i {
|
||||||
|
color: var(--secondary-color);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 层级4: 更深层次的分组 */
|
||||||
|
.group-level-4, .category-level-4 {
|
||||||
|
margin: 0.8rem 0 0.8rem 1rem;
|
||||||
|
padding: 0.8rem;
|
||||||
|
background: rgba(var(--card-bg-rgb), 0.2);
|
||||||
|
border-left: 1px solid var(--tertiary-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
width: calc(100% - 1.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-level-4 .group-header h5,
|
||||||
|
.category-level-4 .category-header h5 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
color: var(--text-bright);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-level-4 .group-header h5 i,
|
||||||
|
.category-level-4 .category-header h5 i {
|
||||||
|
color: var(--tertiary-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 切换图标样式 */
|
||||||
|
.toggle-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: auto;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header:hover .toggle-icon,
|
||||||
|
.group-header:hover .toggle-icon {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 展开/折叠动画 */
|
||||||
|
.category-content, .group-content {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||||
|
max-height: 9999px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category.collapsed .category-content,
|
||||||
|
.group.collapsed .group-content {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category.collapsed .toggle-icon i,
|
||||||
|
.group.collapsed .toggle-icon i {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空内容提示 */
|
||||||
|
.empty-content {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子容器样式 */
|
||||||
|
.subcategories-container,
|
||||||
|
.groups-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保嵌套的网站网格正确显示 */
|
||||||
|
.category-level-2 .sites-grid,
|
||||||
|
.group-level-3 .sites-grid,
|
||||||
|
.category-level-3 .sites-grid,
|
||||||
|
.group-level-4 .sites-grid,
|
||||||
|
.category-level-4 .sites-grid {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
gap: 0.8rem;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 - 嵌套结构 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.category-level-2 {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-level-3, .category-level-3 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.8rem;
|
||||||
|
width: calc(100% - 1.3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-level-4, .category-level-4 {
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
width: calc(100% - 0.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-level-2 .sites-grid,
|
||||||
|
.group-level-3 .sites-grid,
|
||||||
|
.category-level-3 .sites-grid,
|
||||||
|
.group-level-4 .sites-grid,
|
||||||
|
.category-level-4 .sites-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.category {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-level-2, .group-level-3, .category-level-3 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.8rem;
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-level-4, .category-level-4 {
|
||||||
|
margin-left: 0.3rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
width: calc(100% - 0.6rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-level-2 .sites-grid,
|
||||||
|
.group-level-3 .sites-grid,
|
||||||
|
.category-level-3 .sites-grid,
|
||||||
|
.group-level-4 .sites-grid,
|
||||||
|
.category-level-4 .sites-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 网站卡片网格 */
|
/* 网站卡片网格 */
|
||||||
.sites-grid {
|
.sites-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
313
config/_default/pages/bookmarks-four-level.yml
Normal file
313
config/_default/pages/bookmarks-four-level.yml
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
# 四层级嵌套书签配置示例
|
||||||
|
# 展示分类 -> 子分类 -> 分组 -> 网站的四层结构
|
||||||
|
# 说明:该页面通常由“书签导入工具”自动生成,手工修改时请保持字段结构一致。
|
||||||
|
title: 我的书签
|
||||||
|
subtitle: 从浏览器导入的书签收藏
|
||||||
|
|
||||||
|
# 指定使用的模板文件名,现有页面模板可见 templates/pages(不含 .hbs)
|
||||||
|
template: bookmarks
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- name: '技术资源'
|
||||||
|
icon: 'fas fa-laptop-code'
|
||||||
|
subcategories:
|
||||||
|
- name: '前端开发'
|
||||||
|
icon: 'fas fa-code'
|
||||||
|
groups:
|
||||||
|
- name: '框架库'
|
||||||
|
icon: 'fas fa-cube'
|
||||||
|
sites:
|
||||||
|
- name: 'React'
|
||||||
|
url: 'https://reactjs.org/'
|
||||||
|
icon: 'fab fa-react'
|
||||||
|
description: 'React官方文档'
|
||||||
|
- name: 'Vue.js'
|
||||||
|
url: 'https://vuejs.org/'
|
||||||
|
icon: 'fab fa-vuejs'
|
||||||
|
description: 'Vue.js官方文档'
|
||||||
|
- name: 'Angular'
|
||||||
|
url: 'https://angular.io/'
|
||||||
|
icon: 'fab fa-angular'
|
||||||
|
description: 'Angular官方文档'
|
||||||
|
- name: 'Svelte'
|
||||||
|
url: 'https://svelte.dev/'
|
||||||
|
icon: 'fas fa-fire'
|
||||||
|
description: 'Svelte官方文档'
|
||||||
|
- name: '状态管理'
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
sites:
|
||||||
|
- name: 'Redux'
|
||||||
|
url: 'https://redux.js.org/'
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
description: 'Redux状态管理'
|
||||||
|
- name: 'Vuex'
|
||||||
|
url: 'https://vuex.vuejs.org/'
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
description: 'Vue状态管理'
|
||||||
|
- name: 'MobX'
|
||||||
|
url: 'https://mobx.js.org/'
|
||||||
|
icon: 'fas fa-react'
|
||||||
|
description: '响应式状态管理'
|
||||||
|
- name: '构建工具'
|
||||||
|
icon: 'fas fa-tools'
|
||||||
|
sites:
|
||||||
|
- name: 'Webpack'
|
||||||
|
url: 'https://webpack.js.org/'
|
||||||
|
icon: 'fas fa-cube'
|
||||||
|
description: '模块打包工具'
|
||||||
|
- name: 'Vite'
|
||||||
|
url: 'https://vitejs.dev/'
|
||||||
|
icon: 'fas fa-bolt'
|
||||||
|
description: '下一代前端构建工具'
|
||||||
|
- name: 'Rollup'
|
||||||
|
url: 'https://rollupjs.org/'
|
||||||
|
icon: 'fas fa-compress'
|
||||||
|
description: '模块打包器'
|
||||||
|
- name: '后端开发'
|
||||||
|
icon: 'fas fa-server'
|
||||||
|
groups:
|
||||||
|
- name: 'Node.js生态'
|
||||||
|
icon: 'fab fa-node-js'
|
||||||
|
sites:
|
||||||
|
- name: 'Express'
|
||||||
|
url: 'https://expressjs.com/'
|
||||||
|
icon: 'fas fa-server'
|
||||||
|
description: 'Node.js Web框架'
|
||||||
|
- name: 'Koa'
|
||||||
|
url: 'https://koajs.com/'
|
||||||
|
icon: 'fas fa-leaf'
|
||||||
|
description: '下一代Node.js框架'
|
||||||
|
- name: 'NestJS'
|
||||||
|
url: 'https://nestjs.com/'
|
||||||
|
icon: 'fas fa-home'
|
||||||
|
description: 'Node.js企业级框架'
|
||||||
|
- name: 'Python框架'
|
||||||
|
icon: 'fab fa-python'
|
||||||
|
sites:
|
||||||
|
- name: 'Django'
|
||||||
|
url: 'https://www.djangoproject.com/'
|
||||||
|
icon: 'fas fa-python'
|
||||||
|
description: 'Python Web框架'
|
||||||
|
- name: 'Flask'
|
||||||
|
url: 'https://flask.palletsprojects.com/'
|
||||||
|
icon: 'fas fa-flask'
|
||||||
|
description: 'Python微框架'
|
||||||
|
- name: 'FastAPI'
|
||||||
|
url: 'https://fastapi.tiangolo.com/'
|
||||||
|
icon: 'fas fa-bolt'
|
||||||
|
description: '现代Python Web框架'
|
||||||
|
- name: '数据库ORM'
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
sites:
|
||||||
|
- name: 'Prisma'
|
||||||
|
url: 'https://www.prisma.io/'
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
description: '下一代数据库工具'
|
||||||
|
- name: 'TypeORM'
|
||||||
|
url: 'https://typeorm.io/'
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
description: 'TypeScript ORM'
|
||||||
|
- name: 'SQLAlchemy'
|
||||||
|
url: 'https://www.sqlalchemy.org/'
|
||||||
|
icon: 'fas fa-database'
|
||||||
|
description: 'Python SQL工具包'
|
||||||
|
|
||||||
|
- name: '设计资源'
|
||||||
|
icon: 'fas fa-palette'
|
||||||
|
subcategories:
|
||||||
|
- name: 'UI设计工具'
|
||||||
|
icon: 'fas fa-paint-brush'
|
||||||
|
groups:
|
||||||
|
- name: '原型设计'
|
||||||
|
icon: 'fas fa-drafting-compass'
|
||||||
|
sites:
|
||||||
|
- name: 'Figma'
|
||||||
|
url: 'https://www.figma.com/'
|
||||||
|
icon: 'fab fa-figma'
|
||||||
|
description: '协作式UI设计工具'
|
||||||
|
- name: 'Sketch'
|
||||||
|
url: 'https://www.sketch.com/'
|
||||||
|
icon: 'fab fa-sketch'
|
||||||
|
description: 'Mac平台UI设计工具'
|
||||||
|
- name: 'Adobe XD'
|
||||||
|
url: 'https://www.adobe.com/products/xd.html'
|
||||||
|
icon: 'fab fa-adobe'
|
||||||
|
description: 'Adobe UI设计工具'
|
||||||
|
- name: '设计系统'
|
||||||
|
icon: 'fas fa-th-large'
|
||||||
|
sites:
|
||||||
|
- name: 'Ant Design'
|
||||||
|
url: 'https://ant.design/'
|
||||||
|
icon: 'fas fa-th'
|
||||||
|
description: '企业级UI设计语言'
|
||||||
|
- name: 'Material Design'
|
||||||
|
url: 'https://material.io/design'
|
||||||
|
icon: 'fas fa-cube'
|
||||||
|
description: 'Google设计系统'
|
||||||
|
- name: 'Bootstrap'
|
||||||
|
url: 'https://getbootstrap.com/'
|
||||||
|
icon: 'fab fa-bootstrap'
|
||||||
|
description: '响应式CSS框架'
|
||||||
|
- name: '交互设计'
|
||||||
|
icon: 'fas fa-hand-pointer'
|
||||||
|
sites:
|
||||||
|
- name: 'Principle'
|
||||||
|
url: 'https://principleformac.com/'
|
||||||
|
icon: 'fas fa-magic'
|
||||||
|
description: '交互设计工具'
|
||||||
|
- name: 'Framer'
|
||||||
|
url: 'https://www.framer.com/'
|
||||||
|
icon: 'fas fa-mobile-alt'
|
||||||
|
description: '交互式设计工具'
|
||||||
|
- name: 'ProtoPie'
|
||||||
|
url: 'https://www.protopie.io/'
|
||||||
|
icon: 'fas fa-chart-pie'
|
||||||
|
description: '原型制作工具'
|
||||||
|
- name: '视觉资源'
|
||||||
|
icon: 'fas fa-image'
|
||||||
|
groups:
|
||||||
|
- name: '图标库'
|
||||||
|
icon: 'fas fa-icons'
|
||||||
|
sites:
|
||||||
|
- name: 'Font Awesome'
|
||||||
|
url: 'https://fontawesome.com/'
|
||||||
|
icon: 'fab fa-font-awesome'
|
||||||
|
description: '图标库'
|
||||||
|
- name: 'Iconfont'
|
||||||
|
url: 'https://www.iconfont.cn/'
|
||||||
|
icon: 'fas fa-icons'
|
||||||
|
description: '阿里巴巴图标库'
|
||||||
|
- name: 'Feather Icons'
|
||||||
|
url: 'https://feathericons.com/'
|
||||||
|
icon: 'fas fa-feather'
|
||||||
|
description: '简洁的图标库'
|
||||||
|
- name: '插画资源'
|
||||||
|
icon: 'fas fa-paint-brush'
|
||||||
|
sites:
|
||||||
|
- name: 'UnDraw'
|
||||||
|
url: 'https://undraw.co/'
|
||||||
|
icon: 'fas fa-palette'
|
||||||
|
description: '开源插画库'
|
||||||
|
- name: 'Illustrations.co'
|
||||||
|
url: 'https://illustrations.co/'
|
||||||
|
icon: 'fas fa-image'
|
||||||
|
description: '插画资源'
|
||||||
|
- name: 'Storyset'
|
||||||
|
url: 'https://storyset.com/'
|
||||||
|
icon: 'fas fa-book'
|
||||||
|
description: '动画插画库'
|
||||||
|
- name: '配色方案'
|
||||||
|
icon: 'fas fa-palette'
|
||||||
|
sites:
|
||||||
|
- name: 'Coolors'
|
||||||
|
url: 'https://coolors.co/'
|
||||||
|
icon: 'fas fa-palette'
|
||||||
|
description: '在线配色方案生成器'
|
||||||
|
- name: 'Adobe Color'
|
||||||
|
url: 'https://color.adobe.com/'
|
||||||
|
icon: 'fab fa-adobe'
|
||||||
|
description: 'Adobe配色工具'
|
||||||
|
- name: 'Paletton'
|
||||||
|
url: 'https://paletton.com/'
|
||||||
|
icon: 'fas fa-palette'
|
||||||
|
description: '配色方案设计工具'
|
||||||
|
|
||||||
|
- name: '开发工具'
|
||||||
|
icon: 'fas fa-tools'
|
||||||
|
subcategories:
|
||||||
|
- name: '编程工具'
|
||||||
|
icon: 'fas fa-code'
|
||||||
|
groups:
|
||||||
|
- name: '代码编辑器'
|
||||||
|
icon: 'fas fa-code'
|
||||||
|
sites:
|
||||||
|
- name: 'Visual Studio Code'
|
||||||
|
url: 'https://code.visualstudio.com/'
|
||||||
|
icon: 'fas fa-code'
|
||||||
|
description: '微软代码编辑器'
|
||||||
|
- name: 'Sublime Text'
|
||||||
|
url: 'https://www.sublimetext.com/'
|
||||||
|
icon: 'fas fa-file-code'
|
||||||
|
description: '轻量级代码编辑器'
|
||||||
|
- name: 'WebStorm'
|
||||||
|
url: 'https://www.jetbrains.com/webstorm/'
|
||||||
|
icon: 'fab fa-js'
|
||||||
|
description: 'JetBrains前端IDE'
|
||||||
|
- name: '版本控制'
|
||||||
|
icon: 'fas fa-code-branch'
|
||||||
|
sites:
|
||||||
|
- name: 'GitHub'
|
||||||
|
url: 'https://github.com/'
|
||||||
|
icon: 'fab fa-github'
|
||||||
|
description: '代码托管平台'
|
||||||
|
- name: 'GitLab'
|
||||||
|
url: 'https://gitlab.com/'
|
||||||
|
icon: 'fab fa-gitlab'
|
||||||
|
description: 'Git代码管理平台'
|
||||||
|
- name: 'Bitbucket'
|
||||||
|
url: 'https://bitbucket.org/'
|
||||||
|
icon: 'fab fa-bitbucket'
|
||||||
|
description: 'Atlassian代码托管'
|
||||||
|
- name: '调试工具'
|
||||||
|
icon: 'fas fa-bug'
|
||||||
|
sites:
|
||||||
|
- name: 'Chrome DevTools'
|
||||||
|
url: 'https://developers.google.com/web/tools/chrome-devtools'
|
||||||
|
icon: 'fab fa-chrome'
|
||||||
|
description: 'Chrome开发者工具'
|
||||||
|
- name: 'Firefox DevTools'
|
||||||
|
url: 'https://developer.mozilla.org/en-US/docs/Tools'
|
||||||
|
icon: 'fab fa-firefox'
|
||||||
|
description: 'Firefox开发者工具'
|
||||||
|
- name: 'Postman'
|
||||||
|
url: 'https://www.postman.com/'
|
||||||
|
icon: 'fas fa-paper-plane'
|
||||||
|
description: 'API测试工具'
|
||||||
|
- name: '部署运维'
|
||||||
|
icon: 'fas fa-server'
|
||||||
|
groups:
|
||||||
|
- name: '云服务'
|
||||||
|
icon: 'fas fa-cloud'
|
||||||
|
sites:
|
||||||
|
- name: 'AWS'
|
||||||
|
url: 'https://aws.amazon.com/'
|
||||||
|
icon: 'fab fa-aws'
|
||||||
|
description: '亚马逊云服务'
|
||||||
|
- name: 'Azure'
|
||||||
|
url: 'https://azure.microsoft.com/'
|
||||||
|
icon: 'fab fa-microsoft'
|
||||||
|
description: '微软云服务'
|
||||||
|
- name: 'Google Cloud'
|
||||||
|
url: 'https://cloud.google.com/'
|
||||||
|
icon: 'fab fa-google'
|
||||||
|
description: '谷歌云服务'
|
||||||
|
- name: '容器化'
|
||||||
|
icon: 'fas fa-cube'
|
||||||
|
sites:
|
||||||
|
- name: 'Docker'
|
||||||
|
url: 'https://www.docker.com/'
|
||||||
|
icon: 'fab fa-docker'
|
||||||
|
description: '容器化平台'
|
||||||
|
- name: 'Kubernetes'
|
||||||
|
url: 'https://kubernetes.io/'
|
||||||
|
icon: 'fas fa-dharmachakra'
|
||||||
|
description: '容器编排系统'
|
||||||
|
- name: 'Podman'
|
||||||
|
url: 'https://podman.io/'
|
||||||
|
icon: 'fas fa-cube'
|
||||||
|
description: '无守护进程容器引擎'
|
||||||
|
- name: 'CI/CD'
|
||||||
|
icon: 'fas fa-infinity'
|
||||||
|
sites:
|
||||||
|
- name: 'GitHub Actions'
|
||||||
|
url: 'https://github.com/features/actions'
|
||||||
|
icon: 'fab fa-github'
|
||||||
|
description: 'GitHub自动化工具'
|
||||||
|
- name: 'Jenkins'
|
||||||
|
url: 'https://www.jenkins.io/'
|
||||||
|
icon: 'fas fa-infinity'
|
||||||
|
description: '开源自动化服务器'
|
||||||
|
- name: 'GitLab CI'
|
||||||
|
url: 'https://docs.gitlab.com/ee/ci/'
|
||||||
|
icon: 'fab fa-gitlab'
|
||||||
|
description: 'GitLab CI/CD工具'
|
||||||
@@ -69,3 +69,6 @@ navigation:
|
|||||||
- name: 书签
|
- name: 书签
|
||||||
icon: fas fa-bookmark
|
icon: fas fa-bookmark
|
||||||
id: bookmarks
|
id: bookmarks
|
||||||
|
- name: 书签(多层级)
|
||||||
|
icon: fas fa-bookmark
|
||||||
|
id: bookmarks-four-level
|
||||||
|
|||||||
@@ -103,11 +103,10 @@ function getLatestBookmarkFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析书签HTML内容
|
// 解析书签HTML内容,支持2-4层级嵌套结构
|
||||||
function parseBookmarks(htmlContent) {
|
function parseBookmarks(htmlContent) {
|
||||||
// 简单的正则表达式匹配方法解析书签文件
|
// 正则表达式匹配文件夹和书签
|
||||||
// 注意:这是一个简化实现,可能不适用于所有浏览器的书签格式
|
const folderRegex = /<DT><H3([^>]*)>(.*?)<\/H3>/g;
|
||||||
const folderRegex = /<DT><H3[^>]*>(.*?)<\/H3>/g;
|
|
||||||
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
const bookmarkRegex = /<DT><A HREF="([^"]+)"[^>]*>(.*?)<\/A>/g;
|
||||||
|
|
||||||
// 储存解析结果
|
// 储存解析结果
|
||||||
@@ -115,31 +114,89 @@ function parseBookmarks(htmlContent) {
|
|||||||
categories: []
|
categories: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提取文件夹
|
// 递归解析嵌套文件夹
|
||||||
let match;
|
function parseNestedFolder(htmlContent, parentPath = [], level = 1) {
|
||||||
let folderMatches = [];
|
const folders = [];
|
||||||
while ((match = folderRegex.exec(htmlContent)) !== null) {
|
let match;
|
||||||
folderMatches.push({
|
|
||||||
index: match.index,
|
// 使用正则表达式匹配文件夹
|
||||||
name: match[1].trim(),
|
folderRegex.lastIndex = 0;
|
||||||
end: match.index + match[0].length
|
|
||||||
});
|
while ((match = folderRegex.exec(htmlContent)) !== null) {
|
||||||
|
const folderName = match[2].trim();
|
||||||
|
const folderStart = match.index;
|
||||||
|
const folderEnd = match.index + match[0].length;
|
||||||
|
|
||||||
|
// 查找文件夹的结束位置
|
||||||
|
let folderContentEnd = htmlContent.length;
|
||||||
|
let depth = 1;
|
||||||
|
let pos = folderEnd;
|
||||||
|
|
||||||
|
while (pos < htmlContent.length && depth > 0) {
|
||||||
|
const dlStart = htmlContent.substring(pos).match(/<DL><p>/gi);
|
||||||
|
const dlEnd = htmlContent.substring(pos).match(/<\/DL><p>/gi);
|
||||||
|
|
||||||
|
if (dlStart && dlStart.index < (dlEnd ? dlEnd.index : htmlContent.length)) {
|
||||||
|
depth++;
|
||||||
|
pos += dlStart.index + dlStart[0].length;
|
||||||
|
} else if (dlEnd) {
|
||||||
|
depth--;
|
||||||
|
pos += dlEnd.index + dlEnd[0].length;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
folderContentEnd = pos;
|
||||||
|
const folderContent = htmlContent.substring(folderEnd, folderContentEnd);
|
||||||
|
|
||||||
|
// 解析文件夹内容
|
||||||
|
const folder = {
|
||||||
|
name: folderName,
|
||||||
|
icon: 'fas fa-folder',
|
||||||
|
path: [...parentPath, folderName]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否包含子文件夹
|
||||||
|
const hasSubfolders = folderRegex.test(folderContent);
|
||||||
|
folderRegex.lastIndex = 0;
|
||||||
|
|
||||||
|
if (hasSubfolders && level < 4) {
|
||||||
|
// 递归解析子文件夹
|
||||||
|
const subfolders = parseNestedFolder(folderContent, folder.path, level + 1);
|
||||||
|
|
||||||
|
// 根据层级深度决定数据结构
|
||||||
|
if (level === 1) {
|
||||||
|
folder.subcategories = subfolders;
|
||||||
|
} else if (level === 2) {
|
||||||
|
folder.groups = subfolders;
|
||||||
|
} else if (level === 3) {
|
||||||
|
// 层级3直接解析书签
|
||||||
|
folder.sites = parseSitesInFolder(folderContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 解析书签
|
||||||
|
folder.sites = parseSitesInFolder(folderContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只添加包含内容的文件夹
|
||||||
|
const hasContent = folder.sites && folder.sites.length > 0 ||
|
||||||
|
folder.subcategories && folder.subcategories.length > 0 ||
|
||||||
|
folder.groups && folder.groups.length > 0;
|
||||||
|
|
||||||
|
if (hasContent) {
|
||||||
|
folders.push(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对每个文件夹,提取其中的书签
|
// 解析文件夹中的书签
|
||||||
for (let i = 0; i < folderMatches.length; i++) {
|
function parseSitesInFolder(folderContent) {
|
||||||
const folder = folderMatches[i];
|
|
||||||
const nextFolder = folderMatches[i + 1];
|
|
||||||
|
|
||||||
// 确定当前文件夹的内容范围
|
|
||||||
const folderContent = nextFolder
|
|
||||||
? htmlContent.substring(folder.end, nextFolder.index)
|
|
||||||
: htmlContent.substring(folder.end);
|
|
||||||
|
|
||||||
// 从文件夹内容中提取书签
|
|
||||||
const sites = [];
|
const sites = [];
|
||||||
let bookmarkMatch;
|
let bookmarkMatch;
|
||||||
bookmarkRegex.lastIndex = 0; // 重置regex索引
|
bookmarkRegex.lastIndex = 0;
|
||||||
|
|
||||||
while ((bookmarkMatch = bookmarkRegex.exec(folderContent)) !== null) {
|
while ((bookmarkMatch = bookmarkRegex.exec(folderContent)) !== null) {
|
||||||
const url = bookmarkMatch[1];
|
const url = bookmarkMatch[1];
|
||||||
@@ -162,16 +219,12 @@ function parseBookmarks(htmlContent) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只添加包含书签的文件夹
|
return sites;
|
||||||
if (sites.length > 0) {
|
|
||||||
bookmarks.categories.push({
|
|
||||||
name: folder.name,
|
|
||||||
icon: 'fas fa-folder', // 默认使用文件夹图标
|
|
||||||
sites: sites
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 开始解析
|
||||||
|
bookmarks.categories = parseNestedFolder(htmlContent);
|
||||||
|
|
||||||
return bookmarks;
|
return bookmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,21 @@ function safeLoadYamlConfig(filePath) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
return yaml.load(fileContent);
|
// 使用 loadAll 而不是 load 来支持多文档 YAML 文件
|
||||||
|
const docs = yaml.loadAll(fileContent);
|
||||||
|
|
||||||
|
// 如果只有一个文档,直接返回
|
||||||
|
if (docs.length === 1) {
|
||||||
|
return docs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有多个文档,返回第一个文档(忽略后面的文档)
|
||||||
|
if (docs.length > 1) {
|
||||||
|
console.warn(`Warning: Multiple documents found in ${filePath}. Using the first document only.`);
|
||||||
|
return docs[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleConfigLoadError(filePath, error);
|
handleConfigLoadError(filePath, error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -181,6 +181,19 @@ function encodeURIComponentHelper(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数学加法运算助手函数
|
||||||
|
* @param {number} a 第一个数
|
||||||
|
* @param {number} b 第二个数
|
||||||
|
* @returns {number} 两数之和
|
||||||
|
* @example {{add level 1}}
|
||||||
|
*/
|
||||||
|
function add(a, b) {
|
||||||
|
const numA = parseInt(a, 10) || 0;
|
||||||
|
const numB = parseInt(b, 10) || 0;
|
||||||
|
return numA + numB;
|
||||||
|
}
|
||||||
|
|
||||||
// 导出所有工具类助手函数
|
// 导出所有工具类助手函数
|
||||||
module.exports = {
|
module.exports = {
|
||||||
slice,
|
slice,
|
||||||
@@ -191,5 +204,6 @@ module.exports = {
|
|||||||
range,
|
range,
|
||||||
pick,
|
pick,
|
||||||
keys,
|
keys,
|
||||||
encodeURIComponent: encodeURIComponentHelper
|
encodeURIComponent: encodeURIComponentHelper,
|
||||||
|
add
|
||||||
};
|
};
|
||||||
|
|||||||
134
src/script.js
134
src/script.js
@@ -342,6 +342,137 @@ window.MeNav = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 多层级嵌套书签功能
|
||||||
|
window.MeNav.expandAll = function() {
|
||||||
|
document.querySelectorAll('.category.collapsed, .group.collapsed').forEach(element => {
|
||||||
|
element.classList.remove('collapsed');
|
||||||
|
saveToggleState(element, 'expanded');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.MeNav.collapseAll = function() {
|
||||||
|
document.querySelectorAll('.category:not(.collapsed), .group:not(.collapsed)').forEach(element => {
|
||||||
|
element.classList.add('collapsed');
|
||||||
|
saveToggleState(element, 'collapsed');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.MeNav.toggleCategory = function(categoryName, subcategoryName = null, groupName = null) {
|
||||||
|
const selector = groupName
|
||||||
|
? `[data-name="${categoryName}"] [data-name="${subcategoryName}"] [data-name="${groupName}"]`
|
||||||
|
: subcategoryName
|
||||||
|
? `[data-name="${categoryName}"] [data-name="${subcategoryName}"]`
|
||||||
|
: `[data-name="${categoryName}"]`;
|
||||||
|
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
toggleNestedElement(element);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.MeNav.getNestedStructure = function() {
|
||||||
|
// 返回完整的嵌套结构数据
|
||||||
|
const categories = [];
|
||||||
|
document.querySelectorAll('.category-level-1').forEach(cat => {
|
||||||
|
categories.push(extractNestedData(cat));
|
||||||
|
});
|
||||||
|
return categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换嵌套元素
|
||||||
|
function toggleNestedElement(container) {
|
||||||
|
const isCollapsed = container.classList.contains('collapsed');
|
||||||
|
|
||||||
|
if (isCollapsed) {
|
||||||
|
container.classList.remove('collapsed');
|
||||||
|
saveToggleState(container, 'expanded');
|
||||||
|
} else {
|
||||||
|
container.classList.add('collapsed');
|
||||||
|
saveToggleState(container, 'collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发自定义事件
|
||||||
|
const event = new CustomEvent('nestedToggle', {
|
||||||
|
detail: {
|
||||||
|
element: container,
|
||||||
|
type: container.dataset.type,
|
||||||
|
name: container.dataset.name,
|
||||||
|
isCollapsed: !isCollapsed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存切换状态
|
||||||
|
function saveToggleState(element, state) {
|
||||||
|
const type = element.dataset.type;
|
||||||
|
const name = element.dataset.name;
|
||||||
|
const level = element.dataset.level || '1';
|
||||||
|
const key = `menav-toggle-${type}-${level}-${name}`;
|
||||||
|
localStorage.setItem(key, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复切换状态
|
||||||
|
function restoreToggleState(element) {
|
||||||
|
const type = element.dataset.type;
|
||||||
|
const name = element.dataset.name;
|
||||||
|
const level = element.dataset.level || '1';
|
||||||
|
const key = `menav-toggle-${type}-${level}-${name}`;
|
||||||
|
const savedState = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (savedState === 'collapsed') {
|
||||||
|
element.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化嵌套分类
|
||||||
|
function initializeNestedCategories() {
|
||||||
|
// 为所有可折叠元素添加切换功能
|
||||||
|
document.querySelectorAll('[data-toggle="category"], [data-toggle="group"]').forEach(header => {
|
||||||
|
header.addEventListener('click', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const container = this.parentElement;
|
||||||
|
toggleNestedElement(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 恢复保存的状态
|
||||||
|
restoreToggleState(header.parentElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取嵌套数据
|
||||||
|
function extractNestedData(element) {
|
||||||
|
const data = {
|
||||||
|
name: element.dataset.name,
|
||||||
|
type: element.dataset.type,
|
||||||
|
level: element.dataset.level,
|
||||||
|
isCollapsed: element.classList.contains('collapsed')
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取子元素数据
|
||||||
|
const subcategories = element.querySelectorAll(':scope > .category-content > .subcategories-container > .category');
|
||||||
|
if (subcategories.length > 0) {
|
||||||
|
data.subcategories = Array.from(subcategories).map(sub => extractNestedData(sub));
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = element.querySelectorAll(':scope > .category-content > .groups-container > .group');
|
||||||
|
if (groups.length > 0) {
|
||||||
|
data.groups = Array.from(groups).map(group => extractNestedData(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sites = element.querySelectorAll(':scope > .category-content > .sites-grid > .site-card, :scope > .group-content > .sites-grid > .site-card');
|
||||||
|
if (sites.length > 0) {
|
||||||
|
data.sites = Array.from(sites).map(site => ({
|
||||||
|
name: site.dataset.name,
|
||||||
|
url: site.dataset.url,
|
||||||
|
icon: site.dataset.icon,
|
||||||
|
description: site.dataset.description
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// 先声明所有状态变量
|
// 先声明所有状态变量
|
||||||
let isSearchActive = false;
|
let isSearchActive = false;
|
||||||
@@ -1312,6 +1443,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化嵌套分类功能
|
||||||
|
initializeNestedCategories();
|
||||||
|
|
||||||
// 初始化搜索索引(使用requestIdleCallback或setTimeout延迟初始化,避免影响页面加载)
|
// 初始化搜索索引(使用requestIdleCallback或setTimeout延迟初始化,避免影响页面加载)
|
||||||
if ('requestIdleCallback' in window) {
|
if ('requestIdleCallback' in window) {
|
||||||
requestIdleCallback(() => initSearchIndex());
|
requestIdleCallback(() => initSearchIndex());
|
||||||
|
|||||||
@@ -1,12 +1,53 @@
|
|||||||
<section class="category" id="{{name}}" data-type="category" data-name="{{name}}" data-icon="{{icon}}" data-container="categories">
|
<section class="category {{#if level}}category-level-{{level}}{{else}}category-level-1{{/if}}"
|
||||||
<h2 data-editable="category-name"><i class="{{icon}}"></i> {{name}}</h2>
|
id="{{name}}"
|
||||||
<div class="sites-grid" data-container="sites">
|
data-type="category"
|
||||||
{{#if sites.length}}
|
data-name="{{name}}"
|
||||||
{{#each sites}}
|
data-icon="{{icon}}"
|
||||||
{{> site-card}}
|
data-level="{{#if level}}{{level}}{{else}}1{{/if}}"
|
||||||
{{/each}}
|
data-container="categories">
|
||||||
|
|
||||||
|
<div class="category-header" data-toggle="category">
|
||||||
|
<h{{#if level}}{{add level 1}}{{else}}2{{/if}} data-editable="category-name">
|
||||||
|
<i class="{{icon}}"></i>
|
||||||
|
{{name}}
|
||||||
|
{{#if subcategories}}
|
||||||
|
<span class="toggle-icon">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if groups}}
|
||||||
|
<span class="toggle-icon">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</h{{#if level}}{{add level 1}}{{else}}2{{/if}}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="category-content">
|
||||||
|
{{#if subcategories}}
|
||||||
|
<div class="subcategories-container" data-container="subcategories">
|
||||||
|
{{#each subcategories}}
|
||||||
|
{{> category level=2}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{else if groups}}
|
||||||
|
<div class="groups-container" data-container="groups">
|
||||||
|
{{#each groups}}
|
||||||
|
{{> group}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{else if sites}}
|
||||||
|
<div class="sites-grid" data-container="sites">
|
||||||
|
{{#if sites.length}}
|
||||||
|
{{#each sites}}
|
||||||
|
{{> site-card}}
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<p class="empty-sites">暂无网站</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="empty-sites">暂无网站</p>
|
<p class="empty-content">暂无内容</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
32
templates/components/group.hbs
Normal file
32
templates/components/group.hbs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="group {{#if level}}group-level-{{level}}{{else}}group-level-3{{/if}}"
|
||||||
|
data-type="group"
|
||||||
|
data-name="{{name}}"
|
||||||
|
data-icon="{{icon}}"
|
||||||
|
data-level="{{#if level}}{{level}}{{else}}3{{/if}}">
|
||||||
|
|
||||||
|
<div class="group-header" data-toggle="group">
|
||||||
|
<h{{#if level}}{{add level 1}}{{else}}3{{/if}} data-editable="group-name">
|
||||||
|
<i class="{{icon}}"></i>
|
||||||
|
{{name}}
|
||||||
|
<span class="toggle-icon">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</span>
|
||||||
|
</h{{#if level}}{{add level 1}}{{else}}3{{/if}}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-content">
|
||||||
|
{{#if sites}}
|
||||||
|
<div class="sites-grid" data-container="sites">
|
||||||
|
{{#if sites.length}}
|
||||||
|
{{#each sites}}
|
||||||
|
{{> site-card}}
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<p class="empty-sites">暂无网站</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<p class="empty-content">暂无网站</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user