Merge branch 'main' into feat/mail-encypt

This commit is contained in:
tangly1024
2025-07-27 21:24:37 +08:00
committed by GitHub
390 changed files with 4508 additions and 1516 deletions

View File

@@ -3,7 +3,7 @@ import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import algoliasearch from 'algoliasearch'
import throttle from 'lodash/throttle'
import Link from 'next/link'
import SmartLink from '@/components/SmartLink'
import { useRouter } from 'next/router'
import {
Fragment,
@@ -104,7 +104,8 @@ export default function AlgoliaSearchModal({ cRef }) {
// 跳转Search结果
const onJumpSearchResult = () => {
if (searchResults.length > 0) {
window.location.href = `${siteConfig('SUB_PATH', '')}/${searchResults[activeIndex].slug}`
const searchResult = searchResults[activeIndex]
window.location.href = `${siteConfig('SUB_PATH', '')}/${searchResult.slug || searchResult.objectID}`
}
}
@@ -246,12 +247,12 @@ export default function AlgoliaSearchModal({ cRef }) {
id='search-wrapper'
className={`${
isModalOpen ? 'opacity-100' : 'invisible opacity-0 pointer-events-none'
} z-30 fixed h-screen w-screen left-0 top-0 sm:mt-12 flex items-start justify-center mt-0`}>
} z-30 fixed h-screen w-screen left-0 top-0 sm:mt-[10vh] flex items-start justify-center mt-0`}>
{/* 模态框 */}
<div
className={`${
isModalOpen ? 'opacity-100' : 'invisible opacity-0 translate-y-10'
} flex flex-col justify-between w-full min-h-[10rem] h-full md:h-fit max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}>
} max-h-[80vh] flex flex-col justify-between w-full min-h-[10rem] h-full md:h-fit max-w-xl dark:bg-hexo-black-gray dark:border-gray-800 bg-white dark:bg- p-5 rounded-lg z-50 shadow border hover:border-blue-600 duration-300 transition-all `}>
<div className='flex justify-between items-center'>
<div className='text-2xl text-blue-600 dark:text-yellow-600 font-bold'>
搜索
@@ -356,7 +357,7 @@ function TagGroups() {
<div id='tags-group' className='dark:border-gray-700 space-y-2'>
{firstTenTags?.map((tag, index) => {
return (
<Link
<SmartLink
passHref
key={index}
href={`/tag/${encodeURIComponent(tag.name)}`}
@@ -372,7 +373,7 @@ function TagGroups() {
<></>
)}
</div>
</Link>
</SmartLink>
)
})}
</div>

View File

@@ -0,0 +1,73 @@
import { siteConfig } from '@/lib/config'
/**
* 文章过期提醒组件
* 当文章超过指定天数时显示提醒
* @param {Object} props - 组件属性
* @param {Object} props.post - 文章数据
* @param {number} [props.daysThreshold=90] - 过期阈值(天)
* @returns {JSX.Element|null}
*/
export default function ArticleExpirationNotice({
post,
daysThreshold = siteConfig('ARTICLE_EXPIRATION_DAYS', 90)
}) {
const articleExpirationEnabled = siteConfig(
'ARTICLE_EXPIRATION_ENABLED',
false
)
if (!articleExpirationEnabled || !post?.lastEditedDay) {
return null
}
const postDate = new Date(post.lastEditedDay)
const today = new Date()
const diffTime = Math.abs(today - postDate)
const daysOld = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
const isVisible = daysOld >= daysThreshold
if (!isVisible) {
return null
}
// 使用 %%DAYS%% 作为占位符
const articleExpirationMessage = siteConfig(
'ARTICLE_EXPIRATION_MESSAGE',
'这篇文章发布于 %%DAYS%% 天前,内容可能已过时,请谨慎参考。'
)
const articleExpirationMessageParts =
articleExpirationMessage.split('%%DAYS%%')
// 直接返回 JSX 内容
return (
<div
className={
'p-4 rounded-lg border border-blue-300 bg-blue-50 dark:bg-blue-900/20 text-gray-800 dark:text-gray-200 shadow-sm'
}>
<div className='flex items-start'>
<i className='fas fa-exclamation-triangle text-blue-500 dark:text-blue-400 mt-0.5 mr-2 flex-shrink-0' />
<div className='ml-1'>
<div className='text-blue-600 dark:text-blue-400 font-medium'>
{siteConfig('ARTICLE_EXPIRATION_TITLE', '温馨提醒')}
</div>
<div className='flex items-center mt-1 text-sm text-gray-700 dark:text-gray-300'>
<i className='far fa-clock text-red-500 dark:text-red-400 mr-1' />
<span>
{(() => {
return (
<>
{articleExpirationMessageParts[0]}
<span className='text-red-500 dark:text-red-400 font-bold'>
{daysOld}
</span>
{articleExpirationMessageParts[1]}
</>
)
})()}
</span>
</div>
</div>
</div>
</div>
)
}

View File

@@ -2,7 +2,7 @@ import useWindowSize from '@/hooks/useWindowSize'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { THEMES, saveDarkModeToLocalStorage } from '@/themes/theme'
import Link from 'next/link'
import SmartLink from '@/components/SmartLink'
import { useRouter } from 'next/router'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
@@ -198,23 +198,23 @@ export default function CustomContextMenu(props) {
)}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_CATEGORY && (
<Link
<SmartLink
href='/category'
title={locale.MENU.CATEGORY}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-square-minus mr-2' />
<div className='whitespace-nowrap'>{locale.MENU.CATEGORY}</div>
</Link>
</SmartLink>
)}
{CUSTOM_RIGHT_CLICK_CONTEXT_MENU_TAG && (
<Link
<SmartLink
href='/tag'
title={locale.MENU.TAGS}
className='w-full px-2 h-10 flex justify-start items-center flex-nowrap cursor-pointer hover:bg-blue-600 hover:text-white rounded-lg duration-200 transition-all'>
<i className='fa-solid fa-tag mr-2' />
<div className='whitespace-nowrap'>{locale.MENU.TAGS}</div>
</Link>
</SmartLink>
)}
</div>

66
components/NotByAI.js Normal file
View File

@@ -0,0 +1,66 @@
import { useGlobal } from '@/lib/global'
const LANGS = {
'en-US': 'en',
'zh-CN': 'zh',
'zh-HK': 'zh-HK',
'zh-TW': 'zh-TW',
'fr-FR': 'fr',
'tr-TR': 'tr',
'ja-JP': 'ja'
}
/**
* 获取当前not-by-ai文件路径
* 如果匹配到完整的“国家-地区”语言,则显示国家的语言
* @returns string
*/
export function generateNotByAiPath(langString) {
const supportedLocales = Object.keys(LANGS)
let userLocale
// 将语言字符串拆分为语言和地区代码,例如将 "zh-CN" 拆分为 "zh" 和 "CN"
const [language, region] = langString?.split(/[-_]/)
// 优先匹配语言和地区都匹配的情况
const specificLocale = `${language}-${region}`
if (supportedLocales.includes(specificLocale)) {
userLocale = LANGS[specificLocale]
}
// 然后尝试匹配只有语言匹配的情况
if (!userLocale) {
const languageOnlyLocales = supportedLocales.filter(locale =>
locale.startsWith(language)
)
if (languageOnlyLocales.length > 0) {
userLocale = LANGS[languageOnlyLocales[0]]
}
}
// 如果还没匹配到,则返回最接近的
if (!userLocale) {
const fallbackLocale = supportedLocales.find(locale =>
locale.startsWith('en')
)
userLocale = LANGS[fallbackLocale]
}
return userLocale ?? 'zh'
}
/**
* 版权声明
* @returns
*/
export default function NotByAI() {
const { lang, isDarkMode } = useGlobal()
return (
<img
className='transform hover:scale-110 duration-150'
src={`/svg/not-by-ai/${generateNotByAiPath(lang)}/Written-By-Human-Not-By-AI-Badge-${isDarkMode ? 'black' : 'white'}.svg`}
alt='not-by-ai'
/>
)
}

View File

@@ -64,7 +64,7 @@ const Player = () => {
<link
rel='stylesheet'
type='text/css'
href='https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/aplayer/1.10.1/APlayer.min.css'
href='https://cdn.jsdelivr.net/npm/aplayer@1.10.0/dist/APlayer.min.css'
/>
{meting ? (
<meting-js

52
components/SmartLink.js Normal file
View File

@@ -0,0 +1,52 @@
import Link from 'next/link'
import { siteConfig } from '@/lib/config'
// 过滤 <a> 标签不能识别的 props
const filterDOMProps = props => {
const { passHref, legacyBehavior, ...rest } = props
return rest
}
const SmartLink = ({ href, children, ...rest }) => {
const LINK = siteConfig('LINK')
// 获取 URL 字符串用于判断是否是外链
let urlString = ''
if (typeof href === 'string') {
urlString = href
} else if (
typeof href === 'object' &&
href !== null &&
typeof href.pathname === 'string'
) {
urlString = href.pathname
}
const isExternal = urlString.startsWith('http') && !urlString.startsWith(LINK)
if (isExternal) {
// 对于外部链接,必须是 string 类型
const externalUrl =
typeof href === 'string' ? href : new URL(href.pathname, LINK).toString()
return (
<a
href={externalUrl}
target='_blank'
rel='noopener noreferrer'
{...filterDOMProps(rest)}>
{children}
</a>
)
}
// 内部链接(可为对象形式)
return (
<Link href={href} {...rest}>
{children}
</Link>
)
}
export default SmartLink

View File

@@ -1,5 +1,5 @@
import { siteConfig } from '@/lib/config'
import Link from 'next/link'
import SmartLink from '@/components/SmartLink'
import { useRouter } from 'next/router'
/**
* 跳转仪表盘的按钮
@@ -24,7 +24,7 @@ export default function DashboardButton({ className }) {
<button
type='button'
className={`${className || ''} text-white bg-gray-800 hover:bg-gray-900 hover:ring-4 hover:ring-gray-300 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2 me-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700`}>
<Link href='/dashboard'>仪表盘</Link>
<SmartLink href='/dashboard'>仪表盘</SmartLink>
</button>
)
}

View File

@@ -1,7 +1,7 @@
import LazyImage from '@/components/LazyImage'
import { useGlobal } from '@/lib/global'
import formatDate from '@/lib/utils/formatDate'
import Link from 'next/link'
import SmartLink from '@/components/SmartLink'
import DashboardSignOutButton from './DashboardSignOutButton'
/**
@@ -25,11 +25,11 @@ export default function DashboardHeader() {
<div class='font-medium dark:text-white'>
<div className='flex items-center gap-x-2'>
<span>{user?.fullName}</span>
<Link href='/dashboard/membership'>
<SmartLink href='/dashboard/membership'>
<span class='bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-300'>
普通用户
</span>
</Link>
</SmartLink>
</div>
<div className='text-sm text-gray-500 gap-x-2 flex dark:text-gray-400'>
<span>{user?.username}</span>

View File

@@ -1,4 +1,4 @@
import Link from 'next/link'
import SmartLink from '@/components/SmartLink'
/**
* 联盟行销
@@ -140,11 +140,11 @@ export default function DashboardItemAffliate() {
for='remember'
className='ms-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
我以阅读并同意{' '}
<Link
<SmartLink
href='/terms-of-use'
className='text-blue-600 hover:underline dark:text-blue-500'>
服务协议
</Link>
</SmartLink>
.
</label>
</div>

View File

@@ -1,4 +1,4 @@
import Link from 'next/link'
import SmartLink from '@/components/SmartLink'
/**
* 仪表盘菜单
@@ -43,12 +43,12 @@ export default function DashboardMenuList() {
className={`rounded-lg cursor-pointer block ${
isActive ? 'bg-blue-100 text-blue-600' : 'hover:bg-gray-100'
}`}>
<Link
<SmartLink
href={item.href}
className='block py-2 px-4 w-full items-center justify-center'>
<i className={`${item.icon} w-6 mr-2`}></i>
<span className='whitespace-nowrap'>{item.title}</span>
</Link>
</SmartLink>
</li>
)
})}