Merge branch 'main' into original-main-fix-DOMException

This commit is contained in:
tangly1024
2024-08-28 12:18:53 +08:00
committed by GitHub
192 changed files with 7978 additions and 3287 deletions

View File

@@ -2,7 +2,7 @@
import BLOG from '@/blog.config'
import { useGlobal } from './global'
import { deepClone } from './utils'
import { deepClone, isUrl } from './utils'
/**
* 读取配置顺序
@@ -19,7 +19,7 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
return null
}
// 特殊配置处理;某些配置只在服务端生效而Global的NOTION_CONFIG仅限前端组件使用因此需要从extendConfig中读取
// 特殊配置处理;以下配置只在服务端生效而Global的NOTION_CONFIG仅限前端组件使用因此需要从extendConfig中读取
switch (key) {
case 'NEXT_REVALIDATE_SECOND':
case 'POST_RECOMMEND_COUNT':
@@ -32,6 +32,8 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
case 'POST_LIST_STYLE':
case 'POST_LIST_PREVIEW':
case 'POST_URL_PREFIX_MAPPING_CATEGORY':
case 'IS_TAG_COLOR_DISTINGUISHED':
case 'TAG_SORT_BY_COUNT':
return convertVal(extendConfig[key] || defaultVal || BLOG[key])
default:
}
@@ -53,8 +55,8 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
let siteInfo = null
if (global) {
val = global.NOTION_CONFIG?.[key]
siteInfo = global.siteInfo
val = global.NOTION_CONFIG?.[key] || global.THEME_CONFIG?.[key]
}
if (!val) {
@@ -89,46 +91,62 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
return defaultVal
}
// 从Notion_CONFIG读取的配置通常都是字符串适当转义
return convertVal(val)
}
/**
* 配置默认都是string类型
* 识别配置的值是否数字、布尔、[]数组,若是则转成对应类型
* 从环境变量和NotionConfig读取的配置都是string类型
* 这里识别配置的字符值若为否 数字、布尔、[]数组,{}对象,若是则转成对应类型
* 使用JSON和eval两个函数
* @param {*} val
* @returns
*/
export const convertVal = val => {
if (typeof val === 'string') {
// 解析布尔
if (val === 'true' || val === 'false') {
return JSON.parse(val)
}
// 解析数字parseInt将字符串转换为数字
if (/^\d+$/.test(val)) {
return parseInt(val)
}
// 转移 [] , {} 这种json串为json对象
try {
const parsedJson = JSON.parse(val)
// 检查解析后的结果是否是对象或数组
if (typeof parsedJson === 'object' && parsedJson !== null) {
return parsedJson
}
} catch (error) {
// JSON 解析失败,返回原始字符串值
return val
}
}
try {
return JSON.parse(val)
} catch (error) {
// 如果值是一个字符串但不是有效的 JSON 格式,直接返回字符串
// 如果传入参数本身就是obj、数组、boolean 就无需处理
if (typeof val !== 'string' || !val) {
return val
}
// 解析数字parseInt将字符串转换为数字
if (/^\d+$/.test(val)) {
return parseInt(val)
}
// 检测是否url
if (isUrl(val)) {
return val
}
// 检测是否url
if (val === 'true' || val === 'false') {
return JSON.parse(val)
}
// 配置值前可能有污染的空格
if (val.indexOf('[') < 0 && val.indexOf('{') < 0) {
return val
}
// 转换 [] , {} , true/false 这类字符串为对象
try {
// 尝试解析json
const parsedJson = JSON.parse(val)
if (parsedJson !== null) {
return parsedJson
}
} catch (error) {
// try {
// // 尝试解析对象,对象解析能力不如上一步的json
// const evalObj = eval('(' + val + ')')
// if (evalObj !== null) {
// return evalObj
// }
// } catch (error) {
// // Ojbject 解析失败,返回原始字符串值
// return val
// }
return val
}
return val
}
/**

View File

@@ -5,18 +5,18 @@ import getAllPageIds from '@/lib/notion/getAllPageIds'
import { getAllTags } from '@/lib/notion/getAllTags'
import { getConfigMapFromConfigPage } from '@/lib/notion/getNotionConfig'
import getPageProperties, {
adjustPageProperties
adjustPageProperties
} from '@/lib/notion/getPageProperties'
import { fetchInBatches, getPostBlocks } from '@/lib/notion/getPostBlocks'
import { fetchInBatches, getPage } from '@/lib/notion/getPostBlocks'
import { compressImage, mapImgUrl } from '@/lib/notion/mapImage'
import { deepClone } from '@/lib/utils'
import { idToUuid } from 'notion-utils'
import { siteConfig } from '../config'
import { extractLangId, extractLangPrefix } from '../utils/pageId'
import { extractLangId, extractLangPrefix, getShortId } from '../utils/pageId'
export { getAllTags } from '../notion/getAllTags'
export { getPost } from '../notion/getNotionPost'
export { getPostBlocks } from '../notion/getPostBlocks'
export { getPage as getPostBlocks } from '../notion/getPostBlocks'
/**
* 获取博客数据; 基于Notion实现
@@ -77,7 +77,18 @@ export async function getNotionPageData({ pageId, from }) {
}
// 返回给前端的数据做处理
const db = deepClone(data)
return handleDataBeforeReturn(deepClone(data))
}
/**
* 返回给浏览器前端的数据处理
* 适当脱敏
* 减少体积
* 其它处理
* @param {*} db
*/
function handleDataBeforeReturn(db) {
// 清理多余数据
delete db.block
delete db.schema
delete db.rawMetadata
@@ -91,42 +102,88 @@ export async function getNotionPageData({ pageId, from }) {
// 清理多余的块
if (db?.notice) {
db.notice = cleanBlock(db?.notice)
delete db.notice?.id
}
if (db?.post) {
db.post = cleanBlock(db?.post)
}
db.tagOptions = cleanIds(db?.tagOptions)
db.categoryOptions = cleanIds(db?.categoryOptions)
db.customMenu = cleanIds(db?.customMenu)
// db.latestPosts = shortenIds(db?.latestPosts)
db.allNavPages = shortenIds(db?.allNavPages)
// db.allPages = cleanBlocks(db?.allPages)
return db
}
/**
* 清理一组数据的id
* @param {*} items
* @returns
*/
function shortenIds(items) {
if (items && Array.isArray(items)) {
return deepClone(
items.map(item => {
item.short_id = getShortId(item.id)
delete item.id
return item
})
)
}
return items
}
/**
* 清理一组数据的id
* @param {*} items
* @returns
*/
function cleanIds(items) {
if (items && Array.isArray(items)) {
return deepClone(
items.map(item => {
delete item.id
return item
})
)
}
return items
}
/**
* 清理block数据
*/
function cleanBlock(post) {
function cleanBlock(item) {
const post = deepClone(item)
const pageBlock = post?.blockMap?.block
for (const i in pageBlock) {
pageBlock[i] = cleanBlock(pageBlock[i])
delete pageBlock[i]?.role
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.space_id
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.format?.copied_from_pointer
delete pageBlock[i]?.value?.format?.block_locked_by
delete pageBlock[i]?.value?.parent_table
delete pageBlock[i]?.value?.copied_from_pointer
delete pageBlock[i]?.value?.copied_from
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.permissions
delete pageBlock[i]?.value?.alive
}
// delete post?.id
// delete post?.blockMap?.collection
if (pageBlock) {
for (const i in pageBlock) {
pageBlock[i] = cleanBlock(pageBlock[i])
delete pageBlock[i]?.role
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.space_id
delete pageBlock[i]?.value?.version
delete pageBlock[i]?.value?.format?.copied_from_pointer
delete pageBlock[i]?.value?.format?.block_locked_by
delete pageBlock[i]?.value?.parent_table
delete pageBlock[i]?.value?.copied_from_pointer
delete pageBlock[i]?.value?.copied_from
delete pageBlock[i]?.value?.created_by_table
delete pageBlock[i]?.value?.created_by_id
delete pageBlock[i]?.value?.last_edited_by_table
delete pageBlock[i]?.value?.last_edited_by_id
delete pageBlock[i]?.value?.permissions
delete pageBlock[i]?.value?.alive
}
}
return post
}
@@ -290,7 +347,6 @@ export function getNavPages({ allPages }) {
return (
post &&
post?.slug &&
!post?.slug?.startsWith('http') &&
post?.type === 'Post' &&
post?.status === 'Published'
)
@@ -320,7 +376,7 @@ async function getNotice(post) {
return null
}
post.blockMap = await getPostBlocks(post.id, 'data-notice')
post.blockMap = await getPage(post.id, 'data-notice')
return post
}
@@ -372,7 +428,7 @@ const EmptyData = pageId => {
*/
async function getDataBaseInfoByNotionAPI({ pageId, from }) {
console.log('[Fetching Data]', pageId, from)
const pageRecordMap = await getPostBlocks(pageId, from)
const pageRecordMap = await getPage(pageId, from)
if (!pageRecordMap) {
console.error('can`t get Notion Data ; Which id is: ', pageId)
return {}
@@ -455,6 +511,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
adjustPageProperties(element, NOTION_CONFIG)
})
// 站点基础信息
const siteInfo = getSiteInfo({ collection, block, pageId })
// 文章计数
@@ -468,7 +525,7 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
return (
post &&
post?.slug &&
!post?.slug?.startsWith('http') &&
// !post?.slug?.startsWith('http') &&
(post?.status === 'Invisible' || post?.status === 'Published')
)
})
@@ -490,11 +547,17 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
)
})?.[0]
)
// 所有分类
const categoryOptions = getAllCategories({
allPages,
categoryOptions: getCategoryOptions(schema)
})
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
// 所有标签
const tagOptions = getAllTags({
allPages,
tagOptions: getTagOptions(schema),
NOTION_CONFIG
})
// 旧的菜单
const customNav = getCustomNav({
allPages: collectionData.filter(

View File

@@ -1,5 +1,6 @@
import {
THEMES,
getThemeConfig,
initDarkMode,
saveDarkModeToLocalStorage
} from '@/themes/theme'
@@ -29,11 +30,14 @@ export function GlobalContextProvider(props) {
tagOptions,
NOTION_CONFIG
} = props
const [lang, updateLang] = useState(NOTION_CONFIG?.LANG || LANG) // 默认语言
const [locale, updateLocale] = useState(
generateLocaleDict(NOTION_CONFIG?.LANG || LANG)
) // 默认语言
const [theme, setTheme] = useState(NOTION_CONFIG?.THEME || THEME) // 默认博客主题
const [THEME_CONFIG, SET_THEME_CONFIG] = useState(null) // 主题配置
const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE
const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式
const [onLoading, setOnLoading] = useState(false) // 抓取文章数据
@@ -54,6 +58,12 @@ export function GlobalContextProvider(props) {
return newTheme
}
// 抓取主题配置
const updateThemeConfig = async theme => {
const config = await getThemeConfig(theme)
SET_THEME_CONFIG(config)
}
// 切换深色模式
const toggleDarkMode = () => {
const newStatus = !isDarkMode
@@ -79,7 +89,10 @@ export function GlobalContextProvider(props) {
useEffect(() => {
initDarkMode(updateDarkMode, defaultDarkMode)
initLocale(lang, locale, updateLang, updateLocale)
redirectUserLang(NOTION_PAGE_ID)
// 可以
if (NOTION_CONFIG?.REDIRECT_LANG) {
redirectUserLang(NOTION_PAGE_ID)
}
}, [])
// 加载进度条
@@ -96,6 +109,9 @@ export function GlobalContextProvider(props) {
setOnLoading(false)
}
const currentTheme = router?.query?.theme || theme
updateThemeConfig(currentTheme)
router.events.on('routeChangeStart', handleStart)
router.events.on('routeChangeError', handleStop)
router.events.on('routeChangeComplete', handleStop)
@@ -111,6 +127,7 @@ export function GlobalContextProvider(props) {
value={{
fullWidth,
NOTION_CONFIG,
THEME_CONFIG,
toggleDarkMode,
onLoading,
setOnLoading,

View File

@@ -14,11 +14,15 @@ export default {
INDEX: 'Home',
RSS: 'RSS',
SEARCH: 'Search',
NAVIGATOR: 'NAV',
ABOUT: 'About',
MAIL: 'E-Mail',
ARCHIVE: 'Archive'
},
COMMON: {
THEME: 'Theme',
ARTICLE_LIST: 'Article List',
RECOMMEND_POSTS: 'Recommend Posts',
MORE: 'More',
NO_MORE: 'No More',
LATEST_POSTS: 'Latest posts',
@@ -33,15 +37,21 @@ export default {
COPYRIGHT: 'Copyright',
AUTHOR: 'Author',
URL: 'URL',
NOW: 'NOW',
RECOMMEND_BADGES: 'Recommend',
BLOG: 'Blog',
POSTS: 'Posts',
ARTICLE: 'Article',
VISITORS: 'Visitors',
VIEWS: 'Views',
COPYRIGHT_NOTICE: 'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!',
PAGE_URL_COPIED: 'Page URL copied',
COPYRIGHT_NOTICE:
'All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!',
RESULT_OF_SEARCH: 'Results Found',
ARTICLE_DETAIL: 'Article Details',
PASSWORD_ERROR: 'Password Error!',
ARTICLE_LOCK_TIPS: 'Please Enter the password:',
ARTICLE_UNLOCK_TIPS: 'Article Unlocked',
NO_RESULTS_FOUND: 'No results found.',
SUBMIT: 'Submit',
POST_TIME: 'Post on',
@@ -55,7 +65,8 @@ export default {
START_READING: 'Start Reading',
MINUTE: 'min',
WORD_COUNT: 'Words',
READ_TIME: 'Read Time'
READ_TIME: 'Read Time',
NEXT_POST: '下一篇'
},
PAGINATION: {
PREV: 'Prev',

View File

@@ -29,11 +29,14 @@ export default {
ARTICLE: '記事',
VISITORS: '人の訪問者',
VIEWS: '回の閲覧',
COPYRIGHT_NOTICE: 'この記事はCC BY-NC-SA 4.0 ライセンスの下でライセンスされています。転載する場合には出典を明らかにしてください。',
COPYRIGHT_NOTICE:
'この記事はCC BY-NC-SA 4.0 ライセンスの下でライセンスされています。転載する場合には出典を明らかにしてください。',
RESULT_OF_SEARCH: '個の検索結果',
ARTICLE_DETAIL: '記事の詳細',
PASSWORD_ERROR: 'パスワードが違います!',
ARTICLE_LOCK_TIPS: 'この記事はロックされています。アクセスパスワードを入力してください。',
ARTICLE_LOCK_TIPS:
'この記事はロックされています。アクセスパスワードを入力してください。',
ARTICLE_UNLOCK_TIPS: '記事がロック解除されました',
SUBMIT: '送信',
POST_TIME: '公開日',
LAST_EDITED_TIME: '最終更新日',

View File

@@ -20,6 +20,9 @@ export default {
ARCHIVE: '归档'
},
COMMON: {
THEME: 'Theme',
ARTICLE_LIST: '文章列表',
RECOMMEND_POSTS: '推荐文章',
MORE: '更多',
NO_MORE: '没有更多了',
LATEST_POSTS: '最新发布',
@@ -35,16 +38,21 @@ export default {
AUTHOR: '作者',
URL: '链接',
ANALYTICS: '统计',
RECOMMEND_BADGES: '荐',
BLOG: '博客',
NOW: '此刻',
POSTS: '篇文章',
ARTICLE: '文章',
VISITORS: '位访客',
VIEWS: '次查看',
PAGE_URL_COPIED: '页面地址已复制',
COPYRIGHT_NOTICE: '本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。',
RESULT_OF_SEARCH: '篇搜索到的结果',
NO_RESULTS_FOUND: '没有找到文章',
ARTICLE_DETAIL: '文章详情',
PASSWORD_ERROR: '密码错误!',
ARTICLE_LOCK_TIPS: '文章已上锁,请输入访问密码',
ARTICLE_UNLOCK_TIPS: '文章已解锁',
SUBMIT: '提交',
POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新',
@@ -57,7 +65,8 @@ export default {
START_READING: '开始阅读',
MINUTE: '分钟',
WORD_COUNT: '字数',
READ_TIME: '阅读时长'
READ_TIME: '阅读时长',
NEXT_POST: '下一篇'
},
PAGINATION: {
PREV: '上页',

View File

@@ -5,7 +5,39 @@ export default {
RSS: '訂閱',
SEARCH: '搜尋',
ABOUT: '關於',
MAIL: '電郵'
MAIL: '電郵',
NAVIGATOR: '導航',
ARCHIVE: '封存'
},
COMMON: {
ARTICLE_LIST: '文章列表',
MORE: '更多',
NO_MORE: '沒有更多了',
LATEST_POSTS: '最新文章',
TAGS: '標籤',
NO_TAG: '無標籤',
CATEGORY: '分類',
SHARE: '分享',
SCAN_QR_CODE: 'QRCode',
URL_COPIED: '連結已複製!',
TABLE_OF_CONTENTS: '目錄',
RELATE_POSTS: '相關文章',
COPYRIGHT: '著作權',
AUTHOR: '作者',
URL: '連結',
ANALYTICS: '分析',
POSTS: '篇文章',
ARTICLE: '文章',
VISITORS: '位訪客',
VIEWS: '次查看',
COPYRIGHT_NOTICE: '本文採用 CC BY-NC-SA 4.0 許可協議,轉載請註明出處。',
RESULT_OF_SEARCH: '篇搜尋到的结果',
ARTICLE_DETAIL: '完整文章',
PASSWORD_ERROR: '密碼錯誤!',
ARTICLE_LOCK_TIPS: '文章已上鎖,請輸入訪問密碼',
SUBMIT: '提交',
POST_TIME: '发布于',
LAST_EDITED_TIME: '最后更新'
},
PAGINATION: {
PREV: '上一頁',
@@ -13,7 +45,7 @@ export default {
},
SEARCH: {
ARTICLES: '搜尋文章',
TAGS: '搜尋標'
TAGS: '搜尋標'
},
POST: {
BACK: '返回',

View File

@@ -10,6 +10,7 @@ export default {
ARCHIVE: '封存'
},
COMMON: {
ARTICLE_LIST: '文章列表',
MORE: '更多',
NO_MORE: '沒有更多了',
LATEST_POSTS: '最新文章',

View File

@@ -1,33 +1,44 @@
import { uuidToId } from 'notion-utils'
import { idToUuid } from 'notion-utils'
import { checkStrIsNotionId, getLastPartOfUrl, isBrowser } from '../utils'
/**
* 处理页面内连接跳转:
* 1. 若是本站域名则在当前窗口打开不开新窗口
* 2. 若是Notion笔记中的内链尝试转换成博客中现有的文章地址
* 1.若是本站域名则在当前窗口打开不开新窗口
* 2.url是notion-id转成站内文章链接
*/
export const mapPageUrl = allPages => {
export const convertInnerUrl = allPages => {
if (isBrowser) {
const allAnchorTags = document
?.getElementById('notion-article')
?.getElementsByTagName('a')
if (!allAnchorTags) {
return
}
const currentURL = window.location.origin + window.location.pathname
const allAnchorTags = document.getElementsByTagName('a') // 或者使用 document.querySelectorAll('a') 获取 NodeList
// url替换成slug
for (const anchorTag of allAnchorTags) {
// 检查url
if (anchorTag?.href) {
// 如果url是一个Notion_id尝试匹配成博客的文章内链
const slug = getLastPartOfUrl(anchorTag.href)
if (checkStrIsNotionId(slug)) {
const slugPage = allPages?.find(page => uuidToId(page.id) === slug)
const slugPage = allPages?.find(page => {
return idToUuid(slug).indexOf(page.short_id) === 0
})
if (slugPage) {
anchorTag.href = slugPage?.href
}
}
}
}
// 链接在当前页面打开
for (const anchorTag of allAnchorTags) {
if (anchorTag?.target === '_blank') {
const hrefWithoutQueryHash = anchorTag.href.split('?')[0].split('#')[0]
const hrefWithRelativeHash =
currentURL.split('#')[0] + anchorTag.href.split('#')[1]
currentURL.split('#')[0] || '' + anchorTag.href.split('#')[1] || ''
if (
currentURL === hrefWithoutQueryHash ||
currentURL === hrefWithRelativeHash

View File

@@ -4,7 +4,7 @@ import { isIterable } from '../utils'
* 获取所有文章的标签
* @param allPosts
* @param sliceCount 默认截取数量为12若为0则返回全部
* @param tagOptions tags的下拉选项
* @param categoryOptions categories的下拉选项
* @returns {Promise<{}|*[]>}
*/
@@ -13,8 +13,14 @@ import { isIterable } from '../utils'
* @param allPosts
* @returns {Promise<{}|*[]>}
*/
export function getAllCategories({ allPages, categoryOptions, sliceCount = 0 }) {
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
export function getAllCategories({
allPages,
categoryOptions,
sliceCount = 0
}) {
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (!allPosts || !categoryOptions) {
return []
}

View File

@@ -1,5 +1,5 @@
import { siteConfig } from '../config'
import { isIterable } from '../utils'
import BLOG from '@/blog.config'
/**
* 获取所有文章的标签
@@ -8,8 +8,15 @@ import BLOG from '@/blog.config'
* @param tagOptions tags的下拉选项
* @returns {Promise<{}|*[]>}
*/
export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
export function getAllTags({
allPages,
sliceCount = 0,
tagOptions,
NOTION_CONFIG
}) {
const allPosts = allPages?.filter(
page => page.type === 'Post' && page.status === 'Published'
)
if (!allPosts || !tagOptions) {
return []
@@ -27,7 +34,12 @@ export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
})
const list = []
const { IS_TAG_COLOR_DISTINGUISHED } = BLOG
const IS_TAG_COLOR_DISTINGUISHED = siteConfig(
'IS_TAG_COLOR_DISTINGUISHED',
false,
NOTION_CONFIG
)
const TAG_SORT_BY_COUNT = siteConfig('TAG_SORT_BY_COUNT', true, NOTION_CONFIG)
if (isIterable(tagOptions)) {
if (!IS_TAG_COLOR_DISTINGUISHED) {
// 如果不区分颜色, 那么不同颜色相同名称的tag当做同一种tag
@@ -52,7 +64,10 @@ export function getAllTags({ allPages, sliceCount = 0, tagOptions }) {
}
// 按照数量排序
// list.sort((a, b) => b.count - a.count)
if (TAG_SORT_BY_COUNT) {
list.sort((a, b) => b.count - a.count)
}
if (sliceCount && sliceCount > 0) {
return list.slice(0, sliceCount)
} else {

View File

@@ -9,7 +9,7 @@
import { getDateValue, getTextContent } from 'notion-utils'
import { deepClone } from '../utils'
import getAllPageIds from './getAllPageIds'
import { getPostBlocks } from './getPostBlocks'
import { getPage } from './getPostBlocks'
/**
* 从Notion中读取Config配置表
@@ -41,12 +41,12 @@ export async function getConfigMapFromConfigPage(allPages) {
}
const configPageId = configPage.id
// console.log('[Notion配置]请求配置数据 ', configPage.id)
let pageRecordMap = await getPostBlocks(configPageId, 'config-table')
let pageRecordMap = await getPage(configPageId, 'config-table')
// console.log('配置中心Page', configPageId, pageRecordMap)
let content = pageRecordMap.block[configPageId].value.content
for (const table of ['Config-Table', 'CONFIG-TABLE']) {
if (content) break
pageRecordMap = await getPostBlocks(configPageId, table)
pageRecordMap = await getPage(configPageId, table)
content = pageRecordMap.block[configPageId].value.content
}

View File

@@ -2,7 +2,7 @@ import BLOG from '@/blog.config'
import { idToUuid } from 'notion-utils'
import { defaultMapImageUrl } from 'react-notion-x'
import formatDate from '../utils/formatDate'
import { getPostBlocks } from './getPostBlocks'
import { getPage } from './getPostBlocks'
/**
* 根据页面ID获取内容
@@ -10,7 +10,7 @@ import { getPostBlocks } from './getPostBlocks'
* @returns
*/
export async function getPost(pageId) {
const blockMap = await getPostBlocks(pageId, 'slug')
const blockMap = await getPage(pageId, 'slug')
if (!blockMap) {
return null
}

View File

@@ -6,10 +6,11 @@ import formatDate from '../utils/formatDate'
import md5 from 'js-md5'
import { siteConfig } from '../config'
import {
checkContainHttp,
convertUrlStartWithOneSlash,
sliceUrlFromHttp
checkStartWithHttp,
convertUrlStartWithOneSlash,
getLastSegmentFromUrl
} from '../utils'
import { extractLangPrefix } from '../utils/pageId'
import { mapImgUrl } from './mapImage'
/**
@@ -94,6 +95,7 @@ export default async function getPageProperties(
properties.type = properties.type?.[0] || ''
properties.status = properties.status?.[0] || ''
properties.category = properties.category?.[0] || ''
properties.comment = properties.comment?.[0] || ''
// 映射值用户个性化type和status字段的下拉框选项在此映射回代码的英文标识
mapProperties(properties)
@@ -185,26 +187,38 @@ export function adjustPageProperties(properties, NOTION_CONFIG) {
properties.name = properties.title ?? ''
}
// 开启伪静态路径
if (JSON.parse(NOTION_CONFIG?.PSEUDO_STATIC || BLOG.PSEUDO_STATIC)) {
if (
!properties?.href?.endsWith('.html') &&
!properties?.href?.startsWith('http')
) {
properties.href += '.html'
}
}
// 检查处理外链
properties.href = checkContainHttp(properties?.href)
? sliceUrlFromHttp(properties?.href)
: convertUrlStartWithOneSlash(properties?.href)
// 设置链接在页内或新页面打开
if (properties.href?.indexOf('http') === 0) {
// http or https 开头的视为外链
if (checkStartWithHttp(properties?.href)) {
properties.href = properties?.slug
properties.target = '_blank'
} else {
properties.target = '_self'
// 伪静态路径右侧拼接.html
if (siteConfig('PSEUDO_STATIC', false, NOTION_CONFIG)) {
if (
!properties?.href?.endsWith('.html') &&
properties?.href !== '' &&
properties?.href !== '#' &&
properties?.href !== '/'
) {
properties.href += '.html'
}
}
// 相对路径转绝对路径url左侧拼接 /
properties.href = convertUrlStartWithOneSlash(properties?.href)
}
// 如果跳转链接是多语言,则在新窗口打开
if (BLOG.NOTION_PAGE_ID.indexOf(',') > 0) {
const siteIds = BLOG.NOTION_PAGE_ID.split(',')
for (let index = 0; index < siteIds.length; index++) {
const siteId = siteIds[index]
const prefix = extractLangPrefix(siteId)
if (getLastSegmentFromUrl(properties.href) === prefix) {
properties.target = '_blank'
}
}
}
// 密码字段md5
@@ -221,6 +235,10 @@ export function adjustPageProperties(properties, NOTION_CONFIG) {
* @returns
*/
function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
// 外链不处理
if (checkStartWithHttp(postProperties.slug)) {
return postProperties.slug
}
let fullPrefix = ''
const allSlugPatterns = siteConfig(
'POST_URL_PREFIX',
@@ -272,5 +290,10 @@ function generateCustomizeSlug(postProperties, NOTION_CONFIG) {
if (fullPrefix.endsWith('/')) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
}
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
if(fullPrefix){
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}else{
return `${postProperties.slug ?? postProperties.id}`
}
}

View File

@@ -10,7 +10,7 @@ import { deepClone, delay } from '../utils'
* @param {*} slice
* @returns
*/
export async function getPostBlocks(id, from, slice) {
export async function getPage(id, from, slice) {
const cacheKey = 'page_block_' + id
let pageBlock = await getDataFromCache(cacheKey)
if (pageBlock) {
@@ -27,28 +27,6 @@ export async function getPostBlocks(id, from, slice) {
return pageBlock
}
/**
* 针对在getDataBaseInfoByNotionAPI=>getPostBlocks中获取不到的溢出的block-id用此方法另外抓取
* @param {*} id
* @param {*} from
* @returns
*/
export async function getSingleBlock(id, from) {
const cacheKey = 'single_block_' + id
let pageBlock = await getDataFromCache(cacheKey)
if (pageBlock) {
// console.log('[API<<--缓存]', `from:${from}`, cacheKey)
return pageBlock
}
pageBlock = await getPageWithRetry(id, 'single_' + from)
if (pageBlock) {
await setDataToCache(cacheKey, pageBlock)
}
return pageBlock
}
/**
* 调用接口,失败会重试
* @param {*} id
@@ -103,49 +81,69 @@ export async function getPageWithRetry(id, from, retryAttempts = 3) {
function filterPostBlocks(id, blockMap, slice) {
const clonePageBlock = deepClone(blockMap)
let count = 0
const blocksToProcess = Object.keys(clonePageBlock?.block || {})
// 循环遍历文档的每个block
for (const i in clonePageBlock?.block) {
const b = clonePageBlock?.block[i]
if (slice && slice > 0 && count > slice) {
delete clonePageBlock?.block[i]
continue
for (let i = 0; i < blocksToProcess.length; i++) {
const blockId = blocksToProcess[i]
const b = clonePageBlock?.block[blockId]
if (slice && slice > 0 && count > slice) {
delete clonePageBlock?.block[blockId]
continue
}
// 当BlockId等于PageId时移除
if (b?.value?.id === id) {
// 此block含有敏感信息
delete b?.value?.properties
continue
}
count++
if (b?.value?.type === 'sync_block' && b?.value?.children) {
const childBlocks = b.value.children
// 移除同步块
delete clonePageBlock.block[blockId]
// 用子块替代同步块
childBlocks.forEach((childBlock, index) => {
const newBlockId = `${blockId}_child_${index}`
clonePageBlock.block[newBlockId] = childBlock
blocksToProcess.splice(i + index + 1, 0, newBlockId)
})
// 重新处理新加入的子块
i--
continue
}
// 处理 c++、c#、汇编等语言名字映射
if (b?.value?.type === 'code') {
if (b?.value?.properties?.language?.[0][0] === 'C++') {
b.value.properties.language[0][0] = 'cpp'
}
// 当BlockId等于PageId时移除
if (b?.value?.id === id) {
// 此block含有敏感信息
delete b?.value?.properties
continue
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
}
count++
// 处理 c++、c#、汇编等语言名字映射
if (b?.value?.type === 'code') {
if (b?.value?.properties?.language?.[0][0] === 'C++') {
b.value.properties.language[0][0] = 'cpp'
}
if (b?.value?.properties?.language?.[0][0] === 'C#') {
b.value.properties.language[0][0] = 'csharp'
}
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
}
}
// 如果是文件或嵌入式PDF需要重新加密签名
if (
(b?.value?.type === 'file' ||
b?.value?.type === 'pdf' ||
b?.value?.type === 'video' ||
b?.value?.type === 'audio') &&
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0
) {
const oldUrl = b?.value?.properties?.source?.[0][0]
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}`
b.value.properties.source[0][0] = newUrl
if (b?.value?.properties?.language?.[0][0] === 'Assembly') {
b.value.properties.language[0][0] = 'asm6502'
}
}
// 如果是文件或嵌入式PDF需要重新加密签名
if (
(b?.value?.type === 'file' ||
b?.value?.type === 'pdf' ||
b?.value?.type === 'video' ||
b?.value?.type === 'audio') &&
b?.value?.properties?.source?.[0][0] &&
b?.value?.properties?.source?.[0][0].indexOf('amazonaws.com') > 0
) {
const oldUrl = b?.value?.properties?.source?.[0][0]
const newUrl = `https://notion.so/signed/${encodeURIComponent(oldUrl)}?table=block&id=${b?.value?.id}`
b.value.properties.source[0][0] = newUrl
}
}
// 去掉不用的字段
if (id === BLOG.NOTION_PAGE_ID) {
@@ -162,6 +160,11 @@ function filterPostBlocks(id, blockMap, slice) {
* @returns
*/
export const fetchInBatches = async (ids, batchSize = 100) => {
// 如果 ids 不是数组,则将其转换为数组
if (!Array.isArray(ids)) {
ids = [ids]
}
const authToken = BLOG.NOTION_ACCESS_TOKEN || null
const api = new NotionAPI({
authToken,
@@ -171,7 +174,7 @@ export const fetchInBatches = async (ids, batchSize = 100) => {
let fetchedBlocks = {}
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize)
console.log('[API-->>请求] Fetching missing blocks', ids.length)
console.log('[API-->>请求] Fetching missing blocks', batch, ids.length)
const start = new Date().getTime()
const pageChunk = await api.getBlocks(batch)
const end = new Date().getTime()

View File

@@ -110,6 +110,8 @@ const compressImage = (image, width, quality = 50, fmt = 'webp') => {
return image
}
if (image.includes(".svg")) return image
if (!width || width === 0) {
width = siteConfig('IMAGE_COMPRESS_WIDTH')
}

37
lib/password.js Normal file
View File

@@ -0,0 +1,37 @@
import { isBrowser } from './utils'
/**
* 获取默认密码
* 用户可以通过url中拼接参数输入密码
* 输入过一次的密码会被存储在浏览器中,便于下一次免密访问
* 返回的是一组历史密码,便于客户端多次尝试
*/
export const getPasswordQuery = path => {
// 使用 URL 对象解析 URL
const url = new URL(path, isBrowser ? window.location.origin : '')
// 获取查询参数
const queryParams = Object.fromEntries(url.searchParams.entries())
// 请求中带着密码
if (queryParams.password) {
// 将已输入密码作为默认密码存放在 localStorage便于下次读取并自动尝试
localStorage.setItem('password_default', queryParams.password)
}
// 获取路径部分
const cleanedPath = url.pathname
// 从 localStorage 中获取相关密码
const storedPassword = localStorage.getItem('password_' + cleanedPath)
const defaultPassword = localStorage.getItem('password_default')
// 将所有密码存储在一个数组中,并过滤掉无效值
const passwords = [
queryParams.password,
storedPassword,
defaultPassword
].filter(Boolean)
return passwords
}

View File

@@ -6,7 +6,7 @@ import algoliasearch from 'algoliasearch'
* 生成全文索引
* @param {*} allPages
*/
const generateAlgoliaSearch = async({ allPages, force = false }) => {
const generateAlgoliaSearch = async ({ allPages, force = false }) => {
allPages?.forEach(p => {
// 判断这篇文章是否需要重新创建索引
if (p && !p.password) {
@@ -19,7 +19,7 @@ const generateAlgoliaSearch = async({ allPages, force = false }) => {
* 上传数据
* 根据上次修改文章日期和上次更新索引数据判断是否需要更新algolia索引
*/
const uploadDataToAlgolia = async(post) => {
const uploadDataToAlgolia = async post => {
// Connect and authenticate with your Algolia app
const client = algoliasearch(BLOG.ALGOLIA_APP_ID, BLOG.ALGOLIA_ADMIN_APP_KEY)
@@ -61,14 +61,18 @@ const uploadDataToAlgolia = async(post) => {
summary: post.summary,
lastEditedDate: post.lastEditedDate, // 更新文章时间
lastIndexDate: new Date(), // 更新索引时间
content: truncate(getPageContentText(post, post.blockMap), 9000) // 索引9000个字节因为api限制总请求内容上限1万个字节
content: truncate(getPageContentText(post, post.blockMap), 8192) // 索引8192个字符API限制总请求内容上限1万个字节
}
// console.log('更新Algolia索引', record)
index.saveObject(record).wait().then(r => {
console.log('Algolia索引更新', r)
}).catch(err => {
console.log('Algolia异常', err)
})
index
.saveObject(record)
.wait()
.then(r => {
console.log('Algolia索引更新', r)
})
.catch(err => {
console.log('Algolia异常', err)
})
}
}

View File

@@ -47,7 +47,12 @@ export const memorize = Component => {
return memo(MemoizedComponent)
}
// 转换外链
/**
* 诸如 article/https://test.com 等被错误拼接前缀的slug进行处理
* 转换为 https://test.com
* @param {*} str
* @returns
*/
export function sliceUrlFromHttp(str) {
// 检查字符串是否包含http
if (str?.includes('http:') || str?.includes('https:')) {
@@ -81,12 +86,25 @@ export function convertUrlStartWithOneSlash(str) {
return str
}
/**
* 是否是一个相对或绝对路径的ur类
* @param {*} str
* @returns
*/
export function isUrl(str) {
if (!str) {
return false
}
return str?.indexOf('/') === 0 || checkStartWithHttp(str)
}
// 检查是否外链
export function checkContainHttp(str) {
export function checkStartWithHttp(str) {
// 检查字符串是否包含http
if (str?.includes('http:') || str?.includes('https:')) {
if (str?.indexOf('http:') === 0 || str?.indexOf('https:') === 0) {
// 如果包含找到http的位置
return str?.indexOf('http') > -1
return true
} else {
// 不包含
return false
@@ -354,3 +372,19 @@ export const scanAndConvertToLinks = node => {
}
}
}
/**
* 获取url最后一个斜杆后面的内容
* @param {*} url
* @returns
*/
export function getLastSegmentFromUrl(url) {
if (!url) {
return ''
}
// 去掉 URL 中的查询参数部分
const trimmedUrl = url.split('?')[0]
// 获取最后一个斜杠后面的内容
const segments = trimmedUrl.split('/')
return segments[segments.length - 1]
}

View File

@@ -30,4 +30,18 @@ function extractLangId(str) {
}
}
module.exports = { extractLangPrefix, extractLangId }
/**
* 列表中用过来区分page只需要端的id足够
*/
function getShortId(uuid) {
if (!uuid || uuid.indexOf('-') < 0) {
return uuid
}
// 找到第一个 '-' 的位置
const index = uuid.indexOf('-')
// 截取从开始到第一个 '-' 之前的部分
return uuid.substring(0, index)
}
module.exports = { extractLangPrefix, extractLangId, getShortId }

View File

@@ -1,7 +1,7 @@
/**
* 文章相关工具
*/
import { checkContainHttp } from '.'
import { checkStartWithHttp } from '.'
/**
* 获取文章的关联推荐文章列表,目前根据标签关联性筛选
@@ -50,7 +50,7 @@ export function checkSlugHasNoSlash(row) {
}
return (
(slug.match(/\//g) || []).length === 0 &&
!checkContainHttp(slug) &&
!checkStartWithHttp(slug) &&
row.type.indexOf('Menu') < 0
)
}
@@ -67,7 +67,7 @@ export function checkSlugHasOneSlash(row) {
}
return (
(slug.match(/\//g) || []).length === 1 &&
!checkContainHttp(slug) &&
!checkStartWithHttp(slug) &&
row.type.indexOf('Menu') < 0
)
}
@@ -85,6 +85,6 @@ export function checkSlugHasMorThanTwoSlash(row) {
return (
(slug.match(/\//g) || []).length >= 2 &&
row.type.indexOf('Menu') < 0 &&
!checkContainHttp(slug)
!checkStartWithHttp(slug)
)
}