refactor: 完成Handlebars模板组件化
This commit is contained in:
7
config/user/navigation.yml
Normal file
7
config/user/navigation.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
- name: 首页
|
||||
icon: fas fa-home
|
||||
id: home
|
||||
active: true
|
||||
- name: 书签
|
||||
icon: fas fa-bookmark
|
||||
id: bookmarks
|
||||
29
config/user/pages/bookmarks.yml
Normal file
29
config/user/pages/bookmarks.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
title: 我的书签
|
||||
subtitle: 从浏览器导入的书签收藏
|
||||
categories:
|
||||
- name: 技术资源
|
||||
icon: fas fa-folder
|
||||
sites:
|
||||
- name: GitHub
|
||||
url: https://github.com/
|
||||
icon: fab fa-github
|
||||
description: "从书签导入: GitHub"
|
||||
- name: Stack Overflow
|
||||
url: https://stackoverflow.com/
|
||||
icon: fab fa-stack-overflow
|
||||
description: "从书签导入: Stack Overflow"
|
||||
- name: MDN Web Docs
|
||||
url: https://developer.mozilla.org/
|
||||
icon: fas fa-link
|
||||
description: "从书签导入: MDN Web Docs"
|
||||
- name: 社交媒体
|
||||
icon: fas fa-folder
|
||||
sites:
|
||||
- name: Twitter
|
||||
url: https://twitter.com/
|
||||
icon: fab fa-twitter
|
||||
description: "从书签导入: Twitter"
|
||||
- name: LinkedIn
|
||||
url: https://www.linkedin.com/
|
||||
icon: fab fa-linkedin
|
||||
description: "从书签导入: LinkedIn"
|
||||
100
config/user/pages/home.yml
Normal file
100
config/user/pages/home.yml
Normal file
@@ -0,0 +1,100 @@
|
||||
categories:
|
||||
- name: 常用网站
|
||||
icon: fas fa-star
|
||||
sites:
|
||||
- name: Linux.do
|
||||
url: https://linux.do/
|
||||
icon: fab fa-linux
|
||||
description: 新的理想型社区
|
||||
- name: Google
|
||||
url: https://www.google.com
|
||||
icon: fab fa-google
|
||||
description: 全球最大的搜索引擎
|
||||
- name: GitHub
|
||||
url: https://www.github.com
|
||||
icon: fab fa-github
|
||||
description: 代码托管平台
|
||||
- name: Stack Overflow
|
||||
url: https://stackoverflow.com
|
||||
icon: fab fa-stack-overflow
|
||||
description: 程序员问答社区
|
||||
- name: ChatGPT
|
||||
url: https://chat.openai.com
|
||||
icon: fas fa-robot
|
||||
description: AI智能助手
|
||||
- name: 学习资源
|
||||
icon: fas fa-graduation-cap
|
||||
sites:
|
||||
- name: 哔哩哔哩
|
||||
url: https://www.bilibili.com
|
||||
icon: fas fa-play-circle
|
||||
description: 视频学习平台
|
||||
- name: 知乎
|
||||
url: https://www.zhihu.com
|
||||
icon: fas fa-question-circle
|
||||
description: 问答社区
|
||||
- name: 掘金
|
||||
url: https://juejin.cn
|
||||
icon: fas fa-book
|
||||
description: 高质量技术社区
|
||||
- name: LeetCode
|
||||
url: https://leetcode.cn
|
||||
icon: fas fa-code
|
||||
description: 算法刷题平台
|
||||
- name: 开发工具
|
||||
icon: fas fa-tools
|
||||
sites:
|
||||
- name: VS Code
|
||||
url: https://code.visualstudio.com
|
||||
icon: fas fa-code
|
||||
description: 强大的代码编辑器
|
||||
- name: Postman
|
||||
url: https://www.postman.com
|
||||
icon: fas fa-paper-plane
|
||||
description: API调试工具
|
||||
- name: Git
|
||||
url: https://git-scm.com
|
||||
icon: fab fa-git-alt
|
||||
description: 版本控制工具
|
||||
- name: Docker
|
||||
url: https://www.docker.com
|
||||
icon: fab fa-docker
|
||||
description: 容器化平台
|
||||
- name: 设计资源
|
||||
icon: fas fa-palette
|
||||
sites:
|
||||
- name: Figma
|
||||
url: https://www.figma.com
|
||||
icon: fab fa-figma
|
||||
description: 在线设计工具
|
||||
- name: Dribbble
|
||||
url: https://dribbble.com
|
||||
icon: fab fa-dribbble
|
||||
description: 设计师社区
|
||||
- name: Behance
|
||||
url: https://www.behance.net
|
||||
icon: fab fa-behance
|
||||
description: 创意设计平台
|
||||
- name: IconFont
|
||||
url: https://www.iconfont.cn
|
||||
icon: fas fa-icons
|
||||
description: 图标资源库
|
||||
- name: 在线工具
|
||||
icon: fas fa-wrench
|
||||
sites:
|
||||
- name: JSON Editor
|
||||
url: https://jsoneditoronline.org
|
||||
icon: fas fa-code-branch
|
||||
description: JSON在线编辑器
|
||||
- name: Can I Use
|
||||
url: https://caniuse.com
|
||||
icon: fas fa-browser
|
||||
description: 浏览器兼容性查询
|
||||
- name: TinyPNG
|
||||
url: https://tinypng.com
|
||||
icon: fas fa-compress
|
||||
description: 图片压缩工具
|
||||
- name: Carbon
|
||||
url: https://carbon.now.sh
|
||||
icon: fas fa-code
|
||||
description: 代码图片生成器
|
||||
38
config/user/site.yml
Normal file
38
config/user/site.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
title: 我的导航
|
||||
description: 个人网络导航站
|
||||
author: Your Name
|
||||
favicon: favicon.ico
|
||||
logo_text: 导航站11
|
||||
|
||||
fonts:
|
||||
title:
|
||||
family: Poppins
|
||||
weight: 600
|
||||
source: google
|
||||
subtitle:
|
||||
family: Quicksand
|
||||
weight: 500
|
||||
source: google
|
||||
body:
|
||||
family: Noto Sans SC
|
||||
weight: 400
|
||||
source: google
|
||||
|
||||
profile:
|
||||
title: Hello,
|
||||
subtitle: Welcome to My Navigation
|
||||
description: 导航菜单
|
||||
|
||||
social:
|
||||
- name: GitHub
|
||||
url: https://github.com
|
||||
icon: fab fa-github
|
||||
- name: Telegram
|
||||
url: https://t.me
|
||||
icon: fab fa-telegram
|
||||
- name: Twitter
|
||||
url: https://twitter.com
|
||||
icon: fab fa-twitter
|
||||
- name: Steam
|
||||
url: https://steam.com
|
||||
icon: fab fa-steam
|
||||
@@ -22,6 +22,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-yaml": "^4.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"ansi-regex": "^6.0.1",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"supports-color": "^9.4.0",
|
||||
|
||||
946
src/generator.js
946
src/generator.js
File diff suppressed because it is too large
Load Diff
173
src/helpers/conditions.js
Normal file
173
src/helpers/conditions.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Handlebars条件判断助手函数
|
||||
* 提供各种条件判断功能
|
||||
*/
|
||||
|
||||
/**
|
||||
* 比较两个值是否相等
|
||||
* @param {any} v1 比较值1
|
||||
* @param {any} v2 比较值2
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#ifEquals type "article"}}文章{{else}}页面{{/ifEquals}}
|
||||
*/
|
||||
function ifEquals(v1, v2, options) {
|
||||
return v1 === v2 ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个值是否不相等
|
||||
* @param {any} v1 比较值1
|
||||
* @param {any} v2 比较值2
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#ifNotEquals status "completed"}}进行中{{else}}已完成{{/ifNotEquals}}
|
||||
*/
|
||||
function ifNotEquals(v1, v2, options) {
|
||||
return v1 !== v2 ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用条件比较
|
||||
* @param {any} v1 比较值1
|
||||
* @param {string} operator 比较运算符 ('==', '===', '!=', '!==', '<', '<=', '>', '>=')
|
||||
* @param {any} v2 比较值2
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#ifCond count '>' 0}}有内容{{else}}无内容{{/ifCond}}
|
||||
*/
|
||||
function ifCond(v1, operator, v2, options) {
|
||||
switch (operator) {
|
||||
case '==':
|
||||
return (v1 == v2) ? options.fn(this) : options.inverse(this);
|
||||
case '===':
|
||||
return (v1 === v2) ? options.fn(this) : options.inverse(this);
|
||||
case '!=':
|
||||
return (v1 != v2) ? options.fn(this) : options.inverse(this);
|
||||
case '!==':
|
||||
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
|
||||
case '<':
|
||||
return (v1 < v2) ? options.fn(this) : options.inverse(this);
|
||||
case '<=':
|
||||
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
|
||||
case '>':
|
||||
return (v1 > v2) ? options.fn(this) : options.inverse(this);
|
||||
case '>=':
|
||||
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
|
||||
case '&&':
|
||||
return (v1 && v2) ? options.fn(this) : options.inverse(this);
|
||||
case '||':
|
||||
return (v1 || v2) ? options.fn(this) : options.inverse(this);
|
||||
default:
|
||||
return options.inverse(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为空(null、undefined、空字符串、空数组或空对象)
|
||||
* @param {any} value 要检查的值
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#isEmpty items}}无内容{{else}}有内容{{/isEmpty}}
|
||||
*/
|
||||
function isEmpty(value, options) {
|
||||
if (value === null || value === undefined) {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && Object.keys(value).length === 0) {
|
||||
return options.fn(this);
|
||||
}
|
||||
|
||||
return options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否非空
|
||||
* @param {any} value 要检查的值
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#isNotEmpty items}}有内容{{else}}无内容{{/isNotEmpty}}
|
||||
*/
|
||||
function isNotEmpty(value, options) {
|
||||
return isEmpty(value, {
|
||||
fn: options.inverse,
|
||||
inverse: options.fn
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件与操作
|
||||
* @param {any} a 条件A
|
||||
* @param {any} b 条件B
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#and isPremium isActive}}高级活跃用户{{else}}其他用户{{/and}}
|
||||
*/
|
||||
function and(a, b, options) {
|
||||
return (a && b) ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件或操作
|
||||
* @param {any} a 条件A
|
||||
* @param {any} b 条件B
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#or isPremium isAdmin}}有权限{{else}}无权限{{/or}}
|
||||
*/
|
||||
function or(a, b, options) {
|
||||
return (a || b) ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个条件的或操作
|
||||
* 使用方式:{{#or (ifEquals a b) (ifEquals c d)}}满足条件{{else}}不满足条件{{/or}}
|
||||
* @param {...any} args 多个条件值
|
||||
* @returns {boolean} 条件结果
|
||||
*/
|
||||
function orHelper() {
|
||||
// 最后一个参数是options对象
|
||||
const options = arguments[arguments.length - 1];
|
||||
|
||||
// 检查是否至少有一个为true的参数
|
||||
for (let i = 0; i < arguments.length - 1; i++) {
|
||||
if (arguments[i]) {
|
||||
return options.fn(this);
|
||||
}
|
||||
}
|
||||
|
||||
return options.inverse(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件非操作
|
||||
* @param {any} value 条件值
|
||||
* @param {object} options Handlebars选项
|
||||
* @returns {string} 渲染结果
|
||||
* @example {{#not isDisabled}}启用{{else}}禁用{{/not}}
|
||||
*/
|
||||
function not(value, options) {
|
||||
return !value ? options.fn(this) : options.inverse(this);
|
||||
}
|
||||
|
||||
// 导出所有条件判断助手函数
|
||||
module.exports = {
|
||||
ifEquals,
|
||||
ifNotEquals,
|
||||
ifCond,
|
||||
isEmpty,
|
||||
isNotEmpty,
|
||||
and,
|
||||
or,
|
||||
orHelper,
|
||||
not
|
||||
};
|
||||
101
src/helpers/formatters.js
Normal file
101
src/helpers/formatters.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Handlebars格式化助手函数
|
||||
* 提供日期、文本等格式化功能
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {Date|string} date 日期对象或日期字符串
|
||||
* @param {string} format 格式化模式
|
||||
* @returns {string} 格式化后的日期字符串
|
||||
* @example {{formatDate date "YYYY-MM-DD"}}
|
||||
*/
|
||||
function formatDate(date, format) {
|
||||
if (!date) return '';
|
||||
|
||||
// 将字符串转换为日期对象
|
||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||
|
||||
if (!(dateObj instanceof Date) || isNaN(dateObj)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 获取日期组件
|
||||
const year = dateObj.getFullYear();
|
||||
const month = dateObj.getMonth() + 1;
|
||||
const day = dateObj.getDate();
|
||||
const hours = dateObj.getHours();
|
||||
const minutes = dateObj.getMinutes();
|
||||
const seconds = dateObj.getSeconds();
|
||||
|
||||
// 格式化日期字符串
|
||||
if (!format) format = 'YYYY-MM-DD';
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month.toString().padStart(2, '0'))
|
||||
.replace('DD', day.toString().padStart(2, '0'))
|
||||
.replace('HH', hours.toString().padStart(2, '0'))
|
||||
.replace('mm', minutes.toString().padStart(2, '0'))
|
||||
.replace('ss', seconds.toString().padStart(2, '0'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制文本长度,超出部分显示省略号
|
||||
* @param {string} text 输入文本
|
||||
* @param {number} length 最大长度
|
||||
* @returns {string} 处理后的文本
|
||||
* @example {{limit description 100}}
|
||||
*/
|
||||
function limit(text, length) {
|
||||
if (!text) return '';
|
||||
|
||||
text = String(text);
|
||||
|
||||
if (text.length <= length) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.substring(0, length) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换文本为小写
|
||||
* @param {string} text 输入文本
|
||||
* @returns {string} 小写文本
|
||||
* @example {{toLowerCase title}}
|
||||
*/
|
||||
function toLowerCase(text) {
|
||||
if (!text) return '';
|
||||
return String(text).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换文本为大写
|
||||
* @param {string} text 输入文本
|
||||
* @returns {string} 大写文本
|
||||
* @example {{toUpperCase code}}
|
||||
*/
|
||||
function toUpperCase(text) {
|
||||
if (!text) return '';
|
||||
return String(text).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为JSON字符串(用于调试)
|
||||
* @param {any} obj 要转换的对象
|
||||
* @returns {string} JSON字符串
|
||||
* @example {{json this}}
|
||||
*/
|
||||
function json(obj) {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
}
|
||||
|
||||
// 导出所有格式化助手函数
|
||||
module.exports = {
|
||||
formatDate,
|
||||
limit,
|
||||
toLowerCase,
|
||||
toUpperCase,
|
||||
json
|
||||
};
|
||||
64
src/helpers/index.js
Normal file
64
src/helpers/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Handlebars助手函数中心
|
||||
*
|
||||
* 导入并重导出所有助手函数,方便在generator中统一注册
|
||||
*/
|
||||
|
||||
const formatters = require('./formatters');
|
||||
const conditions = require('./conditions');
|
||||
const utils = require('./utils');
|
||||
|
||||
/**
|
||||
* 注册所有助手函数到Handlebars实例
|
||||
* @param {Handlebars} handlebars Handlebars实例
|
||||
*/
|
||||
function registerAllHelpers(handlebars) {
|
||||
// 注册格式化助手函数
|
||||
Object.entries(formatters).forEach(([name, helper]) => {
|
||||
handlebars.registerHelper(name, helper);
|
||||
console.log(`Registered formatter helper: ${name}`);
|
||||
});
|
||||
|
||||
// 注册条件判断助手函数
|
||||
Object.entries(conditions).forEach(([name, helper]) => {
|
||||
handlebars.registerHelper(name, helper);
|
||||
console.log(`Registered condition helper: ${name}`);
|
||||
});
|
||||
|
||||
// 注册工具类助手函数
|
||||
Object.entries(utils).forEach(([name, helper]) => {
|
||||
handlebars.registerHelper(name, helper);
|
||||
console.log(`Registered utility helper: ${name}`);
|
||||
});
|
||||
|
||||
// 注册HTML转义函数(作为助手函数,方便在模板中调用)
|
||||
handlebars.registerHelper('escapeHtml', function(text) {
|
||||
if (text === undefined || text === null) {
|
||||
return '';
|
||||
}
|
||||
return String(text)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
});
|
||||
|
||||
// 注册非转义助手函数(安全输出HTML)
|
||||
handlebars.registerHelper('safeHtml', function(text) {
|
||||
if (text === undefined || text === null) {
|
||||
return '';
|
||||
}
|
||||
return new handlebars.SafeString(text);
|
||||
});
|
||||
|
||||
console.log('All Handlebars helpers registered successfully.');
|
||||
}
|
||||
|
||||
// 导出所有助手函数和注册函数
|
||||
module.exports = {
|
||||
formatters,
|
||||
conditions,
|
||||
utils,
|
||||
registerAllHelpers
|
||||
};
|
||||
179
src/helpers/utils.js
Normal file
179
src/helpers/utils.js
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Handlebars通用工具类助手函数
|
||||
* 提供数组处理、字符串处理等实用功能
|
||||
*/
|
||||
|
||||
/**
|
||||
* 数组或字符串切片操作
|
||||
* @param {Array|string} array 要处理的数组或字符串
|
||||
* @param {number} start 起始索引
|
||||
* @param {number} [end] 结束索引(可选)
|
||||
* @returns {Array|string} 切片结果
|
||||
* @example {{slice array 0 5}}
|
||||
*/
|
||||
function slice(array, start, end) {
|
||||
if (!array) return [];
|
||||
|
||||
if (typeof array === 'string') {
|
||||
return end ? array.slice(start, end) : array.slice(start);
|
||||
}
|
||||
|
||||
if (Array.isArray(array)) {
|
||||
return end ? array.slice(start, end) : array.slice(start);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并数组
|
||||
* @param {...Array} arrays 要合并的数组
|
||||
* @returns {Array} 合并后的数组
|
||||
* @example {{concat array1 array2 array3}}
|
||||
*/
|
||||
function concat() {
|
||||
const args = Array.from(arguments);
|
||||
const options = args.pop(); // 最后一个参数是Handlebars的options对象
|
||||
|
||||
// 过滤掉非数组参数
|
||||
const validArrays = args.filter(arg => Array.isArray(arg));
|
||||
|
||||
if (validArrays.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.prototype.concat.apply([], validArrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数组或对象的长度/大小
|
||||
* @param {Array|Object|string} value 要计算长度的值
|
||||
* @returns {number} 长度或大小
|
||||
* @example {{size array}}
|
||||
*/
|
||||
function size(value) {
|
||||
if (!value) return 0;
|
||||
|
||||
if (Array.isArray(value) || typeof value === 'string') {
|
||||
return value.length;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return Object.keys(value).length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数组的第一个元素
|
||||
* @param {Array} array 数组
|
||||
* @returns {any} 第一个元素
|
||||
* @example {{first items}}
|
||||
*/
|
||||
function first(array) {
|
||||
if (!array || !Array.isArray(array) || array.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return array[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数组的最后一个元素
|
||||
* @param {Array} array 数组
|
||||
* @returns {any} 最后一个元素
|
||||
* @example {{last items}}
|
||||
*/
|
||||
function last(array) {
|
||||
if (!array || !Array.isArray(array) || array.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个连续范围的数组(用于循环)
|
||||
* @param {number} start 起始值
|
||||
* @param {number} end 结束值
|
||||
* @param {number} [step=1] 步长
|
||||
* @returns {Array} 范围数组
|
||||
* @example {{#each (range 1 5)}}{{this}}{{/each}}
|
||||
*/
|
||||
function range(start, end, step = 1) {
|
||||
const result = [];
|
||||
|
||||
if (typeof start !== 'number' || typeof end !== 'number') {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (step <= 0) {
|
||||
step = 1;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i += step) {
|
||||
result.push(i);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从对象中选择指定的属性(创建新对象)
|
||||
* @param {Object} object 源对象
|
||||
* @param {...string} keys 要选择的属性键
|
||||
* @returns {Object} 包含选定属性的新对象
|
||||
* @example {{json (pick user "name" "email")}}
|
||||
*/
|
||||
function pick() {
|
||||
const args = Array.from(arguments);
|
||||
const options = args.pop(); // 最后一个参数是Handlebars的options对象
|
||||
|
||||
if (args.length < 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const obj = args[0];
|
||||
const keys = args.slice(1);
|
||||
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = {};
|
||||
|
||||
keys.forEach(key => {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象的所有键转换为数组
|
||||
* @param {Object} object 输入对象
|
||||
* @returns {Array} 键数组
|
||||
* @example {{#each (keys obj)}}{{this}}{{/each}}
|
||||
*/
|
||||
function keys(object) {
|
||||
if (!object || typeof object !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(object);
|
||||
}
|
||||
|
||||
// 导出所有工具类助手函数
|
||||
module.exports = {
|
||||
slice,
|
||||
concat,
|
||||
size,
|
||||
first,
|
||||
last,
|
||||
range,
|
||||
pick,
|
||||
keys
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#each this}}
|
||||
<div class="nav-item-wrapper">
|
||||
<a href="#" class="nav-item{{#if active}} active{{/if}}" data-page="{{id}}">
|
||||
<a href="#" class="nav-item{{#if isActive}} active{{/if}}{{#if active}} active{{/if}}" data-page="{{id}}">
|
||||
<div class="icon-container">
|
||||
<i class="{{icon}}"></i>
|
||||
</div>
|
||||
|
||||
11
templates/components/search-results.hbs
Normal file
11
templates/components/search-results.hbs
Normal file
@@ -0,0 +1,11 @@
|
||||
<!-- 搜索结果组件 -->
|
||||
<div class="welcome-section">
|
||||
<h2>搜索结果</h2>
|
||||
<p class="subtitle">在所有页面中找到的匹配项</p>
|
||||
</div>
|
||||
{{#each navigation}}
|
||||
<section class="category search-section" data-section="{{id}}" style="display: none;">
|
||||
<h2><i class="{{icon}}"></i> {{name}}匹配项</h2>
|
||||
<div class="sites-grid"></div>
|
||||
</section>
|
||||
{{/each}}
|
||||
@@ -1,5 +1,5 @@
|
||||
{{#if url}}
|
||||
<a href="{{url}}" class="site-card" title="{{name}} - {{description}}">
|
||||
<a href="{{url}}" class="site-card{{#if style}} site-card-{{style}}{{/if}}" title="{{name}} - {{description}}" {{#if external}}target="_blank" rel="noopener"{{/if}}>
|
||||
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
||||
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
||||
<p>{{description}}</p>
|
||||
|
||||
11
templates/components/social-links.hbs
Normal file
11
templates/components/social-links.hbs
Normal file
@@ -0,0 +1,11 @@
|
||||
{{#if this}}
|
||||
{{#each this}}
|
||||
<a href="{{url}}" class="nav-item" target="_blank" rel="noopener">
|
||||
<div class="icon-container">
|
||||
<i class="{{icon}}"></i>
|
||||
</div>
|
||||
<span class="nav-text">{{name}}</span>
|
||||
<i class="fas fa-external-link-alt external-icon"></i>
|
||||
</a>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
@@ -1,103 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{SITE_TITLE}}</title>
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
||||
{{GOOGLE_FONTS}}
|
||||
<style>
|
||||
{{{FONT_VARIABLES}}}
|
||||
</style>
|
||||
<!-- 预设主题和侧边栏状态,避免闪烁 -->
|
||||
<script>
|
||||
(function() {
|
||||
// 读取并应用主题设置
|
||||
var savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'light') {
|
||||
document.documentElement.classList.add('theme-preload');
|
||||
}
|
||||
|
||||
// 读取并应用侧边栏状态
|
||||
var sidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
if (sidebarCollapsed && !isMobile) {
|
||||
document.documentElement.classList.add('sidebar-collapsed-preload');
|
||||
}
|
||||
|
||||
// 添加这个类用于控制初始渲染
|
||||
document.documentElement.classList.add('preload');
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="loading">
|
||||
<!-- 滚动进度指示条 -->
|
||||
<div class="scroll-progress"></div>
|
||||
<div class="layout">
|
||||
<!-- 移动端按钮 -->
|
||||
<div class="mobile-buttons">
|
||||
<button class="menu-toggle" aria-label="切换菜单">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<button class="search-toggle" aria-label="切换搜索">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="overlay"></div>
|
||||
|
||||
<!-- 左侧导航 -->
|
||||
<nav class="sidebar">
|
||||
<div class="logo">
|
||||
<h1>{{SITE_LOGO_TEXT}}</h1>
|
||||
<button class="sidebar-toggle" aria-label="收起/展开侧边栏">
|
||||
<i class="fas fa-chevron-left toggle-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div class="nav-section">
|
||||
{{NAVIGATION}}
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
{{SOCIAL_LINKS}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
<p>© {{CURRENT_YEAR}} <a href="https://github.com/rbetree/menav" target="_blank" rel="noopener">MeNav</a></p>
|
||||
<p>by <a href="https://github.com/rbetree" target="_blank" rel="noopener">rbetree</a></p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<main class="content">
|
||||
<!-- 搜索框容器 -->
|
||||
<div class="search-container">
|
||||
<div class="search-box">
|
||||
<input type="text" id="search" placeholder="搜索...">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ALL_PAGES}}
|
||||
|
||||
{{SEARCH_RESULTS}}
|
||||
</main>
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
<button class="theme-toggle" aria-label="切换主题">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -72,7 +72,11 @@
|
||||
<div class="section-title">
|
||||
<i class="fas fa-link"></i>
|
||||
</div>
|
||||
{{#if social}}
|
||||
{{> social-links social}}
|
||||
{{else}}
|
||||
{{{socialLinks}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,21 +96,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{{body}}}
|
||||
|
||||
<!-- 搜索结果页 -->
|
||||
<div class="page" id="search-results">
|
||||
<div class="welcome-section">
|
||||
<h2>搜索结果</h2>
|
||||
<p class="subtitle">在所有页面中找到的匹配项</p>
|
||||
</div>
|
||||
{{#each navigation}}
|
||||
<section class="category search-section" data-section="{{id}}" style="display: none;">
|
||||
<h2><i class="{{icon}}"></i> {{name}}匹配项</h2>
|
||||
<div class="sites-grid"></div>
|
||||
</section>
|
||||
{{/each}}
|
||||
<!-- 页面容器 -->
|
||||
{{#each pages}}
|
||||
<div class="page {{@key}}{{#if @first}} active{{/if}}" id="{{@key}}">
|
||||
{{{this}}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</main>
|
||||
|
||||
<!-- 主题切换按钮 -->
|
||||
|
||||
7
templates/pages/articles.hbs
Normal file
7
templates/pages/articles.hbs
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="welcome-section">
|
||||
<h2>{{title}}</h2>
|
||||
<p class="subtitle">{{subtitle}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
7
templates/pages/bookmarks.hbs
Normal file
7
templates/pages/bookmarks.hbs
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="welcome-section">
|
||||
<h2>{{title}}</h2>
|
||||
<p class="subtitle">{{subtitle}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
7
templates/pages/friends.hbs
Normal file
7
templates/pages/friends.hbs
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="welcome-section">
|
||||
<h2>{{title}}</h2>
|
||||
<p class="subtitle">{{subtitle}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
@@ -1,10 +1,8 @@
|
||||
<div class="page active" id="home">
|
||||
<div class="welcome-section">
|
||||
<h2>{{profile.title}}</h2>
|
||||
<h3>{{profile.subtitle}}</h3>
|
||||
<p class="subtitle">{{profile.description}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
<div class="welcome-section">
|
||||
<h2>{{profile.title}}</h2>
|
||||
<h3>{{profile.subtitle}}</h3>
|
||||
<p class="subtitle">{{profile.description}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
142
templates/pages/index.hbs
Normal file
142
templates/pages/index.hbs
Normal file
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{site.title}}</title>
|
||||
<link rel="icon" href="./{{site.favicon}}" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="./{{site.favicon}}" type="image/x-icon">
|
||||
<link href="{{{googleFontsLink}}}" rel="stylesheet">
|
||||
<style>
|
||||
{{{fontVariables}}}
|
||||
</style>
|
||||
<!-- 预设主题和侧边栏状态,避免闪烁 -->
|
||||
<script>
|
||||
(function() {
|
||||
// 读取并应用主题设置
|
||||
var savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'light') {
|
||||
document.documentElement.classList.add('theme-preload');
|
||||
}
|
||||
|
||||
// 读取并应用侧边栏状态
|
||||
var sidebarCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
if (sidebarCollapsed && !isMobile) {
|
||||
document.documentElement.classList.add('sidebar-collapsed-preload');
|
||||
}
|
||||
|
||||
// 添加这个类用于控制初始渲染
|
||||
document.documentElement.classList.add('preload');
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body class="loading">
|
||||
<!-- 滚动进度指示条 -->
|
||||
<div class="scroll-progress"></div>
|
||||
<div class="layout">
|
||||
<!-- 移动端按钮 -->
|
||||
<div class="mobile-buttons">
|
||||
<button class="menu-toggle" aria-label="切换菜单">
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<button class="search-toggle" aria-label="切换搜索">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div class="overlay"></div>
|
||||
|
||||
<!-- 左侧导航 -->
|
||||
<nav class="sidebar">
|
||||
<div class="logo">
|
||||
<h1>{{site.logo_text}}</h1>
|
||||
<button class="sidebar-toggle" aria-label="收起/展开侧边栏">
|
||||
<i class="fas fa-chevron-left toggle-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<div class="nav-section">
|
||||
{{#each navigationData}}
|
||||
{{> navigation}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="social-links">
|
||||
{{> social-links}}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<main class="content">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="main-header">
|
||||
<div class="left-actions">
|
||||
<button class="theme-toggle" aria-label="切换主题">
|
||||
<i class="fas fa-moon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="search-container">
|
||||
<div class="search-input-container">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" class="search-input" placeholder="搜索..." aria-label="搜索">
|
||||
<button class="search-clear" aria-label="清除搜索">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-actions">
|
||||
<button class="fullscreen-toggle" aria-label="切换全屏模式">
|
||||
<i class="fas fa-expand"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="main-content">
|
||||
<!-- home页 -->
|
||||
<div class="page active" id="home">
|
||||
{{{pages.home}}}
|
||||
</div>
|
||||
|
||||
<!-- 项目页 -->
|
||||
<div class="page" id="projects">
|
||||
{{{pages.projects}}}
|
||||
</div>
|
||||
|
||||
<!-- 文章页 -->
|
||||
<div class="page" id="articles">
|
||||
{{{pages.articles}}}
|
||||
</div>
|
||||
|
||||
<!-- 朋友页 -->
|
||||
<div class="page" id="friends">
|
||||
{{{pages.friends}}}
|
||||
</div>
|
||||
|
||||
<!-- 书签页 -->
|
||||
<div class="page" id="bookmarks">
|
||||
{{{pages.bookmarks}}}
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果页 -->
|
||||
<div class="page" id="search-results">
|
||||
{{{pages.search-results}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="main-footer">
|
||||
<p>© {{currentYear}} {{site.title}} | {{site.footer}}</p>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
templates/pages/page.hbs
Normal file
9
templates/pages/page.hbs
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="page" id="{{pageId}}">
|
||||
<div class="welcome-section">
|
||||
<h2>{{title}}</h2>
|
||||
<p class="subtitle">{{subtitle}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
</div>
|
||||
7
templates/pages/projects.hbs
Normal file
7
templates/pages/projects.hbs
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="welcome-section">
|
||||
<h2>{{title}}</h2>
|
||||
<p class="subtitle">{{subtitle}}</p>
|
||||
</div>
|
||||
{{#each categories}}
|
||||
{{> category}}
|
||||
{{/each}}
|
||||
11
templates/pages/search-results.hbs
Normal file
11
templates/pages/search-results.hbs
Normal file
@@ -0,0 +1,11 @@
|
||||
<!-- 搜索结果页 -->
|
||||
<div class="welcome-section">
|
||||
<h2>搜索结果</h2>
|
||||
<p class="subtitle">在所有页面中找到的匹配项</p>
|
||||
</div>
|
||||
{{#each navigation}}
|
||||
<section class="category search-section" data-section="{{id}}" style="display: none;">
|
||||
<h2><i class="{{icon}}"></i> {{name}}匹配项</h2>
|
||||
<div class="sites-grid"></div>
|
||||
</section>
|
||||
{{/each}}
|
||||
Reference in New Issue
Block a user