前端:更新依赖项以支持Tailwind CSS排版插件,更新配色,文章展示页排版,新增文章

This commit is contained in:
lsy 2024-12-05 22:46:29 +08:00
parent caa23c6ac5
commit 9eada9af3b
9 changed files with 440 additions and 53 deletions

View File

@ -6,13 +6,19 @@
:root {
--transition-duration: 150ms;
--transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
--hljs-theme: 'github';
}
/* 基础过渡效果 */
:root[class~="dark"] {
--hljs-theme: 'github-dark';
}
/* 确保 Radix UI 主题类包裹整个应用 */
.radix-themes {
transition:
background-color var(--transition-duration) var(--transition-easing),
color var(--transition-duration) var(--transition-easing);
min-height: 100%;
}
/* 基础布局样式 */
@ -21,3 +27,17 @@ body {
height: 100%;
}
/* 添加暗色模式支持 */
.radix-themes-dark {
@apply dark;
}
/* 隐藏不活跃的主题样式 */
[data-theme="light"] .hljs-dark {
display: none;
}
[data-theme="dark"] .hljs-light {
display: none;
}

View File

@ -3,6 +3,7 @@ import layout from "themes/echoes/layout";
import article from "themes/echoes/article";
import about from "themes/echoes/about";
import { useLocation } from "react-router-dom";
import post from "themes/echoes/post";
export default function Routes() {
const location = useLocation();
@ -11,26 +12,33 @@ export default function Routes() {
const args = {
title: "我的页面",
theme: "dark",
nav: '<a href="/">index</a><a href="error">error</a><a href="about">about</a>',
nav: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a>',
};
console.log(path);
path =path.split("/")[1];
path = path.split("/")[1];
if (path[1] === "error") {
if (path === "error") {
return layout.render({
children: ErrorPage.render(args),
args,
});
}
if (path[1] === "about") {
if (path === "about") {
return layout.render({
children: about.render(args),
args,
});
}
if (path === "post") {
return layout.render({
children: post.render(args),
args,
});
}
return layout.render({
children: article.render(args),
args,

View File

@ -200,6 +200,19 @@ interface ParticleImageProps {
onAnimationComplete?: () => void;
}
// 修改 BG_CONFIG添加尺寸配置
const BG_CONFIG = {
colors: {
from: 'rgb(10,37,77)',
via: 'rgb(8,27,57)',
to: 'rgb(2,8,23)'
},
className: 'bg-gradient-to-br from-[rgb(248,250,252)] via-[rgb(241,245,249)] to-[rgb(236,241,247)] dark:from-[rgb(10,37,77)] dark:via-[rgb(8,27,57)] dark:to-[rgb(2,8,23)]',
size: {
container: 'w-[120px] md:w-[140px] h-[120px] md:h-[140px]'
}
};
export const ParticleImage = ({
src,
status,
@ -230,7 +243,7 @@ export const ParticleImage = ({
// 更新渲染器大小
rendererRef.current.setSize(width, height);
// 只有当尺寸变化超阈值时才重生成粒子
// 只有当尺寸变化超<EFBFBD><EFBFBD><EFBFBD>阈值时才重生成粒子
const currentSize = Math.min(width, height);
const previousSize = sceneRef.current.userData.previousSize || currentSize;
const sizeChange = Math.abs(currentSize - previousSize) / previousSize;
@ -324,7 +337,7 @@ export const ParticleImage = ({
rendererRef.current = renderer;
containerRef.current.appendChild(renderer.domElement);
// 检查是否应该显示笑
// 检查是否应该显示笑
if (src === '') {
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
@ -408,7 +421,7 @@ export const ParticleImage = ({
};
}
// 建错误动<EFBFBD><EFBFBD>函数
// 建错误动函数
const showErrorAnimation = () => {
if (!scene) return;
@ -451,7 +464,7 @@ export const ParticleImage = ({
});
};
// 加载图<EFBFBD><EFBFBD><EFBFBD>
// 加载图
const img = new Image();
img.crossOrigin = 'anonymous';
@ -650,7 +663,7 @@ export const ParticleImage = ({
img.src = src || '';
// 画循环
// 画循环
const animate = () => {
if (renderer && scene && camera) {
renderer.render(scene, camera);
@ -771,16 +784,15 @@ export const ImageLoader = ({
}, [src, preloadImage]);
return (
<div className="relative w-[140px] md:w-[180px] h-[140px] md:h-[180px] shrink-0 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-[rgb(10,37,77)] via-[rgb(8,27,57)] to-[rgb(2,8,23)] rounded-lg overflow-hidden">
<div className={`relative ${BG_CONFIG.size.container} shrink-0 overflow-hidden`}>
<div className={`absolute inset-0 ${BG_CONFIG.className} rounded-lg overflow-hidden`}>
<ParticleImage
src={src}
status={status}
onLoad={() => {
// 粒子动画完成后,延迟显示图片
setTimeout(() => {
setShowImage(true);
}, 800); // 延迟时间可以根据需要调整
}, 800);
}}
onAnimationComplete={() => {
setShowImage(true);

View File

@ -5,7 +5,7 @@ export interface Post {
title?: string; // 标题
metaKeywords: string; // 元关键词
metaDescription: string; // 元描述
content: string; // 内容
content: string; // Markdown 格式的内容
status: string; // 状态
isEditor: boolean; // 是否为编辑器
draftContent?: string; // 草稿内容

View File

@ -18,7 +18,9 @@
"@remix-run/node": "^2.14.0",
"@remix-run/react": "^2.14.0",
"@remix-run/serve": "^2.14.0",
"@tailwindcss/typography": "^0.5.15",
"@types/axios": "^0.14.4",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/three": "^0.170.0",
"axios": "^1.7.7",
"bootstrap-icons": "^1.11.3",
@ -27,8 +29,12 @@
"gsap": "^3.12.5",
"html-react-parser": "^5.1.19",
"isbot": "^4.1.0",
"r": "^0.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1",
"rehype-raw": "^7.0.0",
"three": "^0.171.0"
},
"devDependencies": {

View File

@ -1,4 +1,5 @@
import type { Config } from "tailwindcss";
import typography from '@tailwindcss/typography';
export default {
content: [
@ -8,7 +9,7 @@ export default {
"./hooks/**/*.{js,jsx,ts,tsx}",
"./themes/**/*.{js,jsx,ts,tsx}",
],
darkMode: ["class", '[data-theme="dark"]'],
darkMode: 'class',
important: true,
theme: {
extend: {
@ -29,4 +30,7 @@ export default {
},
},
},
plugins: [
typography,
],
} satisfies Config;

View File

@ -149,40 +149,44 @@ export default new Template({}, ({ http, args }) => {
return (
<Container size="3" className="pt-2 pb-4 md:pb-6 relative">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 px-4 md:px-0">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 px-4 md:px-0">
{articleData.map((article) => (
<Card
key={article.id}
className="group cursor-pointer hover:shadow-lg transition-all duration-300 border border-[--gray-5] hover:border-[--accent-8] relative overflow-hidden"
>
<div className={`p-5 relative flex gap-4`}>
<div className="p-4 relative flex flex-col gap-4">
<div className="flex gap-4">
<ImageLoader
src={article.coverImage}
alt={article.title || ""}
className="group-hover:scale-105 transition-transform duration-500 relative z-[1] w-[140px] h-[140px] md:w-[180px] md:h-[180px] object-cover rounded-lg shrink-0"
className="group-hover:scale-105 transition-transform duration-500 relative z-[1] object-cover rounded-lg shrink-0"
/>
<div className="flex-1 flex flex-col min-w-0">
<div className="flex items-start justify-between gap-3 mb-2">
<div className="flex-1 min-w-0">
<Heading
size="3"
className="group-hover:text-[--accent-9] transition-colors duration-200 line-clamp-2 text-base md:text-lg flex-1"
className="group-hover:text-[--accent-9] transition-colors duration-200 line-clamp-2 text-base mb-2"
>
{article.title}
</Heading>
<Text className="text-[--gray-11] text-xs md:text-sm line-clamp-2 leading-relaxed">
{article.content}
</Text>
</div>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<Text
size="1"
className={`px-2 py-0.5 rounded-full shrink-0 ${article.categoryColor.bg} ${article.categoryColor.text}`}
className={`px-2 py-0.5 rounded-full font-medium ${article.categoryColor.bg} ${article.categoryColor.text}`}
>
{article.category}
</Text>
</div>
<Flex
gap="2"
align="center"
className="text-[--gray-11] mb-3 flex-wrap"
>
<Flex gap="2" align="center" className="text-[--gray-11]">
<CalendarIcon className="w-3 h-3" />
<Text size="1">
{article.publishedAt?.toLocaleDateString("zh-CN", {
@ -196,17 +200,14 @@ export default new Template({}, ({ http, args }) => {
{article.authorName}
</Text>
</Flex>
</div>
<Text className="text-[--gray-11] text-xs md:text-sm line-clamp-2 md:line-clamp-3 leading-relaxed">
{article.content}
</Text>
<Flex gap="2" className="mt-auto pt-3 flex-wrap">
<Flex gap="2" className="flex-wrap">
{article.tags.map((tag) => (
<Text
key={tag.name}
size="1"
className={`px-2 py-0.5 rounded-full ${tag.color.bg} ${tag.color.text}`}
className={`px-2 py-0.5 rounded-full border border-current ${tag.color.text} hover:bg-[--gray-a3] transition-colors`}
>
{tag.name}
</Text>

View File

@ -130,7 +130,7 @@ export default new Layout(({ children, args }) => {
<DropdownMenuPrimitive.Content
align="end"
sideOffset={10}
className="mt-3 p-1 min-w-[180px] rounded-md bg-[--color-panel] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
className="mt-3 p-1 min-w-[180px] rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
>
{loginState ? (
<>

View File

@ -0,0 +1,336 @@
import { Template } from "interface/template";
import ReactMarkdown from 'react-markdown';
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import {
Container,
Heading,
Text,
Flex,
Box,
Avatar,
Button,
Code,
ScrollArea,
Tabs,
Card,
} from "@radix-ui/themes";
import {
CalendarIcon,
HeartIcon,
ChatBubbleIcon,
Share1Icon,
BookmarkIcon,
EyeOpenIcon,
CodeIcon,
} from "@radix-ui/react-icons";
import { Post } from "interface/post";
import { useMemo, useState, useEffect } from "react";
import type { Components } from 'react-markdown';
// 示例文章数据
const mockPost: Post = {
id: 1,
title: "构建现代化的前端开发工作流",
content: `
#
##
- npmyarn pnpm
- Vitewebpack Rollup
- ESLintPrettier
- TypeScript
##
\`\`\`json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true
}
}
\`\`\`
##
GitHub Actions
-
-
-
`,
authorName: "张三",
publishedAt: new Date("2024-03-15"),
coverImage: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6",
metaKeywords: "前端开发,工作流,效率",
metaDescription: "探讨如何构现代的前端开发工作流,提高开发效率。",
status: "published",
isEditor: true,
createdAt: new Date("2024-03-15"),
updatedAt: new Date("2024-03-15"),
};
// 添加标题项接口
interface TocItem {
id: string;
text: string;
level: number;
}
// 在 TocItem 接口旁边添加
interface MarkdownCodeProps {
inline?: boolean;
className?: string;
children: React.ReactNode;
}
// 创建一个 React 组件
function PostContent() {
const [toc, setToc] = useState<TocItem[]>([]);
const [activeId, setActiveId] = useState<string>('');
// 解析文章内容生成目录
useEffect(() => {
const headings = mockPost.content.split('\n')
.filter(line => line.startsWith('#'))
.map(line => {
const match = line.match(/^#+/);
const level = match ? match[0].length : 1;
const text = line.replace(/^#+\s+/, '');
const id = text.toLowerCase().replace(/\s+/g, '-');
return { id, text, level };
});
setToc(headings);
}, [mockPost.content]);
// 监听滚动更新当前标题
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: '-80px 0px -80% 0px' }
);
document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((heading) => {
observer.observe(heading);
});
return () => observer.disconnect();
}, []);
return (
<Container className="py-16 px-4 md:px-8 lg:px-12 bg-[--gray-1]">
<Flex gap="20" className="relative max-w-6xl mx-auto">
{/* 左侧文章主体 */}
<Box className="flex-1 max-w-3xl">
{/* 文章头部 - 增加间距和样式 */}
<Box className="mb-16">
<Heading
size="8"
className="mb-8 leading-tight text-[--gray-12] font-bold tracking-tight"
>
{mockPost.title}
</Heading>
<Flex gap="4" align="center" className="text-[--gray-11]">
<Avatar
size="3"
fallback={mockPost.authorName[0]}
className="border-2 border-[--gray-a5]"
/>
<Text size="2" weight="medium">{mockPost.authorName}</Text>
<Text size="2">·</Text>
<Flex align="center" gap="2">
<CalendarIcon className="w-4 h-4" />
<Text size="2">
{mockPost.publishedAt?.toLocaleDateString("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
})}
</Text>
</Flex>
</Flex>
</Box>
{/* 修改封面图片样式 */}
{mockPost.coverImage && (
<Box className="mb-16 rounded-xl overflow-hidden aspect-[2/1] shadow-lg">
<img
src={mockPost.coverImage}
alt={mockPost.title}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-500"
/>
</Box>
)}
{/* 文章内容 - 优化排版和间距 */}
<Box className="prose prose-lg dark:prose-invert max-w-none">
<ReactMarkdown
components={{
h1: ({ children }) => {
const text = children?.toString() || '';
return (
<h1 id={text.toLowerCase().replace(/\s+/g, '-')} className="!text-[--gray-12]">
{children}
</h1>
);
},
h2: ({ children }) => {
const text = children?.toString() || '';
return (
<h2 id={text.toLowerCase().replace(/\s+/g, '-')} className="!text-[--gray-12]">
{children}
</h2>
);
},
code: ({ inline, className, children }: MarkdownCodeProps) => {
const match = /language-(\w+)/.exec(className || '');
const lang = match ? match[1] : '';
return inline ? (
<code>
{children}
</code>
) : (
<div className="group relative my-6">
{lang && (
<div className="absolute top-3 right-3 px-3 py-1 text-xs font-medium text-[--gray-11] bg-[--gray-3] border border-[--gray-a5] rounded-full">
{lang}
</div>
)}
<SyntaxHighlighter
language={lang || 'text'}
style={{
...oneDark,
'pre[class*="language-"]': {
background: 'transparent',
margin: 0,
padding: 0,
border: 'none',
boxShadow: 'none',
},
'code[class*="language-"]': {
background: 'transparent',
textShadow: 'none',
border: 'none',
boxShadow: 'none',
},
':not(pre) > code[class*="language-"]': {
background: 'transparent',
border: 'none',
boxShadow: 'none',
},
'pre[class*="language-"]::-moz-selection': {
background: 'transparent',
},
'pre[class*="language-"] ::-moz-selection': {
background: 'transparent',
},
'code[class*="language-"]::-moz-selection': {
background: 'transparent',
},
'code[class*="language-"] ::-moz-selection': {
background: 'transparent',
}
}}
customStyle={{
margin: 0,
padding: '1.5rem',
background: 'var(--gray-2)',
fontSize: '0.95rem',
lineHeight: '1.5',
border: '1px solid var(--gray-a5)',
borderRadius: '0.75rem',
}}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
</div>
);
}
} as Partial<Components>}
>
{mockPost.content}
</ReactMarkdown>
</Box>
{/* 优化文章底部标签样式 */}
<Flex gap="2" className="mt-16 pt-8 border-t border-[--gray-a5]">
{mockPost.metaKeywords.split(',').map((tag) => (
<Text
key={tag}
size="2"
className="px-4 py-1.5 rounded-full bg-[--gray-3] border border-[--gray-a5] text-[--gray-11] hover:text-[--gray-12] hover:bg-[--gray-4] transition-all cursor-pointer"
>
{tag.trim()}
</Text>
))}
</Flex>
</Box>
{/* 右侧目录 - 优化样式 */}
<Box className="w-72 hidden lg:block">
<Box className="sticky top-24 p-6 rounded-xl border border-[--gray-a5] bg-[--gray-2]">
<Text
size="2"
weight="medium"
className="mb-6 text-[--gray-12] flex items-center gap-2"
>
<CodeIcon className="w-4 h-4" />
</Text>
<ScrollArea className="h-[calc(100vh-250px)]">
<Box className="space-y-3 pr-4">
{toc.map((item) => (
<a
key={item.id}
href={`#${item.id}`}
className={`
block text-sm leading-relaxed transition-all
${item.level > 1 ? `ml-${(item.level - 1) * 4}` : ''}
${activeId === item.id
? 'text-[--accent-11] font-medium translate-x-1'
: 'text-[--gray-11] hover:text-[--gray-12]'
}
`}
onClick={(e) => {
e.preventDefault();
document.getElementById(item.id)?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}}
>
{item.text}
</a>
))}
</Box>
</ScrollArea>
</Box>
</Box>
</Flex>
</Container>
);
}
// 使用模板包装组件
export default new Template({}, ({ http, args }) => {
return <PostContent />;
});