mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-13 23:16:47 +00:00
Merge pull request #3091 from qixing-jk/feat-aisummary-wordcount
Feat aisummary wordcount
This commit is contained in:
@@ -270,6 +270,17 @@ const BLOG = {
|
||||
// 星空雨特效 黑夜模式才会生效
|
||||
STARRY_SKY: process.env.NEXT_PUBLIC_STARRY_SKY || false, // 开关
|
||||
|
||||
// AI 文章摘要生成
|
||||
AI_SUMMARY_API:
|
||||
process.env.AI_SUMMARY_API||
|
||||
'',
|
||||
AI_SUMMARY_KEY:
|
||||
process.env.AI_SUMMARY_KEY ||
|
||||
'',
|
||||
AI_SUMMARY_CACHE_TIME: process.env.AI_SUMMARY_CACHE_TIME || 1800, // 缓存时间,单位秒
|
||||
AI_SUMMARY_WORD_LIMIT: process.env.AI_SUMMARY_WORD_LIMIT || 1000,
|
||||
|
||||
|
||||
// ********挂件组件相关********
|
||||
// AI 文章摘要生成 @see https://docs_s.tianli0.top/
|
||||
TianliGPT_CSS:
|
||||
|
||||
98
components/AISummary.js
Normal file
98
components/AISummary.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import styles from './AISummary.module.css'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
const AISummary = ({ aiSummary }) => {
|
||||
const { locale } = useGlobal()
|
||||
const [summary, setSummary] = useState(aiSummary)
|
||||
|
||||
useEffect(() => {
|
||||
showAiSummaryAnimation(aiSummary, setSummary)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
aiSummary && (
|
||||
<div className={styles['post-ai']}>
|
||||
<div className={styles['ai-container']}>
|
||||
<div className={styles['ai-header']}>
|
||||
<div className={styles['ai-icon']}>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 24 24'
|
||||
width='24'
|
||||
height='24'>
|
||||
<path
|
||||
fill='#ffffff'
|
||||
d='M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8Z'
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className={styles['ai-title']}>{locale.AI_SUMMARY.NAME}</div>
|
||||
<div className={styles['ai-tag']}>GPT</div>
|
||||
</div>
|
||||
<div className={styles['ai-content']}>
|
||||
<div className={styles['ai-explanation']}>
|
||||
{summary}
|
||||
{summary !== aiSummary && (
|
||||
<span className={styles['blinking-cursor']}></span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const showAiSummaryAnimation = (rawSummary, setSummary) => {
|
||||
if (!rawSummary) return
|
||||
let currentIndex = 0
|
||||
const typingDelay = 20
|
||||
const punctuationDelayMultiplier = 6
|
||||
let animationRunning = true
|
||||
let lastUpdateTime = performance.now()
|
||||
const animate = () => {
|
||||
if (currentIndex < rawSummary.length && animationRunning) {
|
||||
const currentTime = performance.now()
|
||||
const timeDiff = currentTime - lastUpdateTime
|
||||
|
||||
const letter = rawSummary.slice(currentIndex, currentIndex + 1)
|
||||
const isPunctuation = /[,。!、?,.!?]/.test(letter)
|
||||
const delay = isPunctuation
|
||||
? typingDelay * punctuationDelayMultiplier
|
||||
: typingDelay
|
||||
|
||||
if (timeDiff >= delay) {
|
||||
setSummary(rawSummary.slice(0, currentIndex + 1))
|
||||
lastUpdateTime = currentTime
|
||||
currentIndex++
|
||||
|
||||
if (currentIndex < rawSummary.length) {
|
||||
setSummary(rawSummary.slice(0, currentIndex))
|
||||
} else {
|
||||
setSummary(rawSummary)
|
||||
observer.disconnect()
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
animate(rawSummary)
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
animationRunning = entries[0].isIntersecting
|
||||
if (animationRunning && currentIndex === 0) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(animate)
|
||||
}, 200)
|
||||
}
|
||||
},
|
||||
{ threshold: 0 }
|
||||
)
|
||||
let post_ai = document.querySelector('.post-ai')
|
||||
if (post_ai) {
|
||||
observer.observe(post_ai)
|
||||
}
|
||||
}
|
||||
|
||||
export default AISummary
|
||||
53
components/AISummary.module.css
Normal file
53
components/AISummary.module.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.post-ai {
|
||||
font-family: 'Noto Sans SC', sans-serif;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.ai-container {
|
||||
background: linear-gradient(135deg, #f9f9f9 0%, #f5f5f5 100%);
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ai-header {
|
||||
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ai-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.ai-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.ai-tag {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ai-content {
|
||||
padding: 20px;
|
||||
}
|
||||
.ai-explanation {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.blinking-cursor {
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background-color: #333;
|
||||
animation: blink 0.7s infinite;
|
||||
margin-left: 5px;
|
||||
}
|
||||
@keyframes blink {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@@ -1,67 +1,23 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 字数统计
|
||||
* @returns
|
||||
*/
|
||||
export default function WordCount() {
|
||||
export default function WordCount({ wordCount, readTime }) {
|
||||
const { locale } = useGlobal()
|
||||
useEffect(() => {
|
||||
countWords()
|
||||
})
|
||||
|
||||
return <span id='wordCountWrapper' className='flex gap-3 font-light'>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='pl-1 pr-2 fas fa-file-word' />
|
||||
<span id='wordCount'>0</span>
|
||||
</span>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='mr-1 fas fa-clock' />
|
||||
<span></span>
|
||||
<span id='readTime'>0</span> {locale.COMMON.MINUTE}
|
||||
</span>
|
||||
return (
|
||||
<span id='wordCountWrapper' className='flex gap-3 font-light'>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='pl-1 pr-2 fas fa-file-word' />
|
||||
<span>{locale.COMMON.WORD_COUNT}</span>
|
||||
<span id='wordCount'>{wordCount}</span>
|
||||
</span>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='mr-1 fas fa-clock' />
|
||||
<span>{locale.COMMON.READ_TIME}≈</span>
|
||||
<span id='readTime'>{readTime}</span> {locale.COMMON.MINUTE}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
function countWords() {
|
||||
const articleText = deleteHtmlTag(document.querySelector('#article-wrapper #notion-article')?.innerHTML)
|
||||
const wordCount = fnGetCpmisWords(articleText)
|
||||
// 阅读速度 300-500每分钟
|
||||
document.getElementById('wordCount').innerHTML = wordCount
|
||||
document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
|
||||
const wordCountWrapper = document.getElementById('wordCountWrapper')
|
||||
wordCountWrapper.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// 去除html标签
|
||||
function deleteHtmlTag(str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
|
||||
return str
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords(str) {
|
||||
if (!str) {
|
||||
return 0
|
||||
}
|
||||
let sLen = 0
|
||||
try {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
str = str.replace(/[\x00-\xff]/g, 'm')
|
||||
str = str.replace(/m+/g, '*')
|
||||
str = str.replace(/龘+/g, '')
|
||||
sLen = str.length
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return sLen
|
||||
}
|
||||
)
|
||||
}
|
||||
4
lib/cache/cache_manager.js
vendored
4
lib/cache/cache_manager.js
vendored
@@ -20,12 +20,12 @@ export async function getDataFromCache(key, force) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function setDataToCache(key, data) {
|
||||
export async function setDataToCache(key, data, customCacheTime) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
// console.trace('[API-->>缓存写入]:', key)
|
||||
await getApi().setCache(key, data)
|
||||
await getApi().setCache(key, data, customCacheTime)
|
||||
}
|
||||
|
||||
export async function delCacheData(key) {
|
||||
|
||||
4
lib/cache/memory_cache.js
vendored
4
lib/cache/memory_cache.js
vendored
@@ -7,8 +7,8 @@ export async function getCache(key, options) {
|
||||
return await cache.get(key)
|
||||
}
|
||||
|
||||
export async function setCache(key, data) {
|
||||
await cache.put(key, data, cacheTime * 1000)
|
||||
export async function setCache(key, data, customCacheTime) {
|
||||
await cache.put(key, data, (customCacheTime || cacheTime) * 1000)
|
||||
}
|
||||
|
||||
export async function delCache(key) {
|
||||
|
||||
@@ -38,9 +38,10 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
|
||||
case 'TAG_SORT_BY_COUNT':
|
||||
case 'THEME':
|
||||
case 'LINK':
|
||||
case 'NPM_CDN_BASE':
|
||||
case 'CDNJS_CDN_BASE':
|
||||
case 'JSDELIVR_CDN_BASE':
|
||||
case 'AI_SUMMARY_API':
|
||||
case 'AI_SUMMARY_KEY':
|
||||
case 'AI_SUMMARY_CACHE_TIME':
|
||||
case 'AI_SUMMARY_WORD_LIMIT':
|
||||
// LINK比较特殊,
|
||||
if (key === 'LINK') {
|
||||
if (!extendConfig || Object.keys(extendConfig).length === 0) {
|
||||
|
||||
@@ -87,5 +87,8 @@ export default {
|
||||
SUBSCRIBE: 'Subscribe',
|
||||
MSG: 'Get the latest news and articles to your inbox every month.',
|
||||
EMAIL: 'Email'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'AI intelligent summary',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
URL_COPIED: "L'URL est copé!",
|
||||
TABLE_OF_CONTENTS: 'Sommaire',
|
||||
RELATE_POSTS: 'Article similaire',
|
||||
COPYRIGHT: 'Droit d\'auteur',
|
||||
COPYRIGHT: "Droit d'auteur",
|
||||
AUTHOR: 'Auteur',
|
||||
URL: 'Link',
|
||||
ANALYTICS: 'Analytique',
|
||||
@@ -29,7 +29,8 @@ export default {
|
||||
ARTICLE: 'Article(s)',
|
||||
VISITORS: 'Visiteurs',
|
||||
VIEWS: 'Views',
|
||||
COPYRIGHT_NOTICE: 'Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
|
||||
COPYRIGHT_NOTICE:
|
||||
'Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International (CC BY-NC-SA 4.0)',
|
||||
RESULT_OF_SEARCH: 'Résultats',
|
||||
ARTICLE_DETAIL: 'Plus de détails',
|
||||
PASSWORD_ERROR: 'Mot de passe est incorrect!',
|
||||
@@ -50,5 +51,8 @@ export default {
|
||||
POST: {
|
||||
BACK: 'Page precedente',
|
||||
TOP: 'Haut'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: "Résumé intelligent par l'IA",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,5 +58,8 @@ export default {
|
||||
POST: {
|
||||
BACK: '前のページに戻る',
|
||||
TOP: '上に戻る'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'AIインテリジェントサマリー',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,5 +53,8 @@ export default {
|
||||
POST: {
|
||||
BACK: 'Geri',
|
||||
TOP: 'Yukarı'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'Yapay Zeka Akıllı Özet',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,5 +87,8 @@ export default {
|
||||
SUBSCRIBE: '邮件订阅',
|
||||
MSG: '订阅以获取每月更新的新闻和文章,直接发送至您的邮箱。',
|
||||
EMAIL: '邮箱'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'AI智能摘要',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,8 @@ export default {
|
||||
POST: {
|
||||
BACK: '返回',
|
||||
TOP: '回到頂端'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'AI智能摘要',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,5 +52,8 @@ export default {
|
||||
POST: {
|
||||
BACK: '返回',
|
||||
TOP: '回到頂端'
|
||||
},
|
||||
AI_SUMMARY: {
|
||||
NAME: 'AI智能摘要',
|
||||
}
|
||||
}
|
||||
|
||||
32
lib/plugins/aiSummary.js
Normal file
32
lib/plugins/aiSummary.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* get Ai summary
|
||||
* @returns {Promise<string>}
|
||||
* @param aiSummaryAPI
|
||||
* @param aiSummaryKey
|
||||
* @param truncatedText
|
||||
*/
|
||||
export async function getAiSummary(aiSummaryAPI, aiSummaryKey, truncatedText) {
|
||||
try {
|
||||
console.log('请求文章摘要', truncatedText.slice(0, 100))
|
||||
const response = await fetch(aiSummaryAPI, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: aiSummaryKey,
|
||||
content: truncatedText
|
||||
})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
return data.summary
|
||||
} else {
|
||||
throw new Error('Response not ok')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ChucklePostAI:请求失败', error)
|
||||
return '获取文章摘要失败,请稍后再试。'
|
||||
}
|
||||
}
|
||||
27
lib/plugins/wordCount.js
Normal file
27
lib/plugins/wordCount.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
export function countWords(pageContentText) {
|
||||
const wordCount = fnGetCpmisWords(pageContentText)
|
||||
// 阅读速度 300-500每分钟
|
||||
const readTime = Math.floor(wordCount / 400) + 1
|
||||
return { wordCount, readTime }
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords(str) {
|
||||
if (!str) {
|
||||
return 0
|
||||
}
|
||||
let sLen = 0
|
||||
try {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
str = str.replace(/[\x00-\xff]/g, 'm')
|
||||
str = str.replace(/m+/g, '*')
|
||||
str = str.replace(/龘+/g, '')
|
||||
sLen = str.length
|
||||
} catch (e) {}
|
||||
return sLen
|
||||
}
|
||||
@@ -2,6 +2,15 @@
|
||||
* 文章相关工具
|
||||
*/
|
||||
import { checkStartWithHttp } from '.'
|
||||
import { getPostBlocks } from '@/lib/db/getSiteData'
|
||||
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { getDataFromCache, setDataToCache } from '@/lib/cache/cache_manager'
|
||||
import { getPageContentText } from '@/pages/search/[keyword]'
|
||||
import { getAiSummary } from '@/lib/plugins/aiSummary'
|
||||
import BLOG from '@/blog.config'
|
||||
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
|
||||
import { countWords } from '@/lib/plugins/wordCount'
|
||||
|
||||
/**
|
||||
* 获取文章的关联推荐文章列表,目前根据标签关联性筛选
|
||||
@@ -88,3 +97,87 @@ export function checkSlugHasMorThanTwoSlash(row) {
|
||||
!checkStartWithHttp(slug)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章摘要
|
||||
* @param props
|
||||
* @param pageContentText
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function getPageAISummary(props, pageContentText) {
|
||||
const aiSummaryAPI = siteConfig('AI_SUMMARY_API')
|
||||
if (aiSummaryAPI) {
|
||||
const post = props.post
|
||||
const cacheKey = `ai_summary_${post.id}`
|
||||
let aiSummary = await getDataFromCache(cacheKey)
|
||||
if (aiSummary) {
|
||||
props.post.aiSummary = aiSummary
|
||||
} else {
|
||||
const aiSummaryKey = siteConfig('AI_SUMMARY_KEY')
|
||||
const aiSummaryCacheTime = siteConfig('AI_SUMMARY_CACHE_TIME')
|
||||
const wordLimit = siteConfig('AI_SUMMARY_WORD_LIMIT', '1000')
|
||||
let content = ''
|
||||
for (let heading of post.toc) {
|
||||
content += heading.text + ' '
|
||||
}
|
||||
content += pageContentText
|
||||
const combinedText = post.title + ' ' + content
|
||||
const truncatedText = combinedText.slice(0, wordLimit)
|
||||
aiSummary = await getAiSummary(aiSummaryAPI, aiSummaryKey, truncatedText)
|
||||
await setDataToCache(cacheKey, aiSummary, aiSummaryCacheTime)
|
||||
props.post.aiSummary = aiSummary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文章数据
|
||||
* @param props
|
||||
* @param from
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function processPostData(props, from) {
|
||||
// 文章内容加载
|
||||
if (!props?.post?.blockMap) {
|
||||
props.post.blockMap = await getPostBlocks(props.post.id, from)
|
||||
}
|
||||
|
||||
if (props.post?.blockMap?.block) {
|
||||
// 目录默认加载
|
||||
props.post.content = Object.keys(props.post.blockMap.block).filter(
|
||||
key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id
|
||||
)
|
||||
props.post.toc = getPageTableOfContents(props.post, props.post.blockMap)
|
||||
const pageContentText = getPageContentText(props.post, props.post.blockMap)
|
||||
const { wordCount, readTime } = countWords(pageContentText)
|
||||
props.post.wordCount = wordCount
|
||||
props.post.readTime = readTime
|
||||
await getPageAISummary(props, pageContentText)
|
||||
}
|
||||
|
||||
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
||||
if (BLOG.ALGOLIA_APP_ID) {
|
||||
uploadDataToAlgolia(props?.post)
|
||||
}
|
||||
|
||||
// 推荐关联文章处理
|
||||
const allPosts = props.allPages?.filter(
|
||||
page => page.type === 'Post' && page.status === 'Published'
|
||||
)
|
||||
if (allPosts && allPosts.length > 0) {
|
||||
const index = allPosts.indexOf(props.post)
|
||||
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
|
||||
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
|
||||
props.recommendPosts = getRecommendPost(
|
||||
props.post,
|
||||
allPosts,
|
||||
siteConfig('POST_RECOMMEND_COUNT')
|
||||
)
|
||||
} else {
|
||||
props.prev = null
|
||||
props.next = null
|
||||
props.recommendPosts = []
|
||||
}
|
||||
|
||||
delete props.allPages
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
|
||||
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
|
||||
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
|
||||
import { checkSlugHasMorThanTwoSlash, getRecommendPost } from '@/lib/utils/post'
|
||||
import { getGlobalData, getPost } from '@/lib/db/getSiteData'
|
||||
import { checkSlugHasMorThanTwoSlash, processPostData } from '@/lib/utils/post'
|
||||
import { idToUuid } from 'notion-utils'
|
||||
import Slug from '..'
|
||||
|
||||
@@ -79,59 +77,12 @@ export async function getStaticProps({
|
||||
}
|
||||
}
|
||||
|
||||
// 无法获取文章
|
||||
if (!props?.post) {
|
||||
// 无法获取文章
|
||||
props.post = null
|
||||
return {
|
||||
props,
|
||||
revalidate: process.env.EXPORT
|
||||
? undefined
|
||||
: siteConfig(
|
||||
'NEXT_REVALIDATE_SECOND',
|
||||
BLOG.NEXT_REVALIDATE_SECOND,
|
||||
props.NOTION_CONFIG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 文章内容加载
|
||||
if (!props?.post?.blockMap) {
|
||||
props.post.blockMap = await getPostBlocks(props.post.id, from)
|
||||
}
|
||||
|
||||
// 目录默认加载
|
||||
if (props.post?.blockMap?.block) {
|
||||
props.post.content = Object.keys(props.post.blockMap.block).filter(
|
||||
key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id
|
||||
)
|
||||
props.post.toc = getPageTableOfContents(props.post, props.post.blockMap)
|
||||
}
|
||||
|
||||
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
||||
if (BLOG.ALGOLIA_APP_ID) {
|
||||
uploadDataToAlgolia(props?.post)
|
||||
}
|
||||
|
||||
// 推荐关联文章处理
|
||||
const allPosts = props.allPages?.filter(
|
||||
page => page.type === 'Post' && page.status === 'Published'
|
||||
)
|
||||
if (allPosts && allPosts.length > 0) {
|
||||
const index = allPosts.indexOf(props.post)
|
||||
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
|
||||
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
|
||||
props.recommendPosts = getRecommendPost(
|
||||
props.post,
|
||||
allPosts,
|
||||
siteConfig('POST_RECOMMEND_COUNT')
|
||||
)
|
||||
} else {
|
||||
props.prev = null
|
||||
props.next = null
|
||||
props.recommendPosts = []
|
||||
await processPostData(props, from)
|
||||
}
|
||||
|
||||
delete props.allPages
|
||||
return {
|
||||
props,
|
||||
revalidate: process.env.EXPORT
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
|
||||
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
|
||||
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
|
||||
import { checkSlugHasOneSlash, getRecommendPost } from '@/lib/utils/post'
|
||||
import { getGlobalData, getPost } from '@/lib/db/getSiteData'
|
||||
import { checkSlugHasOneSlash, processPostData } from '@/lib/utils/post'
|
||||
import { idToUuid } from 'notion-utils'
|
||||
import Slug from '..'
|
||||
|
||||
@@ -68,59 +66,12 @@ export async function getStaticProps({ params: { prefix, slug }, locale }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 无法获取文章
|
||||
if (!props?.post) {
|
||||
// 无法获取文章
|
||||
props.post = null
|
||||
return {
|
||||
props,
|
||||
revalidate: process.env.EXPORT
|
||||
? undefined
|
||||
: siteConfig(
|
||||
'NEXT_REVALIDATE_SECOND',
|
||||
BLOG.NEXT_REVALIDATE_SECOND,
|
||||
props.NOTION_CONFIG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 文章内容加载
|
||||
if (!props?.post?.blockMap) {
|
||||
props.post.blockMap = await getPostBlocks(props.post.id, from)
|
||||
}
|
||||
|
||||
// 目录默认加载
|
||||
if (props.post?.blockMap?.block) {
|
||||
props.post.content = Object.keys(props.post.blockMap.block).filter(
|
||||
key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id
|
||||
)
|
||||
props.post.toc = getPageTableOfContents(props.post, props.post.blockMap)
|
||||
}
|
||||
|
||||
// 生成全文索引 && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
||||
if (BLOG.ALGOLIA_APP_ID) {
|
||||
uploadDataToAlgolia(props?.post)
|
||||
}
|
||||
|
||||
// 推荐关联文章处理
|
||||
const allPosts = props.allPages?.filter(
|
||||
page => page.type === 'Post' && page.status === 'Published'
|
||||
)
|
||||
if (allPosts && allPosts.length > 0) {
|
||||
const index = allPosts.indexOf(props.post)
|
||||
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
|
||||
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
|
||||
props.recommendPosts = getRecommendPost(
|
||||
props.post,
|
||||
allPosts,
|
||||
siteConfig('POST_RECOMMEND_COUNT')
|
||||
)
|
||||
} else {
|
||||
props.prev = null
|
||||
props.next = null
|
||||
props.recommendPosts = []
|
||||
await processPostData(props, from)
|
||||
}
|
||||
|
||||
delete props.allPages
|
||||
return {
|
||||
props,
|
||||
revalidate: process.env.EXPORT
|
||||
|
||||
@@ -2,12 +2,11 @@ import BLOG from '@/blog.config'
|
||||
import useNotification from '@/components/Notification'
|
||||
import OpenWrite from '@/components/OpenWrite'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
|
||||
import { getGlobalData, getPost } from '@/lib/db/getSiteData'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
|
||||
import { getPasswordQuery } from '@/lib/password'
|
||||
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
|
||||
import { checkSlugHasNoSlash, getRecommendPost } from '@/lib/utils/post'
|
||||
import { checkSlugHasNoSlash, processPostData } from '@/lib/utils/post'
|
||||
import { DynamicLayout } from '@/themes/theme'
|
||||
import md5 from 'js-md5'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -141,59 +140,12 @@ export async function getStaticProps({ params: { prefix }, locale }) {
|
||||
props.post = post
|
||||
}
|
||||
}
|
||||
// 无法获取文章
|
||||
if (!props?.post) {
|
||||
// 无法获取文章
|
||||
props.post = null
|
||||
return {
|
||||
props,
|
||||
revalidate: process.env.EXPORT
|
||||
? undefined
|
||||
: siteConfig(
|
||||
'NEXT_REVALIDATE_SECOND',
|
||||
BLOG.NEXT_REVALIDATE_SECOND,
|
||||
props.NOTION_CONFIG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 文章内容加载
|
||||
if (!props?.post?.blockMap) {
|
||||
props.post.blockMap = await getPostBlocks(props.post.id, from)
|
||||
}
|
||||
|
||||
// 目录默认加载
|
||||
if (props.post?.blockMap?.block) {
|
||||
props.post.content = Object.keys(props.post.blockMap.block).filter(
|
||||
key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id
|
||||
)
|
||||
props.post.toc = getPageTableOfContents(props.post, props.post.blockMap)
|
||||
}
|
||||
|
||||
// 生成全文索引 && process.env.npm_lifecycle_event === 'build' && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA)
|
||||
if (BLOG.ALGOLIA_APP_ID) {
|
||||
uploadDataToAlgolia(props?.post)
|
||||
}
|
||||
|
||||
// 推荐关联文章处理
|
||||
const allPosts = props.allPages?.filter(
|
||||
page => page.type === 'Post' && page.status === 'Published'
|
||||
)
|
||||
if (allPosts && allPosts.length > 0) {
|
||||
const index = allPosts.indexOf(props.post)
|
||||
props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0]
|
||||
props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0]
|
||||
props.recommendPosts = getRecommendPost(
|
||||
props.post,
|
||||
allPosts,
|
||||
siteConfig('POST_RECOMMEND_COUNT')
|
||||
)
|
||||
} else {
|
||||
props.prev = null
|
||||
props.next = null
|
||||
props.recommendPosts = []
|
||||
await processPostData(props, from)
|
||||
}
|
||||
|
||||
delete props.allPages
|
||||
return {
|
||||
props,
|
||||
revalidate: process.env.EXPORT
|
||||
|
||||
@@ -31,9 +31,8 @@ export default function PostHeader({ post, siteInfo, isDarkMode }) {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-shadow: 110px -130px 500px 100px ${isDarkMode
|
||||
? '#CA8A04'
|
||||
: '#0060e0'} inset;
|
||||
box-shadow: 110px -130px 500px 100px
|
||||
${isDarkMode ? '#CA8A04' : '#0060e0'} inset;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
@@ -105,7 +104,10 @@ export default function PostHeader({ post, siteInfo, isDarkMode }) {
|
||||
<section className='flex-wrap dark:text-gray-200 text-opacity-70 shadow-text-md flex text-sm justify-center md:justify-start mt-4 text-white font-light leading-8'>
|
||||
<div className='flex justify-center '>
|
||||
<div className='mr-2'>
|
||||
<WordCount />
|
||||
<WordCount
|
||||
wordCount={post.wordCount}
|
||||
readTime={post.readTime}
|
||||
/>
|
||||
</div>
|
||||
{post?.type !== 'Page' && (
|
||||
<>
|
||||
|
||||
@@ -42,6 +42,7 @@ import SearchNav from './components/SearchNav'
|
||||
import SideRight from './components/SideRight'
|
||||
import CONFIG from './config'
|
||||
import { Style } from './style'
|
||||
import AISummary from '@/components/AISummary'
|
||||
|
||||
/**
|
||||
* 基础布局 采用上中下布局,移动端使用顶部侧边导航栏
|
||||
@@ -306,6 +307,7 @@ const LayoutSlug = props => {
|
||||
<section
|
||||
className='wow fadeInUp p-5 justify-center mx-auto'
|
||||
data-wow-delay='.2s'>
|
||||
<AISummary aiSummary={post.aiSummary}/>
|
||||
<WWAds orientation='horizontal' className='w-full' />
|
||||
{post && <NotionPage post={post} />}
|
||||
<WWAds orientation='horizontal' className='w-full' />
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
import Link from 'next/link'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import TagItemMiddle from './TagItemMiddle'
|
||||
import WordCount from './WordCount'
|
||||
import { formatDateFmt } from '@/lib/utils/formatDate'
|
||||
import WordCount from '@/components/WordCount'
|
||||
|
||||
export const ArticleInfo = (props) => {
|
||||
export const ArticleInfo = props => {
|
||||
const { post } = props
|
||||
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return (
|
||||
<section className='mb-3 dark:text-gray-200'>
|
||||
<div className='my-3'>
|
||||
{post.tagItems && (
|
||||
<div className="flex flex-nowrap overflow-x-auto">
|
||||
{post.tagItems.map(tag => (
|
||||
<TagItemMiddle key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<section className='mb-3 dark:text-gray-200'>
|
||||
<div className='my-3'>
|
||||
{post.tagItems && (
|
||||
<div className='flex flex-nowrap overflow-x-auto'>
|
||||
{post.tagItems.map(tag => (
|
||||
<TagItemMiddle key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap gap-3 mt-5 text-sm'>
|
||||
{post?.type !== 'Page' && (<>
|
||||
<Link
|
||||
href={`/archive#${formatDateFmt(post?.publishDate, 'yyyy-MM')}`}
|
||||
passHref
|
||||
className="cursor-pointer whitespace-nowrap">
|
||||
|
||||
<i className='far fa-calendar-minus fa-fw'/> {locale.COMMON.POST_TIME}: {post?.publishDay}
|
||||
|
||||
</Link>
|
||||
<span className='whitespace-nowrap'>
|
||||
<i className='far fa-calendar-check fa-fw' />{locale.COMMON.LAST_EDITED_TIME}: {post.lastEditedDay}
|
||||
</span>
|
||||
<span className="hidden busuanzi_container_page_pv font-light mr-2">
|
||||
<i className='mr-1 fas fa-eye' /><span className="busuanzi_value_page_pv" />
|
||||
</span>
|
||||
<WordCount />
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<div className='flex flex-wrap gap-3 mt-5 text-sm'>
|
||||
{post?.type !== 'Page' && (
|
||||
<>
|
||||
<Link
|
||||
href={`/archive#${formatDateFmt(post?.publishDate, 'yyyy-MM')}`}
|
||||
passHref
|
||||
className='cursor-pointer whitespace-nowrap'>
|
||||
<i className='far fa-calendar-minus fa-fw' />{' '}
|
||||
{locale.COMMON.POST_TIME}: {post?.publishDay}
|
||||
</Link>
|
||||
<span className='whitespace-nowrap'>
|
||||
<i className='far fa-calendar-check fa-fw' />
|
||||
{locale.COMMON.LAST_EDITED_TIME}: {post.lastEditedDay}
|
||||
</span>
|
||||
<span className='hidden busuanzi_container_page_pv font-light mr-2'>
|
||||
<i className='mr-1 fas fa-eye' />
|
||||
<span className='busuanzi_value_page_pv' />
|
||||
</span>
|
||||
<WordCount wordCount={post.wordCount} readTime={post.readTime} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 字数统计
|
||||
* @returns
|
||||
*/
|
||||
export default function WordCount() {
|
||||
const { locale } = useGlobal()
|
||||
useEffect(() => {
|
||||
countWords()
|
||||
})
|
||||
|
||||
return <span id='wordCountWrapper' className='flex gap-3 font-light'>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='pl-1 pr-2 fas fa-file-word' />
|
||||
<span>{locale.COMMON.WORD_COUNT}</span>
|
||||
<span id='wordCount'>0</span>
|
||||
</span>
|
||||
<span className='flex whitespace-nowrap items-center'>
|
||||
<i className='mr-1 fas fa-clock' />
|
||||
<span></span>
|
||||
<span id='readTime'>0</span> {locale.COMMON.MINUTE}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
function countWords() {
|
||||
const articleText = deleteHtmlTag(document.querySelector('#article-wrapper #notion-article')?.innerHTML)
|
||||
const wordCount = fnGetCpmisWords(articleText)
|
||||
// 阅读速度 300-500每分钟
|
||||
document.getElementById('wordCount').innerHTML = wordCount
|
||||
document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
|
||||
const wordCountWrapper = document.getElementById('wordCountWrapper')
|
||||
wordCountWrapper.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// 去除html标签
|
||||
function deleteHtmlTag(str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
|
||||
return str
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords(str) {
|
||||
if (!str) {
|
||||
return 0
|
||||
}
|
||||
let sLen = 0
|
||||
try {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
str = str.replace(/[\x00-\xff]/g, 'm')
|
||||
str = str.replace(/m+/g, '*')
|
||||
str = str.replace(/龘+/g, '')
|
||||
sLen = str.length
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return sLen
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import ArticleCopyright from './ArticleCopyright'
|
||||
import BlogAround from './BlogAround'
|
||||
import RecommendPosts from './RecommendPosts'
|
||||
import TagItem from './TagItem'
|
||||
import WordCount from './WordCount'
|
||||
import WordCount from '@/components/WordCount'
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -92,7 +92,7 @@ export default function ArticleDetail(props) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<WordCount />
|
||||
<WordCount wordCount={post.wordCount} readTime={post.readTime} />
|
||||
</section>
|
||||
</header>
|
||||
)}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* 字数统计
|
||||
* @returns
|
||||
*/
|
||||
export default function WordCount() {
|
||||
useEffect(() => {
|
||||
countWords()
|
||||
})
|
||||
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return <div id='wordCountWrapper' className='flex justify-center my-auto font-light'>
|
||||
<i className='mr-1 fas fa-file-word my-auto' />
|
||||
<span className='hidden md:block'>{locale.COMMON.WORD_COUNT}≈</span>
|
||||
<strong id='wordCount'>0</strong> | <i className='mr-1 fas fa-clock my-auto' />{locale.COMMON.READ_TIME} <span className='hidden md:block'>≈</span> <strong id='readTime'>0</strong> {locale.COMMON.MINUTE}
|
||||
</div>
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字数统计和阅读时间
|
||||
*/
|
||||
function countWords() {
|
||||
const articleText = deleteHtmlTag(document.querySelector('#article-wrapper #notion-article')?.innerHTML)
|
||||
const wordCount = fnGetCpmisWords(articleText)
|
||||
// 阅读速度 300-500每分钟
|
||||
document.getElementById('wordCount').innerHTML = wordCount
|
||||
document.getElementById('readTime').innerHTML = Math.floor(wordCount / 400) + 1
|
||||
const wordCountWrapper = document.getElementById('wordCountWrapper')
|
||||
wordCountWrapper.classList.remove('hidden')
|
||||
}
|
||||
|
||||
// 去除html标签
|
||||
function deleteHtmlTag(str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
str = str.replace(/<[^>]+>|&[^>]+;/g, '').trim()// 去掉所有的html标签和 之类的特殊符合
|
||||
return str
|
||||
}
|
||||
|
||||
// 用word方式计算正文字数
|
||||
function fnGetCpmisWords(str) {
|
||||
if (!str) {
|
||||
return 0
|
||||
}
|
||||
let sLen = 0
|
||||
try {
|
||||
// eslint-disable-next-line no-irregular-whitespace
|
||||
str = str.replace(/(\r\n+|\s+| +)/g, '龘')
|
||||
// eslint-disable-next-line no-control-regex
|
||||
str = str.replace(/[\x00-\xff]/g, 'm')
|
||||
str = str.replace(/m+/g, '*')
|
||||
str = str.replace(/龘+/g, '')
|
||||
sLen = str.length
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return sLen
|
||||
}
|
||||
Reference in New Issue
Block a user