增开滚动条样式,搜索内容,修复代码块解析html,表情周围有空格才会解析

This commit is contained in:
lsy 2025-03-28 22:14:16 +08:00
parent e356212737
commit a1a4d74493
8 changed files with 252 additions and 121 deletions

View File

@ -1,35 +1,35 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from "@tailwindcss/vite";
import mdx from '@astrojs/mdx'; import mdx from "@astrojs/mdx";
import react from '@astrojs/react'; import react from "@astrojs/react";
import remarkEmoji from 'remark-emoji'; import remarkEmoji from "remark-emoji";
import rehypeExternalLinks from 'rehype-external-links'; import rehypeExternalLinks from "rehype-external-links";
import sitemap from '@astrojs/sitemap'; import sitemap from "@astrojs/sitemap";
import fs from 'node:fs'; import fs from "node:fs";
import path from 'node:path'; import path from "node:path";
import { SITE_URL } from './src/consts'; import { SITE_URL } from "./src/consts";
import vercel from '@astrojs/vercel'; import vercel from "@astrojs/vercel";
function getArticleDate(articleId) { function getArticleDate(articleId) {
try { try {
// 处理多级目录的文章路径 // 处理多级目录的文章路径
const mdPath = path.join(process.cwd(), 'src/content', articleId + '.md'); const mdPath = path.join(process.cwd(), "src/content", articleId + ".md");
const mdxPath = path.join(process.cwd(), 'src/content', articleId + '.mdx'); const mdxPath = path.join(process.cwd(), "src/content", articleId + ".mdx");
let filePath = fs.existsSync(mdPath) ? mdPath : mdxPath; let filePath = fs.existsSync(mdPath) ? mdPath : mdxPath;
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, "utf-8");
const match = content.match(/date:\s*(\d{4}-\d{2}-\d{2})/); const match = content.match(/date:\s*(\d{4}-\d{2}-\d{2})/);
if (match) { if (match) {
return new Date(match[1]).toISOString(); return new Date(match[1]).toISOString();
} }
} }
} catch (error) { } catch (error) {
console.error('Error reading article date:', error); console.error("Error reading article date:", error);
} }
return new Date().toISOString(); // 如果没有日期,返回当前时间 return new Date().toISOString(); // 如果没有日期,返回当前时间
} }
@ -37,11 +37,11 @@ function getArticleDate(articleId) {
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: SITE_URL, site: SITE_URL,
output: 'static', output: "static",
trailingSlash: 'ignore', trailingSlash: "ignore",
build: { build: {
format: 'directory' format: "directory",
}, },
vite: { vite: {
@ -52,94 +52,106 @@ export default defineConfig({
// 手动分块配置 // 手动分块配置
manualChunks: { manualChunks: {
// 将地图组件单独打包 // 将地图组件单独打包
'world-heatmap': ['./src/components/WorldHeatmap.tsx'], "world-heatmap": ["./src/components/WorldHeatmap.tsx"],
// 将 React 相关库单独打包 // 将 React 相关库单独打包
'react-vendor': ['react', 'react-dom'], "react-vendor": ["react", "react-dom"],
// 其他大型依赖也可以单独打包 // 其他大型依赖也可以单独打包
'chart-vendor': ['chart.js'], "chart-vendor": ["chart.js"],
// 将 ECharts 单独打包 // 将 ECharts 单独打包
'echarts-vendor': ['echarts'], "echarts-vendor": ["echarts"],
// 将其他组件打包到一起 // 将其他组件打包到一起
'components': ['./src/components'] components: ["./src/components"],
} },
} },
}, },
// 提高警告阈值,避免不必要的警告 // 提高警告阈值,避免不必要的警告
chunkSizeWarningLimit: 1000 chunkSizeWarningLimit: 1000,
} },
}, },
integrations: [ integrations: [
// MDX 集成配置
mdx({ mdx({
syntaxHighlight: 'prism', // 不使用共享的 markdown 配置
extendMarkdownConfig: false,
// 为 MDX 单独配置所需功能
remarkPlugins: [ remarkPlugins: [
[remarkEmoji, { emoticon: true }] // 添加表情符号支持
[remarkEmoji, { emoticon: true, padded: true }]
], ],
rehypePlugins: [ rehypePlugins: [
[rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }] [rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }]
], ],
gfm: true, // 设置代码块处理行为
shikiConfig: { remarkRehype: {
theme: 'github-dark', allowDangerousHtml: false // 不解析 HTML
langs: [], },
wrap: true, gfm: true
}
}), }),
react(), react(),
sitemap({ sitemap({
filter: (page) => !page.includes('/api/'), filter: (page) => !page.includes("/api/"),
serialize(item) { serialize(item) {
if (!item) return undefined; if (!item) return undefined;
// 文章页面 // 文章页面
if (item.url.includes('/articles/')) { if (item.url.includes("/articles/")) {
// 从 URL 中提取文章 ID // 从 URL 中提取文章 ID
const articleId = item.url.replace(SITE_URL + '/articles/', '').replace(/\/$/, ''); const articleId = item.url
.replace(SITE_URL + "/articles/", "")
.replace(/\/$/, "");
const publishDate = getArticleDate(articleId); const publishDate = getArticleDate(articleId);
return { return {
...item, ...item,
priority: 0.8, priority: 0.8,
lastmod: publishDate lastmod: publishDate,
}; };
} }
// 其他页面 // 其他页面
else { else {
let priority = 0.7; // 默认优先级 let priority = 0.7; // 默认优先级
// 首页最高优先级 // 首页最高优先级
if (item.url === SITE_URL + '/') { if (item.url === SITE_URL + "/") {
priority = 1.0; priority = 1.0;
} }
// 文章列表页次高优先级 // 文章列表页次高优先级
else if (item.url === SITE_URL + '/articles/') { else if (item.url === SITE_URL + "/articles/") {
priority = 0.9; priority = 0.9;
} }
return { return {
...item, ...item,
priority priority,
}; };
} }
} },
}) }),
], ],
// Markdown 配置 // Markdown 配置
markdown: { markdown: {
syntaxHighlight: 'prism', syntaxHighlight: 'prism',
remarkPlugins: [ remarkPlugins: [
[remarkEmoji, { emoticon: true }] [remarkEmoji, { emoticon: true, padded: true }]
], ],
rehypePlugins: [ rehypePlugins: [
[rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }] [rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }]
], ],
gfm: true, gfm: true,
// 设置 remark-rehype 选项以控制HTML处理
remarkRehype: {
// 保留原始HTML格式但仅在非代码块区域
allowDangerousHtml: true,
// 确保代码块内容不被解析
passThrough: ['code']
},
shikiConfig: { shikiConfig: {
theme: 'github-dark', theme: "github-dark",
langs: [], langs: [],
wrap: true, wrap: true,
} },
}, },
adapter: vercel() adapter: vercel(),
}); });

View File

@ -43,7 +43,7 @@ const currentYear = new Date().getFullYear();
</div> </div>
<div class="text-sm text-gray-500 dark:text-gray-500 font-light flex items-center gap-2"> <div class="text-sm text-gray-500 dark:text-gray-500 font-light flex items-center gap-2">
<span>© {currentYear} New Echoes. All rights reserved.</span> <a href="https://blog.lsy22.com" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">© {currentYear} New Echoes. All rights reserved.</a>
<span>·</span> <span>·</span>
<a <a
href="/sitemap-index.xml" href="/sitemap-index.xml"

View File

@ -303,6 +303,7 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
summary?: string; summary?: string;
tags?: string[]; tags?: string[];
image?: string; image?: string;
content?: string;
} }
let articles: Article[] = []; let articles: Article[] = [];
@ -320,6 +321,17 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
} }
} }
// 高亮文本中的匹配部分
function highlightText(text: string, query: string): string {
if (!text || !query.trim()) return text;
// 转义正则表达式中的特殊字符
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
return text.replace(regex, '<mark class="bg-yellow-100 dark:bg-yellow-900/30 text-gray-900 dark:text-gray-100 px-0.5 rounded">$1</mark>');
}
// 搜索文章 // 搜索文章
function searchArticles(query: string, resultsList: HTMLElement, resultsMessage: HTMLElement) { function searchArticles(query: string, resultsList: HTMLElement, resultsMessage: HTMLElement) {
if (!query.trim()) { if (!query.trim()) {
@ -339,16 +351,18 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
// 过滤并排序结果 // 过滤并排序结果
const filteredArticles = articles const filteredArticles = articles
.filter(article => { .filter((article: Article) => {
const title = article.title.toLowerCase(); const title = article.title.toLowerCase();
const tags = article.tags ? article.tags.map(tag => tag.toLowerCase()) : []; const tags = article.tags ? article.tags.map((tag: string) => tag.toLowerCase()) : [];
const summary = article.summary ? article.summary.toLowerCase() : ''; const summary = article.summary ? article.summary.toLowerCase() : '';
const content = article.content ? article.content.toLowerCase() : '';
return title.includes(lowerQuery) || return title.includes(lowerQuery) ||
tags.some(tag => tag.includes(lowerQuery)) || tags.some((tag: string) => tag.includes(lowerQuery)) ||
summary.includes(lowerQuery); summary.includes(lowerQuery) ||
content.includes(lowerQuery);
}) })
.sort((a, b) => { .sort((a: Article, b: Article) => {
// 标题匹配优先 // 标题匹配优先
const aTitle = a.title.toLowerCase(); const aTitle = a.title.toLowerCase();
const bTitle = b.title.toLowerCase(); const bTitle = b.title.toLowerCase();
@ -360,6 +374,17 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
return 1; return 1;
} }
// 内容匹配次之
const aContent = a.content ? a.content.toLowerCase() : '';
const bContent = b.content ? b.content.toLowerCase() : '';
if (aContent.includes(lowerQuery) && !bContent.includes(lowerQuery)) {
return -1;
}
if (!aContent.includes(lowerQuery) && bContent.includes(lowerQuery)) {
return 1;
}
// 日期排序 // 日期排序
return new Date(b.date).getTime() - new Date(a.date).getTime(); return new Date(b.date).getTime() - new Date(a.date).getTime();
}) })
@ -374,14 +399,43 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
// 显示结果 // 显示结果
resultsMessage.style.display = 'none'; resultsMessage.style.display = 'none';
resultsList.innerHTML = filteredArticles.map(article => ` resultsList.innerHTML = filteredArticles.map((article: Article) => {
// 生成匹配的内容片段
let contentMatch = '';
if (article.content && article.content.toLowerCase().includes(lowerQuery)) {
// 找到匹配文本在内容中的位置
const matchIndex = article.content.toLowerCase().indexOf(lowerQuery);
// 计算片段的起始和结束位置
const startPos = Math.max(0, matchIndex - 50);
const endPos = Math.min(article.content.length, matchIndex + 100);
// 提取片段
let snippet = article.content.substring(startPos, endPos);
// 如果不是从文章开头开始,添加省略号
if (startPos > 0) {
snippet = '...' + snippet;
}
// 如果不是到文章结尾,添加省略号
if (endPos < article.content.length) {
snippet = snippet + '...';
}
// 高亮匹配的文本
snippet = highlightText(snippet, query);
contentMatch = `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">${snippet}</p>`;
}
// 高亮标题和摘要中的匹配文本
const highlightedTitle = highlightText(article.title, query);
const highlightedSummary = article.summary ? highlightText(article.summary, query) : '';
return `
<li> <li>
<a href="/articles/${article.id}" class="block px-4 py-3 hover:bg-gray-50/70 dark:hover:bg-gray-700/70 transition-colors duration-200"> <a href="/articles/${article.id}" class="block px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700/70 transition-colors duration-200">
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">${article.title}</h3> <h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">${highlightedTitle}</h3>
${article.summary ? `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">${article.summary}</p>` : ''} ${article.summary ? `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">${highlightedSummary}</p>` : ''}
${contentMatch}
${article.tags && article.tags.length > 0 ? ` ${article.tags && article.tags.length > 0 ? `
<div class="flex flex-wrap gap-1 mt-1.5"> <div class="flex flex-wrap gap-1 mt-1.5">
${article.tags.slice(0, 3).map(tag => ` ${article.tags.slice(0, 3).map((tag: string) => `
<span class="inline-block text-xs bg-primary-50/50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400 py-0.5 px-1.5 rounded-full">#${tag}</span> <span class="inline-block text-xs bg-primary-50/50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400 py-0.5 px-1.5 rounded-full">#${tag}</span>
`).join('')} `).join('')}
${article.tags.length > 3 ? `<span class="text-xs text-gray-400 dark:text-gray-500">+${article.tags.length - 3}</span>` : ''} ${article.tags.length > 3 ? `<span class="text-xs text-gray-400 dark:text-gray-500">+${article.tags.length - 3}</span>` : ''}
@ -389,7 +443,7 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
` : ''} ` : ''}
</a> </a>
</li> </li>
`).join(''); `}).join('');
} }
// 节流搜索 // 节流搜索

View File

@ -201,6 +201,20 @@ function greet(user: User): string {
--- ---
### 1.9 表情符号
| 表情名称 | 语法 | 效果 |
|:--------|:-----|:-----|
| 笑脸 | `:smile:` | :smile: |
| 大笑 | `:laughing:` | :laughing: |
| 哭泣 | `:cry:` | :cry: |
| 心形 | `:heart:` | :heart: |
| 火箭 | `:rocket:` | :rocket: |
| 星星 | `:star:` | :star: |
| 警告 | `:warning:` | :warning: |
| 检查标记 | `:white_check_mark:` | :white_check_mark: |
## 2. HTML/JSX 语法部分 ## 2. HTML/JSX 语法部分
### 2.1 HTML 标签 ### 2.1 HTML 标签
@ -241,50 +255,3 @@ function greet(user: User): string {
</details> </details>
### 2.2 React 组件使用
```mdx
import { Button } from './Button'
<Button variant="primary">
点击我
</Button>
```
### 2.3 导出和使用变量
```mdx
export const myVariable = "Hello MDX!"
# {myVariable}
```
### 2.4 使用 JavaScript 表达式
```mdx
{new Date().getFullYear()}
{['React', 'Vue', 'Angular'].map(framework => (
<li key={framework}>{framework}</li>
))}
```
### 2.5 组件属性传递
```mdx
<Card
title="我的卡片"
description="这是一个示例卡片"
image="path/to/image.jpg"
>
这里是卡片内容
</Card>
```
### 2.6 导入其他 MDX 文件
```mdx
import OtherContent from './other-content.mdx'
<OtherContent />
```

View File

@ -8,14 +8,28 @@ export async function GET() {
// 过滤掉草稿文章,并转换为简化的数据结构 // 过滤掉草稿文章,并转换为简化的数据结构
const formattedArticles = articles const formattedArticles = articles
.filter(article => !article.data.draft) // 过滤掉草稿 .filter(article => !article.data.draft) // 过滤掉草稿
.map(article => ({ .map(article => {
id: article.id, // 提取文章内容,去除 Markdown 标记
title: article.data.title, let contentText = '';
date: article.data.date, if (article.body) {
summary: article.data.summary || '', contentText = article.body
tags: article.data.tags || [], .replace(/---[\s\S]*?---/, '') // 移除 frontmatter
image: article.data.image || '' .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // 将链接转换为纯文本
})) .replace(/[#*`~>]/g, '') // 移除特殊字符
.replace(/\n+/g, ' ') // 将换行转换为空格
.trim();
}
return {
id: article.id,
title: article.data.title,
date: article.data.date,
summary: article.data.summary || '',
tags: article.data.tags || [],
image: article.data.image || '',
content: contentText // 添加文章内容
};
})
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // 按日期排序 .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // 按日期排序
return new Response(JSON.stringify(formattedArticles), { return new Response(JSON.stringify(formattedArticles), {

View File

@ -659,6 +659,25 @@
overflow: hidden; overflow: hidden;
} }
/* 添加 details 内部内容的内边距 */
.prose details > *:not(summary) {
padding: 1.5em;
margin: 0;
}
.prose details > p,
.prose details > ul,
.prose details > ol,
.prose details > div,
.prose details > pre {
margin-top: 0;
margin-bottom: 1em;
}
.prose details > *:last-child {
margin-bottom: 0;
}
.prose details summary { .prose details summary {
padding: 1em; padding: 1em;
cursor: pointer; cursor: pointer;

View File

@ -41,6 +41,14 @@
--color-dark-border: #475569; --color-dark-border: #475569;
--color-dark-text: #e2e8f0; --color-dark-text: #e2e8f0;
--color-dark-text-secondary: #94a3b8; --color-dark-text-secondary: #94a3b8;
/* 滚动条颜色变量 */
--scrollbar-track: #f1f5f9;
--scrollbar-thumb: #94a3b8;
--scrollbar-thumb-hover: #64748b;
--scrollbar-dark-track: #1e293b;
--scrollbar-dark-thumb: #475569;
--scrollbar-dark-thumb-hover: #64748b;
} }
/* 深色模式样式 */ /* 深色模式样式 */
@ -53,4 +61,63 @@
background-color: var(--bg-primary); background-color: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
}
/* 自定义滚动条样式 - 适用于所有浏览器 */
/* 滚动条基础样式 */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
/* 滚动条轨道 */
::-webkit-scrollbar-track {
background: var(--scrollbar-track);
border-radius: 8px;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 8px;
border: 2px solid var(--scrollbar-track);
transition: background-color 0.2s ease;
}
/* 滚动条滑块悬停 */
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
/* 滚动条角落 */
::-webkit-scrollbar-corner {
background: var(--scrollbar-track);
}
/* 深色模式滚动条样式 */
[data-theme='dark'] ::-webkit-scrollbar-track {
background: var(--scrollbar-dark-track);
}
[data-theme='dark'] ::-webkit-scrollbar-thumb {
background: var(--scrollbar-dark-thumb);
border: 2px solid var(--scrollbar-dark-track);
}
[data-theme='dark'] ::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-dark-thumb-hover);
}
[data-theme='dark'] ::-webkit-scrollbar-corner {
background: var(--scrollbar-dark-track);
}
/* Firefox 滚动条样式 */
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
[data-theme='dark'] * {
scrollbar-color: var(--scrollbar-dark-thumb) var(--scrollbar-dark-track);
} }

View File

@ -135,5 +135,3 @@ pre[class*="language-"] {
[data-theme="dark"] pre[class*="language-"] { [data-theme="dark"] pre[class*="language-"] {
background: #282c34 !important; background: #282c34 !important;
} }
/* 添加其他token的暗色模式样式... */