修改配置加载逻辑
配置加载优先级使用完全替换规则,而不是深度合并
This commit is contained in:
43
README.md
43
README.md
@@ -16,7 +16,7 @@
|
||||
- [快速部署到GitHub Pages](#快速部署到github-pages)
|
||||
- [部署到服务器](#部署到服务器)
|
||||
- [设置配置文件](#设置配置文件)
|
||||
- [使用单文件配置](#使用单文件配置)
|
||||
- [使用双文件配置](#使用双文件配置)
|
||||
- [使用模块化配置](#使用模块化配置)
|
||||
- [书签导入功能](#书签导入功能)
|
||||
- [贡献](#贡献)
|
||||
@@ -35,7 +35,7 @@
|
||||
- 👥 支持展示社交媒体链接
|
||||
- 📝 支持多个内容页面(首页、项目、文章、友链)
|
||||
- 📌 支持从浏览器导入书签
|
||||
- 🧩 模块化配置/单文件配置
|
||||
- 🧩 模块化配置/双文件配置
|
||||
- 🔄 可部署到GitHub Pages或任何服务器
|
||||
|
||||
## 近期更新
|
||||
@@ -146,7 +146,7 @@ npm install
|
||||
```
|
||||
|
||||
3. 修改配置
|
||||
- 可以选择使用单文件配置或模块化配置(见[设置配置文件](#设置配置文件))
|
||||
- 可以选择使用双文件配置或模块化配置(见[设置配置文件](#设置配置文件))
|
||||
- 自定义网站内容、导航链接、社交媒体链接等
|
||||
|
||||
4. 本地预览
|
||||
@@ -181,7 +181,7 @@ npm run dev
|
||||
#### 第二步:自定义配置
|
||||
|
||||
1. 创建个人配置文件:
|
||||
- 可以使用单文件配置或模块化配置
|
||||
- 可以使用双文件配置或模块化配置
|
||||
- 推荐使用模块化配置(见[使用模块化配置](#使用模块化配置))
|
||||
- 提交您的配置文件到仓库
|
||||
|
||||
@@ -239,22 +239,24 @@ server {
|
||||
|
||||
## 设置配置文件
|
||||
|
||||
MeNav支持两种配置方式:单文件配置和模块化配置(推荐)。
|
||||
MeNav支持两种配置方式:双文件配置和模块化配置(推荐)。
|
||||
|
||||
在加载配置时遵循以下优先级顺序:
|
||||
|
||||
1. `config/user/` (模块化用户配置)
|
||||
2. `config.user.yml`(单文件用户配置)
|
||||
1. `config/user/` (模块化用户配置)(优先级最高)
|
||||
2. `config.user.yml`和 `bookmarks.user.yml`(双文件用户配置)
|
||||
3. `config/_default/` (模块化默认配置)
|
||||
4. `config.yml`(单文件默认配置)
|
||||
4. `config.yml`和`bookmarks.yml`(双文件默认配置)(优先级最低)
|
||||
|
||||
### 使用单文件配置
|
||||
**注意:** 各优先级配置间采用完全替换策略,而非合并。系统会选择存在的最高优先级配置,完全忽略其他低优先级配置。这确保了模块化配置和双文件配置都能独立使用,避免混合配置带来的意外行为。
|
||||
|
||||
单文件配置是最简单的配置方式,适合小型网站和快速开始。
|
||||
### 使用双文件配置
|
||||
|
||||
双文件配置由 `config.yml`/`config.user.yml` 和 `bookmarks.yml`/`bookmarks.user.yml` 两个文件组成,适合小型网站和快速开始。
|
||||
|
||||
1. **创建配置文件**:
|
||||
- 复制 `config.yml` 为 `config.user.yml`
|
||||
- 编辑 `config.user.yml` 文件
|
||||
- 对于书签配置,请勿手动创建 `bookmarks.user.yml`,它应通过[书签导入功能](#书签导入功能)自动生成
|
||||
- 编辑 `config.user.yml` 添加您的配置
|
||||
|
||||
2. **配置文件结构**:
|
||||
```yaml
|
||||
@@ -376,6 +378,15 @@ config/
|
||||
|
||||
MeNav支持从浏览器导入书签,快速批量添加网站链接。
|
||||
|
||||
在加载配置时遵循以下优先级顺序:
|
||||
|
||||
1. `config/user/pages/bookmarks.yml` (模块化用户配置)
|
||||
2. `bookmarks.user.yml`(双文件用户配置的书签部分)
|
||||
3. `config/_default/pages/bookmarks.yml` (模块化默认配置)
|
||||
4. `bookmarks.yml`(双文件默认配置的书签部分)
|
||||
|
||||
**注意:** 与主配置一样,书签配置也采用完全替换策略,系统只会使用找到的最高优先级书签配置,完全忽略低优先级配置。
|
||||
|
||||
### 导入步骤
|
||||
|
||||
1. **从浏览器导出书签**
|
||||
@@ -392,9 +403,13 @@ MeNav支持从浏览器导入书签,快速批量添加网站链接。
|
||||
3. **书签处理结果**
|
||||
- 生成的书签配置会保存到 `bookmarks.user.yml`
|
||||
- 如果使用模块化配置,也会同时保存到 `config/user/pages/bookmarks.yml`
|
||||
- 书签会自动添加到导航菜单中
|
||||
- 自动开始构建
|
||||
|
||||
> 有关书签导入功能的更多信息,请参阅源代码中的相关注释。
|
||||
### 注意事项
|
||||
|
||||
- 仅支持标准HTML格式的书签文件
|
||||
- 每次只会处理目录中最新的一个书签文件
|
||||
- 处理完成后,书签文件会被自动清除,以防止重复处理
|
||||
|
||||
## 贡献
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# 书签导入目录
|
||||
|
||||
将从浏览器导出的HTML书签文件放在此目录中,系统会自动处理并导入到您的导航站。
|
||||
|
||||
## 使用步骤
|
||||
|
||||
1. 从浏览器导出书签为HTML文件:
|
||||
- **Chrome**: 书签管理器 → 更多 → 导出书签
|
||||
- **Firefox**: 书签库 → 显示所有书签 → 导入和备份 → 导出书签为HTML
|
||||
- **Edge**: 设置 → 收藏夹 → 管理收藏夹 → 导出为HTML文件
|
||||
|
||||
2. 将导出的HTML文件放入此目录中
|
||||
|
||||
3. 提交并推送到GitHub:
|
||||
```bash
|
||||
git add bookmarks/你的书签文件.html
|
||||
git commit -m "添加书签文件"
|
||||
git push
|
||||
```
|
||||
|
||||
4. GitHub Actions将自动处理书签文件,生成`bookmarks.user.yml`,并重新构建站点
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅支持标准HTML格式的书签文件
|
||||
- 每次只会处理目录中最新的一个书签文件
|
||||
- 处理完成后,书签文件会被自动清除,以防止重复处理
|
||||
- 已导入的书签可以在生成的`bookmarks.user.yml`文件中查看和编辑
|
||||
- 系统会优先使用`bookmarks.user.yml`的配置,如果存在`bookmarks.yml`,会自动合并两者的内容(用户配置优先)
|
||||
266
src/generator.js
266
src/generator.js
@@ -16,48 +16,76 @@ function escapeHtml(unsafe) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件合并配置到主配置对象
|
||||
* @param {Object} config 主配置对象
|
||||
* 加载单个配置文件
|
||||
* @param {string} filePath 配置文件路径
|
||||
* @returns {Object|null} 配置对象,如果文件不存在或加载失败则返回null
|
||||
*/
|
||||
function mergeConfigFromFile(config, filePath) {
|
||||
function loadSingleConfig(filePath) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const fileConfig = yaml.load(fileContent);
|
||||
|
||||
// 提取文件名(不含扩展名)作为配置键
|
||||
const configKey = path.basename(filePath, path.extname(filePath));
|
||||
|
||||
// 如果是site或navigation文件,直接合并到主配置
|
||||
if (configKey === 'site' || configKey === 'navigation') {
|
||||
if (!config[configKey]) config[configKey] = {};
|
||||
deepMerge(config[configKey], fileConfig);
|
||||
} else {
|
||||
// 其他配置直接合并到根级别
|
||||
deepMerge(config, fileConfig);
|
||||
}
|
||||
|
||||
console.log(`Loaded and merged configuration from ${filePath}`);
|
||||
console.log(`Loaded configuration from ${filePath}`);
|
||||
return fileConfig;
|
||||
} catch (e) {
|
||||
console.error(`Error loading configuration from ${filePath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载页面配置目录中的所有配置文件
|
||||
* @param {Object} config 主配置对象
|
||||
* @param {string} dirPath 页面配置目录路径
|
||||
* 加载模块化配置目录
|
||||
* @param {string} dirPath 配置目录路径
|
||||
* @returns {Object|null} 配置对象,如果目录不存在或加载失败则返回null
|
||||
*/
|
||||
function loadPageConfigs(config, dirPath) {
|
||||
if (fs.existsSync(dirPath)) {
|
||||
const files = fs.readdirSync(dirPath).filter(file =>
|
||||
function loadModularConfig(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = {
|
||||
site: {},
|
||||
navigation: [],
|
||||
fonts: {},
|
||||
profile: {},
|
||||
social: [],
|
||||
categories: []
|
||||
};
|
||||
|
||||
// 加载基础配置
|
||||
const siteConfigPath = path.join(dirPath, 'site.yml');
|
||||
if (fs.existsSync(siteConfigPath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(siteConfigPath, 'utf8');
|
||||
config.site = yaml.load(fileContent);
|
||||
console.log(`Loaded site configuration from ${siteConfigPath}`);
|
||||
} catch (e) {
|
||||
console.error(`Error loading site configuration from ${siteConfigPath}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
const navConfigPath = path.join(dirPath, 'navigation.yml');
|
||||
if (fs.existsSync(navConfigPath)) {
|
||||
try {
|
||||
const fileContent = fs.readFileSync(navConfigPath, 'utf8');
|
||||
config.navigation = yaml.load(fileContent);
|
||||
console.log(`Loaded navigation configuration from ${navConfigPath}`);
|
||||
} catch (e) {
|
||||
console.error(`Error loading navigation configuration from ${navConfigPath}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载页面配置
|
||||
const pagesPath = path.join(dirPath, 'pages');
|
||||
if (fs.existsSync(pagesPath)) {
|
||||
const files = fs.readdirSync(pagesPath).filter(file =>
|
||||
file.endsWith('.yml') || file.endsWith('.yaml'));
|
||||
|
||||
files.forEach(file => {
|
||||
try {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const filePath = path.join(pagesPath, file);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const fileConfig = yaml.load(fileContent);
|
||||
|
||||
@@ -69,49 +97,12 @@ function loadPageConfigs(config, dirPath) {
|
||||
|
||||
console.log(`Loaded page configuration from ${filePath}`);
|
||||
} catch (e) {
|
||||
console.error(`Error loading page configuration from ${path.join(dirPath, file)}:`, e);
|
||||
console.error(`Error loading page configuration from ${path.join(pagesPath, file)}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并两个对象
|
||||
* @param {Object} target 目标对象
|
||||
* @param {Object} source 源对象
|
||||
* @returns {Object} 合并后的对象
|
||||
*/
|
||||
function deepMerge(target, source) {
|
||||
if (!source) return target;
|
||||
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
if (typeof source[key] === 'object' && source[key] !== null) {
|
||||
// 确保目标对象有这个属性
|
||||
if (!target[key]) {
|
||||
if (Array.isArray(source[key])) {
|
||||
target[key] = [];
|
||||
} else {
|
||||
target[key] = {};
|
||||
}
|
||||
}
|
||||
|
||||
// 递归合并
|
||||
if (Array.isArray(source[key])) {
|
||||
// 对于数组,直接替换或添加
|
||||
target[key] = source[key];
|
||||
} else {
|
||||
// 对于对象,递归合并
|
||||
deepMerge(target[key], source[key]);
|
||||
}
|
||||
} else {
|
||||
// 对于基本类型,直接替换
|
||||
target[key] = source[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
return config;
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
@@ -126,54 +117,44 @@ function loadConfig() {
|
||||
categories: []
|
||||
};
|
||||
|
||||
// 处理配置目录结构,按照优先级从低到高加载
|
||||
// 4. 最低优先级: config.yml (传统默认配置)
|
||||
if (fs.existsSync('config.yml')) {
|
||||
const defaultConfigFile = fs.readFileSync('config.yml', 'utf8');
|
||||
const defaultConfig = yaml.load(defaultConfigFile);
|
||||
deepMerge(config, defaultConfig);
|
||||
console.log('Loaded legacy default config.yml');
|
||||
}
|
||||
|
||||
// 3. 其次优先级: config/_default/ 目录
|
||||
if (fs.existsSync('config/_default')) {
|
||||
console.log('Loading modular default configuration from config/_default/');
|
||||
|
||||
// 加载基础配置
|
||||
mergeConfigFromFile(config, 'config/_default/site.yml');
|
||||
mergeConfigFromFile(config, 'config/_default/navigation.yml');
|
||||
|
||||
// 加载页面配置
|
||||
if (fs.existsSync('config/_default/pages')) {
|
||||
loadPageConfigs(config, 'config/_default/pages');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 次高优先级: config.user.yml (传统用户配置)
|
||||
if (fs.existsSync('config.user.yml')) {
|
||||
const userConfigFile = fs.readFileSync('config.user.yml', 'utf8');
|
||||
const userConfig = yaml.load(userConfigFile);
|
||||
|
||||
// 深度合并配置
|
||||
deepMerge(config, userConfig);
|
||||
console.log('Merged legacy user configuration from config.user.yml');
|
||||
}
|
||||
// 检查所有可能的配置来源是否存在
|
||||
const hasUserModularConfig = fs.existsSync('config/user');
|
||||
const hasUserLegacyConfig = fs.existsSync('config.user.yml');
|
||||
const hasDefaultModularConfig = fs.existsSync('config/_default');
|
||||
const hasDefaultLegacyConfig = fs.existsSync('config.yml');
|
||||
|
||||
// 根据优先级顺序选择最高优先级的配置
|
||||
if (hasUserModularConfig) {
|
||||
// 1. 最高优先级: config/user/ 目录
|
||||
if (fs.existsSync('config/user')) {
|
||||
console.log('Loading modular user configuration from config/user/ (highest priority)');
|
||||
|
||||
// 覆盖基础配置
|
||||
mergeConfigFromFile(config, 'config/user/site.yml');
|
||||
mergeConfigFromFile(config, 'config/user/navigation.yml');
|
||||
|
||||
// 覆盖页面配置
|
||||
if (fs.existsSync('config/user/pages')) {
|
||||
loadPageConfigs(config, 'config/user/pages');
|
||||
}
|
||||
console.log('Using modular user configuration from config/user/ (highest priority)');
|
||||
config = loadModularConfig('config/user');
|
||||
} else if (hasUserLegacyConfig) {
|
||||
// 2. 次高优先级: config.user.yml (传统用户配置)
|
||||
console.log('Using legacy user configuration from config.user.yml');
|
||||
const userConfigFile = fs.readFileSync('config.user.yml', 'utf8');
|
||||
config = yaml.load(userConfigFile);
|
||||
} else if (hasDefaultModularConfig) {
|
||||
// 3. 其次优先级: config/_default/ 目录
|
||||
console.log('Using modular default configuration from config/_default/');
|
||||
config = loadModularConfig('config/_default');
|
||||
} else if (hasDefaultLegacyConfig) {
|
||||
// 4. 最低优先级: config.yml (传统默认配置)
|
||||
console.log('Using legacy default config.yml');
|
||||
const defaultConfigFile = fs.readFileSync('config.yml', 'utf8');
|
||||
config = yaml.load(defaultConfigFile);
|
||||
} else {
|
||||
console.log('No configuration found, using default empty config');
|
||||
}
|
||||
|
||||
// 处理书签文件(保持现有功能)
|
||||
// 确保配置具有必要的结构
|
||||
config.site = config.site || {};
|
||||
config.navigation = config.navigation || [];
|
||||
config.fonts = config.fonts || {};
|
||||
config.profile = config.profile || {};
|
||||
config.social = config.social || [];
|
||||
config.categories = config.categories || [];
|
||||
|
||||
// 处理书签文件(保持现有功能,但使用新逻辑)
|
||||
try {
|
||||
let bookmarksConfig = null;
|
||||
let bookmarksSource = null;
|
||||
@@ -310,18 +291,30 @@ function generateNavigation(navigation, config) {
|
||||
|
||||
// 生成网站卡片HTML
|
||||
function generateSiteCards(sites) {
|
||||
if (!sites || !Array.isArray(sites) || sites.length === 0) {
|
||||
return `<p class="empty-sites">暂无网站</p>`;
|
||||
}
|
||||
|
||||
return sites.map(site => `
|
||||
<a href="${escapeHtml(site.url)}" class="site-card" title="${escapeHtml(site.name)} - ${escapeHtml(site.description)}">
|
||||
<i class="${escapeHtml(site.icon)}"></i>
|
||||
<h3>${escapeHtml(site.name)}</h3>
|
||||
<p>${escapeHtml(site.description)}</p>
|
||||
<a href="${escapeHtml(site.url)}" class="site-card" title="${escapeHtml(site.name)} - ${escapeHtml(site.description || '')}">
|
||||
<i class="${escapeHtml(site.icon || 'fas fa-link')}"></i>
|
||||
<h3>${escapeHtml(site.name || '未命名站点')}</h3>
|
||||
<p>${escapeHtml(site.description || '')}</p>
|
||||
</a>`).join('\n');
|
||||
}
|
||||
|
||||
// 生成分类HTML
|
||||
// 生成分类板块
|
||||
function generateCategories(categories) {
|
||||
return categories.map(category => `
|
||||
if (!categories || !Array.isArray(categories) || categories.length === 0) {
|
||||
return `
|
||||
<section class="category">
|
||||
<h2><i class="fas fa-info-circle"></i> 暂无分类</h2>
|
||||
<p>请在配置文件中添加分类</p>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
return categories.map(category => `
|
||||
<section class="category" id="${escapeHtml(category.name)}">
|
||||
<h2><i class="${escapeHtml(category.icon)}"></i> ${escapeHtml(category.name)}</h2>
|
||||
<div class="sites-grid">
|
||||
${generateSiteCards(category.sites)}
|
||||
@@ -331,45 +324,66 @@ function generateCategories(categories) {
|
||||
|
||||
// 生成社交链接HTML
|
||||
function generateSocialLinks(social) {
|
||||
if (!social || !Array.isArray(social) || social.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return social.map(link => `
|
||||
<a href="${escapeHtml(link.url)}" class="nav-item" target="_blank">
|
||||
<div class="icon-container">
|
||||
<i class="${escapeHtml(link.icon)}"></i>
|
||||
<i class="${escapeHtml(link.icon || 'fas fa-link')}"></i>
|
||||
</div>
|
||||
<span class="nav-text">${escapeHtml(link.name)}</span>
|
||||
<span class="nav-text">${escapeHtml(link.name || '社交链接')}</span>
|
||||
<i class="fas fa-external-link-alt external-icon"></i>
|
||||
</a>`).join('\n');
|
||||
}
|
||||
|
||||
// 生成欢迎区域和首页内容
|
||||
function generateHomeContent(config) {
|
||||
const profile = config.profile || {};
|
||||
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(config.profile.title)}</h2>
|
||||
<h3>${escapeHtml(config.profile.subtitle)}</h3>
|
||||
<p class="subtitle">${escapeHtml(config.profile.description)}</p>
|
||||
<h2>${escapeHtml(profile.title || '欢迎使用')}</h2>
|
||||
<h3>${escapeHtml(profile.subtitle || '个人导航站')}</h3>
|
||||
<p class="subtitle">${escapeHtml(profile.description || '快速访问您的常用网站')}</p>
|
||||
</div>
|
||||
${generateCategories(config.categories)}`;
|
||||
}
|
||||
|
||||
// 生成页面内容
|
||||
function generatePageContent(pageId, data) {
|
||||
// 如果是book、marks页面,使用bookmarks配置
|
||||
if (pageId === 'bookmarks' && data) {
|
||||
// 确保数据对象存在且有必要的字段
|
||||
if (!data) {
|
||||
console.error(`Missing data for page: ${pageId}`);
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(data.title)}</h2>
|
||||
<p class="subtitle">${escapeHtml(data.subtitle)}</p>
|
||||
<h2>页面未配置</h2>
|
||||
<p class="subtitle">请配置 ${pageId} 页面</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
const title = data.title || `${pageId} 页面`;
|
||||
const subtitle = data.subtitle || '';
|
||||
const categories = data.categories || [];
|
||||
|
||||
// 如果是书签页面,使用bookmarks配置
|
||||
if (pageId === 'bookmarks') {
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(title)}</h2>
|
||||
<p class="subtitle">${escapeHtml(subtitle)}</p>
|
||||
</div>
|
||||
${generateCategories(data.categories)}`;
|
||||
${generateCategories(categories)}`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="welcome-section">
|
||||
<h2>${escapeHtml(data.title)}</h2>
|
||||
<p class="subtitle">${escapeHtml(data.subtitle)}</p>
|
||||
<h2>${escapeHtml(title)}</h2>
|
||||
<p class="subtitle">${escapeHtml(subtitle)}</p>
|
||||
</div>
|
||||
${generateCategories(data.categories)}`;
|
||||
${generateCategories(categories)}`;
|
||||
}
|
||||
|
||||
// 生成搜索结果页面
|
||||
|
||||
Reference in New Issue
Block a user