diff --git a/.env.local b/.env.local index cfc0b2cd..2dacdbef 100644 --- a/.env.local +++ b/.env.local @@ -1,15 +1,15 @@ # 环境变量 @see https://www.nextjs.cn/docs/basic-features/environment-variables -NEXT_PUBLIC_VERSION=4.2.0 +NEXT_PUBLIC_VERSION=4.3.1 # 可在此添加环境变量,去掉最左边的(# )注释即可 # Notion页面ID,必须 -# NOTION_PAGE_ID= +# NOTION_PAGE_ID=097e5f674880459d8e1b4407758dc4fb # 非必须 # NEXT_PUBLIC_PSEUDO_STATIC= # NEXT_PUBLIC_REVALIDATE_SECOND= -# NEXT_PUBLIC_THEME= +# NEXT_PUBLIC_THEME=matery # NEXT_PUBLIC_THEME_SWITCH= # NEXT_PUBLIC_LANG= # NEXT_PUBLIC_APPEARANCE= diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4ea399c0..c063ca6c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,6 +27,7 @@ assignees: tangly1024 - 【必填】NotionNext版本 [例如. 4.0.18] - 【必填】主题 [例如. hexo] +- 【必填】部署方案 [例如. vercel] - 【可选】操作系统: [例如. iOS, Android, macOS, windows] - 【可选】浏览器 [例如. chrome, safari, firefox] diff --git a/.github/workflows/docker-ghcr.yaml b/.github/workflows/docker-ghcr.yaml index 86036f2f..db58d8d8 100644 --- a/.github/workflows/docker-ghcr.yaml +++ b/.github/workflows/docker-ghcr.yaml @@ -30,6 +30,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} @@ -54,6 +57,7 @@ jobs: uses: docker/build-push-action@v2 with: context: . + platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 1befa8c4..42744c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,6 @@ yarn-error.log* # dev /data.json -/yarn.lock /pnpm-lock.yaml .idea .vscode @@ -45,4 +44,9 @@ yarn-error.log* # sitemap /public/robots.txt /public/sitemap.xml -/public/rss/* \ No newline at end of file +/public/rss/* + + +# yarn +package-lock.json +# yarn.lock diff --git a/blog.config.js b/blog.config.js index 50e16921..746829cb 100644 --- a/blog.config.js +++ b/blog.config.js @@ -37,14 +37,17 @@ const BLOG = { BLOG_FAVICON: process.env.NEXT_PUBLIC_FAVICON || '/favicon.ico', // blog favicon 配置, 默认使用 /public/favicon.ico,支持在线图片,如 https://img.imesong.com/favicon.png + IMAGE_COMPRESS_WIDTH: process.env.NEXT_PUBLIC_IMAGE_COMPRESS_WIDTH || 800, // 图片压缩宽度默认值,作用于博客封面和文章内容 越小加载图片越快 + IMAGE_ZOOM_IN_WIDTH: process.env.NEXT_PUBLIC_IMAGE_ZOOM_IN_WIDTH || 1200, // 文章图片点击放大后的画质宽度,不代表在网页中的实际展示宽度 RANDOM_IMAGE_URL: process.env.NEXT_PUBLIC_RANDOM_IMAGE_URL || '', // 随机图片API,如果未配置下面的关键字,主页封面,头像,文章封面图都会被替换为随机图片 RANDOM_IMAGE_REPLACE_TEXT: process.env.NEXT_PUBLIC_RANDOM_IMAGE_NOT_REPLACE_TEXT || 'images.unsplash.com', // 触发替换图片的 url 关键字(多个支持用英文逗号分开),只有图片地址中包含此关键字才会替换为上方随机图片url // eg: images.unsplash.com(notion图床的所有图片都会替换),如果你在 notion 里已经添加了一个随机图片 url,恰巧那个服务跑路或者挂掉,想一键切换所有配图可以将该 url 配置在这里 // 默认下会将你上传到 notion的主页封面图和头像也给替换,建议将主页封面图和头像放在其他图床,在 notion 里配置 link 即可。 // START ************网站字体***************** - - FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-sans', // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115 + // ['font-serif','font-sans'] 两种可选,分别是衬线和无衬线: 参考 https://www.jianshu.com/p/55e410bd2115 + // 后面空格隔开的font-light的字体粗细,留空是默认粗细;参考 https://www.tailwindcss.cn/docs/font-weight + FONT_STYLE: process.env.NEXT_PUBLIC_FONT_STYLE || 'font-sans font-light', // 字体CSS 例如 https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css FONT_URL: [ // 'https://npm.elemecdn.com/lxgw-wenkai-webfont@1.6.0/style.css', @@ -88,9 +91,33 @@ const BLOG = { FONT_AWESOME: process.env.NEXT_PUBLIC_FONT_AWESOME_PATH || 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // font-awesome 字体图标地址; 可选 /css/all.min.css , https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/font-awesome/6.0.0/css/all.min.css // END ************网站字体***************** + + // 路径和组件映射,不同路径分别展示主题的什么组件 + LAYOUT_MAPPINGS: { + '-1': 'LayoutBase', + '/': 'LayoutIndex', + '/archive': 'LayoutArchive', + '/page/[page]': 'LayoutPostList', + '/category/[category]': 'LayoutPostList', + '/category/[category]/page/[page]': 'LayoutPostList', + '/tag/[tag]': 'LayoutPostList', + '/tag/[tag]/page/[page]': 'LayoutPostList', + '/search': 'LayoutSearch', + '/search/[keyword]': 'LayoutSearch', + '/search/[keyword]/page/[page]': 'LayoutSearch', + '/404': 'Layout404', + '/tag': 'LayoutTagIndex', + '/category': 'LayoutCategoryIndex', + '/[prefix]': 'LayoutSlug', + '/[prefix]/[slug]': 'LayoutSlug', + '/[prefix]/[slug]/[...suffix]': 'LayoutSlug', + '/signin': 'LayoutSignIn', + '/signup': 'LayoutSignUp' + }, + CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为false、则全栈禁止复制内容。 CUSTOM_RIGHT_CLICK_CONTEXT_MENU: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU || true, // 自定义右键菜单,覆盖系统菜单 - CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, + CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH: process.env.NEXT_PUBLIC_CUSTOM_RIGHT_CLICK_CONTEXT_MENU_THEME_SWITCH || true, // 右键菜单是否允许切换主题 // 自定义外部脚本,外部样式 CUSTOM_EXTERNAL_JS: [''], // e.g. ['http://xx.com/script.js','http://xx.com/script.js'] @@ -151,6 +178,8 @@ const BLOG = { POSTS_PER_PAGE: 12, // post counts per page POSTS_SORT_BY: process.env.NEXT_PUBLIC_POST_SORT_BY || 'notion', // 排序方式 'date'按时间,'notion'由notion控制 + POST_WAITING_TIME_FOR_404: process.env.NEXT_PUBLIC_POST_WAITING_TIME_FOR_404 || '8', // 文章加载超时时间,单位秒;超时后跳转到404页面 + ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || null, // 在这里查看 https://dashboard.algolia.com/account/api-keys/ ALGOLIA_ADMIN_APP_KEY: process.env.ALGOLIA_ADMIN_APP_KEY || null, // 管理后台的KEY,不要暴露在代码中,在这里查看 https://dashboard.algolia.com/account/api-keys/ ALGOLIA_SEARCH_ONLY_APP_KEY: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_APP_KEY || null, // 客户端搜索用的KEY @@ -244,7 +273,7 @@ const BLOG = { // ********挂件组件相关******** // ----> 评论互动 可同时开启多个支持 WALINE VALINE GISCUS CUSDIS UTTERRANCES GITALK - COMMENT_HIDE_SINGLE_TAB: process.env.NEXT_PUBLIC_COMMENT_HIDE_SINGLE_TAB || false, //Whether hide the tab when there's no tabs. 只有一个评论组件时是否隐藏切换组件的标签页 + COMMENT_HIDE_SINGLE_TAB: process.env.NEXT_PUBLIC_COMMENT_HIDE_SINGLE_TAB || false, // Whether hide the tab when there's no tabs. 只有一个评论组件时是否隐藏切换组件的标签页 // artalk 评论插件 COMMENT_ARTALK_SERVER: process.env.NEXT_PUBLIC_COMMENT_ARTALK_SERVER || '', // ArtalkServert后端地址 https://artalk.js.org/guide/deploy.html @@ -352,6 +381,9 @@ const BLOG = { SEO_BAIDU_SITE_VERIFICATION: process.env.NEXT_PUBLIC_SEO_BAIDU_SITE_VERIFICATION || '', // Remove the value or replace it with your own google site verification code + // 微软 Clarity 站点分析 + CLARITY_ID: process.env.NEXT_PUBLIC_CLARITY_ID || null, // 只需要复制Clarity脚本中的ID部分,ID是一个十位的英文数字组合 + // <---- 站点统计 // START---->营收相关 @@ -417,7 +449,7 @@ const BLOG = { // 开发相关 NOTION_ACCESS_TOKEN: process.env.NOTION_ACCESS_TOKEN || '', // Useful if you prefer not to make your database public DEBUG: process.env.NEXT_PUBLIC_DEBUG || false, // 是否显示调试按钮 - ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build', // 缓存在开发调试和打包过程中选择性开启,正式部署开启此功能意义不大。 + ENABLE_CACHE: process.env.ENABLE_CACHE || process.env.npm_lifecycle_event === 'build' || process.env.npm_lifecycle_event === 'export', // 在打包过程中默认开启缓存,开发或运行时开启此功能意义不大。 isProd: process.env.VERCEL_ENV === 'production', // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables) BUNDLE_ANALYZER: process.env.ANALYZE === 'true' || false, // 是否展示编译依赖内容与大小 VERSION: process.env.NEXT_PUBLIC_VERSION // 版本号 diff --git a/components/AlgoliaSearchModal.js b/components/AlgoliaSearchModal.js index 7c6bfdcb..f7bcdeb9 100644 --- a/components/AlgoliaSearchModal.js +++ b/components/AlgoliaSearchModal.js @@ -1,10 +1,11 @@ -import { useState, useImperativeHandle, useRef } from 'react' +import { useState, useImperativeHandle, useRef, useEffect } from 'react' import algoliasearch from 'algoliasearch' import replaceSearchResult from '@/components/Mark' import Link from 'next/link' import { useGlobal } from '@/lib/global' import throttle from 'lodash/throttle' import { siteConfig } from '@/lib/config' +import { useHotkeys } from 'react-hotkeys-hook'; /** * 结合 Algolia 实现的弹出式搜索框 @@ -19,7 +20,62 @@ export default function AlgoliaSearchModal({ cRef }) { const [totalPage, setTotalPage] = useState(0) const [totalHit, setTotalHit] = useState(0) const [useTime, setUseTime] = useState(0) + const inputRef = useRef(null) + const [activeIndex, setActiveIndex] = useState(0) + useHotkeys('ctrl+k', (e) => { + e.preventDefault() + setIsModalOpen(true) + }) + // 方向键调整选中 + useHotkeys('down', (e) => { + e.preventDefault() + if (activeIndex < searchResults.length - 1) { + setActiveIndex(activeIndex + 1) + } + }, { enableOnFormTags: true }) + useHotkeys('up', (e) => { + e.preventDefault() + if (activeIndex > 0) { + setActiveIndex(activeIndex - 1) + } + }, { enableOnFormTags: true }) + // esc关闭 + useHotkeys('esc', (e) => { + e.preventDefault() + setIsModalOpen(false) + }, { enableOnFormTags: true }) + // 跳转Search结果 + const onJumpSearchResult = () => { + if (searchResults.length > 0) { + window.location.href = `${siteConfig('SUB_PATH', '')}/${searchResults[activeIndex].slug}` + } + } + // enter跳转 + useHotkeys('enter', (e) => { + if (searchResults.length > 0) { + onJumpSearchResult(index) + } + }, { enableOnFormTags: true }) + const resetSearch = () => { + setActiveIndex(0) + setKeyword('') + setSearchResults([]) + setUseTime(0) + setTotalPage(0) + setTotalHit(0) + if (inputRef.current) inputRef.current.value = '' + } + + useEffect(() => { + if (isModalOpen) { + setTimeout(() => { + inputRef.current?.focus() + }, 100) + } else { + resetSearch() + } + }, [isModalOpen]) /** * 对外暴露方法 */ @@ -45,6 +101,7 @@ export default function AlgoliaSearchModal({ cRef }) { setUseTime(0) setTotalPage(0) setTotalHit(0) + setActiveIndex(0) if (!query || query === '') { return } @@ -56,7 +113,6 @@ export default function AlgoliaSearchModal({ cRef }) { setTotalPage(nbPages) setTotalHit(nbHits) setSearchResults(hits) - const doms = document.getElementById('search-wrapper').getElementsByClassName('replace') setTimeout(() => { @@ -65,29 +121,44 @@ export default function AlgoliaSearchModal({ cRef }) { search: query, target: { element: 'span', - className: 'text-blue-600 border-b border-dashed' + className: 'font-bold border-b border-dashed' } }) - }, 150) + }, 200) // 延时高亮 } catch (error) { console.error('Algolia search error:', error) } } - const throttledHandleSearch = useRef(throttle(handleSearch, 300)) // 设置节流延迟时间 + // 定义节流函数,确保在用户停止输入一段时间后才会调用处理搜索的方法 + const throttledHandleInputChange = useRef(throttle((query, page = 0) => { + handleSearch(query, page); + }, 1000)); + + // 用于存储搜索延迟的计时器 + const searchTimer = useRef(null); // 修改input的onChange事件处理函数 const handleInputChange = (e) => { - const query = e.target.value - throttledHandleSearch.current(query, 0) - } + const query = e.target.value; + + // 如果已经有计时器在等待搜索,先清除之前的计时器 + if (searchTimer.current) { + clearTimeout(searchTimer.current); + } + + // 设置新的计时器,在用户停止输入一段时间后触发搜索 + searchTimer.current = setTimeout(() => { + throttledHandleInputChange.current(query); + }, 800); + }; /** * 切换页码 * @param {*} page */ const switchPage = (page) => { - throttledHandleSearch.current(keyword, page) + throttledHandleInputChange.current(keyword, page) } /** @@ -100,22 +171,19 @@ export default function AlgoliaSearchModal({ cRef }) { if (!siteConfig('ALGOLIA_APP_ID')) { return <>> } - return (
无法找到相关结果 + "{keyword}"
++ 共搜索到 {totalHit} 条结果,用时 {useTime} 毫秒 +
+ )} +