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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
"ansi-regex": "^6.0.1",
|
"ansi-regex": "^6.0.1",
|
||||||
"ansi-styles": "^6.2.1",
|
"ansi-styles": "^6.2.1",
|
||||||
"supports-color": "^9.4.0",
|
"supports-color": "^9.4.0",
|
||||||
|
|||||||
790
src/generator.js
790
src/generator.js
@@ -3,8 +3,12 @@ const yaml = require('js-yaml');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Handlebars = require('handlebars');
|
const Handlebars = require('handlebars');
|
||||||
|
|
||||||
|
// 导入Handlebars助手函数
|
||||||
|
const { registerAllHelpers } = require('./helpers');
|
||||||
|
|
||||||
// 注册Handlebars实例和辅助函数
|
// 注册Handlebars实例和辅助函数
|
||||||
const handlebars = Handlebars.create();
|
const handlebars = Handlebars.create();
|
||||||
|
registerAllHelpers(handlebars);
|
||||||
|
|
||||||
// 加载和注册Handlebars模板的函数
|
// 加载和注册Handlebars模板的函数
|
||||||
function loadHandlebarsTemplates() {
|
function loadHandlebarsTemplates() {
|
||||||
@@ -12,8 +16,7 @@ function loadHandlebarsTemplates() {
|
|||||||
|
|
||||||
// 检查基本模板目录是否存在
|
// 检查基本模板目录是否存在
|
||||||
if (!fs.existsSync(templatesDir)) {
|
if (!fs.existsSync(templatesDir)) {
|
||||||
console.warn('Templates directory not found. Using fallback HTML generation.');
|
throw new Error('Templates directory not found. Cannot proceed without templates.');
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载布局模板
|
// 加载布局模板
|
||||||
@@ -28,6 +31,8 @@ function loadHandlebarsTemplates() {
|
|||||||
console.log(`Registered layout template: ${layoutName}`);
|
console.log(`Registered layout template: ${layoutName}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Layouts directory not found. Cannot proceed without layout templates.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载组件模板
|
// 加载组件模板
|
||||||
@@ -42,6 +47,8 @@ function loadHandlebarsTemplates() {
|
|||||||
console.log(`Registered component template: ${componentName}`);
|
console.log(`Registered component template: ${componentName}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Components directory not found. Cannot proceed without component templates.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 识别并注册默认布局模板
|
// 识别并注册默认布局模板
|
||||||
@@ -50,8 +57,7 @@ function loadHandlebarsTemplates() {
|
|||||||
console.log('Default layout template found and registered.');
|
console.log('Default layout template found and registered.');
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.warn('Default layout template not found. Using fallback HTML generation.');
|
throw new Error('Default layout template not found. Cannot proceed without default layout.');
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +67,7 @@ function renderTemplate(templateName, data, useLayout = true) {
|
|||||||
|
|
||||||
// 检查模板是否存在
|
// 检查模板是否存在
|
||||||
if (!fs.existsSync(templatePath)) {
|
if (!fs.existsSync(templatePath)) {
|
||||||
console.warn(`Template ${templateName}.hbs not found. Using fallback HTML generation.`);
|
throw new Error(`Template ${templateName}.hbs not found. Cannot proceed without template.`);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -72,14 +77,17 @@ function renderTemplate(templateName, data, useLayout = true) {
|
|||||||
// 渲染页面内容
|
// 渲染页面内容
|
||||||
const pageContent = template(data);
|
const pageContent = template(data);
|
||||||
|
|
||||||
// 如果不使用布局或者默认布局不存在,直接返回页面内容
|
// 如果不使用布局,直接返回页面内容
|
||||||
if (!useLayout) {
|
if (!useLayout) {
|
||||||
return pageContent;
|
return pageContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用布局模板
|
// 使用布局模板
|
||||||
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
||||||
if (fs.existsSync(defaultLayoutPath)) {
|
if (!fs.existsSync(defaultLayoutPath)) {
|
||||||
|
throw new Error('Default layout template not found. Cannot proceed without layout.');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 准备布局数据,包含页面内容
|
// 准备布局数据,包含页面内容
|
||||||
const layoutData = {
|
const layoutData = {
|
||||||
@@ -94,18 +102,10 @@ function renderTemplate(templateName, data, useLayout = true) {
|
|||||||
// 渲染完整页面
|
// 渲染完整页面
|
||||||
return layoutTemplate(layoutData);
|
return layoutTemplate(layoutData);
|
||||||
} catch (layoutError) {
|
} catch (layoutError) {
|
||||||
console.error(`Error rendering layout for ${templateName}:`, layoutError);
|
throw new Error(`Error rendering layout for ${templateName}: ${layoutError.message}`);
|
||||||
// 如果布局渲染失败,尝试返回页面内容
|
|
||||||
return pageContent;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果找不到布局模板,返回页面内容
|
|
||||||
console.warn('Default layout template not found. Returning page content only.');
|
|
||||||
return pageContent;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error rendering template ${templateName}:`, error);
|
throw new Error(`Error rendering template ${templateName}: ${error.message}`);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +226,189 @@ function loadModularConfig(dirPath) {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保配置对象具有必要的默认值
|
||||||
|
* @param {Object} config 配置对象
|
||||||
|
* @returns {Object} 处理后的配置对象
|
||||||
|
*/
|
||||||
|
function ensureConfigDefaults(config) {
|
||||||
|
// 创建一个新对象,避免修改原始配置
|
||||||
|
const result = { ...config };
|
||||||
|
|
||||||
|
// 确保基本结构存在
|
||||||
|
result.site = result.site || {};
|
||||||
|
result.navigation = result.navigation || [];
|
||||||
|
result.fonts = result.fonts || {};
|
||||||
|
result.profile = result.profile || {};
|
||||||
|
result.social = result.social || [];
|
||||||
|
result.categories = result.categories || [];
|
||||||
|
|
||||||
|
// 站点基本信息默认值
|
||||||
|
result.site.title = result.site.title || 'MeNav导航';
|
||||||
|
result.site.favicon = result.site.favicon || 'favicon.ico';
|
||||||
|
result.site.logo = result.site.logo || null;
|
||||||
|
result.site.footer = result.site.footer || '';
|
||||||
|
result.site.theme = result.site.theme || {
|
||||||
|
primary: '#4a89dc',
|
||||||
|
background: '#f5f7fa',
|
||||||
|
modeToggle: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保主题颜色设置存在
|
||||||
|
if (!result.site.theme) {
|
||||||
|
result.site.theme = {
|
||||||
|
primary: '#4a89dc',
|
||||||
|
background: '#f5f7fa',
|
||||||
|
modeToggle: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户资料默认值
|
||||||
|
result.profile = result.profile || {};
|
||||||
|
result.profile.title = result.profile.title || '欢迎使用';
|
||||||
|
result.profile.subtitle = result.profile.subtitle || 'MeNav个人导航系统';
|
||||||
|
result.profile.description = result.profile.description || '简单易用的个人导航站点';
|
||||||
|
|
||||||
|
// 为每个类别和站点设置默认值
|
||||||
|
result.categories = result.categories || [];
|
||||||
|
result.categories.forEach(category => {
|
||||||
|
category.name = category.name || '未命名分类';
|
||||||
|
category.sites = category.sites || [];
|
||||||
|
|
||||||
|
// 为每个站点设置默认值
|
||||||
|
category.sites.forEach(site => {
|
||||||
|
site.name = site.name || '未命名站点';
|
||||||
|
site.url = site.url || '#';
|
||||||
|
site.description = site.description || '';
|
||||||
|
site.icon = site.icon || 'fas fa-link';
|
||||||
|
site.external = typeof site.external === 'boolean' ? site.external : true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证配置是否有效
|
||||||
|
* @param {Object} config 配置对象
|
||||||
|
* @returns {boolean} 配置是否有效
|
||||||
|
*/
|
||||||
|
function validateConfig(config) {
|
||||||
|
// 基本结构检查
|
||||||
|
if (!config || typeof config !== 'object') {
|
||||||
|
console.error('配置无效: 配置必须是一个对象');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查必要的顶级属性
|
||||||
|
if (!config.site) {
|
||||||
|
console.warn('配置警告: 缺少site配置节点');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航配置检查
|
||||||
|
if (!Array.isArray(config.navigation)) {
|
||||||
|
console.warn('配置警告: navigation不是数组类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证分类和站点结构
|
||||||
|
if (!Array.isArray(config.categories)) {
|
||||||
|
console.warn('配置警告: categories不是数组类型');
|
||||||
|
} else {
|
||||||
|
// 检查分类结构
|
||||||
|
config.categories.forEach((category, index) => {
|
||||||
|
if (!category.name) {
|
||||||
|
console.warn(`配置警告: 第${index+1}个分类没有name属性`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(category.sites)) {
|
||||||
|
console.warn(`配置警告: 分类 "${category.name || `#${index+1}`}" 的sites不是数组类型`);
|
||||||
|
} else {
|
||||||
|
// 检查站点URL
|
||||||
|
category.sites.forEach((site, siteIndex) => {
|
||||||
|
if (!site.url) {
|
||||||
|
console.warn(`配置警告: 分类 "${category.name || `#${index+1}`}" 中第${siteIndex+1}个站点缺少url属性`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备渲染数据,添加模板所需的特殊属性
|
||||||
|
* @param {Object} config 配置对象
|
||||||
|
* @returns {Object} 增强的渲染数据
|
||||||
|
*/
|
||||||
|
function prepareRenderData(config) {
|
||||||
|
// 创建渲染数据对象,包含原始配置
|
||||||
|
const renderData = { ...config };
|
||||||
|
|
||||||
|
// 添加额外渲染数据
|
||||||
|
renderData._meta = {
|
||||||
|
generated_at: new Date(),
|
||||||
|
version: process.env.npm_package_version || '1.0.0',
|
||||||
|
generator: 'MeNav'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保navigation是数组
|
||||||
|
if (!Array.isArray(renderData.navigation)) {
|
||||||
|
renderData.navigation = [];
|
||||||
|
console.warn('Warning: navigation is not an array. Using empty array instead.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加导航项的活动状态标记和子菜单
|
||||||
|
if (Array.isArray(renderData.navigation)) {
|
||||||
|
renderData.navigation = renderData.navigation.map((item, index) => {
|
||||||
|
const navItem = {
|
||||||
|
...item,
|
||||||
|
isActive: index === 0, // 默认第一项为活动项
|
||||||
|
id: item.id || `nav-${index}`,
|
||||||
|
active: index === 0 // 兼容原有逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 为导航项添加子菜单
|
||||||
|
// 首页页面添加子菜单(分类)
|
||||||
|
if (item.id === 'home' && Array.isArray(renderData.categories)) {
|
||||||
|
navItem.submenu = renderData.categories;
|
||||||
|
}
|
||||||
|
// 书签页面添加子菜单(分类)
|
||||||
|
else if (item.id === 'bookmarks' && renderData.bookmarks && Array.isArray(renderData.bookmarks.categories)) {
|
||||||
|
navItem.submenu = renderData.bookmarks.categories;
|
||||||
|
}
|
||||||
|
// 项目页面添加子菜单
|
||||||
|
else if (item.id === 'projects' && renderData.projects && Array.isArray(renderData.projects.categories)) {
|
||||||
|
navItem.submenu = renderData.projects.categories;
|
||||||
|
}
|
||||||
|
// 文章页面添加子菜单
|
||||||
|
else if (item.id === 'articles' && renderData.articles && Array.isArray(renderData.articles.categories)) {
|
||||||
|
navItem.submenu = renderData.articles.categories;
|
||||||
|
}
|
||||||
|
// 友链页面添加子菜单
|
||||||
|
else if (item.id === 'friends' && renderData.friends && Array.isArray(renderData.friends.categories)) {
|
||||||
|
navItem.submenu = renderData.friends.categories;
|
||||||
|
}
|
||||||
|
// 通用处理:任意自定义页面的子菜单生成
|
||||||
|
else if (renderData[item.id] && renderData[item.id].categories && Array.isArray(renderData[item.id].categories)) {
|
||||||
|
navItem.submenu = renderData[item.id].categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navItem;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为Handlebars模板特别准备navigationData数组
|
||||||
|
renderData.navigationData = renderData.navigation;
|
||||||
|
|
||||||
|
// 确保social数据格式正确
|
||||||
|
if (Array.isArray(renderData.social)) {
|
||||||
|
renderData.socialLinks = renderData.social; // 兼容模板中的不同引用名
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderData;
|
||||||
|
}
|
||||||
|
|
||||||
// 读取配置文件
|
// 读取配置文件
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
// 初始化空配置对象
|
// 初始化空配置对象
|
||||||
@@ -248,73 +431,39 @@ function loadConfig() {
|
|||||||
console.log('Using modular user configuration from config/user/ (highest priority)');
|
console.log('Using modular user configuration from config/user/ (highest priority)');
|
||||||
config = loadModularConfig('config/user');
|
config = loadModularConfig('config/user');
|
||||||
} else if (hasDefaultModularConfig) {
|
} else if (hasDefaultModularConfig) {
|
||||||
// 2. 其次优先级: config/_default/ 目录
|
// 2. 次高优先级: config/_default/ 目录
|
||||||
console.log('Using modular default configuration from config/_default/');
|
console.log('Using modular default configuration from config/_default/');
|
||||||
|
|
||||||
// 从模块化默认配置加载
|
|
||||||
config = loadModularConfig('config/_default');
|
config = loadModularConfig('config/_default');
|
||||||
|
} else {
|
||||||
|
// 3. 最低优先级: 旧版单文件配置 (config.yml or config.yaml)
|
||||||
|
console.log('Using legacy single-file configuration');
|
||||||
|
const legacyConfigPath = fs.existsSync('config.yml') ? 'config.yml' : 'config.yaml';
|
||||||
|
|
||||||
// 检查并加载home.yml中的categories(如果loadModularConfig未正确处理)
|
if (fs.existsSync(legacyConfigPath)) {
|
||||||
const homePath = path.join('config', '_default', 'pages', 'home.yml');
|
|
||||||
if (fs.existsSync(homePath) && (!config.categories || config.categories.length === 0)) {
|
|
||||||
try {
|
try {
|
||||||
const homeContent = fs.readFileSync(homePath, 'utf8');
|
const fileContent = fs.readFileSync(legacyConfigPath, 'utf8');
|
||||||
const homeConfig = yaml.load(homeContent);
|
config = yaml.load(fileContent);
|
||||||
|
console.log(`Loaded legacy configuration from ${legacyConfigPath}`);
|
||||||
if (homeConfig && homeConfig.categories) {
|
|
||||||
// 直接设置categories
|
|
||||||
config.categories = homeConfig.categories;
|
|
||||||
|
|
||||||
// 确保home配置也正确设置
|
|
||||||
if (!config.home) {
|
|
||||||
config.home = homeConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error loading home.yml: ${e.message}`);
|
console.error(`Error loading configuration from ${legacyConfigPath}:`, e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('No configuration found, using default empty config');
|
console.error('No configuration found. Please create a configuration file.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保配置具有必要的结构
|
// 确保配置有默认值并通过验证
|
||||||
config.site = config.site || {};
|
config = ensureConfigDefaults(config);
|
||||||
config.navigation = config.navigation || [];
|
|
||||||
config.fonts = config.fonts || {};
|
|
||||||
config.profile = config.profile || {};
|
|
||||||
config.social = config.social || [];
|
|
||||||
config.categories = config.categories || [];
|
|
||||||
|
|
||||||
// 处理书签文件
|
if (!validateConfig(config)) {
|
||||||
try {
|
console.warn('Configuration validation warnings found. Continuing with defaults.');
|
||||||
let bookmarksConfig = null;
|
|
||||||
let bookmarksSource = null;
|
|
||||||
|
|
||||||
// 按照优先级顺序处理书签配置
|
|
||||||
// 1. 模块化用户书签配置 (最高优先级)
|
|
||||||
if (fs.existsSync('config/user/pages/bookmarks.yml')) {
|
|
||||||
const userBookmarksFile = fs.readFileSync('config/user/pages/bookmarks.yml', 'utf8');
|
|
||||||
bookmarksConfig = yaml.load(userBookmarksFile);
|
|
||||||
bookmarksSource = 'config/user/pages/bookmarks.yml';
|
|
||||||
}
|
|
||||||
// 2. 模块化默认书签配置
|
|
||||||
else if (fs.existsSync('config/_default/pages/bookmarks.yml')) {
|
|
||||||
const defaultBookmarksFile = fs.readFileSync('config/_default/pages/bookmarks.yml', 'utf8');
|
|
||||||
bookmarksConfig = yaml.load(defaultBookmarksFile);
|
|
||||||
bookmarksSource = 'config/_default/pages/bookmarks.yml';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加书签页面配置
|
// 准备渲染数据
|
||||||
if (bookmarksConfig) {
|
const renderData = prepareRenderData(config);
|
||||||
config.bookmarks = bookmarksConfig;
|
|
||||||
console.log(`Using bookmarks configuration from ${bookmarksSource}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading bookmarks configuration:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
return renderData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成导航菜单
|
// 生成导航菜单
|
||||||
@@ -449,6 +598,21 @@ function generateSocialLinks(social) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 尝试使用Handlebars模板
|
||||||
|
try {
|
||||||
|
const socialLinksPath = path.join(process.cwd(), 'templates', 'components', 'social-links.hbs');
|
||||||
|
if (fs.existsSync(socialLinksPath)) {
|
||||||
|
const templateContent = fs.readFileSync(socialLinksPath, 'utf8');
|
||||||
|
const template = handlebars.compile(templateContent);
|
||||||
|
// 确保数据格式正确
|
||||||
|
return template(social); // 社交链接模板直接接收数组
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering social-links template:', error);
|
||||||
|
// 出错时回退到原始生成方法
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到原始生成方法
|
||||||
return social.map(link => `
|
return social.map(link => `
|
||||||
<a href="${escapeHtml(link.url)}" class="nav-item" target="_blank">
|
<a href="${escapeHtml(link.url)}" class="nav-item" target="_blank">
|
||||||
<div class="icon-container">
|
<div class="icon-container">
|
||||||
@@ -572,136 +736,184 @@ function generateFontVariables(config) {
|
|||||||
return css;
|
return css;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成完整的HTML
|
/**
|
||||||
function generateHTML(config) {
|
* 渲染单个页面
|
||||||
const googleFontsLink = generateGoogleFontsLink(config);
|
* @param {string} pageId 页面ID
|
||||||
const fontVariables = generateFontVariables(config);
|
* @param {Object} config 配置数据
|
||||||
const currentYear = new Date().getFullYear();
|
* @returns {string} 渲染后的HTML
|
||||||
|
*/
|
||||||
|
function renderPage(pageId, config) {
|
||||||
|
// 准备页面数据
|
||||||
|
const data = {
|
||||||
|
...config,
|
||||||
|
currentPage: pageId
|
||||||
|
};
|
||||||
|
|
||||||
// 处理所有页面内容
|
// 确保navigation是数组
|
||||||
const pageContents = {};
|
if (!Array.isArray(config.navigation)) {
|
||||||
|
console.warn('Warning: config.navigation is not an array in renderPage. Using empty array.');
|
||||||
|
data.navigation = [];
|
||||||
|
} else {
|
||||||
|
// 设置当前页面为活动页,其他页面为非活动
|
||||||
|
data.navigation = config.navigation.map(nav => {
|
||||||
|
const navItem = {
|
||||||
|
...nav,
|
||||||
|
isActive: nav.id === pageId,
|
||||||
|
active: nav.id === pageId // 兼容原有逻辑
|
||||||
|
};
|
||||||
|
|
||||||
// 首页内容
|
// 确保子菜单信息正确
|
||||||
pageContents.home = generateHomeContent(config);
|
// 首页页面添加子菜单(分类)
|
||||||
|
if (nav.id === 'home' && Array.isArray(config.categories)) {
|
||||||
// 动态生成所有其他页面的内容
|
navItem.submenu = config.categories;
|
||||||
if (config.navigation && Array.isArray(config.navigation)) {
|
}
|
||||||
config.navigation.forEach(navItem => {
|
// 书签页面添加子菜单(分类)
|
||||||
const pageId = navItem.id;
|
else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
|
||||||
// 跳过已处理的首页和搜索结果页
|
navItem.submenu = config.bookmarks.categories;
|
||||||
if (pageId === 'home' || pageId === 'search-results') {
|
}
|
||||||
return;
|
// 项目页面添加子菜单
|
||||||
|
else if (nav.id === 'projects' && config.projects && Array.isArray(config.projects.categories)) {
|
||||||
|
navItem.submenu = config.projects.categories;
|
||||||
|
}
|
||||||
|
// 文章页面添加子菜单
|
||||||
|
else if (nav.id === 'articles' && config.articles && Array.isArray(config.articles.categories)) {
|
||||||
|
navItem.submenu = config.articles.categories;
|
||||||
|
}
|
||||||
|
// 友链页面添加子菜单
|
||||||
|
else if (nav.id === 'friends' && config.friends && Array.isArray(config.friends.categories)) {
|
||||||
|
navItem.submenu = config.friends.categories;
|
||||||
|
}
|
||||||
|
// 通用处理:任意自定义页面的子菜单生成
|
||||||
|
else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) {
|
||||||
|
navItem.submenu = config[nav.id].categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果配置中存在该页面的配置,则生成页面内容
|
return navItem;
|
||||||
if (config[pageId]) {
|
|
||||||
pageContents[pageId] = generatePageContent(pageId, config[pageId]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成首页HTML
|
// 确保socialLinks字段存在
|
||||||
const homeHTML = `
|
data.socialLinks = Array.isArray(config.social) ? config.social : [];
|
||||||
<!-- home页 -->
|
|
||||||
<div class="page active" id="home">
|
|
||||||
${pageContents.home}
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
// 生成其他页面的HTML
|
// 确保navigationData可用(针对模板使用)
|
||||||
const dynamicPagesHTML = Object.entries(pageContents)
|
data.navigationData = data.navigation;
|
||||||
.filter(([id]) => id !== 'home') // 排除首页
|
|
||||||
.map(([id, content]) => `
|
|
||||||
<!-- ${id}页 -->
|
|
||||||
<div class="page" id="${id}">
|
|
||||||
${content}
|
|
||||||
</div>`)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
// 生成搜索结果页面
|
// 页面特定的额外数据
|
||||||
const searchResultsHTML = generateSearchResultsPage(config);
|
if (config[pageId]) {
|
||||||
|
Object.assign(data, config[pageId]);
|
||||||
|
}
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
// 直接渲染页面内容,不使用layout布局(因为layout会在generateHTML中统一应用)
|
||||||
<html lang="zh-CN">
|
return renderTemplate(pageId, data, false);
|
||||||
<head>
|
}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>${escapeHtml(config.site.title)}</title>
|
|
||||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
|
||||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
|
||||||
${googleFontsLink}
|
|
||||||
<style>
|
|
||||||
${fontVariables}
|
|
||||||
</style>
|
|
||||||
<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="loaded">
|
|
||||||
<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>
|
* 生成所有页面的HTML内容
|
||||||
|
* @param {Object} config 配置对象
|
||||||
|
* @returns {Object} 包含所有页面HTML的对象
|
||||||
|
*/
|
||||||
|
function generateAllPagesHTML(config) {
|
||||||
|
// 初始化模板系统(这已经在main中执行过,但为了确保,我们在这里再次调用)
|
||||||
|
loadHandlebarsTemplates();
|
||||||
|
console.log('Handlebars templates available. Using template rendering.');
|
||||||
|
|
||||||
<!-- 左侧导航 -->
|
// 页面内容集合
|
||||||
<nav class="sidebar">
|
const pages = {};
|
||||||
<div class="logo">
|
|
||||||
<h1>${escapeHtml(config.site.logo_text || '导航站')}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-content">
|
// 渲染配置中定义的所有页面
|
||||||
<div class="nav-section">
|
if (Array.isArray(config.navigation)) {
|
||||||
${generateNavigation(config.navigation, config)}
|
config.navigation.forEach(navItem => {
|
||||||
</div>
|
const pageId = navItem.id;
|
||||||
|
|
||||||
<div class="nav-section">
|
// 渲染页面内容
|
||||||
<div class="section-title">
|
pages[pageId] = renderPage(pageId, config);
|
||||||
<i class="fas fa-link"></i>
|
});
|
||||||
</div>
|
}
|
||||||
${generateSocialLinks(config.social)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="copyright">
|
// 确保首页存在
|
||||||
<p>© ${currentYear} <a href="https://github.com/rbetree/menav" target="_blank">MeNav</a></p>
|
if (!pages.home) {
|
||||||
<p>by <a href="https://github.com/rbetree" target="_blank">rbetree</a></p>
|
pages.home = renderPage('home', config);
|
||||||
</div>
|
}
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- 右侧内容区 -->
|
// 确保搜索结果页存在
|
||||||
<main class="content">
|
if (!pages['search-results']) {
|
||||||
<!-- 搜索框容器 -->
|
pages['search-results'] = renderPage('search-results', config);
|
||||||
<div class="search-container">
|
}
|
||||||
<div class="search-box">
|
|
||||||
<input type="text" id="search" placeholder="搜索...">
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${homeHTML}
|
return pages;
|
||||||
${dynamicPagesHTML}
|
}
|
||||||
|
|
||||||
<!-- 搜索结果页 -->
|
/**
|
||||||
<div class="page" id="search-results">
|
* 生成完整的HTML
|
||||||
${searchResultsHTML}
|
* @param {Object} config 配置对象
|
||||||
</div>
|
* @returns {string} 完整HTML
|
||||||
</main>
|
*/
|
||||||
|
function generateHTML(config) {
|
||||||
|
// 获取所有页面内容
|
||||||
|
const pages = generateAllPagesHTML(config);
|
||||||
|
|
||||||
<!-- 主题切换按钮 -->
|
// 获取当前年份
|
||||||
<button class="theme-toggle" aria-label="切换主题">
|
const currentYear = new Date().getFullYear();
|
||||||
<i class="fas fa-moon"></i>
|
|
||||||
</button>
|
// 准备导航数据,添加submenu字段
|
||||||
</div>
|
const navigationData = config.navigation.map(nav => {
|
||||||
<script src="script.js"></script>
|
const navItem = { ...nav };
|
||||||
</body>
|
|
||||||
</html>`;
|
// 根据页面ID获取对应的子菜单项(分类)
|
||||||
|
if (nav.id === 'home' && Array.isArray(config.categories)) {
|
||||||
|
navItem.submenu = config.categories;
|
||||||
|
}
|
||||||
|
// 书签页面添加子菜单(分类)
|
||||||
|
else if (nav.id === 'bookmarks' && config.bookmarks && Array.isArray(config.bookmarks.categories)) {
|
||||||
|
navItem.submenu = config.bookmarks.categories;
|
||||||
|
}
|
||||||
|
// 其他页面添加子菜单
|
||||||
|
else if (config[nav.id] && config[nav.id].categories && Array.isArray(config[nav.id].categories)) {
|
||||||
|
navItem.submenu = config[nav.id].categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 准备Google Fonts链接
|
||||||
|
const googleFontsLink = generateGoogleFontsLink(config);
|
||||||
|
|
||||||
|
// 准备CSS字体变量
|
||||||
|
const fontVariables = generateFontVariables(config);
|
||||||
|
|
||||||
|
// 准备社交链接
|
||||||
|
const socialLinks = generateSocialLinks(config.social);
|
||||||
|
|
||||||
|
// 使用主布局模板
|
||||||
|
const layoutData = {
|
||||||
|
...config,
|
||||||
|
pages,
|
||||||
|
googleFontsLink,
|
||||||
|
fontVariables,
|
||||||
|
navigationData,
|
||||||
|
currentYear,
|
||||||
|
socialLinks,
|
||||||
|
navigation: generateNavigation(config.navigation, config), // 兼容旧版
|
||||||
|
social: Array.isArray(config.social) ? config.social : [] // 兼容旧版
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用Handlebars模板
|
||||||
|
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
||||||
|
if (!fs.existsSync(defaultLayoutPath)) {
|
||||||
|
throw new Error('Default layout template not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载默认布局模板
|
||||||
|
const layoutContent = fs.readFileSync(defaultLayoutPath, 'utf8');
|
||||||
|
const layoutTemplate = handlebars.compile(layoutContent);
|
||||||
|
|
||||||
|
// 渲染模板
|
||||||
|
return layoutTemplate(layoutData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering main HTML template:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制静态文件
|
// 复制静态文件
|
||||||
@@ -751,10 +963,17 @@ function processTemplate(template, config) {
|
|||||||
const googleFontsLink = generateGoogleFontsLink(config);
|
const googleFontsLink = generateGoogleFontsLink(config);
|
||||||
const fontVariables = generateFontVariables(config);
|
const fontVariables = generateFontVariables(config);
|
||||||
|
|
||||||
// 如果Handlebars模板系统可用,优先使用Handlebars渲染
|
// 使用Handlebars渲染
|
||||||
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
const defaultLayoutPath = path.join(process.cwd(), 'templates', 'layouts', 'default.hbs');
|
||||||
if (fs.existsSync(defaultLayoutPath)) {
|
if (!fs.existsSync(defaultLayoutPath)) {
|
||||||
try {
|
throw new Error('Default layout template not found. Cannot proceed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保config.navigation是数组
|
||||||
|
if (!Array.isArray(config.navigation)) {
|
||||||
|
throw new Error('config.navigation is not an array in processTemplate.');
|
||||||
|
}
|
||||||
|
|
||||||
// 准备导航数据,添加submenu字段
|
// 准备导航数据,添加submenu字段
|
||||||
const navigationData = config.navigation.map(nav => {
|
const navigationData = config.navigation.map(nav => {
|
||||||
const navItem = { ...nav };
|
const navItem = { ...nav };
|
||||||
@@ -789,17 +1008,18 @@ function processTemplate(template, config) {
|
|||||||
|
|
||||||
// 准备模板数据
|
// 准备模板数据
|
||||||
const templateData = {
|
const templateData = {
|
||||||
site: config.site,
|
site: config.site || {},
|
||||||
navigation: generateNavigation(config.navigation, config),
|
navigation: generateNavigation(config.navigation, config),
|
||||||
navigationData: navigationData,
|
navigationData: navigationData, // 带有子菜单的导航数据
|
||||||
social: config.social,
|
social: Array.isArray(config.social) ? config.social : [], // 社交数据
|
||||||
categories: config.categories,
|
categories: Array.isArray(config.categories) ? config.categories : [],
|
||||||
profile: config.profile,
|
profile: config.profile || {},
|
||||||
googleFontsLink: googleFontsLink,
|
googleFontsLink: googleFontsLink,
|
||||||
fontVariables: fontVariables,
|
fontVariables: fontVariables,
|
||||||
currentYear: currentYear,
|
currentYear: currentYear,
|
||||||
socialLinks: generateSocialLinks(config.social),
|
socialLinks: generateSocialLinks(config.social), // 使用生成的HTML
|
||||||
searchResults: generateSearchResultsPage(config)
|
searchResults: generateSearchResultsPage(config),
|
||||||
|
body: config.content || '' // 支持布局模板用
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载默认布局模板
|
// 加载默认布局模板
|
||||||
@@ -808,128 +1028,6 @@ function processTemplate(template, config) {
|
|||||||
|
|
||||||
// 渲染模板
|
// 渲染模板
|
||||||
return layoutTemplate(templateData);
|
return layoutTemplate(templateData);
|
||||||
} catch (error) {
|
|
||||||
console.error('Error using Handlebars template:', error);
|
|
||||||
console.log('Falling back to placeholder replacement method.');
|
|
||||||
// 出错时回退到原始占位符替换方法
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成所有页面的HTML
|
|
||||||
let allPagesHTML = generateAllPagesHTML(config);
|
|
||||||
|
|
||||||
// 创建替换映射
|
|
||||||
const replacements = {
|
|
||||||
'{{SITE_TITLE}}': escapeHtml(config.site.title),
|
|
||||||
'{{SITE_LOGO_TEXT}}': escapeHtml(config.site.logo_text || '导航站'),
|
|
||||||
'{{GOOGLE_FONTS}}': googleFontsLink,
|
|
||||||
'{{{FONT_VARIABLES}}}': fontVariables,
|
|
||||||
'{{NAVIGATION}}': generateNavigation(config.navigation, config),
|
|
||||||
'{{SOCIAL_LINKS}}': generateSocialLinks(config.social),
|
|
||||||
'{{CURRENT_YEAR}}': currentYear,
|
|
||||||
'{{SEARCH_RESULTS}}': generateSearchResultsPage(config),
|
|
||||||
'{{ALL_PAGES}}': allPagesHTML
|
|
||||||
};
|
|
||||||
|
|
||||||
// 执行替换
|
|
||||||
let processedTemplate = template;
|
|
||||||
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
||||||
// 使用正则表达式进行全局替换
|
|
||||||
const regex = new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
|
|
||||||
processedTemplate = processedTemplate.replace(regex, value || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成所有页面的HTML
|
|
||||||
function generateAllPagesHTML(config) {
|
|
||||||
let allPagesHTML = '';
|
|
||||||
|
|
||||||
// 准备Handlebars渲染所需的通用数据
|
|
||||||
const templateData = {
|
|
||||||
site: config.site,
|
|
||||||
navigation: config.navigation,
|
|
||||||
navigationData: config.navigation,
|
|
||||||
social: config.social,
|
|
||||||
categories: config.categories,
|
|
||||||
profile: config.profile,
|
|
||||||
googleFontsLink: generateGoogleFontsLink(config),
|
|
||||||
fontVariables: generateFontVariables(config),
|
|
||||||
currentYear: new Date().getFullYear(),
|
|
||||||
socialLinks: generateSocialLinks(config.social),
|
|
||||||
searchResults: generateSearchResultsPage(config)
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确保按照导航顺序生成页面
|
|
||||||
if (config.navigation && Array.isArray(config.navigation)) {
|
|
||||||
// 按照导航中的顺序生成页面
|
|
||||||
config.navigation.forEach(navItem => {
|
|
||||||
const pageId = navItem.id;
|
|
||||||
|
|
||||||
// 跳过搜索结果页
|
|
||||||
if (pageId === 'search-results') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pageContent = '';
|
|
||||||
let isActive = pageId === 'home' ? ' active' : '';
|
|
||||||
|
|
||||||
// 首先尝试使用模板渲染
|
|
||||||
const pageTemplatePath = path.join(process.cwd(), 'templates', 'pages', `${pageId}.hbs`);
|
|
||||||
if (fs.existsSync(pageTemplatePath)) {
|
|
||||||
// 准备页面特定数据
|
|
||||||
const pageTemplateData = { ...templateData };
|
|
||||||
|
|
||||||
// 添加页面特定数据
|
|
||||||
if (pageId === 'home') {
|
|
||||||
// home页面不需要额外数据,已经包含了categories
|
|
||||||
} else if (config[pageId]) {
|
|
||||||
// 其他页面可能有自己的配置
|
|
||||||
Object.assign(pageTemplateData, config[pageId]);
|
|
||||||
if (config[pageId].categories) {
|
|
||||||
pageTemplateData.categories = config[pageId].categories;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用Handlebars直接编译模板(不使用布局)
|
|
||||||
const templateContent = fs.readFileSync(pageTemplatePath, 'utf8');
|
|
||||||
const template = handlebars.compile(templateContent);
|
|
||||||
pageContent = template(pageTemplateData);
|
|
||||||
console.log(`Rendered ${pageId} page using Handlebars template.`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error rendering ${pageId} template:`, error);
|
|
||||||
// 回退到原始生成逻辑
|
|
||||||
pageContent = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果模板渲染失败或模板不存在,使用原始生成逻辑
|
|
||||||
if (!pageContent) {
|
|
||||||
// 根据页面ID生成对应内容
|
|
||||||
if (pageId === 'home') {
|
|
||||||
pageContent = generateHomeContent(config);
|
|
||||||
} else if (config[pageId]) {
|
|
||||||
pageContent = generatePageContent(pageId, config[pageId]);
|
|
||||||
} else {
|
|
||||||
pageContent = `<div class="welcome-section">
|
|
||||||
<h2>页面未配置</h2>
|
|
||||||
<p class="subtitle">请配置 ${pageId} 页面</p>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加页面HTML
|
|
||||||
allPagesHTML += `
|
|
||||||
<!-- ${pageId}页 -->
|
|
||||||
<div class="page${isActive}" id="${pageId}">
|
|
||||||
${pageContent}
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return allPagesHTML;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调试函数
|
// 调试函数
|
||||||
@@ -958,64 +1056,13 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化Handlebars模板系统
|
// 初始化Handlebars模板系统
|
||||||
const handlebarsAvailable = loadHandlebarsTemplates();
|
loadHandlebarsTemplates();
|
||||||
|
|
||||||
// 准备Handlebars渲染所需的通用数据
|
console.log('Handlebars templates are initialized.');
|
||||||
const templateData = {
|
|
||||||
site: config.site,
|
|
||||||
navigation: config.navigation,
|
|
||||||
navigationData: config.navigation,
|
|
||||||
social: config.social,
|
|
||||||
categories: config.categories,
|
|
||||||
profile: config.profile,
|
|
||||||
googleFontsLink: generateGoogleFontsLink(config),
|
|
||||||
fontVariables: generateFontVariables(config),
|
|
||||||
currentYear: new Date().getFullYear(),
|
|
||||||
socialLinks: generateSocialLinks(config.social),
|
|
||||||
searchResults: generateSearchResultsPage(config)
|
|
||||||
};
|
|
||||||
|
|
||||||
let htmlContent = '';
|
// 使用generateHTML函数生成完整的HTML
|
||||||
|
const htmlContent = generateHTML(config);
|
||||||
// 尝试使用Handlebars模板渲染
|
console.log('Successfully rendered all pages using Handlebars templates.');
|
||||||
if (handlebarsAvailable) {
|
|
||||||
console.log('Handlebars templates are available.');
|
|
||||||
|
|
||||||
// 渲染逻辑:先尝试使用页面模板和默认布局渲染
|
|
||||||
const renderedContent = renderTemplate('home', templateData);
|
|
||||||
|
|
||||||
if (renderedContent) {
|
|
||||||
// 使用模板成功渲染
|
|
||||||
htmlContent = renderedContent;
|
|
||||||
console.log('Successfully rendered using Handlebars templates.');
|
|
||||||
} else {
|
|
||||||
// 模板渲染失败,回退到传统模板处理
|
|
||||||
console.log('Failed to render with Handlebars templates, using traditional template processing.');
|
|
||||||
|
|
||||||
const templatePath = 'templates/index.html';
|
|
||||||
if (fs.existsSync(templatePath)) {
|
|
||||||
const template = fs.readFileSync(templatePath, 'utf8');
|
|
||||||
htmlContent = processTemplate(template, config);
|
|
||||||
} else {
|
|
||||||
// 如果没有任何模板,使用纯生成的HTML
|
|
||||||
htmlContent = generateHTML(config);
|
|
||||||
console.log('No template files found, using generated HTML.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handlebars不可用,使用传统模板处理
|
|
||||||
console.log('Handlebars templates are not available, using traditional template processing.');
|
|
||||||
|
|
||||||
const templatePath = 'templates/index.html';
|
|
||||||
if (fs.existsSync(templatePath)) {
|
|
||||||
const template = fs.readFileSync(templatePath, 'utf8');
|
|
||||||
htmlContent = processTemplate(template, config);
|
|
||||||
} else {
|
|
||||||
// 如果没有任何模板,使用纯生成的HTML
|
|
||||||
htmlContent = generateHTML(config);
|
|
||||||
console.log('No template files found, using generated HTML.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成HTML
|
// 生成HTML
|
||||||
fs.writeFileSync('dist/index.html', htmlContent);
|
fs.writeFileSync('dist/index.html', htmlContent);
|
||||||
@@ -1042,3 +1089,4 @@ module.exports = {
|
|||||||
renderTemplate,
|
renderTemplate,
|
||||||
generateAllPagesHTML
|
generateAllPagesHTML
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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}}
|
{{#each this}}
|
||||||
<div class="nav-item-wrapper">
|
<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">
|
<div class="icon-container">
|
||||||
<i class="{{icon}}"></i>
|
<i class="{{icon}}"></i>
|
||||||
</div>
|
</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}}
|
{{#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>
|
<i class="{{#if icon}}{{icon}}{{else}}fas fa-link{{/if}}"></i>
|
||||||
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
<h3>{{#if name}}{{name}}{{else}}未命名站点{{/if}}</h3>
|
||||||
<p>{{description}}</p>
|
<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">
|
<div class="section-title">
|
||||||
<i class="fas fa-link"></i>
|
<i class="fas fa-link"></i>
|
||||||
</div>
|
</div>
|
||||||
|
{{#if social}}
|
||||||
|
{{> social-links social}}
|
||||||
|
{{else}}
|
||||||
{{{socialLinks}}}
|
{{{socialLinks}}}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -92,21 +96,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{{body}}}
|
<!-- 页面容器 -->
|
||||||
|
{{#each pages}}
|
||||||
<!-- 搜索结果页 -->
|
<div class="page {{@key}}{{#if @first}} active{{/if}}" id="{{@key}}">
|
||||||
<div class="page" id="search-results">
|
{{{this}}}
|
||||||
<div class="welcome-section">
|
|
||||||
<h2>搜索结果</h2>
|
|
||||||
<p class="subtitle">在所有页面中找到的匹配项</p>
|
|
||||||
</div>
|
</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}}
|
||||||
</div>
|
|
||||||
</main>
|
</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">
|
||||||
<div class="welcome-section">
|
|
||||||
<h2>{{profile.title}}</h2>
|
<h2>{{profile.title}}</h2>
|
||||||
<h3>{{profile.subtitle}}</h3>
|
<h3>{{profile.subtitle}}</h3>
|
||||||
<p class="subtitle">{{profile.description}}</p>
|
<p class="subtitle">{{profile.description}}</p>
|
||||||
</div>
|
|
||||||
{{#each categories}}
|
|
||||||
{{> category}}
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
</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