diff --git a/components/CanvasEmail.js b/components/CanvasEmail.js new file mode 100644 index 00000000..1de23fee --- /dev/null +++ b/components/CanvasEmail.js @@ -0,0 +1,95 @@ +import { useEffect, useRef, useState } from 'react' + +const CanvasEmail = ({ email, className = '' }) => { + const canvasRef = useRef(null) + const textRef = useRef(null) + const [isCopied, setIsCopied] = useState(false) + + useEffect(() => { + if (!textRef.current || !canvasRef.current) return + + const canvas = canvasRef.current + const ctx = canvas.getContext('2d') + const textElement = textRef.current + + // Get computed styles from the hidden text element + const style = window.getComputedStyle(textElement) + const font = style.font + const color = style.color + + // Set canvas font and measure text + ctx.font = font + const metrics = ctx.measureText(email) + const fontSize = parseInt(style.fontSize) + const lineHeight = fontSize * 1.2 + + // Set canvas dimensions + const scale = window.devicePixelRatio || 1 + canvas.width = metrics.width * scale + canvas.height = lineHeight * scale + canvas.style.width = `${metrics.width}px` + canvas.style.height = `${lineHeight}px` + + // Redraw with high DPI support + ctx.scale(scale, scale) + ctx.font = font + ctx.fillStyle = color + ctx.textBaseline = 'top' // Changed to 'top' for better vertical alignment + ctx.fillText(email, 0, 0) + + // Handle copy to clipboard + const handleCopy = e => { + e.preventDefault() + navigator.clipboard.writeText(email).then(() => { + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + }) + } + + canvas.style.cursor = 'pointer' + canvas.addEventListener('click', handleCopy) + return () => canvas.removeEventListener('click', handleCopy) + }, [email]) + + return ( + + {/* Hidden span for measuring text metrics */} + + + {/* Canvas that displays the text */} + + + ) +} + +export default CanvasEmail diff --git a/conf/contact.config.js b/conf/contact.config.js index d2a53274..2784e360 100644 --- a/conf/contact.config.js +++ b/conf/contact.config.js @@ -3,7 +3,12 @@ */ module.exports = { // 社交链接,不需要可留空白,例如 CONTACT_WEIBO:'' - CONTACT_EMAIL: process.env.NEXT_PUBLIC_CONTACT_EMAIL || '', // 邮箱地址 例如mail@tangly1024.com + CONTACT_EMAIL: + (process.env.NEXT_PUBLIC_CONTACT_EMAIL && + btoa( + unescape(encodeURIComponent(process.env.NEXT_PUBLIC_CONTACT_EMAIL)) + )) || + '', // 邮箱地址 例如mail@tangly1024.com CONTACT_WEIBO: process.env.NEXT_PUBLIC_CONTACT_WEIBO || '', // 你的微博个人主页 CONTACT_TWITTER: process.env.NEXT_PUBLIC_CONTACT_TWITTER || '', // 你的twitter个人主页 CONTACT_GITHUB: process.env.NEXT_PUBLIC_CONTACT_GITHUB || '', // 你的github个人主页 例如 https://github.com/tangly1024 diff --git a/lib/notion/getNotionConfig.js b/lib/notion/getNotionConfig.js index 029c7b96..d32b46fa 100644 --- a/lib/notion/getNotionConfig.js +++ b/lib/notion/getNotionConfig.js @@ -10,6 +10,7 @@ import { getDateValue, getTextContent } from 'notion-utils' import { deepClone } from '../utils' import getAllPageIds from './getAllPageIds' import { getPage } from './getPostBlocks' +import { encryptEmail } from '@/lib/plugins/mailEncrypt' /** * 从Notion中读取Config配置表 @@ -157,9 +158,14 @@ export async function getConfigMapFromConfigPage(allPages) { // 只导入生效的配置 if (config.enable) { // console.log('[Notion配置]', config.key, config.value) - notionConfig[config.key] = - parseTextToJson(config.value) || config.value || null - // 配置不能是undefined,至少是null + if (config.key === 'CONTACT_EMAIL') { + notionConfig[config.key] = + (config.value && encryptEmail(config.value)) || null + } else { + notionConfig[config.key] = + parseTextToJson(config.value) || config.value || null + // 配置不能是undefined,至少是null + } } } } diff --git a/lib/plugins/mailEncrypt.js b/lib/plugins/mailEncrypt.js new file mode 100644 index 00000000..a7c6c2bc --- /dev/null +++ b/lib/plugins/mailEncrypt.js @@ -0,0 +1,21 @@ +export const handleEmailClick = (e, emailIcon, CONTACT_EMAIL) => { + if (CONTACT_EMAIL && emailIcon && !emailIcon.current.href) { + e.preventDefault() + const email = decryptEmail(CONTACT_EMAIL) + emailIcon.current.href = `mailto:${email}` + emailIcon.current.click() + } +} + +export const encryptEmail = email => { + return btoa(unescape(encodeURIComponent(email))) +} + +export const decryptEmail = encryptedEmail => { + try { + return decodeURIComponent(escape(atob(encryptedEmail))) + } catch (error) { + console.error('解密邮箱失败:', error) + return encryptedEmail + } +} diff --git a/lib/rss.js b/lib/rss.js index 1ec3daa7..04c667ce 100644 --- a/lib/rss.js +++ b/lib/rss.js @@ -4,6 +4,7 @@ import { getPostBlocks } from '@/lib/db/getSiteData' import { Feed } from 'feed' import fs from 'fs' import ReactDOMServer from 'react-dom/server' +import { decryptEmail } from '@/lib/plugins/mailEncrypt' /** * 生成RSS内容 @@ -37,7 +38,9 @@ export async function generateRss(props) { const AUTHOR = NOTION_CONFIG?.AUTHOR || BLOG.AUTHOR const LANG = NOTION_CONFIG?.LANG || BLOG.LANG const SUB_PATH = NOTION_CONFIG?.SUB_PATH || BLOG.SUB_PATH - const CONTACT_EMAIL = NOTION_CONFIG?.CONTACT_EMAIL || BLOG.CONTACT_EMAIL + const CONTACT_EMAIL = decryptEmail( + NOTION_CONFIG?.CONTACT_EMAIL || BLOG.CONTACT_EMAIL + ) // 检查 feed 文件是否在10分钟内更新过 if (isFeedRecentlyUpdated('./public/rss/feed.xml', 10)) { diff --git a/themes/commerce/components/Footer.js b/themes/commerce/components/Footer.js index ba36e511..a9a7f262 100644 --- a/themes/commerce/components/Footer.js +++ b/themes/commerce/components/Footer.js @@ -4,6 +4,9 @@ import CopyRightDate from '@/components/CopyRightDate' import { siteConfig } from '@/lib/config' import SmartLink from '@/components/SmartLink' import CONFIG from '../config' +import { decryptEmail, handleEmailClick } from '@/lib/plugins/mailEncrypt' +import { useRef } from 'react' +import CanvasEmail from '@/components/CanvasEmail' /** * 页脚 @@ -18,6 +21,10 @@ const Footer = props => { parseInt(since) < currentYear ? since + '-' + currentYear : currentYear const { categoryOptions, customMenu } = props + const CONTACT_EMAIL = siteConfig('CONTACT_EMAIL') + + const emailIcon = useRef(null) + return (