Merge branch 'main' into pr/jiashu1024/1622

This commit is contained in:
tangly1024.com
2023-11-20 17:02:15 +08:00
361 changed files with 2469 additions and 2234 deletions

View File

@@ -3,15 +3,6 @@ import FileCache from './local_file_cache'
import MongoCache from './mongo_db_cache'
import BLOG from '@/blog.config'
let api
if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) {
api = MongoCache
} else if (process.env.ENABLE_FILE_CACHE) {
api = FileCache
} else {
api = MemoryCache
}
/**
* 为减少频繁接口请求notion数据将被缓存
* @param {*} key
@@ -19,11 +10,11 @@ if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) {
*/
export async function getDataFromCache(key, force) {
if (BLOG.ENABLE_CACHE || force) {
const dataFromCache = await api.getCache(key)
const dataFromCache = await getApi().getCache(key)
if (JSON.stringify(dataFromCache) === '[]') {
return null
}
return api.getCache(key)
return getApi().getCache(key)
} else {
return null
}
@@ -33,12 +24,26 @@ export async function setDataToCache(key, data) {
if (!data) {
return
}
await api.setCache(key, data)
await getApi().setCache(key, data)
}
export async function delCacheData(key) {
if (!BLOG.ENABLE_CACHE) {
return
}
await api.delCache(key)
await getApi().delCache(key)
}
/**
* 缓存实现类
* @returns
*/
function getApi() {
if (process.env.MONGO_DB_URL && process.env.MONGO_DB_NAME) {
return MongoCache
} else if (process.env.ENABLE_FILE_CACHE) {
return FileCache
} else {
return MemoryCache
}
}

View File

@@ -41,7 +41,7 @@ export async function setCache (key, data) {
fs.writeFileSync(jsonFile, JSON.stringify(json))
}
export async function delCache (key, data) {
export async function delCache (key) {
const exist = await fs.existsSync(jsonFile)
const json = exist ? JSON.parse(await fs.readFileSync(jsonFile)) : {}
delete json.key
@@ -49,4 +49,12 @@ export async function delCache (key, data) {
fs.writeFileSync(jsonFile, JSON.stringify(json))
}
/**
* 清理缓存
*/
export async function cleanCache() {
const json = {}
fs.writeFileSync(jsonFile, JSON.stringify(json))
}
export default { getCache, setCache, delCache }

93
lib/config.js Normal file
View File

@@ -0,0 +1,93 @@
'use client'
import BLOG from '@/blog.config'
import { useGlobal } from './global'
import { deepClone } from './utils'
/**
* 读取配置顺序
* 1. 优先读取NotionConfig表
* 2. 其次读取环境变量
* 3. 再读取blog.config.js / 或各个主题的CONFIG文件
* @param {*} key 参数名
* @param {*} defaultVal ; 参数不存在默认返回值
* @param {*} extendConfig ; 参考配置对象{key:val}如果notion中找不到优先尝试在这里面查找
* @returns
*/
export const siteConfig = (key, defaultVal = null, extendConfig = null) => {
let global = null
try {
// eslint-disable-next-line react-hooks/rules-of-hooks
global = useGlobal()
} catch (error) {}
// 首先 配置最优先读取NOTION中的表格配置
let val = null
let siteInfo = null
if (global) {
val = global.NOTION_CONFIG?.[key]
siteInfo = global.siteInfo
// console.log('当前变量', key, val)
}
if (!val) {
// 这里针对部分key做一些兼容处理
switch (key) {
case 'HOME_BANNER_IMAGE':
val = siteInfo?.pageCover // 封面图取Notion的封面
break
case 'AVATAR':
val = siteInfo?.icon // 封面图取Notion的头像
break
case 'TITLE':
val = siteInfo?.title // 标题取Notion中的标题
break
case 'DESCRIPTION':
val = siteInfo?.description // 标题取Notion中的标题
break
}
}
// 其次 有传入的配置参考,则尝试读取
if (!val && extendConfig) {
val = extendConfig[key]
}
// 其次 NOTION没有找到配置则会读取blog.config.js文件
if (!val) {
val = BLOG[key]
}
if (!val) {
return defaultVal
} else {
if (typeof val === 'string') {
return val;
} else {
try {
return JSON.parse(val);
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
return val;
}
}
}
}
/**
* 读取所有配置
* 1. 优先读取NotionConfig表
* 2. 其次读取环境变量
* 3. 再读取blog.config.js文件
* @param {*} key
* @returns
*/
export const siteConfigMap = () => {
const val = deepClone(BLOG)
for (const key in val) {
val[key] = siteConfig(key)
// console.log('site', key, val[key], siteConfig(key))
}
return val
}

View File

@@ -1,34 +1,65 @@
import { generateLocaleDict, initLocale } from './lang'
import { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { THEMES, initDarkMode, saveDarkModeToCookies } from '@/themes/theme'
import BLOG from '@/blog.config'
import { THEMES, initDarkMode } from '@/themes/theme'
import NProgress from 'nprogress'
import { getQueryVariable, isBrowser } from './utils'
const GlobalContext = createContext()
/**
* 全局变量Provider包括语言本地化、样式主题、搜索词
* 定义全局变量,包括语言、主题、深色模式、加载状态
* @param children
* @returns {JSX.Element}
* @constructor
*/
export function GlobalContextProvider(props) {
const { children, siteInfo, categoryOptions, tagOptions } = props
const { children, siteInfo, categoryOptions, tagOptions, NOTION_CONFIG } = props
const router = useRouter()
const [lang, updateLang] = useState(BLOG.LANG) // 默认语言
const [locale, updateLocale] = useState(generateLocaleDict(BLOG.LANG)) // 默认语言
const [theme, setTheme] = useState(BLOG.THEME) // 默认博客主题
const [isDarkMode, updateDarkMode] = useState(BLOG.APPEARANCE === 'dark') // 默认深色模式
const [lang, updateLang] = useState(NOTION_CONFIG?.LANG || BLOG.LANG) // 默认语言
const [locale, updateLocale] = useState(generateLocaleDict(NOTION_CONFIG?.LANG || BLOG.LANG)) // 默认语言
const [theme, setTheme] = useState(NOTION_CONFIG?.THEME || BLOG.THEME) // 默认博客主题
const [isDarkMode, updateDarkMode] = useState(NOTION_CONFIG?.APPEARANCE || BLOG.APPEARANCE === 'dark') // 默认深色模式
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
// 切换主题
function switchTheme() {
const currentIndex = THEMES.indexOf(theme)
const newIndex = currentIndex < THEMES.length - 1 ? currentIndex + 1 : 0
const newTheme = THEMES[newIndex]
const query = router.query
query.theme = newTheme
router.push({ pathname: router.pathname, query })
return newTheme
}
// 切换深色模式
const toggleDarkMode = () => {
const newStatus = !isDarkMode
saveDarkModeToCookies(newStatus)
updateDarkMode(newStatus)
const htmlElement = document.getElementsByTagName('html')[0]
htmlElement.classList?.remove(newStatus ? 'light' : 'dark')
htmlElement.classList?.add(newStatus ? 'dark' : 'light')
}
/**
* 更新语言
*/
function changeLang(lang) {
if (lang) {
updateLang(lang)
updateLocale(generateLocaleDict(lang))
}
}
useEffect(() => {
initLocale(lang, locale, updateLang, updateLocale)
initDarkMode(updateDarkMode)
initTheme()
initLocale(lang, locale, updateLang, updateLocale)
}, [])
// 加载进度条
useEffect(() => {
const handleStart = (url) => {
NProgress.start()
@@ -43,8 +74,7 @@ export function GlobalContextProvider(props) {
NProgress.done()
setOnLoading(false)
}
const queryTheme = getQueryVariable('theme') || BLOG.THEME
setTheme(queryTheme)
router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeError', handleStop)
router.events.on('routeChangeComplete', handleStop)
@@ -55,21 +85,14 @@ export function GlobalContextProvider(props) {
}
}, [router])
// 切换主题
function switchTheme() {
const currentIndex = THEMES.indexOf(theme)
const newIndex = currentIndex < THEMES.length - 1 ? currentIndex + 1 : 0
const newTheme = THEMES[newIndex]
const query = router.query
query.theme = newTheme
router.push({ pathname: router.pathname, query })
return newTheme
}
return (
<GlobalContext.Provider value={{
NOTION_CONFIG,
toggleDarkMode,
onLoading,
setOnLoading,
lang,
changeLang,
locale,
updateLocale,
isDarkMode,
@@ -86,23 +109,4 @@ export function GlobalContextProvider(props) {
)
}
/**
* 切换主题时的特殊处理
* @param {*} setTheme
*/
const initTheme = () => {
if (isBrowser) {
setTimeout(() => {
const elements = document.querySelectorAll('[id^="theme-"]')
if (elements?.length > 1) {
elements[elements.length - 1].scrollIntoView()
// 删除前面的元素,只保留最后一个元素
for (let i = 0; i < elements.length - 1; i++) {
elements[i].parentNode.removeChild(elements[i])
}
}
}, 500)
}
}
export const useGlobal = () => useContext(GlobalContext)

View File

@@ -12,7 +12,7 @@ import { getQueryVariable, isBrowser, mergeDeep } from './utils'
* 在这里配置所有支持的语言
* 国家-地区
*/
const lang = {
const LANGS = {
'en-US': enUS,
'zh-CN': zhCN,
'zh-HK': zhHK,
@@ -22,7 +22,7 @@ const lang = {
'ja-JP': jaJP
}
export default lang
export default LANGS
/**
* 获取当前语言字典
@@ -30,33 +30,33 @@ export default lang
* @returns 不同语言对应字典
*/
export function generateLocaleDict(langString) {
const supportedLocales = Object.keys(lang)
const supportedLocales = Object.keys(LANGS)
let userLocale
// 将语言字符串拆分为语言和地区代码,例如将 "zh-CN" 拆分为 "zh" 和 "CN"
const [language, region] = langString.split(/[-_]/)
const [language, region] = langString?.split(/[-_]/)
// 优先匹配语言和地区都匹配的情况
const specificLocale = `${language}-${region}`
if (supportedLocales.includes(specificLocale)) {
userLocale = lang[specificLocale]
userLocale = LANGS[specificLocale]
}
// 然后尝试匹配只有语言匹配的情况
if (!userLocale) {
const languageOnlyLocales = supportedLocales.filter(locale => locale.startsWith(language))
if (languageOnlyLocales.length > 0) {
userLocale = lang[languageOnlyLocales[0]]
userLocale = LANGS[languageOnlyLocales[0]]
}
}
// 如果还没匹配到,则返回最接近的语言包
if (!userLocale) {
const fallbackLocale = supportedLocales.find(locale => locale.startsWith('en'))
userLocale = lang[fallbackLocale]
userLocale = LANGS[fallbackLocale]
}
return mergeDeep({}, lang['en-US'], userLocale)
return mergeDeep({}, LANGS['en-US'], userLocale)
}
/**
@@ -65,11 +65,12 @@ export function generateLocaleDict(langString) {
*/
export function initLocale(lang, locale, changeLang, changeLocale) {
if (isBrowser) {
const queryLang = getQueryVariable('lang') || loadLangFromCookies() || window.navigator.language
const queryLang = getQueryVariable('lang') || loadLangFromCookies()
let currentLang = lang
if (queryLang !== lang) {
if (queryLang && queryLang !== 'undefined' && queryLang !== lang) {
currentLang = queryLang
}
changeLang(currentLang)
saveLangToCookies(currentLang)

View File

@@ -1,5 +1,5 @@
export default {
LOCALE: 'en-US',
LOCALE: 'English',
MENU: {
WALK_AROUND: 'Walk Around',
CATEGORY: 'Category',

View File

@@ -1,5 +1,5 @@
export default {
LOCALE: 'fr-FR',
LOCALE: 'français',
NAV: {
INDEX: 'Accueil',
RSS: 'RSS',

View File

@@ -1,5 +1,5 @@
export default {
LOCALE: 'ja-JP',
LOCALE: '日本語',
NAV: {
INDEX: 'ホーム',
RSS: '購読',

View File

@@ -1,5 +1,5 @@
export default {
LOCALE: 'tr-TR',
LOCALE: 'Türkçe',
NAV: {
INDEX: 'Blog',
RSS: 'RSS',

View File

@@ -1,5 +1,5 @@
export default {
LOCALE: 'zh-CN',
LOCALE: '中文(简体)',
MENU: {
WALK_AROUND: '随便逛逛',
CATEGORY: '博客分类',

View File

@@ -1,4 +1,5 @@
export default {
LOCALE: '中文(繁体香港)',
NAV: {
INDEX: '網誌',
RSS: '訂閱',

View File

@@ -1,5 +1,5 @@
export default {
LOCALE: 'zh-TW',
LOCALE: '中文(繁体台湾)',
NAV: {
INDEX: '部落格',
RSS: '訂閱',

View File

@@ -0,0 +1,136 @@
/**
* 从Notion中读取站点配置;
* 在Notion模板中创建一个类型为CONFIG的页面再添加一个数据库表格即可用于填写配置
* Notion数据库配置优先级最高将覆盖vercel环境变量以及blog.config.js中的配置
* --注意--
* 数据库请从模板复制 https://www.notion.so/tanghh/287869a92e3d4d598cf366bd6994755e
*
*/
import { getDateValue, getTextContent } from 'notion-utils'
import { getPostBlocks } from './getPostBlocks'
import getAllPageIds from './getAllPageIds'
/**
* 从Notion中读取Config配置表
* @param {*} allPages
* @returns
*/
export async function getConfigMapFromConfigPage(allPages) {
// 默认返回配置文件
const notionConfig = {}
if (!allPages || !Array.isArray(allPages) || allPages.length === 0) {
console.warn('[Notion配置] 忽略的配置')
return null
}
const configPage = allPages?.find(post => {
return post && post?.type && post?.type === 'CONFIG'
})
if (!configPage) {
console.warn('[Notion配置] 未找到配置页面')
return null
}
const configPageId = configPage.id
// console.log('[Notion配置]请求配置数据 ', configPage.id)
const pageRecordMap = await getPostBlocks(configPageId, 'config-table')
// console.log('配置中心Page', configPageId, pageRecordMap)
const content = pageRecordMap.block[configPageId].value.content
if (!content) {
console.warn('[Notion配置] 未找到配置表格', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value)
return null
}
// 找到配置文件中的database
// for (const contentId of content) {
// console.log('内容', contentId, configPageRecordMap.block[contentId].value.type === 'collection_view')
// }
const configTableId = content?.find(contentId => {
return pageRecordMap.block[contentId].value.type === 'collection_view'
})
// eslint-disable-next-line no-constant-condition, no-self-compare
if (!configTableId) {
console.warn('[Notion配置]未找到配置表格数据', pageRecordMap.block[configPageId], pageRecordMap.block[configPageId].value)
return null
}
// 页面查找
const databaseRecordMap = pageRecordMap.block[configTableId]
const block = pageRecordMap.block || {}
const rawMetadata = databaseRecordMap.value
// Check Type Page-Database和Inline-Database
if (
rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view'
) {
console.error(`pageId "${configTableId}" is not a database`)
return null
}
// console.log('表格', databaseRecordMap, block, rawMetadata)
const collectionId = rawMetadata?.collection_id
const collection = pageRecordMap.collection[collectionId].value
const collectionQuery = pageRecordMap.collection_query
const collectionView = pageRecordMap.collection_view
const schema = collection?.schema
const viewIds = rawMetadata?.view_ids
const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds)
if (pageIds?.length === 0) {
console.error('[Notion配置]获取到的文章列表为空请检查notion模板', collectionQuery, collection, collectionView, viewIds, databaseRecordMap)
}
// 遍历用户的表格
for (let i = 0; i < pageIds.length; i++) {
const id = pageIds[i]
const value = block[id]?.value
if (!value) {
continue
}
const rawProperties = Object.entries(block?.[id]?.value?.properties || [])
const excludeProperties = ['date', 'select', 'multi_select', 'person']
const properties = {}
for (let i = 0; i < rawProperties.length; i++) {
const [key, val] = rawProperties[i]
properties.id = id
if (schema[key]?.type && !excludeProperties.includes(schema[key].type)) {
properties[schema[key].name] = getTextContent(val)
} else {
switch (schema[key]?.type) {
case 'date': {
const dateProperty = getDateValue(val)
delete dateProperty.type
properties[schema[key].name] = dateProperty
break
}
case 'select':
case 'multi_select': {
const selects = getTextContent(val)
if (selects[0]?.length) {
properties[schema[key].name] = selects.split(',')
}
break
}
default:
break
}
}
}
if (properties) {
// 将表格中的字段映射成 英文
const config = {
enable: properties['启用'] === 'Yes',
key: properties['配置名'],
value: properties['配置值']
}
// 只导入生效的配置
if (config.enable) {
// console.log('[Notion配置]', config.key, config.value)
notionConfig[config.key] = config.value
}
}
}
return notionConfig
}

View File

@@ -8,6 +8,7 @@ import getAllPageIds from './getAllPageIds'
import { getAllTags } from './getAllTags'
import getPageProperties from './getPageProperties'
import { mapImgUrl, compressImage } from './mapImage'
import { getConfigMapFromConfigPage } from './getNotionConfig'
/**
* 获取博客数据
@@ -272,6 +273,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
// 文章计数
let postCount = 0
// 查找所有的Post和Page
const allPages = collectionData.filter(post => {
if (post?.type === 'Post' && post.status === 'Published') {
@@ -282,6 +284,9 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
(post?.status === 'Invisible' || post?.status === 'Published')
})
// 站点配置优先读取配置表格否则读取blog.config.js 文件
const NOTION_CONFIG = await getConfigMapFromConfigPage(collectionData) || {}
// Sort by date
if (BLOG.POSTS_SORT_BY === 'date') {
allPages.sort((a, b) => {
@@ -300,6 +305,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
const allNavPages = getNavPages({ allPages })
return {
NOTION_CONFIG,
notice,
siteInfo,
allPages,

View File

@@ -6,6 +6,15 @@ import formatDate from '../formatDate'
import md5 from 'js-md5'
import { mapImgUrl } from './mapImage'
/**
* 获取页面元素成员属性
* @param {*} id
* @param {*} block
* @param {*} schema
* @param {*} authToken
* @param {*} tagOptions
* @returns
*/
export default async function getPageProperties(id, block, schema, authToken, tagOptions) {
const rawProperties = Object.entries(block?.[id]?.value?.properties || [])
const excludeProperties = ['date', 'select', 'multi_select', 'person']
@@ -108,6 +117,7 @@ export default async function getPageProperties(id, block, schema, authToken, ta
properties.slug += '.html'
}
}
// 密码字段md5
properties.password = properties.password ? md5(properties.slug + properties.password) : ''
return properties
}

View File

@@ -75,7 +75,6 @@ export function getQueryVariable(key) {
}
return (false)
}
/**
* 获取 URL 中指定参数的值
* @param {string} url
@@ -83,8 +82,10 @@ export function getQueryVariable(key) {
* @returns {string|null}
*/
export function getQueryParam(url, param) {
const searchParams = new URLSearchParams(url.split('?')[1])
return searchParams.get(param)
// 移除哈希部分
const urlWithoutHash = url.split('#')[0];
const searchParams = new URLSearchParams(urlWithoutHash.split('?')[1]);
return searchParams.get(param);
}
/**
@@ -202,3 +203,46 @@ export const isMobile = () => {
return isMobile
}
/**
* 扫描页面上的所有文本节点将url格式的文本转为可点击链接
* @param {*} node
*/
export const scanAndConvertToLinks = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
const urlRegex = /https?:\/\/[^\s]+/g;
let lastIndex = 0;
let match;
const newNode = document.createElement('span');
while ((match = urlRegex.exec(text)) !== null) {
const beforeText = text.substring(lastIndex, match.index);
const url = match[0];
if (beforeText) {
newNode.appendChild(document.createTextNode(beforeText));
}
const link = document.createElement('a');
link.href = url;
link.target = '_blank'
link.textContent = url;
newNode.appendChild(link);
lastIndex = urlRegex.lastIndex;
}
if (lastIndex < text.length) {
newNode.appendChild(document.createTextNode(text.substring(lastIndex)));
}
node.parentNode.replaceChild(newNode, node);
} else if (node.nodeType === Node.ELEMENT_NODE) {
for (const childNode of node.childNodes) {
scanAndConvertToLinks(childNode);
}
}
}