diff --git a/.eslintrc.js b/.eslintrc.js index 9836da77..c6fbb20a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,11 +4,7 @@ module.exports = { es2021: true, node: true }, - extends: [ - 'plugin:react/recommended', - 'plugin:@next/next/recommended', - 'standard' - ], + extends: ['plugin:react/recommended', 'plugin:@next/next/recommended', 'standard', 'prettier'], parserOptions: { ecmaFeatures: { jsx: true @@ -16,10 +12,7 @@ module.exports = { ecmaVersion: 12, sourceType: 'module' }, - plugins: [ - 'react', - 'react-hooks' - ], + plugins: ['react', 'react-hooks', 'prettier'], settings: { react: { version: 'detect' diff --git a/.prettierrc.json b/.prettierrc.json index 5f5ab6df..f0bc11c8 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,10 @@ { - "singleQuote": true, - "semi": false, - "trailingComma": "none", - "arrowParens": "avoid" -} + "singleQuote": true, + "semi": false, + "trailingComma": "none", + "arrowParens": "avoid", + "printWidth": 120, + "bracketSpacing": true, + "jsxSingleQuote": true, + "jsxBracketSameLine": true +} \ No newline at end of file diff --git a/components/AlgoliaSearchModal.js b/components/AlgoliaSearchModal.js index 54465826..ea727a48 100644 --- a/components/AlgoliaSearchModal.js +++ b/components/AlgoliaSearchModal.js @@ -1,4 +1,4 @@ -import { useState, useImperativeHandle, useRef, useEffect } from 'react' +import { useState, useImperativeHandle, useRef, useEffect, Fragment } from 'react' import algoliasearch from 'algoliasearch' import replaceSearchResult from '@/components/Mark' import Link from 'next/link' @@ -7,6 +7,22 @@ import throttle from 'lodash/throttle' import { siteConfig } from '@/lib/config' import { useHotkeys } from 'react-hotkeys-hook'; +const ShortCutActions = [ + { + key: '↑ ↓', + action: '选择' + }, + { + key: 'Enter', + action: '跳转' + }, + { + key: 'Esc', + action: '关闭' + } + +] + /** * 结合 Algolia 实现的弹出式搜索框 * 打开方式 cRef.current.openSearch() @@ -234,6 +250,15 @@ export default function AlgoliaSearchModal({ cRef }) {
+ {totalHit === 0 && (
+ { + ShortCutActions.map((action, index) => { + return
{action.key}
+ {action.action}
+ }) + } +
) + }
{totalHit > 0 && (

diff --git a/components/WalineComponent.js b/components/WalineComponent.js index 37247d48..960f153f 100644 --- a/components/WalineComponent.js +++ b/components/WalineComponent.js @@ -1,4 +1,4 @@ -import React, { createRef } from 'react' +import { createRef, useEffect } from 'react' import { init } from '@waline/client' import { useRouter } from 'next/router' import '@waline/client/dist/waline.css' @@ -21,7 +21,7 @@ const WalineComponent = (props) => { } } - React.useEffect(() => { + useEffect(() => { if (!waline) { waline = init({ ...props, diff --git a/lib/db/getSiteData.js b/lib/db/getSiteData.js index 85dd0c7a..997a97c6 100755 --- a/lib/db/getSiteData.js +++ b/lib/db/getSiteData.js @@ -25,10 +25,7 @@ export { getPost } from '../notion/getNotionPost' * @returns * */ -export async function getGlobalData({ - pageId = BLOG.NOTION_PAGE_ID, - from -}) { +export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) { // 从notion获取 const data = await getNotionPageData({ pageId, from }) const db = deepClone(data) @@ -89,7 +86,9 @@ function cleanBlock(post) { * @returns */ function getLatestPosts({ allPages, from, latestPostCount }) { - const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published') + const allPosts = allPages?.filter( + page => page.type === 'Post' && page.status === 'Published' + ) const latestPosts = Object.create(allPosts).sort((a, b) => { const dateA = new Date(a?.lastEditedDate || a?.publishDate) @@ -140,7 +139,13 @@ function getCustomNav({ allPages }) { p.to = '/' + p.slug } } - customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, target: '_blank', show: true }) + customNav.push({ + icon: p.icon || null, + name: p.title, + to: p.slug, + target: '_blank', + show: true + }) }) } return customNav @@ -152,7 +157,12 @@ function getCustomNav({ allPages }) { * @returns */ function getCustomMenu({ collectionData }) { - const menuPages = collectionData.filter(post => post.status === 'Published' && (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu)) + const menuPages = collectionData.filter( + post => + post.status === 'Published' && + (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || + post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu) + ) const menus = [] if (menuPages && menuPages.length > 0) { menuPages.forEach(e => { @@ -190,7 +200,9 @@ function getCustomMenu({ collectionData }) { */ function getTagOptions(schema) { if (!schema) return {} - const tagSchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.tags) + const tagSchema = Object.values(schema).find( + e => e.name === BLOG.NOTION_PROPERTY_NAME.tags + ) return tagSchema?.options || [] } @@ -201,7 +213,9 @@ function getTagOptions(schema) { */ function getCategoryOptions(schema) { if (!schema) return {} - const categorySchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.category) + const categorySchema = Object.values(schema).find( + e => e.name === BLOG.NOTION_PROPERTY_NAME.category + ) return categorySchema?.options || [] } @@ -213,9 +227,15 @@ function getCategoryOptions(schema) { */ function getSiteInfo({ collection, block }) { const title = collection?.name?.[0][0] || BLOG.TITLE - const description = collection?.description ? Object.assign(collection).description[0][0] : BLOG.DESCRIPTION - const pageCover = collection?.cover ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) : BLOG.HOME_BANNER_IMAGE - let icon = collection?.icon ? mapImgUrl(collection?.icon, collection, 'collection') : BLOG.AVATAR + const description = collection?.description + ? Object.assign(collection).description[0][0] + : BLOG.DESCRIPTION + const pageCover = collection?.cover + ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) + : BLOG.HOME_BANNER_IMAGE + let icon = collection?.icon + ? mapImgUrl(collection?.icon, collection, 'collection') + : BLOG.AVATAR // 用户头像压缩一下 icon = compressImage(icon) @@ -236,7 +256,13 @@ function getSiteInfo({ collection, block }) { */ export function getNavPages({ allPages }) { const allNavPages = allPages?.filter(post => { - return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published' + return ( + post && + post?.slug && + !post?.slug?.startsWith('http') && + post?.type === 'Post' && + post?.status === 'Published' + ) }) return allNavPages.map(item => ({ @@ -248,7 +274,8 @@ export function getNavPages({ allPages }) { summary: item.summary || null, slug: item.slug, pageIcon: item.pageIcon || '', - lastEditedDate: item.lastEditedDate + lastEditedDate: item.lastEditedDate, + publishDate: item.publishDate })) } @@ -265,19 +292,26 @@ async function getNotice(post) { } // 没有数据时返回 -const EmptyData = (pageId) => { +const EmptyData = pageId => { const empty = { notice: null, siteInfo: getSiteInfo({}), - allPages: [{ - id: 1, - title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, - summary: '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', - status: 'Published', - type: 'Post', - slug: '13a171332816461db29d50e9f575b00d', - date: { start_date: '2023-04-24', lastEditedDay: '2023-04-24', tagItems: [] } - }], + allPages: [ + { + id: 1, + title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`, + summary: + '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next', + status: 'Published', + type: 'Post', + slug: '13a171332816461db29d50e9f575b00d', + date: { + start_date: '2023-04-24', + lastEditedDay: '2023-04-24', + tagItems: [] + } + } + ], allNavPages: [], collection: [], collectionQuery: {}, @@ -313,7 +347,8 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { const rawMetadata = block[pageId]?.value // Check Type Page-Database和Inline-Database if ( - rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view' + rawMetadata?.type !== 'collection_view_page' && + rawMetadata?.type !== 'collection_view' ) { console.error(`pageId "${pageId}" is not a database`) return EmptyData(pageId) @@ -328,9 +363,21 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { const viewIds = rawMetadata?.view_ids const collectionData = [] - const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds) + const pageIds = getAllPageIds( + collectionQuery, + collectionId, + collectionView, + viewIds + ) if (pageIds?.length === 0) { - console.error('获取到的文章列表为空,请检查notion模板', collectionQuery, collection, collectionView, viewIds, pageRecordMap) + console.error( + '获取到的文章列表为空,请检查notion模板', + collectionQuery, + collection, + collectionView, + viewIds, + pageRecordMap + ) } else { // console.log('有效Page数量', pageIds?.length) } @@ -343,7 +390,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { // 如果找不到文章对应的block,说明发生了溢出,使用pageID再去请求 const pageBlock = await getSingleBlock(id, from) if (pageBlock.block[id].value) { - const properties = (await getPageProperties(id, pageBlock.block[id].value, schema, null, getTagOptions(schema))) || null + const properties = + (await getPageProperties( + id, + pageBlock.block[id].value, + schema, + null, + getTagOptions(schema) + )) || null if (properties) { collectionData.push(properties) } @@ -351,7 +405,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { continue } - const properties = (await getPageProperties(id, value, schema, null, getTagOptions(schema))) || null + const properties = + (await getPageProperties( + id, + value, + schema, + null, + getTagOptions(schema) + )) || null if (properties) { collectionData.push(properties) } @@ -365,13 +426,16 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { if (post?.type === 'Post' && post.status === 'Published') { postCount++ } - return post && post?.slug && - (!post?.slug?.startsWith('http')) && + return ( + post && + post?.slug && + !post?.slug?.startsWith('http') && (post?.status === 'Invisible' || post?.status === 'Published') + ) }) // 站点配置优先读取配置表格,否则读取blog.config.js 文件 - const NOTION_CONFIG = await getConfigMapFromConfigPage(collectionData) || {} + const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {} // Sort by date if (BLOG.POSTS_SORT_BY === 'date') { @@ -380,13 +444,27 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { }) } - const notice = await getNotice(collectionData.filter(post => { - return post && post?.type && post?.type === 'Notice' && post.status === 'Published' - })?.[0]) - const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) }) + const notice = await getNotice( + collectionData.filter(post => { + return ( + post && + post?.type && + post?.type === 'Notice' && + post.status === 'Published' + ) + })?.[0] + ) + const categoryOptions = getAllCategories({ + allPages, + categoryOptions: getCategoryOptions(schema) + }) const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) }) // 旧的菜单 - const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') }) + const customNav = getCustomNav({ + allPages: collectionData.filter( + post => post?.type === 'Page' && post.status === 'Published' + ) + }) // 新的菜单 const customMenu = await getCustomMenu({ collectionData }) const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 }) diff --git a/lib/global.js b/lib/global.js index 4eadfa19..17f31ace 100644 --- a/lib/global.js +++ b/lib/global.js @@ -16,7 +16,8 @@ export function GlobalContextProvider(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 [isDarkMode, updateDarkMode] = useState(NOTION_CONFIG?.APPEARANCE || APPEARANCE === 'dark') // 默认深色模式 + const defaultDarkMode = NOTION_CONFIG?.APPEARANCE || APPEARANCE + const [isDarkMode, updateDarkMode] = useState(defaultDarkMode === 'dark') // 默认深色模式 const [onLoading, setOnLoading] = useState(false) // 抓取文章数据 const router = useRouter() @@ -57,7 +58,7 @@ export function GlobalContextProvider(props) { } useEffect(() => { - initDarkMode(updateDarkMode) + initDarkMode(updateDarkMode,defaultDarkMode) initLocale(lang, locale, updateLang, updateLocale) }, []) diff --git a/lib/utils/index.js b/lib/utils/index.js index d632e1c3..9f5891e2 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -16,7 +16,7 @@ export const isSearchEngineBot = () => { return false } // 获取用户代理字符串 - const userAgent = navigator.userAgent; + const userAgent = navigator.userAgent // 使用正则表达式检测是否包含搜索引擎爬虫关键字 return /Googlebot|bingbot|Baidu/.test(userAgent) } @@ -24,8 +24,8 @@ export const isSearchEngineBot = () => { /** * 组件持久化 */ -export const memorize = (Component) => { - const MemoizedComponent = (props) => { +export const memorize = Component => { + const MemoizedComponent = props => { return } return memo(MemoizedComponent) @@ -34,26 +34,26 @@ export const memorize = (Component) => { // 转换外链 export function sliceUrlFromHttp(str) { // 检查字符串是否包含http - if (str.includes('http:') || str.includes('https:')) { + if (str?.includes('http:') || str?.includes('https:')) { // 如果包含,找到http的位置 - const index = str.indexOf('http'); + const index = str?.indexOf('http') // 返回http之后的部分 - return str.slice(index, str.length); + return str.slice(index, str.length) } else { // 如果不包含,返回原字符串 - return str; + return str } } // 检查是否外链 export function checkContainHttp(str) { // 检查字符串是否包含http - if (str.includes('http:') || str.includes('https:')) { + if (str?.includes('http:') || str?.includes('https:')) { // 如果包含,找到http的位置 - return str.indexOf('http') > -1 + return str?.indexOf('http') > -1 } else { // 不包含 - return false; + return false } } @@ -65,7 +65,10 @@ export function checkContainHttp(str) { */ export function loadExternalResource(url, type) { // 检查是否已存在 - const elements = type === 'js' ? document.querySelectorAll(`[src='${url}']`) : document.querySelectorAll(`[href='${url}']`) + const elements = + type === 'js' + ? document.querySelectorAll(`[src='${url}']`) + : document.querySelectorAll(`[href='${url}']`) return new Promise((resolve, reject) => { if (elements.length > 0 || !url) { @@ -112,9 +115,11 @@ export function getQueryVariable(key) { const vars = query.split('&') for (let i = 0; i < vars.length; i++) { const pair = vars[i].split('=') - if (pair[0] === key) { return pair[1] } + if (pair[0] === key) { + return pair[1] + } } - return (false) + return false } /** * 获取 URL 中指定参数的值 @@ -124,9 +129,9 @@ export function getQueryVariable(key) { */ export function getQueryParam(url, param) { // 移除哈希部分 - const urlWithoutHash = url.split('#')[0]; - const searchParams = new URLSearchParams(urlWithoutHash.split('?')[1]); - return searchParams.get(param); + const urlWithoutHash = url.split('#')[0] + const searchParams = new URLSearchParams(urlWithoutHash.split('?')[1]) + return searchParams.get(param) } /** @@ -157,7 +162,7 @@ export function mergeDeep(target, ...sources) { * @returns {boolean} */ export function isObject(item) { - return (item && typeof item === 'object' && !Array.isArray(item)) + return item && typeof item === 'object' && !Array.isArray(item) } /** @@ -210,10 +215,7 @@ export const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) * @returns {*} */ export const getListByPage = function (list, pageIndex, pageSize) { - return list.slice( - 0, - pageIndex * pageSize - ) + return list.slice(0, pageIndex * pageSize) } /** @@ -230,7 +232,7 @@ export const isMobile = () => { // isMobile = true // } - if (!isMobile && (/Mobi|Android|iPhone/i.test(navigator.userAgent))) { + if (!isMobile && /Mobi|Android|iPhone/i.test(navigator.userAgent)) { isMobile = true } @@ -249,41 +251,41 @@ export const isMobile = () => { * 扫描页面上的所有文本节点,将url格式的文本转为可点击链接 * @param {*} node */ -export const scanAndConvertToLinks = (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 text = node.textContent + const urlRegex = /https?:\/\/[^\s]+/g + let lastIndex = 0 + let match - const newNode = document.createElement('span'); + const newNode = document.createElement('span') while ((match = urlRegex.exec(text)) !== null) { - const beforeText = text.substring(lastIndex, match.index); - const url = match[0]; + const beforeText = text.substring(lastIndex, match.index) + const url = match[0] if (beforeText) { - newNode.appendChild(document.createTextNode(beforeText)); + newNode.appendChild(document.createTextNode(beforeText)) } - const link = document.createElement('a'); - link.href = url; + const link = document.createElement('a') + link.href = url link.target = '_blank' - link.textContent = url; + link.textContent = url - newNode.appendChild(link); + newNode.appendChild(link) - lastIndex = urlRegex.lastIndex; + lastIndex = urlRegex.lastIndex } if (lastIndex < text.length) { - newNode.appendChild(document.createTextNode(text.substring(lastIndex))); + newNode.appendChild(document.createTextNode(text.substring(lastIndex))) } - node.parentNode.replaceChild(newNode, node); + node.parentNode.replaceChild(newNode, node) } else if (node.nodeType === Node.ELEMENT_NODE) { for (const childNode of node.childNodes) { - scanAndConvertToLinks(childNode); + scanAndConvertToLinks(childNode) } } } diff --git a/package.json b/package.json index 0ccd17e1..13b60baf 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,11 @@ "cross-env": "^7.0.3", "eslint": "^7.26.0", "eslint-config-next": "^13.1.1", + "eslint-config-prettier": "^9.1.0", "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.23.0", "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.6.0", @@ -76,4 +78,4 @@ "url": "https://github.com/tangly/NotionNext/issues", "email": "tlyong1992@hotmail.com" } -} \ No newline at end of file +} diff --git a/public/dplayer.htm b/public/dplayer.htm new file mode 100644 index 00000000..73760fa3 --- /dev/null +++ b/public/dplayer.htm @@ -0,0 +1,59 @@ + + + + + + + DPlayer Video Player + + + + + + + +

+ + + + + + + + + + \ No newline at end of file diff --git a/styles/notion.css b/styles/notion.css index b78ab06b..e231c45f 100644 --- a/styles/notion.css +++ b/styles/notion.css @@ -116,7 +116,6 @@ } } - .notion-page-content-inner { position: relative; display: flex; @@ -179,7 +178,6 @@ color: var(--select-color-2) !important; } - .notion-app { position: relative; background: var(--bg-color); @@ -211,17 +209,17 @@ /* width: auto !important; */ } -@media (max-width: 768px){ - .medium-zoom-image--opened { - object-fit: fill !important; - height: auto !important; - } +@media (max-width: 768px) { + .medium-zoom-image--opened { + object-fit: fill !important; + height: auto !important; + } } -@media (min-width: 768px){ - .medium-zoom-image--opened { - object-fit: scale-down !important; - } +@media (min-width: 768px) { + .medium-zoom-image--opened { + object-fit: scale-down !important; + } } .notion-frame { @@ -405,13 +403,13 @@ summary > .notion-h { .notion-h1 { font-size: 1.575em; margin-top: 1.08em; - @apply border-b w-full + @apply border-b w-full; } .notion-h2 { - @apply w-full + @apply w-full; } .notion-h3 { - @apply w-full + @apply w-full; } .notion-header-anchor { @@ -443,7 +441,7 @@ summary > .notion-h { .notion-h:hover .notion-hash-link { opacity: 1; - @apply dark:fill-gray-200 + @apply dark:fill-gray-200; } .notion-hash-link { @@ -560,10 +558,12 @@ summary > .notion-h { color: inherit; word-break: break-word; text-decoration: inherit; - border-bottom: .05em solid !important; + border-bottom: 0.05em solid !important; border-color: var(--fg-color-2); opacity: 0.7; - transition: border-color 100ms ease-in, opacity 100ms ease-in; + transition: + border-color 100ms ease-in, + opacity 100ms ease-in; } .notion-link:hover { @@ -601,7 +601,7 @@ summary > .notion-h { margin: 2px 4px 0 2px; fill: var(--fg-color-6); color: var(--fg-color-icon); - @apply dark:fill-gray-200 + @apply dark:fill-gray-200; } img.notion-page-icon, @@ -667,12 +667,12 @@ svg.notion-page-icon { } .notion-list-numbered > .notion-list-numbered { - list-style-type: lower-alpha; + list-style-type: lower-alpha; } .notion-list-numbered > .notion-list-numbered > .notion-list-numbered { - list-style-type: lower-roman; - } + list-style-type: lower-roman; +} .notion-list-disc li { padding-left: 0.1em; @@ -701,7 +701,7 @@ svg.notion-page-icon { } .notion-asset-wrapper-image > div { - height: auto !important; + height: auto !important; } .notion-asset-wrapper-full { @@ -709,7 +709,7 @@ svg.notion-page-icon { } .notion-asset-wrapper img { - width: 90%; + /* width: 90%; */ /* height: 100%; */ height: auto !important; max-height: 100%; @@ -851,7 +851,7 @@ code[class*='language-'] { .notion-bookmark-link { display: flex; margin-top: 6px; - @apply w-52 md:w-80 + @apply w-52 md:w-80; } .notion-bookmark-link > img { @@ -919,7 +919,7 @@ code[class*='language-'] { font-size: 14px; line-height: 1.4; color: var(--fg-color-3); - @apply dark:text-gray-300 + @apply dark:text-gray-300; } .notion-callout { @@ -1122,7 +1122,7 @@ code[class*='language-'] { .notion-table-of-contents { width: 100%; margin: 4px 0; - @apply bg-gray-50 dark:bg-gray-900 p-2 + @apply bg-gray-50 dark:bg-gray-900 p-2; } .notion-table-of-contents-item { @@ -1142,8 +1142,7 @@ code[class*='language-'] { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - @apply dark:text-white - + @apply dark:text-white; } .notion-table-of-contents-item:hover { @@ -1296,8 +1295,8 @@ code[class*='language-'] { transition: background 20ms ease-in 0s; color: inherit; text-decoration: none; - - @apply dark:stroke-slate-200 + + @apply dark:stroke-slate-200; } .notion-file-link:hover { @@ -1337,7 +1336,7 @@ code[class*='language-'] { line-height: 16px; margin-left: 6px; - @apply dark:text-gray-400 !important + @apply dark:text-gray-400 !important; } .notion-audio { @@ -1396,8 +1395,8 @@ code[class*='language-'] { white-space: normal; } -.katex-display>.katex>.katex-html>.tag { - position: inherit !important; +.katex-display > .katex > .katex-html > .tag { + position: inherit !important; } .notion-page-title { @@ -1424,13 +1423,13 @@ code[class*='language-'] { @apply max-w-0; } -.notion-collection-card{ +.notion-collection-card { /* cursor: default !important; */ } .notion-collection-card-property .notion-link { border-bottom: 0 none; - cursor: pointer + cursor: pointer; } .notion-collection-card-property .notion-page-title { @@ -1469,7 +1468,7 @@ code[class*='language-'] { } .notion-collection-row { - @apply hidden + @apply hidden; } .notion-collection-row-body { @@ -1612,7 +1611,8 @@ code[class*='language-'] { } .lazy-image-real.medium-zoom-image { - transition: transform 0.3s cubic-bezier(0.2, 0, 0.2, 1), + transition: + transform 0.3s cubic-bezier(0.2, 0, 0.2, 1), opacity 400ms ease-out !important; will-change: opacity, transform; } @@ -1700,12 +1700,16 @@ svg + .notion-page-title-text { @apply text-gray-600 dark:text-gray-300; } -.notion-gray_background,.notion-brown_background, -.notion-orange_background,.notion-yellow_background, -.notion-blue_background,.notion-purple_background, -.notion-teal_background,.notion-red_background, -.notion-pink_background{ - @apply dark:text-black +.notion-gray_background, +.notion-brown_background, +.notion-orange_background, +.notion-yellow_background, +.notion-blue_background, +.notion-purple_background, +.notion-teal_background, +.notion-red_background, +.notion-pink_background { + @apply dark:text-black; } .notion-bookmark:hover { @@ -1737,7 +1741,7 @@ svg + .notion-page-title-text { padding: 4px 2px; white-space: nowrap; overflow: hidden; - @apply px-0 !important + @apply px-0 !important; } .notion-collection-header-title { @@ -1791,7 +1795,7 @@ svg + .notion-page-title-text { /* fill: var(--fg-color); */ fill: rgba(55, 53, 47); margin-right: 6px; - @apply dark:fill-gray-200 + @apply dark:fill-gray-200; } .notion-collection-view-type-title { @@ -1799,13 +1803,13 @@ svg + .notion-page-title-text { overflow: hidden; text-overflow: ellipsis; color: var(--fg-color); - @apply dark:text-gray-200 + @apply dark:text-gray-200; } .notion-table { align-self: center; overflow: auto hidden; - @apply w-full !important + @apply w-full !important; } .notion-table-view { @@ -1814,13 +1818,13 @@ svg + .notion-page-title-text { min-width: var(--notion-max-width); padding-left: 0; transition: padding 200ms ease-out; - @apply px-0 !important + @apply px-0 !important; } .notion-table-header { display: flex; position: absolute; - z-index:30; + z-index: 30; height: 33px; color: var(--fg-color-3); min-width: var(--notion-max-width); @@ -1872,7 +1876,7 @@ svg + .notion-page-title-text { line-height: 120%; min-width: 0; font-size: 14px; - @apply dark:text-gray-200 + @apply dark:text-gray-200; } .notion-collection-column-title-icon { @@ -1883,12 +1887,11 @@ svg + .notion-page-title-text { min-height: 14px; fill: var(--fg-color-2); margin-right: 6px; - @apply dark:text-gray-200 dark:fill-gray-200 - + @apply dark:text-gray-200 dark:fill-gray-200; } .notion-collection-view-tabs-content-item-active { - @apply dark:border-gray-300 + @apply dark:border-gray-300; } .notion-collection-column-title-body { @@ -1947,12 +1950,12 @@ svg + .notion-page-title-text { padding: 7px 8px 0; } -.notion-simple-table { +.notion-simple-table { @apply whitespace-nowrap overflow-x-auto block w-full border-0 !important; } .notion-asset-wrapper-pdf > div { - display: block !important; + display: block !important; } /* https://github.com/kchen0x */ @@ -1972,47 +1975,49 @@ svg + .notion-page-title-text { /* color: var(--notion-gray); */ } -.notion-asset-wrapper-pdf>div{ - width:unset!important +.notion-asset-wrapper-pdf > div { + width: unset !important; } /* pdf预览适配页面 */ -.react-pdf__Page__canvas,.react-pdf__Page__textContent{ - width: 100% !important; - height: auto !important; +.react-pdf__Page__canvas, +.react-pdf__Page__textContent { + width: 100% !important; + height: auto !important; } - /* simple table设置 */ -table,thead,tbody{ - display:block +table, +thead, +tbody { + display: block; } -thead, tbody tr { - display:table; - width:100%; - table-layout:fixed; +thead, +tbody tr { + display: table; + width: 100%; + table-layout: fixed; } -.notion-collection-card{ - @apply dark:text-gray-200 dark:bg-gray-800 dark:hover:bg-black +.notion-collection-card { + @apply dark:text-gray-200 dark:bg-gray-800 dark:hover:bg-black; } -.notion-code-copy{ - display: none; +.notion-code-copy { + display: none; } - -pre[class*="language-mermaid"] { - @apply bg-gray-50 dark:bg-gray-200 !important; +pre[class*='language-mermaid'] { + @apply bg-gray-50 dark:bg-gray-200 !important; } /* mermaid 原文隐藏 */ code.language-mermaid { - display:none + display: none; } -.code-toolbar{ +.code-toolbar { @apply w-full shadow-md pb-0; } @@ -2036,7 +2041,7 @@ code.language-mermaid { @apply dark:border-gray-200 !important; } -.notion-external-image > svg > g > path{ +.notion-external-image > svg > g > path { @apply dark:fill-gray-200 !important; } @@ -2049,17 +2054,16 @@ code.language-mermaid { } /* 表格头 */ -.notion-simple-table tr:first-child td{ +.notion-simple-table tr:first-child td { background-color: #f5f6f8; @apply text-center font-bold dark:bg-gray-800 !important; - } -.notion-simple-table td{ - border: 1px solid var(#eee) !important +.notion-simple-table td { + border: 1px solid var(#eee) !important; } /* 竖屏视频高度bug */ -figure.notion-asset-wrapper.notion-asset-wrapper-video>div { - height: 100% !important; +figure.notion-asset-wrapper.notion-asset-wrapper-video > div { + height: 100% !important; } diff --git a/themes/gitbook/index.js b/themes/gitbook/index.js index 6a4affae..5f4169ba 100644 --- a/themes/gitbook/index.js +++ b/themes/gitbook/index.js @@ -46,13 +46,13 @@ export const useGitBookGlobal = () => useContext(ThemeGlobalGitbook) * 给最新的文章标一个红点 */ function getNavPagesWithLatest(allNavPages, latestPosts, post) { - // 检测需要去除红点的文章 ; localStorage 的 posts_read = {"${post.id}":"Date()"} 保存了所有已读的页面id,和阅读时间; - // 如果页面在这里面则不显示红点 - const postRead = JSON.parse(localStorage.getItem('post_read') || '[]'); - if (post && !postRead.includes(post.id)) { - postRead.push(post.id); + // localStorage 保存id和上次阅读时间戳: posts_read_time = {"${post.id}":"Date()"} + const postReadTime = JSON.parse(localStorage.getItem('post_read_time') || '{}'); + if (post) { + postReadTime[post.id] = new Date().getTime(); } - localStorage.setItem('post_read', JSON.stringify(postRead)); + // 更新 + localStorage.setItem('post_read_time', JSON.stringify(postReadTime)); return allNavPages?.map(item => { const res = { @@ -66,7 +66,10 @@ function getNavPagesWithLatest(allNavPages, latestPosts, post) { pageIcon: item.pageIcon || '', lastEditedDate: item.lastEditedDate } - if (latestPosts.some(post => post.id === item.id) && !postRead.includes(item.id)) { + // 属于最新文章通常6篇 && (无阅读记录 || 最近更新时间大于上次阅读时间) + if (latestPosts.some(post => post.id === item.id) && + (!postReadTime[item.id] || postReadTime[item.id] < new Date(item.lastEditedDate).getTime()) + ) { return { ...res, isLatest: true }; } else { return res; diff --git a/themes/heo/components/PaginationNumber.js b/themes/heo/components/PaginationNumber.js index 4a1022ee..54c1bf34 100644 --- a/themes/heo/components/PaginationNumber.js +++ b/themes/heo/components/PaginationNumber.js @@ -17,105 +17,126 @@ const PaginationNumber = ({ page, totalPage }) => { const currentPage = +page const showNext = page < totalPage const showPrev = currentPage !== 1 - const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '') + const pagePrefix = router.asPath + .split('?')[0] + .replace(/\/page\/[1-9]\d*/, '') + .replace(/\/$/, '') const pages = generatePages(pagePrefix, page, currentPage, totalPage) const [value, setValue] = useState('') - const handleInputChange = (event) => { + const handleInputChange = event => { const newValue = event.target.value.replace(/[^0-9]/g, '') setValue(newValue) } /** - * 调到指定页 - */ + * 调到指定页 + */ const jumpToPage = () => { if (value) { - router.push(value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`) + router.push( + value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}` + ) } } - return (<> - - {/* pc端分页按钮 */} -
- {/* 上一页 */} - -
- -
- {locale.PAGINATION.PREV} -
-
- - - - {/* 分页 */} -
- {pages} - - {/* 跳转页码 */} -
- -
- -
-
+ return ( + <> + {/* pc端分页按钮 */} +
+ {/* 上一页 */} + +
+ +
+ {locale.PAGINATION.PREV}
+
+ - {/* 下一页 */} - + {/* 分页 */} +
+ {pages} -
- -
- {locale.PAGINATION.NEXT} -
-
- + {/* 跳转页码 */} +
+ +
+ +
+
- {/* 移动端分页 */} + {/* 下一页 */} + +
+ +
+ {locale.PAGINATION.NEXT} +
+
+ +
-
- {/* 上一页 */} - - {locale.PAGINATION.PREV} - + {/* 移动端分页 */} - {showPrev && showNext &&
} +
+ {/* 上一页 */} + + {locale.PAGINATION.PREV} + - {/* 下一页 */} - - {locale.PAGINATION.NEXT} - -
- ) + {showPrev && showNext &&
} + + {/* 下一页 */} + + {locale.PAGINATION.NEXT} + +
+ + ) } /** @@ -127,21 +148,23 @@ const PaginationNumber = ({ page, totalPage }) => { */ function getPageElement(page, currentPage, pagePrefix) { const selected = page + '' === currentPage + '' + if (!page) { + return <> + } return ( - ( - - {page} - - ) + + {page} + ) } @@ -171,7 +194,11 @@ function generatePages(pagePrefix, page, currentPage, totalPage) { startPage = totalPage - dynamicGroupCount } if (startPage > 2) { - pages.push(
...
) + pages.push( +
+ ...{' '} +
+ ) } for (let i = 0; i < dynamicGroupCount; i++) { diff --git a/themes/hexo/components/ArticleRecommend.js b/themes/hexo/components/ArticleRecommend.js index a3a40b7d..0bbcd937 100644 --- a/themes/hexo/components/ArticleRecommend.js +++ b/themes/hexo/components/ArticleRecommend.js @@ -1,9 +1,9 @@ -import Link from 'next/link' -import CONFIG from '../config' +import LazyImage from '@/components/LazyImage' import { siteConfig } from '@/lib/config' import { useGlobal } from '@/lib/global' -import LazyImage from '@/components/LazyImage' import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +import Link from 'next/link' +import CONFIG from '../config' /** * 关联推荐文章 @@ -13,50 +13,52 @@ import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' export default function ArticleRecommend({ recommendPosts, siteInfo }) { const { locale } = useGlobal() - if ( - !siteConfig('HEXO_ARTICLE_RECOMMEND', null, CONFIG) || - !recommendPosts || - recommendPosts.length === 0 - ) { + if (!siteConfig('HEXO_ARTICLE_RECOMMEND', null, CONFIG) || !recommendPosts || recommendPosts.length === 0) { return <> } return ( -
-
-
- - {locale.COMMON.RELATE_POSTS} -
-
-
- {recommendPosts.map(post => { - const headerImage = post?.pageCoverThumbnail - ? post.pageCoverThumbnail - : siteInfo?.pageCover - const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}` - - return ( - ( - -
-
-
- {post.title} -
-
- -
- - ) - ) - })} -
+
+
+
+ + {locale.COMMON.RELATE_POSTS}
+
+
+ {recommendPosts.map(post => { + const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover + const url = checkContainHttp(post.slug) + ? sliceUrlFromHttp(post.slug) + : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return ( + +
+
+
+ {post.title} +
+
+ + + {/* 卡片的阴影遮罩,为了凸显图片上的文字 */} +
+
+
+
+ + ) + })} +
+
) } diff --git a/themes/movie/components/Announcement.js b/themes/movie/components/Announcement.js new file mode 100644 index 00000000..1f265417 --- /dev/null +++ b/themes/movie/components/Announcement.js @@ -0,0 +1,22 @@ +import { useGlobal } from '@/lib/global' +import dynamic from 'next/dynamic' + +const NotionPage = dynamic(() => import('@/components/NotionPage')) + +const Announcement = ({ post, className }) => { + const { locale } = useGlobal() + if (!post || Object.keys(post).length === 0) { + return <> + } + return +} +export default Announcement diff --git a/themes/movie/components/ArchiveDateList.js b/themes/movie/components/ArchiveDateList.js new file mode 100644 index 00000000..cd389028 --- /dev/null +++ b/themes/movie/components/ArchiveDateList.js @@ -0,0 +1,38 @@ +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import Link from 'next/link' + +export default function ArchiveDateList(props) { + const postsSortByDate = Object.create(props.allNavPages) + const { locale } = useGlobal() + + postsSortByDate.sort((a, b) => { + return b?.publishDate - a?.publishDate + }) + + let dates = [] + postsSortByDate.forEach(post => { + const date = formatDateFmt(post.publishDate, 'yyyy-MM') + if (!dates[date]) { + dates.push(date) + } + }) + dates = dates.slice(0, 5) + return ( +
+
{locale.NAV.ARCHIVE}
+ {dates?.map((date, index) => { + return ( +
+ + {date} + +
+ ) + })} +
+ ) +} diff --git a/themes/movie/components/ArticleInfo.js b/themes/movie/components/ArticleInfo.js new file mode 100644 index 00000000..9997579a --- /dev/null +++ b/themes/movie/components/ArticleInfo.js @@ -0,0 +1,46 @@ +import { useGlobal } from '@/lib/global' +import { formatDateFmt } from '@/lib/utils/formatDate' +import Link from 'next/link' + +export const ArticleInfo = props => { + const { post } = props + const { locale } = useGlobal() + + return ( +
+

{post?.title}

+ +
+ + {post?.publishDay} + + + {post?.type !== 'Page' && ( + <> + + {post?.category} + + + )} + +
+ {post?.tags?.length > 0 && ( + <> + {locale.COMMON.TAGS} : + + )} + {post?.tags?.map(tag => { + return ( + + {tag} + + ) + })} +
+
+
+ ) +} diff --git a/themes/movie/components/ArticleLock.js b/themes/movie/components/ArticleLock.js new file mode 100644 index 00000000..3744c183 --- /dev/null +++ b/themes/movie/components/ArticleLock.js @@ -0,0 +1,52 @@ +import { useGlobal } from '@/lib/global' +import { useEffect, useRef } from 'react' + +/** + * 加密文章校验组件 + * @param {password, validPassword} props + * @param password 正确的密码 + * @param validPassword(bool) 回调函数,校验正确回调入参为true + * @returns + */ +export const ArticleLock = props => { + const { validPassword } = props + const { locale } = useGlobal() + + const submitPassword = () => { + const p = document.getElementById('password') + if (!validPassword(p?.value)) { + const tips = document.getElementById('tips') + if (tips) { + tips.innerHTML = '' + tips.innerHTML = `
${locale.COMMON.PASSWORD_ERROR}
` + } + } + } + const passwordInputRef = useRef(null) + useEffect(() => { + // 选中密码输入框并将其聚焦 + passwordInputRef.current.focus() + }, []) + + return
+
+
{locale.COMMON.ARTICLE_LOCK_TIPS}
+
+ { + if (e.key === 'Enter') { + submitPassword() + } + }} + ref={passwordInputRef} // 绑定ref到passwordInputRef变量 + className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50' + > +
+  {locale.COMMON.SUBMIT} +
+
+
+
+
+
+} diff --git a/themes/movie/components/BlogListGroupByDate.js b/themes/movie/components/BlogListGroupByDate.js new file mode 100644 index 00000000..7a8fe083 --- /dev/null +++ b/themes/movie/components/BlogListGroupByDate.js @@ -0,0 +1,38 @@ +import { siteConfig } from '@/lib/config' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +import Link from 'next/link' + +/** + * 按照日期将文章分组 + * 归档页面用到 + * @param {*} param0 + * @returns + */ +export default function BlogListGroupByDate({ archiveTitle, archivePosts }) { + return
+
+ {archiveTitle} +
+ +
    + {archivePosts[archiveTitle].map(post => { + const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return
  • +
    + + {post?.publishDay} + {' '} +   + + {post.title} + +
    +
  • + })} +
+
+} diff --git a/themes/movie/components/BlogListPage.js b/themes/movie/components/BlogListPage.js new file mode 100644 index 00000000..648f2ee2 --- /dev/null +++ b/themes/movie/components/BlogListPage.js @@ -0,0 +1,25 @@ +import { siteConfig } from '@/lib/config' +import CONFIG from '../config' +import BlogPostCard from './BlogPostCard' +import PaginationNumber from './PaginationNumber' + +export const BlogListPage = props => { + const { page = 1, posts, postCount } = props + const totalPage = Math.ceil(postCount / parseInt(siteConfig('POSTS_PER_PAGE'))) + + const showPageCover = siteConfig('MOVIE_POST_LIST_COVER', null, CONFIG) + + return ( +
+
+ {posts?.map(post => ( + + ))} +
+ + +
+ ) +} diff --git a/themes/movie/components/BlogListScroll.js b/themes/movie/components/BlogListScroll.js new file mode 100644 index 00000000..597ba7dc --- /dev/null +++ b/themes/movie/components/BlogListScroll.js @@ -0,0 +1,60 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import throttle from 'lodash.throttle' +import { useCallback, useEffect, useRef, useState } from 'react' +import CONFIG from '../config' +import BlogPostCard from './BlogPostCard' + +export const BlogListScroll = props => { + const { posts } = props + const { locale } = useGlobal() + + const [page, updatePage] = useState(1) + + let hasMore = false + const postsToShow = posts ? Object.assign(posts).slice(0, parseInt(siteConfig('POSTS_PER_PAGE')) * page) : [] + + if (posts) { + const totalCount = posts.length + hasMore = page * parseInt(siteConfig('POSTS_PER_PAGE')) < totalCount + } + const handleGetMore = () => { + if (!hasMore) return + updatePage(page + 1) + } + + const targetRef = useRef(null) + + // 监听滚动自动分页加载 + const scrollTrigger = useCallback( + throttle(() => { + const scrollS = window.scrollY + window.outerHeight + const clientHeight = targetRef ? (targetRef.current ? targetRef.current.clientHeight : 0) : 0 + if (scrollS > clientHeight + 100) { + handleGetMore() + } + }, 500) + ) + const showPageCover = siteConfig('MOVIE_POST_LIST_COVER', null, CONFIG) + + useEffect(() => { + window.addEventListener('scroll', scrollTrigger) + + return () => { + window.removeEventListener('scroll', scrollTrigger) + } + }) + + return ( +
+ {postsToShow?.map(post => ( + + ))} + +
+ {' '} + {hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '} +
+
+ ) +} diff --git a/themes/movie/components/BlogPostCard.js b/themes/movie/components/BlogPostCard.js new file mode 100644 index 00000000..61954610 --- /dev/null +++ b/themes/movie/components/BlogPostCard.js @@ -0,0 +1,60 @@ +import LazyImage from '@/components/LazyImage' +import NotionIcon from '@/components/NotionIcon' +import { siteConfig } from '@/lib/config' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +import Link from 'next/link' +import TagItemMini from './TagItemMini' + +const BlogPostCard = ({ index, post, showSummary, siteInfo }) => { + // 主题默认强制显示图片 + if (post && !post.pageCoverThumbnail) { + post.pageCoverThumbnail = siteInfo?.pageCover || siteConfig('RANDOM_IMAGE_URL') + } + + const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return ( +
+ + {/* 固定高度 ,空白用图片拉升填充 */} +
+ {/* 图片 填充卡片 */} +
+ +
+ +
+ {post?.tagItems && post?.tagItems.length > 0 && ( + <> +
+ {post.tagItems.map(tag => ( + + ))} +
+ + )} +
+ {/* 阴影遮罩 */} +

+ + {post.title} +

+ +

+ {post?.summary} +

+ +
+
+
+
+ +
+ ) +} + +export default BlogPostCard diff --git a/themes/movie/components/BlogRecommend.js b/themes/movie/components/BlogRecommend.js new file mode 100644 index 00000000..9e434558 --- /dev/null +++ b/themes/movie/components/BlogRecommend.js @@ -0,0 +1,62 @@ +import LazyImage from '@/components/LazyImage' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' +import Link from 'next/link' +import CONFIG from '../config' + +/** + * 关联推荐文章 + * @param {prev,next} param0 + * @returns + */ +export default function BlogRecommend(props) { + const { recommendPosts, siteInfo } = props + const { locale } = useGlobal() + if (!siteConfig('MOVIE_ARTICLE_RECOMMEND', null, CONFIG) || !recommendPosts || recommendPosts.length === 0) { + return <> + } + + return ( +
+
+
+ + {locale.COMMON.RELATE_POSTS} +
+
+
+ {recommendPosts.map(post => { + const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover + const url = checkContainHttp(post.slug) + ? sliceUrlFromHttp(post.slug) + : `${siteConfig('SUB_PATH', '')}/${post.slug}` + + return ( + +
+
+
{post.title}
+
+ {/* 卡片的阴影遮罩,为了凸显图片上的文字 */} +
+
+
+ + +
+ + ) + })} +
+
+ ) +} diff --git a/themes/movie/components/CategoryGroup.js b/themes/movie/components/CategoryGroup.js new file mode 100644 index 00000000..63c3917c --- /dev/null +++ b/themes/movie/components/CategoryGroup.js @@ -0,0 +1,43 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' + +const CategoryGroup = props => { + const { currentCategory, categoryOptions } = props + const { locale } = useGlobal() + if (!categoryOptions || categoryOptions.length === 0) return <> + const categoryCount = siteConfig('PREVIEW_CATEGORY_COUNT') + const categories = categoryOptions.slice(0, categoryCount) + return ( + <> +
+

{locale.COMMON.CATEGORY}

+
+ {categories.map(category => { + const selected = currentCategory === category.name + return ( + + + {category.name}({category.count}) + + ) + })} +
+
+ + ) +} + +export default CategoryGroup diff --git a/themes/movie/components/CategoryItem.js b/themes/movie/components/CategoryItem.js new file mode 100644 index 00000000..1190965b --- /dev/null +++ b/themes/movie/components/CategoryItem.js @@ -0,0 +1,20 @@ +import Link from 'next/link' + +/** + * 文章分类 + * @param {*} param0 + * @returns + */ +export default function CategoryItem({ category }) { + return ( + +
+ {category.name}({category.count}) +
+ + ) +} diff --git a/themes/movie/components/ExampleRecentComments.js b/themes/movie/components/ExampleRecentComments.js new file mode 100644 index 00000000..9dbdfa7f --- /dev/null +++ b/themes/movie/components/ExampleRecentComments.js @@ -0,0 +1,35 @@ +import { useEffect, useState } from 'react' +import { siteConfig } from '@/lib/config' +import Link from 'next/link' +import { RecentComments } from '@waline/client' + +/** + * @see https://waline.js.org/guide/get-started.html + * @param {*} props + * @returns + */ +const ExampleRecentComments = (props) => { + const [comments, updateComments] = useState([]) + const [onLoading, changeLoading] = useState(true) + useEffect(() => { + RecentComments({ + serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'), + count: 5 + }).then(({ comments }) => { + changeLoading(false) + updateComments(comments) + }) + }, []) + + return <> + {onLoading &&
Loading...
} + {!onLoading && comments && comments.length === 0 &&
No Comments
} + {!onLoading && comments && comments.length > 0 && comments.map((comment) =>
+
+
--{comment.nick}
+
)} + + +} + +export default ExampleRecentComments diff --git a/themes/movie/components/Footer.js b/themes/movie/components/Footer.js new file mode 100644 index 00000000..77c8bad6 --- /dev/null +++ b/themes/movie/components/Footer.js @@ -0,0 +1,26 @@ +import DarkModeButton from '@/components/DarkModeButton' +import { siteConfig } from '@/lib/config' + +export const Footer = (props) => { + const d = new Date() + const currentYear = d.getFullYear() + const since = siteConfig('SINCE') + const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear + + return +} diff --git a/themes/movie/components/Header.js b/themes/movie/components/Header.js new file mode 100644 index 00000000..a0f2d78c --- /dev/null +++ b/themes/movie/components/Header.js @@ -0,0 +1,166 @@ +import Collapse from '@/components/Collapse' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { useMovieGlobal } from '..' +import CONFIG from '../config' +import { MenuItemCollapse } from './MenuItemCollapse' +import { MenuItemDrop } from './MenuItemDrop' + +/** + * 网站顶部 + * @returns + */ +export const Header = props => { + const { collapseRef, searchModal } = useMovieGlobal() + const router = useRouter() + const { customNav, customMenu } = props + const { locale } = useGlobal() + const [isOpen, setIsOpen] = useState(false) + const [showSearch, setShowSearch] = useState(false) + const toggleMenuOpen = () => { + setIsOpen(!isOpen) + } + let links = [ + { + id: 1, + icon: 'fa-solid fa-house', + name: locale.NAV.INDEX, + to: '/', + show: siteConfig('MOVIE_MENU_INDEX', null, CONFIG) + }, + { + id: 2, + icon: 'fas fa-search', + name: locale.NAV.SEARCH, + to: '/search', + show: siteConfig('MOVIE_MENU_SEARCH', null, CONFIG) + }, + { + id: 3, + icon: 'fas fa-archive', + name: locale.NAV.ARCHIVE, + to: '/archive', + show: siteConfig('MOVIE_MENU_ARCHIVE', null, CONFIG) + } + // { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: siteConfig('MENU_CATEGORY', null, CONFIG) }, + // { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: siteConfig('MENU_TAG', null, CONFIG) } + ] + + if (customNav) { + links = links.concat(customNav) + } + + for (let i = 0; i < links.length; i++) { + if (links[i].id !== i) { + links[i].id = i + } + } + + // 如果 开启自定义菜单,则覆盖Page生成的菜单 + if (siteConfig('CUSTOM_MENU')) { + links = customMenu + } + + // 展示搜索框 + const toggleShowSearchInput = () => { + if (siteConfig('ALGOLIA_APP_ID')) { + searchModal.current.openSearch() + } else { + setShowSearch(!showSearch) + } + } + + useEffect(() => { + if (showSearch) { + setTimeout(() => { + document.getElementById('search').focus() + }, 100) + } + }, [showSearch]) + + const onKeyUp = e => { + if (e.keyCode === 13) { + const search = document.getElementById('search').value + if (search) { + router.push({ pathname: '/search/' + search }) + } + } + } + + const handleSearch = () => { + const search = document.getElementById('search').value + if (search) { + router.push({ pathname: '/search/' + search }) + } + } + + return ( + <> +
+ + {siteConfig('TITLE')} + + +
+ {/* 右侧菜单 */} + <> + + +
+ +
+
+ + +
+ + {/* 移动端按钮 */} +
+
+ {isOpen ? : } +
+
+ +
+
+ + + {/* 移动端菜单 */} + + {links?.map( + (link, index) => + link && + link.show && ( + collapseRef.current?.updateCollapseHeight(param)} + key={index} + link={link} + /> + ) + )} + + + + ) +} diff --git a/themes/movie/components/JumpToTopButton.js b/themes/movie/components/JumpToTopButton.js new file mode 100644 index 00000000..f5e22b61 --- /dev/null +++ b/themes/movie/components/JumpToTopButton.js @@ -0,0 +1,18 @@ +import { useGlobal } from '@/lib/global' + +/** + * 跳转到网页顶部 + * 当屏幕下滑500像素后会出现该控件 + * @param targetRef 关联高度的目标html标签 + * @param showPercent 是否显示百分比 + * @returns {JSX.Element} + * @constructor + */ +const JumpToTopButton = () => { + const { locale } = useGlobal() + return
window.scrollTo({ top: 0, behavior: 'smooth' })} + > +
+} + +export default JumpToTopButton diff --git a/themes/movie/components/LatestPostsGroup.js b/themes/movie/components/LatestPostsGroup.js new file mode 100644 index 00000000..b7ada599 --- /dev/null +++ b/themes/movie/components/LatestPostsGroup.js @@ -0,0 +1,62 @@ +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' + +/** + * 最新文章列表 + * @param posts 所有文章数据 + * @param sliceCount 截取展示的数量 默认6 + * @constructor + */ +const LatestPostsGroup = ({ latestPosts }) => { + // 获取当前路径 + const currentPath = useRouter().asPath + const { locale } = useGlobal() + + if (!latestPosts) { + return <> + } + + return ( +
+
+
+ + {locale.COMMON.LATEST_POSTS} +
+
+ + {latestPosts.map(post => { + const selected = + currentPath === `${siteConfig('SUB_PATH', '')}/${post.slug}` + const url = checkContainHttp(post.slug) + ? sliceUrlFromHttp(post.slug) + : `${siteConfig('SUB_PATH', '')}/${post.slug}` + return ( + +
+
  • {post.title}
  • +
    + + ) + })} +
    + ) +} +export default LatestPostsGroup diff --git a/themes/movie/components/LoadingCover.js b/themes/movie/components/LoadingCover.js new file mode 100644 index 00000000..75976180 --- /dev/null +++ b/themes/movie/components/LoadingCover.js @@ -0,0 +1,8 @@ + +export default function LoadingCover() { + return
    +
    + +
    +
    +} diff --git a/themes/movie/components/MenuItemCollapse.js b/themes/movie/components/MenuItemCollapse.js new file mode 100644 index 00000000..0f91f1d4 --- /dev/null +++ b/themes/movie/components/MenuItemCollapse.js @@ -0,0 +1,77 @@ +import Collapse from '@/components/Collapse' +import Link from 'next/link' +import { useState } from 'react' + +/** + * 折叠菜单 + * @param {*} param0 + * @returns + */ +export const MenuItemCollapse = props => { + const { link } = props + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + const [isOpen, changeIsOpen] = useState(false) + + const toggleShow = () => { + changeShow(!show) + } + + const toggleOpenSubMenu = () => { + changeIsOpen(!isOpen) + } + + if (!link || !link.show) { + return null + } + + return ( + <> +
    + {!hasSubMenu && ( + + + {link?.icon && } + {link?.name} + + + )} + {hasSubMenu && ( +
    + + {link?.icon && } + {link?.name} + + +
    + )} +
    + + {/* 折叠子菜单 */} + {hasSubMenu && ( + + {link.subMenus.map((sLink, index) => { + return ( +
    + + + {link?.icon && } {sLink.title} + + +
    + ) + })} +
    + )} + + ) +} diff --git a/themes/movie/components/MenuItemDrop.js b/themes/movie/components/MenuItemDrop.js new file mode 100644 index 00000000..93f27f44 --- /dev/null +++ b/themes/movie/components/MenuItemDrop.js @@ -0,0 +1,67 @@ +import Link from 'next/link' +import { useState } from 'react' + +export const MenuItemDrop = ({ link }) => { + const [show, changeShow] = useState(false) + const hasSubMenu = link?.subMenus?.length > 0 + + if (!link || !link.show) { + return null + } + + return ( +
    changeShow(true)} + onMouseOut={() => changeShow(false)} + > + {!hasSubMenu && ( + + {link?.icon && } {link?.name} + {hasSubMenu && } + + )} + + {hasSubMenu && ( + <> +
    + {link?.icon && } {link?.name} + +
    + + )} + + {/* 子菜单 */} + {hasSubMenu && ( +
      + {link.subMenus.map((sLink, index) => { + return ( +
    • + + + {link?.icon &&   } + {sLink.title} + + +
    • + ) + })} +
    + )} +
    + ) +} diff --git a/themes/movie/components/NormalMenuItem.js b/themes/movie/components/NormalMenuItem.js new file mode 100644 index 00000000..aec74837 --- /dev/null +++ b/themes/movie/components/NormalMenuItem.js @@ -0,0 +1,14 @@ +import Link from 'next/link' + +/** + * 旧的普通菜单 + * @param {*} props + * @returns + */ +export const NormalMenuItem = (props) => { + const { link } = props + return link?.show && + {link.name} + +} diff --git a/themes/movie/components/PaginationNumber.js b/themes/movie/components/PaginationNumber.js new file mode 100644 index 00000000..effc1835 --- /dev/null +++ b/themes/movie/components/PaginationNumber.js @@ -0,0 +1,220 @@ +import { ChevronDoubleRight } from '@/components/HeroIcons' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useState } from 'react' + +/** + * 数字翻页插件 + * @param page 当前页码 + * @param showNext 是否有下一页 + * @returns {JSX.Element} + * @constructor + */ +const PaginationNumber = ({ page, totalPage }) => { + const router = useRouter() + const [value, setValue] = useState('') + const { locale } = useGlobal() + const currentPage = +page + const showNext = page < totalPage + const showPrev = currentPage !== 1 + const pagePrefix = router.asPath + .split('?')[0] + .replace(/\/page\/[1-9]\d*/, '') + .replace(/\/$/, '') + const pages = generatePages(pagePrefix, page, currentPage, totalPage) + if (pages?.length <= 1) { + return <> + } + + const handleInputChange = event => { + const newValue = event.target.value.replace(/[^0-9]/g, '') + setValue(newValue) + } + + /** + * 调到指定页 + */ + const jumpToPage = () => { + if (value) { + router.push( + value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}` + ) + } + } + + return ( + <> + {/* pc端分页按钮 */} +
    + {/* 上一页 */} + +
    + +
    + {locale.PAGINATION.PREV} +
    +
    + + + {/* 分页 */} +
    + {pages} + + {/* 跳转页码 */} +
    + +
    + +
    +
    +
    + + {/* 下一页 */} + +
    + +
    + {locale.PAGINATION.NEXT} +
    +
    + +
    + + {/* 移动端分页 */} + +
    + {/* 上一页 */} + + {locale.PAGINATION.PREV} + + + {showPrev && showNext &&
    } + + {/* 下一页 */} + + {locale.PAGINATION.NEXT} + +
    + + ) +} + +/** + * 页码按钮 + * @param {*} page + * @param {*} currentPage + * @param {*} pagePrefix + * @returns + */ +function getPageElement(page, currentPage, pagePrefix) { + const selected = page + '' === currentPage + '' + if (!page) { + return <> + } + return ( + + {page} + + ) +} + +/** + * 获取所有页码 + * @param {*} pagePrefix + * @param {*} page + * @param {*} currentPage + * @param {*} totalPage + * @returns + */ +function generatePages(pagePrefix, page, currentPage, totalPage) { + const pages = [] + const groupCount = 7 // 最多显示页签数 + if (totalPage <= groupCount) { + for (let i = 1; i <= totalPage; i++) { + pages.push(getPageElement(i, page, pagePrefix)) + } + } else { + pages.push(getPageElement(1, page, pagePrefix)) + const dynamicGroupCount = groupCount - 2 + let startPage = currentPage - 2 + if (startPage <= 1) { + startPage = 2 + } + if (startPage + dynamicGroupCount > totalPage) { + startPage = totalPage - dynamicGroupCount + } + if (startPage > 2) { + pages.push( +
    + ...{' '} +
    + ) + } + + for (let i = 0; i < dynamicGroupCount; i++) { + if (startPage + i < totalPage) { + pages.push(getPageElement(startPage + i, page, pagePrefix)) + } + } + + if (startPage + dynamicGroupCount < totalPage) { + pages.push(
    ...
    ) + } + + pages.push(getPageElement(totalPage, page, pagePrefix)) + } + return pages +} +export default PaginationNumber diff --git a/themes/movie/components/SearchInput.js b/themes/movie/components/SearchInput.js new file mode 100644 index 00000000..4f375d4e --- /dev/null +++ b/themes/movie/components/SearchInput.js @@ -0,0 +1,87 @@ +import { useRouter } from 'next/router' +import { useGlobal } from '@/lib/global' +import { useImperativeHandle, useRef, useState } from 'react' + +let lock = false + +const SearchInput = ({ currentTag, keyword, cRef }) => { + const { locale } = useGlobal() + const router = useRouter() + const searchInputRef = useRef(null) + useImperativeHandle(cRef, () => { + return { + focus: () => { + searchInputRef?.current?.focus() + } + } + }) + const handleSearch = () => { + const key = searchInputRef.current.value + if (key && key !== '') { + router.push({ pathname: '/search/' + key }).then(r => { + console.log('搜索', key) + }) + } else { + router.push({ pathname: '/' }).then(r => { + }) + } + } + const handleKeyUp = (e) => { + if (e.keyCode === 13) { // 回车 + handleSearch(searchInputRef.current.value) + } else if (e.keyCode === 27) { // ESC + cleanSearch() + } + } + const cleanSearch = () => { + searchInputRef.current.value = '' + setShowClean(false) + } + function lockSearchInput () { + lock = true + } + + function unLockSearchInput () { + lock = false + } + const [showClean, setShowClean] = useState(false) + const updateSearchKey = (val) => { + if (lock) { + return + } + searchInputRef.current.value = val + if (val) { + setShowClean(true) + } else { + setShowClean(false) + } + } + + return
    + updateSearchKey(e.target.value)} + defaultValue={keyword || ''} + /> + +
    + +
    + + {(showClean && +
    + +
    + )} +
    +} + +export default SearchInput diff --git a/themes/movie/components/SideBar.js b/themes/movie/components/SideBar.js new file mode 100644 index 00000000..ea0531b7 --- /dev/null +++ b/themes/movie/components/SideBar.js @@ -0,0 +1,68 @@ +import { siteConfig } from '@/lib/config' +import Live2D from '@/components/Live2D' +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import dynamic from 'next/dynamic' +import Announcement from './Announcement' +const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments')) + +export const SideBar = (props) => { + const { locale } = useGlobal() + const { latestPosts, categoryOptions, notice } = props + return ( +
    + + + + + + + + {siteConfig('COMMENT_WALINE_SERVER_URL') && siteConfig('COMMENT_WALINE_RECENT') && } + + + +
    + ) +} diff --git a/themes/movie/components/SlotBar.js b/themes/movie/components/SlotBar.js new file mode 100644 index 00000000..e3bed727 --- /dev/null +++ b/themes/movie/components/SlotBar.js @@ -0,0 +1,34 @@ +import { useGlobal } from '@/lib/global' + +/** + * 博客列表上方嵌入条 + * @param {*} props + * @returns + */ +export default function SlotBar(props) { + const { tag, category } = props + const { locale } = useGlobal() + + if (tag) { + return ( +
    +
    +
    + {locale.COMMON.TAGS} : {tag}{' '} +
    +
    +
    +
    + ) + } else if (category) { + return ( +
    +
    + {locale.COMMON.CATEGORY} : {category} +
    +
    +
    + ) + } + return <> +} diff --git a/themes/movie/components/TagGroups.js b/themes/movie/components/TagGroups.js new file mode 100644 index 00000000..597cddc1 --- /dev/null +++ b/themes/movie/components/TagGroups.js @@ -0,0 +1,51 @@ +import { useGlobal } from '@/lib/global' +import Link from 'next/link' +import { useRouter } from 'next/router' + +/** + * 标签组 + * @param tags + * @param currentTag + * @returns {JSX.Element} + * @constructor + */ +const TagGroups = ({ tagOptions, className }) => { + const router = useRouter() + const { locale } = useGlobal() + const { tag: currentTag } = router.query + if (!tagOptions) return <> + + return ( +
    +
    {locale.COMMON.TAGS}
    +
    + {tagOptions.map((tag, index) => { + const selected = currentTag === tag.name + return ( + +
    +
    {tag.name}
    + {tag.count ? ( + {tag.count} + ) : ( + <> + )} +
    + + ) + })} +
    +
    + ) +} + +export default TagGroups diff --git a/themes/movie/components/TagItem.js b/themes/movie/components/TagItem.js new file mode 100644 index 00000000..c608c8cb --- /dev/null +++ b/themes/movie/components/TagItem.js @@ -0,0 +1,23 @@ +import Link from 'next/link' + +/** + * 标签 + * @param {*} param0 + * @returns + */ +export default function TagItem({ tag }) { + return ( +
    + +
    + {tag.name + (tag.count ? `(${tag.count})` : '')}{' '} +
    + +
    + ) +} diff --git a/themes/movie/components/TagItemMini.js b/themes/movie/components/TagItemMini.js new file mode 100644 index 00000000..a4b7da1c --- /dev/null +++ b/themes/movie/components/TagItemMini.js @@ -0,0 +1,19 @@ +import Link from 'next/link' + +const TagItemMini = ({ tag, selected = false }) => { + return ( + +
    + {selected && }{' '} + {tag.name + (tag.count ? `(${tag.count})` : '')}{' '} +
    + + ) +} + +export default TagItemMini diff --git a/themes/movie/components/Title.js b/themes/movie/components/Title.js new file mode 100644 index 00000000..73ca9d7f --- /dev/null +++ b/themes/movie/components/Title.js @@ -0,0 +1,20 @@ +import NotionIcon from '@/components/NotionIcon' +import { siteConfig } from '@/lib/config' + +/** + * 标题栏 + * @param {*} props + * @returns + */ +export const Title = (props) => { + const { post } = props + const title = post?.title || siteConfig('TITLE') + const description = post?.description || siteConfig('AUTHOR') + + return
    +

    {title}

    +

    + {description} +

    +
    +} diff --git a/themes/movie/config.js b/themes/movie/config.js new file mode 100644 index 00000000..a2c5fd4d --- /dev/null +++ b/themes/movie/config.js @@ -0,0 +1,16 @@ +/** + * 主题配置文件 + */ +const CONFIG = { + // 菜单配置 + MOVIE_MENU_CATEGORY: true, // 显示分类 + MOVIE_MENU_TAG: true, // 显示标签 + MOVIE_MENU_ARCHIVE: true, // 显示归档 + MOVIE_MENU_SEARCH: true, // 显示搜索 + + MOVIE_ARTICLE_RECOMMEND: true, // 推荐关联内容在文章底部 + MOVIE_VIDEO_COMBINE: true, // 聚合视频,开启后一篇文章内的多个含caption的视频会被合并到文章开头,并展示分集按钮 + + MOVIE_POST_LIST_COVER: true // 列表显示文章封面 +} +export default CONFIG diff --git a/themes/movie/index.js b/themes/movie/index.js new file mode 100644 index 00000000..af940150 --- /dev/null +++ b/themes/movie/index.js @@ -0,0 +1,438 @@ +'use client' + +import AlgoliaSearchModal from '@/components/AlgoliaSearchModal' +import Comment from '@/components/Comment' +import replaceSearchResult from '@/components/Mark' +import NotionPage from '@/components/NotionPage' +import ShareBar from '@/components/ShareBar' +import { siteConfig } from '@/lib/config' +import { useGlobal } from '@/lib/global' +import { loadWowJS } from '@/lib/plugins/wow' +import { isBrowser } from '@/lib/utils' +import { Transition } from '@headlessui/react' +import { useRouter } from 'next/router' +import { createContext, useContext, useEffect, useRef, useState } from 'react' +import ArchiveDateList from './components/ArchiveDateList' +import { ArticleInfo } from './components/ArticleInfo' +import { ArticleLock } from './components/ArticleLock' +import BlogListGroupByDate from './components/BlogListGroupByDate' +import { BlogListPage } from './components/BlogListPage' +import { BlogListScroll } from './components/BlogListScroll' +import BlogRecommend from './components/BlogRecommend' +import CategoryGroup from './components/CategoryGroup' +import CategoryItem from './components/CategoryItem' +import { Footer } from './components/Footer' +import { Header } from './components/Header' +import JumpToTopButton from './components/JumpToTopButton' +import LatestPostsGroup from './components/LatestPostsGroup' +import SlotBar from './components/SlotBar' +import TagGroups from './components/TagGroups' +import TagItem from './components/TagItem' +import CONFIG from './config' +import { Style } from './style' + +// 主题全局状态 +const ThemeGlobalMovie = createContext() +export const useMovieGlobal = () => useContext(ThemeGlobalMovie) + +/** + * 基础布局框架 + * 1.其它页面都嵌入在LayoutBase中 + * 2.采用左右两侧布局,移动端使用顶部导航栏 + * @returns {JSX.Element} + * @constructor + */ +const LayoutBase = props => { + const { children, slotTop } = props + const { onLoading, fullWidth } = useGlobal() + const collapseRef = useRef(null) + + const searchModal = useRef(null) + const [expandMenu, updateExpandMenu] = useState(false) + useEffect(() => { + loadWowJS() + }, []) + + return ( + +
    + + ) +} + +export { Style } diff --git a/themes/plog/components/Modal.js b/themes/plog/components/Modal.js index 17ae4da6..71dba58c 100644 --- a/themes/plog/components/Modal.js +++ b/themes/plog/components/Modal.js @@ -6,17 +6,28 @@ import Link from 'next/link' import { siteConfig } from '@/lib/config' import LazyImage from '@/components/LazyImage' import { compressImage } from '@/lib/notion/mapImage' +import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils' /** * 弹出框 */ export default function Modal(props) { - const { showModal, setShowModal, modalContent, setModalContent } = usePlogGlobal() + const { showModal, setShowModal, modalContent, setModalContent } = + usePlogGlobal() const { siteInfo, posts } = props const cancelButtonRef = useRef(null) - const img = compressImage(modalContent?.pageCover || siteInfo?.pageCover, 1200, 85, 'webp') + const img = compressImage( + modalContent?.pageCover || siteInfo?.pageCover, + 1200, + 85, + 'webp' + ) const imgRef = useRef(null) + const url = checkContainHttp(modalContent?.slug) + ? sliceUrlFromHttp(modalContent?.slug) + : `${siteConfig('SUB_PATH', '')}/${modalContent?.slug}` + // 添加loading状态 const [loading, setLoading] = useState(true) @@ -53,73 +64,111 @@ export default function Modal(props) { } return ( - - - {/* 遮罩 */} - + + {/* 遮罩 */} + +
    + + +
    +
    + + + {/* 添加onLoad事件处理函数 */} + {/* 添加loading状态 */} +
    -
    - - -
    -
    - - - {/* 添加loading状态 */} -
    - -
    - - {/* 添加onLoad事件处理函数 */} - - - {!loading && (<> -
    - -
    -

    {modalContent?.title}

    -
    - - -
    - {modalContent?.summary} -
    - - - {modalContent?.category && ( -
    - - {modalContent?.category} - -
    - )} -
    -
    -
    -
    -
    - )} - -
    -
    -
    +
    -
    -
    + + + + + + {!loading && ( + <> +
    +
    +

    + {modalContent?.title} +

    +
    +
    + {modalContent?.summary} +
    + + {modalContent?.category && ( +
    + + {modalContent?.category} + +
    + )} +
    + {/*
    */} +
    + +
    +
    + +
    + {/*
    */} + + )} + + +
    +
    + + ) } diff --git a/themes/simple/components/NavBar.js b/themes/simple/components/NavBar.js index 38df0736..0392c051 100644 --- a/themes/simple/components/NavBar.js +++ b/themes/simple/components/NavBar.js @@ -9,7 +9,7 @@ import { MenuList } from './MenuList' * @param {*} props * @returns */ -export default function NavBar (props) { +export default function NavBar(props) { const [showSearchInput, changeShowSearchInput] = useState(false) const router = useRouter() const { searchModal } = useSimpleGlobal() @@ -23,7 +23,7 @@ export default function NavBar (props) { } } - const onKeyUp = (e) => { + const onKeyUp = e => { if (e.keyCode === 13) { const search = document.getElementById('theme-simple-search').value if (search) { @@ -33,19 +33,41 @@ export default function NavBar (props) { } return ( - ) } diff --git a/themes/theme.js b/themes/theme.js index 5a237dce..f09d3886 100644 --- a/themes/theme.js +++ b/themes/theme.js @@ -12,7 +12,7 @@ export const { THEMES = [] } = getConfig().publicRuntimeConfig * @param {*} themeQuery * @returns */ -export const getGlobalLayoutByTheme = (themeQuery) => { +export const getGlobalLayoutByTheme = themeQuery => { if (themeQuery !== BLOG.THEME) { return dynamic(() => import(`@/themes/${themeQuery}`).then(m => m[getLayoutNameByPath(-1)]), { ssr: true }) } else { @@ -29,22 +29,26 @@ export const getGlobalLayoutByTheme = (themeQuery) => { export const getLayoutByTheme = ({ router, theme }) => { const themeQuery = getQueryParam(router.asPath, 'theme') || theme if (themeQuery !== BLOG.THEME) { - return dynamic(() => import(`@/themes/${themeQuery}`).then(m => { - setTimeout(() => { - checkThemeDOM() - }, 500); + return dynamic( + () => + import(`@/themes/${themeQuery}`).then(m => { + setTimeout(() => { + checkThemeDOM() + }, 500) - const components = m[getLayoutNameByPath(router.pathname, router.asPath)] - if (components) { - return components - } else { - return m.LayoutSlug - } - }), { ssr: true }) + const components = m[getLayoutNameByPath(router.pathname, router.asPath)] + if (components) { + return components + } else { + return m.LayoutSlug + } + }), + { ssr: true } + ) } else { setTimeout(() => { checkThemeDOM() - }, 100); + }, 100) const components = ThemeComponents[getLayoutNameByPath(router.pathname, router.asPath)] if (components) { return components @@ -59,12 +63,12 @@ export const getLayoutByTheme = ({ router, theme }) => { * @param {*} path * @returns */ -const getLayoutNameByPath = (path) => { +const getLayoutNameByPath = path => { if (LAYOUT_MAPPINGS[path]) { - return LAYOUT_MAPPINGS[path]; + return LAYOUT_MAPPINGS[path] } else { // 没有特殊处理的路径返回默认layout名称 - return 'LayoutSlug'; + return 'LayoutSlug' } } @@ -90,14 +94,19 @@ const checkThemeDOM = () => { * @param updateDarkMode 更改主题ChangeState函数 * @description 读取cookie中存的用户主题 */ -export const initDarkMode = (updateDarkMode) => { +export const initDarkMode = (updateDarkMode, defaultDarkMode) => { // 查看用户设备浏览器是否深色模型 let newDarkMode = isPreferDark() - // 查看cookie中是否用户强制设置深色模式 - const cookieDarkMode = loadDarkModeFromLocalStorage() - if (cookieDarkMode) { - newDarkMode = JSON.parse(cookieDarkMode) + // 查看localStorage中用户记录的是否深色模式 + const userDarkMode = loadDarkModeFromLocalStorage() + if (userDarkMode) { + newDarkMode = userDarkMode + } + + // 如果站点强制设置默认深色,则优先级改过用 + if (defaultDarkMode==='dark') { + newDarkMode = defaultDarkMode } // url查询条件中是否深色模式 @@ -123,7 +132,11 @@ export function isPreferDark() { // 系统深色模式或时间是夜间时,强行置为夜间模式 const date = new Date() const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches - return prefersDarkMode || (BLOG.APPEARANCE_DARK_TIME && (date.getHours() >= BLOG.APPEARANCE_DARK_TIME[0] || date.getHours() < BLOG.APPEARANCE_DARK_TIME[1])) + return ( + prefersDarkMode || + (BLOG.APPEARANCE_DARK_TIME && + (date.getHours() >= BLOG.APPEARANCE_DARK_TIME[0] || date.getHours() < BLOG.APPEARANCE_DARK_TIME[1])) + ) } return false } @@ -137,9 +150,9 @@ export const loadDarkModeFromLocalStorage = () => { } /** - * 保存深色模式 - * @param newTheme - */ -export const saveDarkModeToLocalStorage = (newTheme) => { + * 保存深色模式 + * @param newTheme + */ +export const saveDarkModeToLocalStorage = newTheme => { localStorage.setItem('darkMode', newTheme) } diff --git a/yarn.lock b/yarn.lock index 7935bfbc..8e4cfd52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -869,6 +869,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://r.cnpmjs.org/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + "@polka/url@^1.0.0-next.20", "@polka/url@^1.0.0-next.24": version "1.0.0-next.24" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" @@ -2415,6 +2420,11 @@ eslint-config-next@^13.1.1: eslint-plugin-react "^7.33.2" eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://r.cnpmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + eslint-config-standard@^16.0.2: version "16.0.3" resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516" @@ -2514,6 +2524,14 @@ eslint-plugin-node@^11.1.0: resolve "^1.10.1" semver "^6.1.0" +eslint-plugin-prettier@^5.1.3: + version "5.1.3" + resolved "https://r.cnpmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" + integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.8.6" + eslint-plugin-promise@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz#a596acc32981627eb36d9d75f9666ac1a4564971" @@ -2685,6 +2703,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://r.cnpmjs.org/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -4133,6 +4156,13 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://r2.cnpmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + prettier@3.2.5: version "3.2.5" resolved "https://r.cnpmjs.org/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" @@ -4831,6 +4861,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +synckit@^0.8.6: + version "0.8.8" + resolved "https://r.cnpmjs.org/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + table@^6.0.9: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" @@ -4968,7 +5006,7 @@ tslib@^1.11.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0: +tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==