前端:更新依赖项以支持Markdown解析和目录功能,优化Vite构建配置,重构文章展示逻辑,改进主题切换和样式,移除不再使用的加载组件,修复多个小问题。

This commit is contained in:
lsy 2024-12-08 00:55:12 +08:00
parent 2aaffb9e2b
commit b3c0af91b8
14 changed files with 1002 additions and 538 deletions

View File

@ -11,8 +11,6 @@ import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>,
<RemixBrowser />,
);
});

View File

@ -1,7 +1,7 @@
import React, { createContext, useState } from "react";
import React, { createContext, useEffect, useState } from "react";
import { DEFAULT_CONFIG } from "app/env";
import { HttpClient } from "core/http";
import { ThemeModeToggle } from "hooks/themeMode";
import { ThemeModeToggle } from "hooks/ThemeMode";
import {
Theme,
Button,
@ -13,8 +13,8 @@ import {
Box,
TextField,
} from "@radix-ui/themes";
import { toast } from "hooks/notification";
import { Echoes } from "hooks/echoes";
import { toast } from "hooks/Notification";
import { Echoes } from "hooks/Echoes";
interface SetupContextType {
currentStep: number;
@ -36,7 +36,7 @@ const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({
children,
}) => (
<Box style={{ width: "90%", maxWidth: "600px", margin: "0 auto" }}>
<Heading size="5" mb="4" weight="bold">
<Heading size="5" mb="4" weight="bold" style={{ userSelect: "none" }}>
{title}
</Heading>
<Flex direction="column" gap="4">
@ -237,15 +237,27 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
<div>
<Box mb="6">
<Text as="label" size="2" weight="medium" mb="2" className="block">
<Text color="red">*</Text>
</Text>
<Select.Root value={dbType} onValueChange={setDbType}>
<Select.Trigger />
<Select.Content>
<Select.Trigger className="w-full" />
<Select.Content position="popper" sideOffset={8}>
<Select.Group>
<Select.Item value="postgresql">PostgreSQL</Select.Item>
<Select.Item value="mysql">MySQL</Select.Item>
<Select.Item value="sqllite">SQLite</Select.Item>
<Select.Item value="postgresql">
<Flex gap="2" align="center">
<Text>PostgreSQL</Text>
</Flex>
</Select.Item>
<Select.Item value="mysql">
<Flex gap="2" align="center">
<Text>MySQL</Text>
</Flex>
</Select.Item>
<Select.Item value="sqllite">
<Flex gap="2" align="center">
<Text>SQLite</Text>
</Flex>
</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
@ -461,9 +473,12 @@ const SetupComplete: React.FC = () => (
);
export default function SetupPage() {
const [currentStep, setCurrentStep] = useState(() => {
return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1;
});
const [currentStep, setCurrentStep] = useState(1);
useEffect(() => {
// 在客户端组件挂载后更新状态
const initStatus = Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1;
setCurrentStep(initStatus);
}, []);
return (
<Theme

View File

@ -5,9 +5,9 @@ import {
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { NotificationProvider } from "hooks/notification";
import { NotificationProvider } from "hooks/Notification";
import { Theme } from "@radix-ui/themes";
import { ThemeScript } from "hooks/themeMode";
import { ThemeScript } from "hooks/ThemeMode";
import "~/index.css";
@ -49,6 +49,7 @@ export function Layout() {
<body
className="h-full"
suppressHydrationWarning={true}
data-cz-shortcut-listen="false"
>
<Theme
grayColor="slate"

View File

@ -1,46 +1,47 @@
import ErrorPage from "hooks/error";
import ErrorPage from "hooks/Error";
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";
import { memo, useCallback } from "react";
export default function Routes() {
const location = useLocation();
let path = location.pathname;
const args = {
const args = {
title: "我的页面",
theme: "dark",
nav: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a>',
};
} as const;
console.log(path);
path = path.split("/")[1];
if (path === "error") {
const renderLayout = (children: React.ReactNode) => {
return layout.render({
children: ErrorPage.render(args),
children,
args,
});
};
const Routes = memo(() => {
const location = useLocation();
const path = location.pathname.split("/")[1];
// 使用 useCallback 缓存渲染函数
const renderContent = useCallback((Component: any) => {
return renderLayout(Component.render(args));
}, []);
// 根据路径返回对应组件
if (path === "error") {
return renderContent(ErrorPage);
}
if (path === "about") {
return layout.render({
children: about.render(args),
args,
});
return renderContent(about);
}
if (path === "post") {
return layout.render({
children: post.render(args),
args,
});
return renderContent(post);
}
return layout.render({
children: article.render(args),
args,
});
}
return renderContent(article);
});
export default Routes;

View File

@ -224,7 +224,7 @@ const getOptimalImageParams = (width: number, height: number) => {
const pixelRatio = window.devicePixelRatio || 1;
const isMobile = window.innerWidth <= 768;
// 移动端使用更大的采样间隔来减少<EFBFBD><EFBFBD><EFBFBD>数量
// 移动端使用更大的采样间隔来减少数量
let samplingGap = isMobile
? Math.ceil(Math.max(width, height) / 60) // 移动端降低采样密度
: Math.ceil(Math.max(width, height) / 120); // 桌面端保持较高采密度
@ -394,15 +394,55 @@ export const ParticleImage = ({
// 清理场景资源
if (sceneRef.current) {
cleanupResources(sceneRef.current);
// 遍历场景中的所有对象
sceneRef.current.traverse((object) => {
if (object instanceof THREE.Points) {
const geometry = object.geometry;
const material = object.material as THREE.PointsMaterial;
// 清理几何体
if (geometry) {
// 清空缓冲区数据
if (geometry.attributes.position) {
geometry.attributes.position.array = new Float32Array(0);
}
if (geometry.attributes.color) {
geometry.attributes.color.array = new Float32Array(0);
}
// 移除所有属性
geometry.deleteAttribute('position');
geometry.deleteAttribute('color');
geometry.dispose();
}
// 清理材质
if (material) {
material.dispose();
}
}
});
// 清空场景
while(sceneRef.current.children.length > 0) {
sceneRef.current.remove(sceneRef.current.children[0]);
}
}
// 修改渲染器清理逻辑
if (rendererRef.current) {
const renderer = rendererRef.current;
const domElement = renderer.domElement;
// 使用 requestAnimationFrame 确保在一帧进<E5B8A7><E8BF9B> DOM 操作
// 确保在移除 DOM 元素前停止渲染
renderer.setAnimationLoop(null);
// 清理渲染器上下文
renderer.dispose();
renderer.forceContextLoss();
// 安全地移除 DOM 元素
const domElement = renderer.domElement;
if (containerRef.current?.contains(domElement)) {
requestAnimationFrame(() => {
if (isMountedRef.current && containerRef.current?.contains(domElement)) {
try {
@ -411,13 +451,17 @@ export const ParticleImage = ({
console.warn('清理渲染器DOM元素失败:', e);
}
}
renderer.dispose();
renderer.forceContextLoss();
});
}
// 清空引用
rendererRef.current = undefined;
}
// 清理相机引用
if (cameraRef.current) {
cameraRef.current = undefined;
}
}, []);
// 修改 useEffect 的清理
@ -1013,7 +1057,7 @@ export const ImageLoader = ({
});
};
// 确保src存在再设<EFBFBD><EFBFBD><EFBFBD>
// 确保src存在再设
if (src) {
img.src = src;
}

View File

@ -1,103 +0,0 @@
import React, { createContext, useState, useContext } from "react";
interface LoadingContextType {
isLoading: boolean;
showLoading: () => void;
hideLoading: () => void;
}
const LoadingContext = createContext<LoadingContextType>({
isLoading: false,
showLoading: () => {},
hideLoading: () => {},
});
export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [isLoading, setIsLoading] = useState(false);
const showLoading = () => setIsLoading(true);
const hideLoading = () => setIsLoading(false);
return (
<LoadingContext.Provider value={{ isLoading, showLoading, hideLoading }}>
{children}
{isLoading && (
<div className="fixed inset-0 flex flex-col items-center justify-center bg-black/25 dark:bg-black/40 z-[999999]">
<div className="loading-spinner mb-2" />
<div className="text-custom-p-light dark:text-custom-p-dark text-sm">
...
</div>
</div>
)}
<style>{`
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(59, 130, 246, 0.2);
border-radius: 50%;
border-top-color: #3B82F6;
animation: spin 0.8s linear infinite;
}
.dark .loading-spinner {
border: 3px solid rgba(96, 165, 250, 0.2);
border-top-color: #60A5FA;
}
`}</style>
</LoadingContext.Provider>
);
};
// 全局loading实例
let globalShowLoading: (() => void) | null = null;
let globalHideLoading: (() => void) | null = null;
export const LoadingContainer: React.FC = () => {
const { showLoading, hideLoading } = useContext(LoadingContext);
React.useEffect(() => {
globalShowLoading = showLoading;
globalHideLoading = hideLoading;
return () => {
globalShowLoading = null;
globalHideLoading = null;
};
}, [showLoading, hideLoading]);
return null;
};
// 导出loading方法
export const loading = {
show: () => {
if (!globalShowLoading) {
console.warn("Loading system not initialized");
return;
}
globalShowLoading();
},
hide: () => {
if (!globalHideLoading) {
console.warn("Loading system not initialized");
return;
}
globalHideLoading();
},
};

View File

@ -1,10 +1,17 @@
import { HttpClient } from "core/http";
import { CapabilityService } from "core/capability";
import { Serializable } from "interface/serializableType";
import { createElement, memo } from 'react';
export class Layout {
private http: HttpClient;
private capability: CapabilityService;
private readonly MemoizedElement: React.MemoExoticComponent<(props: {
children: React.ReactNode;
args?: Serializable;
onTouchStart?: (e: TouchEvent) => void;
onTouchEnd?: (e: TouchEvent) => void;
}) => React.ReactNode>;
constructor(
public element: (props: {
@ -20,6 +27,7 @@ export class Layout {
) {
this.http = services?.http || HttpClient.getInstance();
this.capability = services?.capability || CapabilityService.getInstance();
this.MemoizedElement = memo(element);
}
render(props: {
@ -28,7 +36,7 @@ export class Layout {
onTouchStart?: (e: TouchEvent) => void;
onTouchEnd?: (e: TouchEvent) => void;
}) {
return this.element({
return createElement(this.MemoizedElement, {
...props,
onTouchStart: props.onTouchStart,
onTouchEnd: props.onTouchEnd

View File

@ -29,12 +29,16 @@
"gsap": "^3.12.5",
"html-react-parser": "^5.1.19",
"isbot": "^4.1.0",
"markdown-it": "^14.1.0",
"markdown-it-toc-done-right": "^4.2.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",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"three": "^0.171.0"
},
"devDependencies": {
@ -42,6 +46,7 @@
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/lodash": "^4.17.13",
"@types/markdown-it": "^14.1.2",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.7.4",

View File

@ -8,7 +8,7 @@ import {
} from "@radix-ui/react-icons";
import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap";
import { ParticleImage } from "hooks/particleImage";
import { ImageLoader } from "hooks/ParticleImage";
const socialLinks = [
{
@ -93,7 +93,11 @@ export default new Template({}, ({ http, args }) => {
<Flex direction="column" align="center" className="text-center mb-16">
<Box className="w-40 h-40 mb-8 relative">
<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-full overflow-hidden">
<ParticleImage src="/path/to/your/avatar.jpg" />
<ImageLoader
src="/images/avatar-placeholder.png"
alt="avatar"
className="w-full h-full"
/>
</div>
</Box>

View File

@ -9,7 +9,7 @@ import {
import { Post, PostDisplay, Tag } from "interface/fields";
import { useMemo } from "react";
import { ImageLoader } from "hooks/particleImage";
import { ImageLoader } from "hooks/ParticleImage";
import { getColorScheme, hashString } from "themes/echoes/utils/colorScheme";
// 修改模拟文章列表数据
@ -20,7 +20,7 @@ const mockArticles: PostDisplay[] = [
content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...",
authorName: "张三",
publishedAt: new Date("2024-03-15"),
coverImage: "",
coverImage: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",
@ -42,7 +42,7 @@ const mockArticles: PostDisplay[] = [
content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
authorName: "李四",
publishedAt: new Date("2024-03-14"),
coverImage: "https://haowallpaper.com/link/common/file/previewFileIm",
coverImage: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",
@ -63,7 +63,7 @@ const mockArticles: PostDisplay[] = [
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
authorName: "王五",
publishedAt: new Date("2024-03-13"),
coverImage: "https://haowallpaper.com/link/common/file/previewFileImg/15789130517090624",
coverImage: "https://images.unsplash.com/photo-1592609931095-54a2168ae893?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",
@ -84,7 +84,7 @@ const mockArticles: PostDisplay[] = [
content: "移动端开发中的各种适配问题及解决方案...",
authorName: "田六",
publishedAt: new Date("2024-03-13"),
coverImage: "https://avatars.githubusercontent.com/u/2?v=4",
coverImage: "https://images.unsplash.com/photo-1526498460520-4c246339dccb?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",
@ -105,7 +105,7 @@ const mockArticles: PostDisplay[] = [
content: "本文将深入探讨现代全栈开发的各个方面,包括前端框架选择、后端架构设计、数据库优化、微服务部署以及云原生实践...",
authorName: "赵七",
publishedAt: new Date("2024-03-12"),
coverImage: "https://avatars.githubusercontent.com/u/3?v=4",
coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",
@ -136,7 +136,7 @@ const mockArticles: PostDisplay[] = [
content: "探索 TypeScript 的高级类型系统、装饰器、类型编程等特性,以及在大型项目中的最佳实践...",
authorName: "孙八",
publishedAt: new Date("2024-03-11"),
coverImage: "https://avatars.githubusercontent.com/u/4?v=4",
coverImage: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",
@ -160,7 +160,7 @@ const mockArticles: PostDisplay[] = [
content: "全面解析 Web 性能优化策略,包括资源加载优化、渲染性能优化、网络优化等多个维度...",
authorName: "周九",
publishedAt: new Date("2024-03-10"),
coverImage: "https://avatars.githubusercontent.com/u/5?v=4",
coverImage: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",
@ -173,7 +173,7 @@ const mockArticles: PostDisplay[] = [
],
tags: [
{ name: "性能监控" },
{ name: "懒加" },
{ name: "懒加<EFBFBD><EFBFBD><EFBFBD>" },
{ name: "缓存策略" },
{ name: "代码分割" }
]
@ -184,7 +184,7 @@ const mockArticles: PostDisplay[] = [
content: "详细介绍微前端的架构设计、实现方案、应用集成以及实际项目中的经验总结...",
authorName: "吴十",
publishedAt: new Date("2024-03-09"),
coverImage: "https://avatars.githubusercontent.com/u/6?v=4",
coverImage: "https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=500&auto=format",
metaKeywords: "",
metaDescription: "",
status: "published",

View File

@ -1,6 +1,6 @@
import { Layout } from "interface/layout";
import { ThemeModeToggle } from "hooks/themeMode";
import { Echoes } from "hooks/echoes";
import { ThemeModeToggle } from "hooks/ThemeMode";
import { Echoes } from "hooks/Echoes";
import { Container, Flex, Box, Link, TextField, Button } from "@radix-ui/themes";
import {
MagnifyingGlassIcon,
@ -187,8 +187,8 @@ export default new Layout(({ children, args }) => {
<Box
className={`w-10 h-10 flex items-center justify-center ${
scrollProgress > 0
? 'relative translate-x-0 opacity-100 transition-all duration-300 ease-out'
: 'pointer-events-none absolute translate-x-2 opacity-0 transition-all duration-300 ease-in'
? 'block'
: 'hidden'
}`}
>
<Button
@ -226,8 +226,8 @@ export default new Layout(({ children, args }) => {
<Box
className={`w-10 h-10 flex items-center justify-center ${
scrollProgress > 0
? 'relative translate-x-0 opacity-100 transition-all duration-300 ease-out'
: 'pointer-events-none absolute translate-x-2 opacity-0 transition-all duration-300 ease-in'
? 'block'
: 'hidden'
}`}
>
<Button
@ -275,27 +275,63 @@ export default new Layout(({ children, args }) => {
<DropdownMenuPrimitive.Content
align="end"
sideOffset={5}
className="mt-2 p-3 min-w-[280px] rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
className="mt-2 min-w-[280px] rounded-md bg-[--gray-1] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
>
<Box className="flex flex-col gap-2 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:text-[--accent-9]">
<Box className="flex flex-col">
{/* 导航链接区域 */}
<Box className="flex flex-col">
<Box className="flex flex-col [&>a]:px-4 [&>a]:py-2.5 [&>a]:text-[--gray-12] [&>a]:transition-colors [&>a:hover]:bg-[--gray-a3] [&>a]:text-lg [&>a]:text-center [&>a]:border-b [&>a]:border-[--gray-a5] [&>a:first-child]:rounded-t-md [&>a:last-child]:border-b-0">
{parse(navString)}
</Box>
<Box className="mt-3 pt-3 border-t border-[--gray-a5]">
</Box>
{/* 搜索框区域 */}
<Box className="p-4 border-t border-[--gray-a5]">
<TextField.Root
size="2"
variant="surface"
placeholder="搜索..."
className="w-full [&_input]:pl-3"
id="search"
className="w-full [&_input]:pl-3 hover:border-[--accent-9] border transition-colors group"
>
<TextField.Slot
side="right"
className="p-2"
>
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-a12]" />
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-11] transition-colors group-hover:text-[--accent-9]" />
</TextField.Slot>
</TextField.Root>
</Box>
{/* 用户操作区域 */}
<Box className="p-4 border-t border-[--gray-a5]">
<Flex gap="3" align="center">
{/* 用户信息/登录按钮 - 占据 55% 宽度 */}
<Box className="w-[55%]">
<Button
variant="ghost"
className="w-full justify-start gap-2 text-[--gray-12] hover:text-[--accent-9] hover:bg-[--gray-a3] transition-colors"
>
{loginState ? (
<>
<AvatarIcon className="w-5 h-5" />
<span></span>
</>
) : (
<>
<PersonIcon className="w-5 h-5" />
<span>/</span>
</>
)}
</Button>
</Box>
{/* 主题切换按钮 - 占据剩余空间 */}
<Box className="flex-1 flex justify-end [&_button]:w-10 [&_button]:h-10 [&_svg]:w-5 [&_svg]:h-5 [&_button]:text-[--gray-12] [&_button:hover]:text-[--accent-9]">
<ThemeModeToggle />
</Box>
</Flex>
</Box>
</Box>
</DropdownMenuPrimitive.Content>
</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>

File diff suppressed because it is too large Load Diff

View File

@ -11,18 +11,26 @@ export function hashString(str: string): number {
export function getColorScheme(name: string) {
const colorSchemes = [
'amber', 'blue', 'crimson', 'cyan', 'grass',
'green', 'indigo', 'orange', 'pink', 'purple'
{ name: 'amber', bg: 'bg-[--amber-a3]', text: 'text-[--amber-11]', border: 'border-[--amber-a6]' },
{ name: 'blue', bg: 'bg-[--blue-a3]', text: 'text-[--blue-11]', border: 'border-[--blue-a6]' },
{ name: 'crimson', bg: 'bg-[--crimson-a3]', text: 'text-[--crimson-11]', border: 'border-[--crimson-a6]' },
{ name: 'cyan', bg: 'bg-[--cyan-a3]', text: 'text-[--cyan-11]', border: 'border-[--cyan-a6]' },
{ name: 'grass', bg: 'bg-[--grass-a3]', text: 'text-[--grass-11]', border: 'border-[--grass-a6]' },
{ name: 'mint', bg: 'bg-[--mint-a3]', text: 'text-[--mint-11]', border: 'border-[--mint-a6]' },
{ name: 'orange', bg: 'bg-[--orange-a3]', text: 'text-[--orange-11]', border: 'border-[--orange-a6]' },
{ name: 'pink', bg: 'bg-[--pink-a3]', text: 'text-[--pink-11]', border: 'border-[--pink-a6]' },
{ name: 'plum', bg: 'bg-[--plum-a3]', text: 'text-[--plum-11]', border: 'border-[--plum-a6]' },
{ name: 'violet', bg: 'bg-[--violet-a3]', text: 'text-[--violet-11]', border: 'border-[--violet-a6]' }
];
const index = hashString(name) % colorSchemes.length;
const color = colorSchemes[index];
const scheme = colorSchemes[index];
return {
bg: `bg-[--${color}-4]`,
text: `text-[--${color}-11]`,
border: `border-[--${color}-6]`,
hover: `hover:bg-[--${color}-5]`,
bg: scheme.bg,
text: scheme.text,
border: scheme.border,
hover: `hover:${scheme.bg.replace('3', '4')}`,
dot: `bg-current`
};
}

View File

@ -87,5 +87,17 @@ export default defineConfig(async ({ mode }) => {
},
publicDir: resolve(__dirname, "public"),
envPrefix: "VITE_",
build: {
rollupOptions: {
output: {
manualChunks: {
three: ['three'],
gsap: ['gsap']
}
}
},
// 优化大型依赖的处理
chunkSizeWarningLimit: 1000
}
};
});