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 (