增开滚动条样式,搜索内容,修复代码块解析html,表情周围有空格才会解析
This commit is contained in:
parent
e356212737
commit
a1a4d74493
108
astro.config.mjs
108
astro.config.mjs
@ -1,35 +1,35 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import react from '@astrojs/react';
|
||||
import remarkEmoji from 'remark-emoji';
|
||||
import rehypeExternalLinks from 'rehype-external-links';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { SITE_URL } from './src/consts';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import react from "@astrojs/react";
|
||||
import remarkEmoji from "remark-emoji";
|
||||
import rehypeExternalLinks from "rehype-external-links";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { SITE_URL } from "./src/consts";
|
||||
|
||||
import vercel from '@astrojs/vercel';
|
||||
import vercel from "@astrojs/vercel";
|
||||
|
||||
function getArticleDate(articleId) {
|
||||
try {
|
||||
// 处理多级目录的文章路径
|
||||
const mdPath = path.join(process.cwd(), 'src/content', articleId + '.md');
|
||||
const mdxPath = path.join(process.cwd(), 'src/content', articleId + '.mdx');
|
||||
const mdPath = path.join(process.cwd(), "src/content", articleId + ".md");
|
||||
const mdxPath = path.join(process.cwd(), "src/content", articleId + ".mdx");
|
||||
|
||||
let filePath = fs.existsSync(mdPath) ? mdPath : mdxPath;
|
||||
|
||||
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})/);
|
||||
if (match) {
|
||||
return new Date(match[1]).toISOString();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading article date:', error);
|
||||
console.error("Error reading article date:", error);
|
||||
}
|
||||
return new Date().toISOString(); // 如果没有日期,返回当前时间
|
||||
}
|
||||
@ -37,11 +37,11 @@ function getArticleDate(articleId) {
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: SITE_URL,
|
||||
output: 'static',
|
||||
trailingSlash: 'ignore',
|
||||
output: "static",
|
||||
trailingSlash: "ignore",
|
||||
|
||||
build: {
|
||||
format: 'directory'
|
||||
format: "directory",
|
||||
},
|
||||
|
||||
vite: {
|
||||
@ -52,54 +52,59 @@ export default defineConfig({
|
||||
// 手动分块配置
|
||||
manualChunks: {
|
||||
// 将地图组件单独打包
|
||||
'world-heatmap': ['./src/components/WorldHeatmap.tsx'],
|
||||
"world-heatmap": ["./src/components/WorldHeatmap.tsx"],
|
||||
// 将 React 相关库单独打包
|
||||
'react-vendor': ['react', 'react-dom'],
|
||||
"react-vendor": ["react", "react-dom"],
|
||||
// 其他大型依赖也可以单独打包
|
||||
'chart-vendor': ['chart.js'],
|
||||
"chart-vendor": ["chart.js"],
|
||||
// 将 ECharts 单独打包
|
||||
'echarts-vendor': ['echarts'],
|
||||
"echarts-vendor": ["echarts"],
|
||||
// 将其他组件打包到一起
|
||||
'components': ['./src/components']
|
||||
}
|
||||
}
|
||||
components: ["./src/components"],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 提高警告阈值,避免不必要的警告
|
||||
chunkSizeWarningLimit: 1000
|
||||
}
|
||||
chunkSizeWarningLimit: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
integrations: [
|
||||
// MDX 集成配置
|
||||
mdx({
|
||||
syntaxHighlight: 'prism',
|
||||
// 不使用共享的 markdown 配置
|
||||
extendMarkdownConfig: false,
|
||||
// 为 MDX 单独配置所需功能
|
||||
remarkPlugins: [
|
||||
[remarkEmoji, { emoticon: true }]
|
||||
// 添加表情符号支持
|
||||
[remarkEmoji, { emoticon: true, padded: true }]
|
||||
],
|
||||
rehypePlugins: [
|
||||
[rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }]
|
||||
],
|
||||
gfm: true,
|
||||
shikiConfig: {
|
||||
theme: 'github-dark',
|
||||
langs: [],
|
||||
wrap: true,
|
||||
}
|
||||
// 设置代码块处理行为
|
||||
remarkRehype: {
|
||||
allowDangerousHtml: false // 不解析 HTML
|
||||
},
|
||||
gfm: true
|
||||
}),
|
||||
react(),
|
||||
sitemap({
|
||||
filter: (page) => !page.includes('/api/'),
|
||||
filter: (page) => !page.includes("/api/"),
|
||||
serialize(item) {
|
||||
if (!item) return undefined;
|
||||
|
||||
// 文章页面
|
||||
if (item.url.includes('/articles/')) {
|
||||
if (item.url.includes("/articles/")) {
|
||||
// 从 URL 中提取文章 ID
|
||||
const articleId = item.url.replace(SITE_URL + '/articles/', '').replace(/\/$/, '');
|
||||
const articleId = item.url
|
||||
.replace(SITE_URL + "/articles/", "")
|
||||
.replace(/\/$/, "");
|
||||
const publishDate = getArticleDate(articleId);
|
||||
return {
|
||||
...item,
|
||||
priority: 0.8,
|
||||
lastmod: publishDate
|
||||
lastmod: publishDate,
|
||||
};
|
||||
}
|
||||
// 其他页面
|
||||
@ -107,39 +112,46 @@ export default defineConfig({
|
||||
let priority = 0.7; // 默认优先级
|
||||
|
||||
// 首页最高优先级
|
||||
if (item.url === SITE_URL + '/') {
|
||||
if (item.url === SITE_URL + "/") {
|
||||
priority = 1.0;
|
||||
}
|
||||
// 文章列表页次高优先级
|
||||
else if (item.url === SITE_URL + '/articles/') {
|
||||
else if (item.url === SITE_URL + "/articles/") {
|
||||
priority = 0.9;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
priority
|
||||
priority,
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
// Markdown 配置
|
||||
markdown: {
|
||||
syntaxHighlight: 'prism',
|
||||
remarkPlugins: [
|
||||
[remarkEmoji, { emoticon: true }]
|
||||
[remarkEmoji, { emoticon: true, padded: true }]
|
||||
],
|
||||
rehypePlugins: [
|
||||
[rehypeExternalLinks, { target: '_blank', rel: ['nofollow', 'noopener', 'noreferrer'] }]
|
||||
],
|
||||
gfm: true,
|
||||
// 设置 remark-rehype 选项,以控制HTML处理
|
||||
remarkRehype: {
|
||||
// 保留原始HTML格式,但仅在非代码块区域
|
||||
allowDangerousHtml: true,
|
||||
// 确保代码块内容不被解析
|
||||
passThrough: ['code']
|
||||
},
|
||||
shikiConfig: {
|
||||
theme: 'github-dark',
|
||||
theme: "github-dark",
|
||||
langs: [],
|
||||
wrap: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
adapter: vercel()
|
||||
adapter: vercel(),
|
||||
});
|
@ -43,7 +43,7 @@ const currentYear = new Date().getFullYear();
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<a
|
||||
href="/sitemap-index.xml"
|
||||
|
@ -303,6 +303,7 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
|
||||
summary?: string;
|
||||
tags?: string[];
|
||||
image?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!query.trim()) {
|
||||
@ -339,16 +351,18 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
|
||||
|
||||
// 过滤并排序结果
|
||||
const filteredArticles = articles
|
||||
.filter(article => {
|
||||
.filter((article: Article) => {
|
||||
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 content = article.content ? article.content.toLowerCase() : '';
|
||||
|
||||
return title.includes(lowerQuery) ||
|
||||
tags.some(tag => tag.includes(lowerQuery)) ||
|
||||
summary.includes(lowerQuery);
|
||||
tags.some((tag: string) => tag.includes(lowerQuery)) ||
|
||||
summary.includes(lowerQuery) ||
|
||||
content.includes(lowerQuery);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
.sort((a: Article, b: Article) => {
|
||||
// 标题匹配优先
|
||||
const aTitle = a.title.toLowerCase();
|
||||
const bTitle = b.title.toLowerCase();
|
||||
@ -360,6 +374,17 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
|
||||
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();
|
||||
})
|
||||
@ -374,14 +399,43 @@ const normalizedPath = currentPath.endsWith('/') ? currentPath.slice(0, -1) : cu
|
||||
|
||||
// 显示结果
|
||||
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>
|
||||
<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">
|
||||
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">${article.title}</h3>
|
||||
${article.summary ? `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">${article.summary}</p>` : ''}
|
||||
<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">${highlightedTitle}</h3>
|
||||
${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 ? `
|
||||
<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>
|
||||
`).join('')}
|
||||
${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>
|
||||
</li>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// 节流搜索
|
||||
|
@ -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.1 HTML 标签
|
||||
@ -241,50 +255,3 @@ function greet(user: User): string {
|
||||
|
||||
</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 />
|
||||
```
|
@ -8,14 +8,28 @@ export async function GET() {
|
||||
// 过滤掉草稿文章,并转换为简化的数据结构
|
||||
const formattedArticles = articles
|
||||
.filter(article => !article.data.draft) // 过滤掉草稿
|
||||
.map(article => ({
|
||||
.map(article => {
|
||||
// 提取文章内容,去除 Markdown 标记
|
||||
let contentText = '';
|
||||
if (article.body) {
|
||||
contentText = article.body
|
||||
.replace(/---[\s\S]*?---/, '') // 移除 frontmatter
|
||||
.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 || ''
|
||||
}))
|
||||
image: article.data.image || '',
|
||||
content: contentText // 添加文章内容
|
||||
};
|
||||
})
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // 按日期排序
|
||||
|
||||
return new Response(JSON.stringify(formattedArticles), {
|
||||
|
@ -659,6 +659,25 @@
|
||||
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 {
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
|
@ -41,6 +41,14 @@
|
||||
--color-dark-border: #475569;
|
||||
--color-dark-text: #e2e8f0;
|
||||
--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;
|
||||
}
|
||||
|
||||
/* 深色模式样式 */
|
||||
@ -54,3 +62,62 @@
|
||||
background-color: var(--bg-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);
|
||||
}
|
@ -135,5 +135,3 @@ pre[class*="language-"] {
|
||||
[data-theme="dark"] pre[class*="language-"] {
|
||||
background: #282c34 !important;
|
||||
}
|
||||
|
||||
/* 添加其他token的暗色模式样式... */
|
Loading…
Reference in New Issue
Block a user