From b29fe30c71e5f7347a4730539ee885a067b95b31 Mon Sep 17 00:00:00 2001 From: lsy Date: Thu, 5 Dec 2024 02:13:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E6=96=87=E7=AB=A0=E5=B1=95=E7=A4=BA=EF=BC=8C=E5=92=8C?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=9B=BE=E7=89=87=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/routes.tsx | 17 +- frontend/hooks/ParticleImage.tsx | 581 +++++++++++++++++++++++++ frontend/interface/post.ts | 15 + frontend/interface/template.ts | 38 +- frontend/interface/theme.ts | 15 +- frontend/package.json | 6 +- frontend/tailwind.config.ts | 3 + frontend/themes/echoes/about.tsx | 156 +++++++ frontend/themes/echoes/article.tsx | 215 ++++++++- frontend/themes/echoes/layout.tsx | 27 +- frontend/themes/echoes/theme.config.ts | 18 +- 11 files changed, 1036 insertions(+), 55 deletions(-) create mode 100644 frontend/hooks/ParticleImage.tsx create mode 100644 frontend/interface/post.ts create mode 100644 frontend/themes/echoes/about.tsx diff --git a/frontend/app/routes.tsx b/frontend/app/routes.tsx index 2066b72..f29a39a 100644 --- a/frontend/app/routes.tsx +++ b/frontend/app/routes.tsx @@ -1,12 +1,17 @@ import ErrorPage from "hooks/error"; import layout from "themes/echoes/layout"; +import article from "themes/echoes/article"; +import about from "themes/echoes/about"; export default function Routes() { - return layout.element({ - children: <>, - args: { - title: "我的页面", - theme: "dark", - }, + const args = { + title: "我的页面", + theme: "dark", + nav: 'a' + }; + + return layout.render({ + children: article.render(args), + args, }); } diff --git a/frontend/hooks/ParticleImage.tsx b/frontend/hooks/ParticleImage.tsx new file mode 100644 index 0000000..4260e7b --- /dev/null +++ b/frontend/hooks/ParticleImage.tsx @@ -0,0 +1,581 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import * as THREE from 'three'; +import { gsap } from 'gsap'; + +interface Particle { + x: number; + y: number; + z: number; + originalX: number; + originalY: number; + originalColor: THREE.Color; + delay: number; +} + +const createErrorParticles = (width: number, height: number) => { + const particles: Particle[] = []; + const positionArray: number[] = []; + const colorArray: number[] = []; + + const errorColor = new THREE.Color(0.8, 0, 0); // 更深的红色 + + // X 形状的两条线 + const lines = [ + // 左上到右下的线 + { start: [-1, 1], end: [1, -1] }, + // 右上到左下的线 + { start: [1, 1], end: [-1, -1] } + ]; + + // 每条线上的粒子数量 + const particlesPerLine = 50; + + lines.forEach(line => { + for (let i = 0; i < particlesPerLine; i++) { + const t = i / (particlesPerLine - 1); + const x = line.start[0] + (line.end[0] - line.start[0]) * t; + const y = line.start[1] + (line.end[1] - line.start[1]) * t; + + // 添加一些随机偏移 + const randomOffset = 0.1; + const randomX = x + (Math.random() - 0.5) * randomOffset; + const randomY = y + (Math.random() - 0.5) * randomOffset; + + // 缩放到适合容器的大小 + const scaledX = randomX * (width * 0.3); + const scaledY = randomY * (height * 0.3); + + particles.push({ + x: scaledX, + y: scaledY, + z: 0, + originalX: scaledX, + originalY: scaledY, + originalColor: errorColor, + delay: 0 + }); + + // 随机初始位置 + positionArray.push( + (Math.random() - 0.5) * width * 2, + (Math.random() - 0.5) * height * 2, + (Math.random() - 0.5) * 100 + ); + + // 随机初始颜色 + colorArray.push(errorColor.r, errorColor.g, errorColor.b); + } + }); + + return { particles, positionArray, colorArray }; +}; + +// 添加笑脸粒子生成函数 +const createSmileParticles = (width: number, height: number) => { + const particles: Particle[] = []; + const positionArray: number[] = []; + const colorArray: number[] = []; + + // 调整笑脸参数 + const radius = Math.min(width, height) * 0.35; // 脸部大小 + const particlesCount = 400; // 轮廓粒子数量 + + // 修改颜色为更深的金色 + const particleColor = new THREE.Color(0.8, 0.6, 0); // 更深的金色 + + // 创建圆形脸部轮廓 + for (let i = 0; i < particlesCount / 2; i++) { + const angle = (i / (particlesCount / 2)) * Math.PI * 2; + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + + particles.push({ + x, y, z: 0, + originalX: x, + originalY: y, + originalColor: particleColor, + delay: 0 + }); + + positionArray.push( + (Math.random() - 0.5) * width * 2, + (Math.random() - 0.5) * height * 2, + (Math.random() - 0.5) * 100 + ); + colorArray.push(particleColor.r, particleColor.g, particleColor.b); + } + + // 眼睛参数 + const eyeOffset = radius * 0.2; // 眼睛水平间距 + const eyeY = radius * 0.2; // 眼睛垂直位置 + const eyeSize = radius * 0.08; // 眼睛大小 + + // 创建实心眼睛 + [-1, 1].forEach(side => { + // 创建密集的点来填充眼睛 + for (let i = 0; i < 30; i++) { + const r = Math.random() * eyeSize; // 随机半径 + const angle = Math.random() * Math.PI * 2; // 随机角度 + const x = side * eyeOffset + Math.cos(angle) * r; + const y = eyeY + Math.sin(angle) * r; + + particles.push({ + x, y, z: 0, + originalX: x, + originalY: y, + originalColor: particleColor, + delay: 0 + }); + + positionArray.push( + (Math.random() - 0.5) * width * 2, + (Math.random() - 0.5) * height * 2, + (Math.random() - 0.5) * 100 + ); + colorArray.push(particleColor.r, particleColor.g, particleColor.b); + } + }); + + // 嘴巴参数 + const smileWidth = radius * 0.5; // 嘴巴宽度 + const smileY = -radius * 0.3; // 将嘴巴位置向下移动更多 + const smilePoints = 40; // 嘴巴粒子数量 + + // 创建微笑 + for (let i = 0; i < smilePoints; i++) { + const t = i / (smilePoints - 1); + const x = (t * 2 - 1) * smileWidth; + + // 简单的抛物线,向上弯曲的笑脸 + const y = smileY + (Math.pow(x / smileWidth, 2) * radius * 0.2); + + particles.push({ + x, y, z: 0, + originalX: x, + originalY: y, + originalColor: particleColor, + delay: 0 + }); + + positionArray.push( + (Math.random() - 0.5) * width * 2, + (Math.random() - 0.5) * height * 2, + (Math.random() - 0.5) * 100 + ); + colorArray.push(particleColor.r, particleColor.g, particleColor.b); + } + + return { particles, positionArray, colorArray }; +}; + +// 在文件开头添加新的 helper 函数 +const easeOutCubic = (t: number) => { + return 1 - Math.pow(1 - t, 3); +}; + +const customEase = (t: number) => { + return t < 0.5 + ? 4 * t * t * t + : 1 - Math.pow(-2 * t + 2, 3) / 2; +}; + +export const ParticleImage = ({ src, onLoad, onError }: { + src?: string; + onLoad?: () => void; + onError?: () => void; +}) => { + const containerRef = useRef(null); + const sceneRef = useRef(); + const cameraRef = useRef(); + const rendererRef = useRef(); + const animationFrameRef = useRef(); + + // 添加 resize 处理函数 + const handleResize = useCallback(() => { + if (!containerRef.current || !cameraRef.current || !rendererRef.current) return; + + const width = containerRef.current.offsetWidth; + const height = containerRef.current.offsetHeight; + + const camera = cameraRef.current; + camera.left = width / -2.1; + camera.right = width / 2.1; + camera.top = height / 2.1; + camera.bottom = height / -2.1; + camera.updateProjectionMatrix(); + + rendererRef.current.setSize(width, height); + }, []); + + useEffect(() => { + if (!containerRef.current) return; + console.log('Current src:', src); + + const width = containerRef.current.offsetWidth; + const height = containerRef.current.offsetHeight; + + const scene = new THREE.Scene(); + sceneRef.current = scene; + + const camera = new THREE.OrthographicCamera( + width / -1.5, // 扩大视野范围,从 -2 改为 -1.5 + width / 1.5, // 扩大视野范围,从 2 改为 1.5 + height / 1.5, // 扩大视野范围 + height / -1.5, // 扩大视野范围 + 1, + 1000 + ); + camera.position.z = 100; + cameraRef.current = camera; + + const renderer = new THREE.WebGLRenderer({ + alpha: true, + antialias: true + }); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(width, height); + rendererRef.current = renderer; + containerRef.current.appendChild(renderer.domElement); + + const geometry = new THREE.BufferGeometry(); + const material = new THREE.PointsMaterial({ + size: 1.2, + vertexColors: true, + transparent: true, + opacity: 1, + sizeAttenuation: true, + blending: THREE.AdditiveBlending, + depthWrite: false, + depthTest: false + }); + + // 检查是否应该显示笑脸 + if (src === '') { + console.log('Showing smile animation'); + const { particles, positionArray, colorArray } = createSmileParticles(width, height); + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); + + const points = new THREE.Points(geometry, material); + scene.add(points); + + const positionAttribute = geometry.attributes.position; + + particles.forEach((particle, i) => { + const i3 = i * 3; + gsap.to(positionAttribute.array, { + duration: 1, + delay: Math.random() * 0.3, + [i3]: particle.originalX, + [i3 + 1]: particle.originalY, + [i3 + 2]: 0, + ease: "back.out(1.7)", + onUpdate: () => { + positionAttribute.needsUpdate = true; + } + }); + }); + + // 启动动画循环 + const animate = () => { + if (renderer && scene && camera) { + renderer.render(scene, camera); + } + animationFrameRef.current = requestAnimationFrame(animate); + }; + animate(); + + return; + } + + // 创建错误动画函数 + const showErrorAnimation = () => { + if (!scene) return; + + const { particles, positionArray, colorArray } = createErrorParticles(width, height); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); + + const points = new THREE.Points(geometry, material); + scene.clear(); // 清除现有内容 + scene.add(points); + + const positionAttribute = geometry.attributes.position; + + particles.forEach((particle, i) => { + const i3 = i * 3; + gsap.to(positionAttribute.array, { + duration: 0.6, + delay: Math.random() * 0.2, + [i3]: particle.originalX, + [i3 + 1]: particle.originalY, + [i3 + 2]: 0, + ease: "back.out(1.7)", + onUpdate: () => { + positionAttribute.needsUpdate = true; + } + }); + }); + + onError?.(); + }; + + // 加载图片 + const img = new Image(); + img.crossOrigin = 'anonymous'; + + const timeoutId = setTimeout(() => { + showErrorAnimation(); + }, 5000); // 5秒超时 + + img.onload = () => { + clearTimeout(timeoutId); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (ctx) { + canvas.width = width; + canvas.height = height; + + // 计算图片绘制尺寸和位置,确保不会超出容器 + const imgAspect = img.width / img.height; + const containerAspect = width / height; + + let drawWidth = width; + let drawHeight = height; + let offsetX = 0; + let offsetY = 0; + + if (imgAspect > containerAspect) { + // 图片较宽,以容器宽度为准,确保高度不超出 + drawWidth = width; + drawHeight = width / imgAspect; + offsetY = (height - drawHeight) / 2; + } else { + // 图片较高,以容器高度为准,确保宽度不超出 + drawHeight = height; + drawWidth = height * imgAspect; + offsetX = (width - drawWidth) / 2; + } + + // 绘制图片 + ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); + const imageData = ctx.getImageData(0, 0, width, height); + + const particles: Particle[] = []; + const positionArray = []; + const colorArray = []; + const samplingGap = Math.ceil(Math.max(width, height) / 100); // 动态采样间隔,确保粒子数量适中 + + // 采样图片像素 + for (let y = 0; y < height; y += samplingGap) { + for (let x = 0; x < width; x += samplingGap) { + const i = (y * width + x) * 4; + const r = imageData.data[i] / 255; + const g = imageData.data[i + 1] / 255; + const b = imageData.data[i + 2] / 255; + const a = imageData.data[i + 3] / 255; + + if (a > 0.3) { + // 计算距离中心的距离,用于动画延迟 + const distanceToCenter = Math.sqrt( + Math.pow(x - width/2, 2) + + Math.pow(y - height/2, 2) + ); + const maxDistance = Math.sqrt(Math.pow(width/2, 2) + Math.pow(height/2, 2)); + const normalizedDistance = distanceToCenter / maxDistance; + + const px = x - width / 2; + const py = height / 2 - y; + + particles.push({ + x: px, + y: py, + z: 0, + originalX: px, + originalY: py, + originalColor: new THREE.Color(r, g, b), + delay: normalizedDistance * 0.3 // 基于距离的延迟 + }); + + // 随机初始位置(根据距离调整范围) + const spread = 1 - normalizedDistance * 0.5; // 距离越远,初始扩散越小 + positionArray.push( + (Math.random() - 0.5) * width * spread, + (Math.random() - 0.5) * height * spread, + (Math.random() - 0.5) * 50 * spread + ); + + colorArray.push(r, g, b); + } + } + } + + scene.clear(); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); + + const points = new THREE.Points(geometry, material); + scene.add(points); + + // 动画 + const positionAttribute = geometry.attributes.position; + const colorAttribute = geometry.attributes.color; + + let completedAnimations = 0; + const totalAnimations = particles.length * 2; // 位置和颜色动画 + + const checkComplete = () => { + completedAnimations++; + if (completedAnimations === totalAnimations) { + onLoad?.(); // 所有画完成后调用 onLoad + } + }; + + particles.forEach((particle, i) => { + const i3 = i * 3; + + // 位置动画 + gsap.to(positionAttribute.array, { + duration: 1.2 + Math.random() * 0.3, // 减少随机性范围 + delay: particle.delay, // 使用基于距离的延迟 + [i3]: particle.originalX, + [i3 + 1]: particle.originalY, + [i3 + 2]: 0, + ease: customEase, + onUpdate: () => { + positionAttribute.needsUpdate = true; + }, + onComplete: checkComplete + }); + + // 颜色动画 + gsap.to(colorAttribute.array, { + duration: 1, + delay: particle.delay + 0.2, // 稍微延迟颜色变化 + [i3]: particle.originalColor.r, + [i3 + 1]: particle.originalColor.g, + [i3 + 2]: particle.originalColor.b, + ease: "power2.inOut", + onUpdate: () => { + colorAttribute.needsUpdate = true; + }, + onComplete: checkComplete + }); + }); + + // 修改动画序列部分 + const timeline = gsap.timeline({ + defaults: { ease: "power2.inOut" } + }); + + const imgElement = document.querySelector(`img[src="${src}"]`) as HTMLImageElement; + if (imgElement) { + // 设置初始状态 + gsap.set(imgElement, { opacity: 0 }); + + timeline + .to(imgElement, { + opacity: 1, + duration: 0.8, + delay: 1.6 + }) + .to(material, { + opacity: 0, + duration: 0.8 + }, "-=0.6"); // 提前开始消失 + } + } + }; + + img.onerror = () => { + clearTimeout(timeoutId); + showErrorAnimation(); + }; + + img.src = src || ''; + + // 动画循环 + const animate = () => { + if (renderer && scene && camera) { + renderer.render(scene, camera); + } + animationFrameRef.current = requestAnimationFrame(animate); + }; + animate(); + + // 添加 resize 监听 + const resizeObserver = new ResizeObserver(handleResize); + resizeObserver.observe(containerRef.current); + + // 添加窗口 resize 监听 + window.addEventListener('resize', handleResize); + + return () => { + clearTimeout(timeoutId); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + if (renderer && containerRef.current) { + containerRef.current.removeChild(renderer.domElement); + renderer.dispose(); + } + // 清除所有 GSAP 动画 + gsap.killTweensOf(geometry.attributes.position?.array); + + // 移除 resize 监听 + if (containerRef.current) { + resizeObserver.unobserve(containerRef.current); + } + window.removeEventListener('resize', handleResize); + }; + }, [src, onError, handleResize]); + + return
; +}; + +// 图片加载组件 +export const ImageLoader = ({ src, alt, className }: { + src?: string; + alt: string; + className: string; +}) => { + const [isLoading, setIsLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + return ( +
+
+ setIsLoading(false)} + onError={() => { + setIsLoading(false); + setHasError(true); + }} + /> +
+ {!hasError && ( +
+ {alt} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/interface/post.ts b/frontend/interface/post.ts new file mode 100644 index 0000000..e646314 --- /dev/null +++ b/frontend/interface/post.ts @@ -0,0 +1,15 @@ +export interface Post { + id: number; // 自增整数 + authorName: string; // 作者名称 + coverImage?: string; // 封面图片 + title?: string; // 标题 + metaKeywords: string; // 元关键词 + metaDescription: string; // 元描述 + content: string; // 内容 + status: string; // 状态 + isEditor: boolean; // 是否为编辑器 + draftContent?: string; // 草稿内容 + createdAt: Date; // 创建时间 + updatedAt: Date; // 更新时间 + publishedAt?: Date; // 发布时间 +} \ No newline at end of file diff --git a/frontend/interface/template.ts b/frontend/interface/template.ts index 660ba47..5a6ec45 100644 --- a/frontend/interface/template.ts +++ b/frontend/interface/template.ts @@ -1,27 +1,45 @@ import { HttpClient } from "core/http"; import { CapabilityService } from "core/capability"; import { Serializable } from "interface/serializableType"; +import { Layout } from "./layout"; export class Template { + private http: HttpClient; + private capability: CapabilityService; + constructor( public config: { - layout?: string; + layout?: Layout; styles?: string[]; scripts?: string[]; description?: string; }, - public element: (services: { + public element: (props: { http: HttpClient; - capability: CapabilityService; args: Serializable; }) => React.ReactNode, - ) {} + services?: { + http?: HttpClient; + capability?: CapabilityService; + } + ) { + this.http = services?.http || HttpClient.getInstance(); + this.capability = services?.capability || CapabilityService.getInstance(); + } - render(services: { - http: HttpClient; - capability: CapabilityService; - args: Serializable; - }) { - return this.element(services); + render(args: Serializable) { + const content = this.element({ + http: this.http, + args, + }); + + if (this.config.layout) { + return this.config.layout.render({ + children: content, + args, + }); + } + + return content; } } diff --git a/frontend/interface/theme.ts b/frontend/interface/theme.ts index 7574b94..f26187e 100644 --- a/frontend/interface/theme.ts +++ b/frontend/interface/theme.ts @@ -8,17 +8,8 @@ export interface ThemeConfig { description?: string; author?: string; templates: Map; - globalSettings?: { - layout?: string; - css?: string; - }; + layout?: string; configuration: Configuration; - routes: { - article: string; - post: string; - tag: string; - category: string; - error: string; - page: Map; - }; + error?: string; + routes: Map; } diff --git a/frontend/package.json b/frontend/package.json index de9543c..fc95eae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,13 +19,17 @@ "@remix-run/react": "^2.14.0", "@remix-run/serve": "^2.14.0", "@types/axios": "^0.14.4", + "@types/three": "^0.170.0", "axios": "^1.7.7", "bootstrap-icons": "^1.11.3", "cors": "^2.8.5", "express": "^4.21.1", + "gsap": "^3.12.5", + "html-react-parser": "^5.1.19", "isbot": "^4.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "three": "^0.171.0" }, "devDependencies": { "@remix-run/dev": "^2.14.0", diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 7a4f54e..7d48a0f 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -24,6 +24,9 @@ export default { animation: { progress: "progress 3s linear", }, + zIndex: { + '-10': '-10', + } }, }, } satisfies Config; diff --git a/frontend/themes/echoes/about.tsx b/frontend/themes/echoes/about.tsx new file mode 100644 index 0000000..35f10dd --- /dev/null +++ b/frontend/themes/echoes/about.tsx @@ -0,0 +1,156 @@ +import { Template } from "interface/template"; +import { Container, Heading, Text, Box, Flex, Link } from "@radix-ui/themes"; +import { GitHubLogoIcon, TwitterLogoIcon, LinkedInLogoIcon, EnvelopeClosedIcon } from "@radix-ui/react-icons"; +import { ParticleImage } from "hooks/ParticleImage"; +import { useEffect, useRef, useState } from "react"; +import { gsap } from "gsap"; + +const socialLinks = [ + { + icon: , + url: "https://github.com/yourusername", + label: "GitHub" + }, + { + icon: , + url: "https://twitter.com/yourusername", + label: "Twitter" + }, + { + icon: , + url: "https://linkedin.com/in/yourusername", + label: "LinkedIn" + }, + { + icon: , + url: "mailto:your.email@example.com", + label: "Email" + } +]; + +const skills = [ + { name: "React", level: 90 }, + { name: "TypeScript", level: 85 }, + { name: "Node.js", level: 80 }, + { name: "Three.js", level: 75 }, + { name: "Python", level: 70 } +]; + +export default new Template( + {}, + ({ http, args }) => { + const containerRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + setIsVisible(true); + + const ctx = gsap.context(() => { + // 标题动画 + gsap.from(".animate-title", { + y: 30, + opacity: 0, + duration: 1, + ease: "power3.out", + stagger: 0.2 + }); + + // 技能条动画 + gsap.from(".skill-bar", { + width: 0, + duration: 1.5, + ease: "power3.out", + stagger: 0.1, + scrollTrigger: { + trigger: ".skills-section", + start: "top center+=100", + } + }); + + // 社交链接动画 + gsap.from(".social-link", { + scale: 0, + opacity: 0, + duration: 0.5, + ease: "back.out(1.7)", + stagger: 0.1 + }); + }, containerRef); + + return () => ctx.revert(); + }, []); + + return ( + + + {/* 头部个人介绍 */} + + +
+ +
+
+ + + 你的名字 + + + 全栈开发者 / 设计爱好者 + + + 热爱编程和创新的全栈开发者,专注于创建优雅且高性能的web应用。 + 擅长将复杂的问题简化,追求代码的优雅和用户体验的完美统一。 + +
+ + {/* 技能展示 */} + + 专业技能 + + {skills.map((skill) => ( + + + {skill.name} + {skill.level}% + + + + + + ))} + + + + {/* 社交链接 */} + + {socialLinks.map((link, index) => ( + + {link.icon} + + ))} + +
+
+ ); + } +); \ No newline at end of file diff --git a/frontend/themes/echoes/article.tsx b/frontend/themes/echoes/article.tsx index 6fb99ec..6b16e3e 100644 --- a/frontend/themes/echoes/article.tsx +++ b/frontend/themes/echoes/article.tsx @@ -1,10 +1,219 @@ import { Template } from "interface/template"; +import { Container, Heading, Text, Flex, Card, Button } from "@radix-ui/themes"; +import { CalendarIcon, PersonIcon, ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"; +import { Post } from "interface/post"; +import { useMemo} from "react"; + +import { ImageLoader } from "hooks/ParticleImage"; + +// 模拟文章列表数据 +const mockArticles: Post[] = [ + { + id: 1, + title: "构建现代化的前端开发工作流", + content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...", + authorName: "张三", + publishedAt: new Date("2024-03-15"), + coverImage: "", + metaKeywords: "", + metaDescription: "", + status: "published", + isEditor: false, + createdAt: new Date("2024-03-15"), + updatedAt: new Date("2024-03-15") + }, + { + id: 2, + title: "React 18 新特性详解", + content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...", + authorName: "李四", + publishedAt: new Date("2024-03-14"), + coverImage: "https://avatars.githubusercontent.com/u/2?v=4", + metaKeywords: "", + metaDescription: "", + status: "published", + isEditor: false, + createdAt: new Date("2024-03-14"), + updatedAt: new Date("2024-03-14") + }, + { + id: 3, + title: "JavaScript 性能优化技巧", + content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...", + authorName: "王五", + publishedAt: new Date("2024-03-13"), + coverImage: "https://avatars.githubusercontent.com/u/", + metaKeywords: "", + metaDescription: "", + status: "published", + isEditor: false, + createdAt: new Date("2024-03-13"), + updatedAt: new Date("2024-03-13") + }, + // 可以添加更多模拟文章 +]; + +// 修改颜色组合数组,增加更多颜色选项 +const colorSchemes = [ + { bg: 'bg-blue-100', text: 'text-blue-600' }, + { bg: 'bg-green-100', text: 'text-green-600' }, + { bg: 'bg-purple-100', text: 'text-purple-600' }, + { bg: 'bg-pink-100', text: 'text-pink-600' }, + { bg: 'bg-orange-100', text: 'text-orange-600' }, + { bg: 'bg-teal-100', text: 'text-teal-600' }, + { bg: 'bg-red-100', text: 'text-red-600' }, + { bg: 'bg-indigo-100', text: 'text-indigo-600' }, + { bg: 'bg-yellow-100', text: 'text-yellow-600' }, + { bg: 'bg-cyan-100', text: 'text-cyan-600' }, +]; + +const categories = ['前端开发', '后端开发', 'UI设计', '移动开发', '人工智能']; +const tags = ['React', 'TypeScript', 'Vue', 'Node.js', 'Flutter', 'Python', 'Docker']; + +// 定义 SlideGeometry 类 + + export default new Template( { - layout: "default", }, ({ http, args }) => { - return
Hello World
; - }, + const articleData = useMemo(() => { + return mockArticles.map((article) => { + // 使用更复杂的散列函数来生成看起来更随机的索引 + const hash = (str: string) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return Math.abs(hash); + }; + + // 使用文章的不同属性来生成索引 + const categoryIndex = hash(article.title + article.id.toString()) % categories.length; + const colorIndex = hash(article.authorName + article.id.toString()) % colorSchemes.length; + + // 为标签生成不同的索引 + const tagIndices = tags + .map((_, index) => ({ + index, + sort: hash(article.title + index.toString() + article.id.toString()) + })) + .sort((a, b) => a.sort - b.sort) + .slice(0, 2) + .map(item => item.index); + + return { + ...article, + category: categories[categoryIndex], + categoryColor: colorSchemes[colorIndex], + tags: tagIndices.map(index => ({ + name: tags[index], + color: colorSchemes[hash(tags[index] + article.id.toString()) % colorSchemes.length] + })) + }; + }); + }, []); + + return ( + +
+ {articleData.map(article => ( + +
+ + +
+
+ + {article.title} + + + {article.category} + +
+ + + + + {article.publishedAt?.toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} + + · + {article.authorName} + + + + {article.content} + + + + {article.tags.map(tag => ( + + {tag.name} + + ))} + +
+
+
+ ))} +
+ + + + + + + + +
...
+ +
+ + +
+
+ ); + } ); diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx index 59ce81e..d02fabc 100644 --- a/frontend/themes/echoes/layout.tsx +++ b/frontend/themes/echoes/layout.tsx @@ -14,9 +14,10 @@ import { useState, useEffect } from "react"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import throttle from "lodash/throttle"; import "./styles/layouts.css"; +import parse from 'html-react-parser'; // 直接导出 Layout 实例 -const EchoesLayout = new Layout(({ children, args }) => { +export default new Layout(({ children, args }) => { const [moreState, setMoreState] = useState(false); const [loginState, setLoginState] = useState(true); const [device, setDevice] = useState(""); @@ -34,6 +35,7 @@ const EchoesLayout = new Layout(({ children, args }) => { const handleResize = throttle(() => { if (window.innerWidth >= 1024) { setDevice("desktop"); + setMoreState(false); } else { setDevice("mobile"); } @@ -47,6 +49,8 @@ const EchoesLayout = new Layout(({ children, args }) => { }; }, []); + const navString = typeof args === 'object' && args && 'nav' in args ? args.nav as string : ''; + return ( { radius="large" panelBackground="solid" > - + {/* 导航栏 */} { - 首页 + {parse(navString)} @@ -125,8 +132,8 @@ const EchoesLayout = new Layout(({ children, args }) => { > {loginState ? ( <> @@ -182,8 +189,7 @@ const EchoesLayout = new Layout(({ children, args }) => { className="mt-2 p-3 min-w-[280px] rounded-md bg-[--color-background] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2" > - 首页 - 首页 + {parse(navString)} { variant="surface" placeholder="搜索..." className="w-full [&_input]:pl-3" + id="search" > - + @@ -234,4 +238,3 @@ const EchoesLayout = new Layout(({ children, args }) => { ); }); -export default EchoesLayout; diff --git a/frontend/themes/echoes/theme.config.ts b/frontend/themes/echoes/theme.config.ts index 3ab5d6f..4b126f5 100644 --- a/frontend/themes/echoes/theme.config.ts +++ b/frontend/themes/echoes/theme.config.ts @@ -6,10 +6,13 @@ const themeConfig: ThemeConfig = { version: "1.0.0", description: "一个简约风格的博客主题", author: "lsy", - configuration: {}, - globalSettings: { - layout: "layout.tsx", + configuration: { + "nav": { + title: "导航配置", + data: '你好 不好' + } }, + layout: "layout.tsx", templates: new Map([ [ "page", @@ -21,14 +24,7 @@ const themeConfig: ThemeConfig = { ], ]), - routes: { - article: "", - post: "", - tag: "", - category: "", - error: "", - page: new Map([]), - }, + routes: new Map([]) }; export default themeConfig;