前端:修复粒子动画,模式切换,滚动导致导航栏被覆盖
This commit is contained in:
parent
76ee04bdde
commit
3f52d609a3
@ -7,41 +7,54 @@ import {
|
|||||||
} from "@remix-run/react";
|
} from "@remix-run/react";
|
||||||
import { NotificationProvider } from "hooks/notification";
|
import { NotificationProvider } from "hooks/notification";
|
||||||
import { Theme } from "@radix-ui/themes";
|
import { Theme } from "@radix-ui/themes";
|
||||||
import { useEffect, useState } from "react";
|
import { ThemeScript } from "hooks/themeMode";
|
||||||
|
|
||||||
import "~/index.css";
|
import "~/index.css";
|
||||||
|
|
||||||
export function Layout() {
|
export function Layout() {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="h-full light" suppressHydrationWarning={true}>
|
<html
|
||||||
|
lang="en"
|
||||||
|
className="h-full"
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta
|
||||||
<meta name="generator" content="echoes" />
|
name="viewport"
|
||||||
<title>Echoes</title>
|
content="width=device-width, initial-scale=1"
|
||||||
<script
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `
|
|
||||||
(function() {
|
|
||||||
function getInitialTheme() {
|
|
||||||
const savedTheme = localStorage.getItem('theme-preference');
|
|
||||||
if (savedTheme) return savedTheme;
|
|
||||||
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
? 'dark'
|
|
||||||
: 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.documentElement.className = getInitialTheme();
|
|
||||||
})();
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<meta
|
||||||
|
name="generator"
|
||||||
|
content="echoes"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
httpEquiv="Cache-Control"
|
||||||
|
content="no-cache, no-store, must-revalidate"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
httpEquiv="Pragma"
|
||||||
|
content="no-cache"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
httpEquiv="Expires"
|
||||||
|
content="0"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<title>Echoes</title>
|
||||||
|
<ThemeScript/>
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body className="h-full" suppressHydrationWarning={true}>
|
<body
|
||||||
<Theme grayColor="slate" radius="medium" scaling="100%">
|
className="h-full"
|
||||||
|
suppressHydrationWarning={true}
|
||||||
|
>
|
||||||
|
<Theme
|
||||||
|
grayColor="slate"
|
||||||
|
radius="medium"
|
||||||
|
scaling="100%"
|
||||||
|
>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
|
@ -2,14 +2,35 @@ import ErrorPage from "hooks/error";
|
|||||||
import layout from "themes/echoes/layout";
|
import layout from "themes/echoes/layout";
|
||||||
import article from "themes/echoes/article";
|
import article from "themes/echoes/article";
|
||||||
import about from "themes/echoes/about";
|
import about from "themes/echoes/about";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export default function Routes() {
|
export default function Routes() {
|
||||||
|
const location = useLocation();
|
||||||
|
let path = location.pathname;
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
title: "我的页面",
|
title: "我的页面",
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
nav: '<a href="h">a</a>'
|
nav: '<a href="/">index</a><a href="error">error</a><a href="about">about</a>',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(path);
|
||||||
|
path =path.split("/")[1];
|
||||||
|
|
||||||
|
if (path[1] === "error") {
|
||||||
|
return layout.render({
|
||||||
|
children: ErrorPage.render(args),
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path[1] === "about") {
|
||||||
|
return layout.render({
|
||||||
|
children: about.render(args),
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return layout.render({
|
return layout.render({
|
||||||
children: article.render(args),
|
children: article.render(args),
|
||||||
args,
|
args,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { gsap } from 'gsap';
|
import { gsap } from 'gsap';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
|
||||||
interface Particle {
|
interface Particle {
|
||||||
x: number;
|
x: number;
|
||||||
@ -17,7 +18,10 @@ const createErrorParticles = (width: number, height: number) => {
|
|||||||
const positionArray: number[] = [];
|
const positionArray: number[] = [];
|
||||||
const colorArray: number[] = [];
|
const colorArray: number[] = [];
|
||||||
|
|
||||||
const errorColor = new THREE.Color(0.8, 0, 0); // 更深的红色
|
const errorColor = new THREE.Color(0.8, 0, 0);
|
||||||
|
const size = Math.min(width, height);
|
||||||
|
const scaleFactor = size * 0.3;
|
||||||
|
const particlesPerLine = 50;
|
||||||
|
|
||||||
// X 形状的两条线
|
// X 形状的两条线
|
||||||
const lines = [
|
const lines = [
|
||||||
@ -27,9 +31,6 @@ const createErrorParticles = (width: number, height: number) => {
|
|||||||
{ start: [1, 1], end: [-1, -1] }
|
{ start: [1, 1], end: [-1, -1] }
|
||||||
];
|
];
|
||||||
|
|
||||||
// 每条线上的粒子数量
|
|
||||||
const particlesPerLine = 50;
|
|
||||||
|
|
||||||
lines.forEach(line => {
|
lines.forEach(line => {
|
||||||
for (let i = 0; i < particlesPerLine; i++) {
|
for (let i = 0; i < particlesPerLine; i++) {
|
||||||
const t = i / (particlesPerLine - 1);
|
const t = i / (particlesPerLine - 1);
|
||||||
@ -41,9 +42,6 @@ const createErrorParticles = (width: number, height: number) => {
|
|||||||
const randomX = x + (Math.random() - 0.5) * randomOffset;
|
const randomX = x + (Math.random() - 0.5) * randomOffset;
|
||||||
const randomY = y + (Math.random() - 0.5) * randomOffset;
|
const randomY = y + (Math.random() - 0.5) * randomOffset;
|
||||||
|
|
||||||
// 缩放到适合容器的大小
|
|
||||||
const size = Math.min(width, height);
|
|
||||||
const scaleFactor = size * 0.3;
|
|
||||||
const scaledX = randomX * scaleFactor;
|
const scaledX = randomX * scaleFactor;
|
||||||
const scaledY = randomY * scaleFactor;
|
const scaledY = randomY * scaleFactor;
|
||||||
|
|
||||||
@ -57,21 +55,21 @@ const createErrorParticles = (width: number, height: number) => {
|
|||||||
delay: 0
|
delay: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// 随机初始位置
|
// 修改初始位置生成方式
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const distance = size * 2;
|
||||||
positionArray.push(
|
positionArray.push(
|
||||||
(Math.random() - 0.5) * size * 2,
|
Math.cos(angle) * distance,
|
||||||
(Math.random() - 0.5) * size * 2,
|
Math.sin(angle) * distance,
|
||||||
(Math.random() - 0.5) * 50
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// 随机初始颜色
|
// 初始颜色设置为最终颜色的一半亮度
|
||||||
colorArray.push(errorColor.r, errorColor.g, errorColor.b);
|
colorArray.push(errorColor.r * 0.5, errorColor.g * 0.5, errorColor.b * 0.5);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const size = Math.min(width, height);
|
const particleSize = Math.max(1.2, (size / 200) * 1.2);
|
||||||
const scale = size / 200;
|
|
||||||
const particleSize = Math.max(1.2, scale * 1.2);
|
|
||||||
|
|
||||||
return { particles, positionArray, colorArray, particleSize };
|
return { particles, positionArray, colorArray, particleSize };
|
||||||
};
|
};
|
||||||
@ -82,99 +80,96 @@ const createSmileParticles = (width: number, height: number) => {
|
|||||||
const positionArray: number[] = [];
|
const positionArray: number[] = [];
|
||||||
const colorArray: number[] = [];
|
const colorArray: number[] = [];
|
||||||
|
|
||||||
// 根据容器大小动态调整参数
|
|
||||||
const size = Math.min(width, height);
|
const size = Math.min(width, height);
|
||||||
const scale = size / 200; // 基准子
|
const scale = size / 200;
|
||||||
|
const radius = size * 0.35;
|
||||||
// 调整笑脸参数
|
const particleSize = Math.max(1.2, scale * 1.2);
|
||||||
const radius = size * 0.25; // 更合理的脸部大小比例
|
|
||||||
const particlesCount = Math.floor(150 * scale); // 减少粒子数量
|
|
||||||
const particleSize = Math.max(1.2, scale * 1.2); // 确保粒子大小适应屏幕
|
|
||||||
|
|
||||||
const particleColor = new THREE.Color(0.8, 0.6, 0);
|
const particleColor = new THREE.Color(0.8, 0.6, 0);
|
||||||
|
|
||||||
// 创建圆形脸部轮廓
|
// 预先计算所有需要的粒子位置
|
||||||
for (let i = 0; i < particlesCount / 2; i++) {
|
const allPoints: { x: number; y: number }[] = [];
|
||||||
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,
|
const outlinePoints = Math.floor(60 * scale);
|
||||||
originalX: x,
|
for (let i = 0; i < outlinePoints; i++) {
|
||||||
originalY: y,
|
const angle = (i / outlinePoints) * Math.PI * 2;
|
||||||
originalColor: particleColor,
|
allPoints.push({
|
||||||
delay: 0
|
x: Math.cos(angle) * radius,
|
||||||
|
y: Math.sin(angle) * radius
|
||||||
});
|
});
|
||||||
|
|
||||||
positionArray.push(
|
|
||||||
(Math.random() - 0.5) * size * 4,
|
|
||||||
(Math.random() - 0.5) * size * 4,
|
|
||||||
(Math.random() - 0.5) * 150
|
|
||||||
);
|
|
||||||
colorArray.push(particleColor.r, particleColor.g, particleColor.b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 眼睛参数调整
|
// 修改眼睛的生成方式
|
||||||
const eyeOffset = radius * 0.3; // 增加眼睛间距
|
const eyeOffset = radius * 0.3;
|
||||||
const eyeY = radius * 0.15; // 调整眼睛垂直位置
|
const eyeY = radius * 0.15;
|
||||||
const eyeSize = radius * 0.12; // 增加眼睛大小
|
const eyeSize = radius * 0.1; // 稍微减小眼睛尺寸
|
||||||
|
const eyePoints = Math.floor(20 * scale);
|
||||||
// 眼睛粒子数量也要根据比例调整
|
|
||||||
const eyeParticles = Math.floor(20 * scale);
|
|
||||||
|
|
||||||
[-1, 1].forEach(side => {
|
[-1, 1].forEach(side => {
|
||||||
for (let i = 0; i < eyeParticles; i++) {
|
// 使用同心圆的方式生成眼睛
|
||||||
const r = Math.random() * eyeSize;
|
const eyeCenterX = side * eyeOffset;
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const rings = 3; // 同心圆的数量
|
||||||
const x = side * eyeOffset + Math.cos(angle) * r;
|
|
||||||
const y = eyeY + Math.sin(angle) * r;
|
|
||||||
|
|
||||||
particles.push({
|
for (let ring = 0; ring < rings; ring++) {
|
||||||
x, y, z: 0,
|
const ringRadius = eyeSize * (1 - ring / rings); // 从外到内递减半径
|
||||||
originalX: x,
|
const pointsInRing = Math.floor(eyePoints / rings);
|
||||||
originalY: y,
|
|
||||||
originalColor: particleColor,
|
for (let i = 0; i < pointsInRing; i++) {
|
||||||
delay: 0
|
const angle = (i / pointsInRing) * Math.PI * 2;
|
||||||
|
allPoints.push({
|
||||||
|
x: eyeCenterX + Math.cos(angle) * ringRadius,
|
||||||
|
y: eyeY + Math.sin(angle) * ringRadius
|
||||||
});
|
});
|
||||||
|
|
||||||
positionArray.push(
|
|
||||||
(Math.random() - 0.5) * size * 4,
|
|
||||||
(Math.random() - 0.5) * size * 4,
|
|
||||||
(Math.random() - 0.5) * 150
|
|
||||||
);
|
|
||||||
colorArray.push(particleColor.r, particleColor.g, particleColor.b);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加中心点
|
||||||
|
allPoints.push({
|
||||||
|
x: eyeCenterX,
|
||||||
|
y: eyeY
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 嘴巴参数调整
|
// 计算嘴巴的点
|
||||||
const smileWidth = radius * 0.6; // 增加嘴巴宽度
|
const smileWidth = radius * 0.6;
|
||||||
const smileY = -radius * 0.25; // 调整嘴巴位置
|
const smileY = -radius * 0.35;
|
||||||
const smilePoints = Math.floor(30 * scale); // 根据大小调整嘴巴粒子数量
|
const smilePoints = Math.floor(25 * scale);
|
||||||
|
|
||||||
// 创建微笑
|
|
||||||
for (let i = 0; i < smilePoints; i++) {
|
for (let i = 0; i < smilePoints; i++) {
|
||||||
const t = i / (smilePoints - 1);
|
const t = i / (smilePoints - 1);
|
||||||
const x = (t * 2 - 1) * smileWidth;
|
const x = (t * 2 - 1) * smileWidth;
|
||||||
|
const y = smileY + Math.pow(x / smileWidth, 2) * radius * 0.2;
|
||||||
|
allPoints.push({ x, y });
|
||||||
|
}
|
||||||
|
|
||||||
// 简单的抛物线,向上弯曲的笑脸
|
// 为所有点创建粒子
|
||||||
const y = smileY + (Math.pow(x / smileWidth, 2) * radius * 0.2);
|
allPoints.forEach(point => {
|
||||||
|
|
||||||
particles.push({
|
particles.push({
|
||||||
x, y, z: 0,
|
x: point.x,
|
||||||
originalX: x,
|
y: point.y,
|
||||||
originalY: y,
|
z: 0,
|
||||||
|
originalX: point.x,
|
||||||
|
originalY: point.y,
|
||||||
originalColor: particleColor,
|
originalColor: particleColor,
|
||||||
delay: 0
|
delay: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 生成初始位置(从外围圆形区域开始)
|
||||||
|
const initAngle = Math.random() * Math.PI * 2;
|
||||||
|
const distance = size * 2;
|
||||||
positionArray.push(
|
positionArray.push(
|
||||||
(Math.random() - 0.5) * size * 4,
|
Math.cos(initAngle) * distance,
|
||||||
(Math.random() - 0.5) * size * 4,
|
Math.sin(initAngle) * distance,
|
||||||
(Math.random() - 0.5) * 150
|
0
|
||||||
);
|
);
|
||||||
colorArray.push(particleColor.r, particleColor.g, particleColor.b);
|
|
||||||
}
|
// 初始颜色设置为最终颜色的一半亮度
|
||||||
|
colorArray.push(
|
||||||
|
particleColor.r * 0.5,
|
||||||
|
particleColor.g * 0.5,
|
||||||
|
particleColor.b * 0.5
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return { particles, positionArray, colorArray, particleSize };
|
return { particles, positionArray, colorArray, particleSize };
|
||||||
};
|
};
|
||||||
@ -201,7 +196,7 @@ export const ParticleImage = ({
|
|||||||
src,
|
src,
|
||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
performanceMode = false // 默认关闭
|
performanceMode = false
|
||||||
}: ParticleImageProps) => {
|
}: ParticleImageProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const sceneRef = useRef<THREE.Scene>();
|
const sceneRef = useRef<THREE.Scene>();
|
||||||
@ -209,23 +204,14 @@ export const ParticleImage = ({
|
|||||||
const rendererRef = useRef<THREE.WebGLRenderer>();
|
const rendererRef = useRef<THREE.WebGLRenderer>();
|
||||||
const animationFrameRef = useRef<number>();
|
const animationFrameRef = useRef<number>();
|
||||||
|
|
||||||
const width = containerRef.current?.offsetWidth || 0;
|
// 将 resize 处理逻辑移到组件顶层
|
||||||
const height = containerRef.current?.offsetHeight || 0;
|
|
||||||
const size = Math.min(width, height);
|
|
||||||
const scale = size / 200; // 基准因子
|
|
||||||
|
|
||||||
// 在性能模式下使用更保守的参数
|
|
||||||
const particleCount = performanceMode ?
|
|
||||||
Math.floor(100 * scale) :
|
|
||||||
Math.floor(200 * scale);
|
|
||||||
|
|
||||||
// 添 resize 处理函数
|
|
||||||
const handleResize = useCallback(() => {
|
const handleResize = useCallback(() => {
|
||||||
if (!containerRef.current || !cameraRef.current || !rendererRef.current || !sceneRef.current) return;
|
if (!containerRef.current || !cameraRef.current || !rendererRef.current || !sceneRef.current) return;
|
||||||
|
|
||||||
const width = containerRef.current.offsetWidth;
|
const width = containerRef.current.offsetWidth;
|
||||||
const height = containerRef.current.offsetHeight;
|
const height = containerRef.current.offsetHeight;
|
||||||
|
|
||||||
|
// 更新相机视图
|
||||||
const camera = cameraRef.current;
|
const camera = cameraRef.current;
|
||||||
camera.left = width / -2;
|
camera.left = width / -2;
|
||||||
camera.right = width / 2;
|
camera.right = width / 2;
|
||||||
@ -233,14 +219,26 @@ export const ParticleImage = ({
|
|||||||
camera.bottom = height / -2;
|
camera.bottom = height / -2;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
|
|
||||||
|
// 更新渲染器大小
|
||||||
rendererRef.current.setSize(width, height);
|
rendererRef.current.setSize(width, height);
|
||||||
|
|
||||||
// 重新生成粒子
|
// 只有当尺寸变化超过阈值时才重新生成粒子
|
||||||
if (src === '') {
|
const currentSize = Math.min(width, height);
|
||||||
// 清除现有的 GSAP 动画
|
const previousSize = sceneRef.current.userData.previousSize || currentSize;
|
||||||
|
const sizeChange = Math.abs(currentSize - previousSize) / previousSize;
|
||||||
|
|
||||||
|
if (sizeChange > 0.2 && src === '') {
|
||||||
|
sceneRef.current.userData.previousSize = currentSize;
|
||||||
|
updateParticles(width, height);
|
||||||
|
}
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
// 将粒子更新逻辑抽取为单独的函数
|
||||||
|
const updateParticles = useCallback((width: number, height: number) => {
|
||||||
|
if (!sceneRef.current) return;
|
||||||
|
|
||||||
gsap.killTweensOf('*');
|
gsap.killTweensOf('*');
|
||||||
|
|
||||||
// 重新生成笑脸
|
|
||||||
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
||||||
|
|
||||||
const material = new THREE.PointsMaterial({
|
const material = new THREE.PointsMaterial({
|
||||||
@ -262,7 +260,6 @@ export const ParticleImage = ({
|
|||||||
const points = new THREE.Points(geometry, material);
|
const points = new THREE.Points(geometry, material);
|
||||||
sceneRef.current.add(points);
|
sceneRef.current.add(points);
|
||||||
|
|
||||||
// 修改这部分,添加动画而不是直接设置位置
|
|
||||||
const positionAttribute = geometry.attributes.position;
|
const positionAttribute = geometry.attributes.position;
|
||||||
|
|
||||||
particles.forEach((particle, i) => {
|
particles.forEach((particle, i) => {
|
||||||
@ -286,12 +283,11 @@ export const ParticleImage = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}, []);
|
||||||
}, [src]);
|
|
||||||
|
|
||||||
|
// 主要的 useEffect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
console.log('Current src:', src);
|
|
||||||
|
|
||||||
const width = containerRef.current.offsetWidth;
|
const width = containerRef.current.offsetWidth;
|
||||||
const height = containerRef.current.offsetHeight;
|
const height = containerRef.current.offsetHeight;
|
||||||
@ -322,7 +318,6 @@ export const ParticleImage = ({
|
|||||||
|
|
||||||
// 检查是否应该显示笑脸
|
// 检查是否应该显示笑脸
|
||||||
if (src === '') {
|
if (src === '') {
|
||||||
console.log('Showing smile animation');
|
|
||||||
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
const { particles, positionArray, colorArray, particleSize } = createSmileParticles(width, height);
|
||||||
|
|
||||||
const material = new THREE.PointsMaterial({
|
const material = new THREE.PointsMaterial({
|
||||||
@ -346,7 +341,7 @@ export const ParticleImage = ({
|
|||||||
// 修改动画效果
|
// 修改动画效果
|
||||||
const positionAttribute = geometry.attributes.position;
|
const positionAttribute = geometry.attributes.position;
|
||||||
|
|
||||||
// 计算到中心的距离用于延迟
|
// 算到中心的距离用于延迟
|
||||||
particles.forEach((particle, i) => {
|
particles.forEach((particle, i) => {
|
||||||
const i3 = i * 3;
|
const i3 = i * 3;
|
||||||
const distanceToCenter = Math.sqrt(
|
const distanceToCenter = Math.sqrt(
|
||||||
@ -378,30 +373,30 @@ export const ParticleImage = ({
|
|||||||
};
|
};
|
||||||
animate();
|
animate();
|
||||||
|
|
||||||
// 添加 resize 监听
|
// 设置 resize 监听
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const throttledResize = throttle(handleResize, 200, {
|
||||||
if (containerRef.current) {
|
leading: true,
|
||||||
handleResize();
|
trailing: true
|
||||||
}
|
|
||||||
});
|
});
|
||||||
resizeObserver.observe(containerRef.current);
|
|
||||||
|
|
||||||
// 添加窗口 resize 监听
|
const resizeObserver = new ResizeObserver(throttledResize);
|
||||||
window.addEventListener('resize', handleResize);
|
resizeObserver.observe(containerRef.current);
|
||||||
|
window.addEventListener('resize', throttledResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (animationFrameRef.current) {
|
throttledResize.cancel();
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
|
||||||
}
|
|
||||||
if (renderer && containerRef.current) {
|
|
||||||
containerRef.current.removeChild(renderer.domElement);
|
|
||||||
renderer.dispose();
|
|
||||||
}
|
|
||||||
gsap.killTweensOf('*');
|
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
resizeObserver.unobserve(containerRef.current);
|
resizeObserver.unobserve(containerRef.current);
|
||||||
}
|
}
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', throttledResize);
|
||||||
|
if (animationFrameRef.current) {
|
||||||
|
cancelAnimationFrame(animationFrameRef.current);
|
||||||
|
}
|
||||||
|
if (rendererRef.current && containerRef.current) {
|
||||||
|
containerRef.current.removeChild(rendererRef.current.domElement);
|
||||||
|
rendererRef.current.dispose();
|
||||||
|
}
|
||||||
|
gsap.killTweensOf('*');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,40 +460,45 @@ export const ParticleImage = ({
|
|||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
|
// 计算目标尺寸和裁剪区域
|
||||||
|
const targetAspect = width / height;
|
||||||
|
const imgAspect = img.width / img.height;
|
||||||
|
|
||||||
|
let sourceWidth = img.width;
|
||||||
|
let sourceHeight = img.height;
|
||||||
|
let sourceX = 0;
|
||||||
|
let sourceY = 0;
|
||||||
|
|
||||||
|
// 裁源图片,确保比例匹配目标容器
|
||||||
|
if (imgAspect > targetAspect) {
|
||||||
|
// 图片较宽,需要裁剪两边
|
||||||
|
sourceWidth = img.height * targetAspect;
|
||||||
|
sourceX = (img.width - sourceWidth) / 2;
|
||||||
|
} else {
|
||||||
|
// 图片较高,需要裁剪上下
|
||||||
|
sourceHeight = img.width / targetAspect;
|
||||||
|
sourceY = (img.height - sourceHeight) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置画布尺寸为目标显示尺寸
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
// 计算图片绘制尺寸和位置,确保不会超出容器
|
// 直接绘制裁剪后的图片到目标尺寸
|
||||||
const imgAspect = img.width / img.height;
|
ctx.drawImage(
|
||||||
const containerAspect = width / height;
|
img,
|
||||||
|
sourceX, sourceY, sourceWidth, sourceHeight, // 源图片的裁剪区域
|
||||||
|
0, 0, 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 imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
|
||||||
const particles: Particle[] = [];
|
const particles: Particle[] = [];
|
||||||
const positionArray = [];
|
const positionArray = [];
|
||||||
const colorArray = [];
|
const colorArray = [];
|
||||||
const samplingGap = Math.ceil(Math.max(width, height) / 80); // 减少采样密度
|
const samplingGap = Math.ceil(Math.max(width, height) / 80);
|
||||||
|
|
||||||
// 采样图片像素
|
// 采样已裁剪的图片像素
|
||||||
for (let y = 0; y < height; y += samplingGap) {
|
for (let y = 0; y < height; y += samplingGap) {
|
||||||
for (let x = 0; x < width; x += samplingGap) {
|
for (let x = 0; x < width; x += samplingGap) {
|
||||||
const i = (y * width + x) * 4;
|
const i = (y * width + x) * 4;
|
||||||
@ -508,7 +508,6 @@ export const ParticleImage = ({
|
|||||||
const a = imageData.data[i + 3] / 255;
|
const a = imageData.data[i + 3] / 255;
|
||||||
|
|
||||||
if (a > 0.3) {
|
if (a > 0.3) {
|
||||||
// 计算距离中心的距离,用于动画延迟
|
|
||||||
const distanceToCenter = Math.sqrt(
|
const distanceToCenter = Math.sqrt(
|
||||||
Math.pow(x - width/2, 2) +
|
Math.pow(x - width/2, 2) +
|
||||||
Math.pow(y - height/2, 2)
|
Math.pow(y - height/2, 2)
|
||||||
@ -526,7 +525,7 @@ export const ParticleImage = ({
|
|||||||
originalX: px,
|
originalX: px,
|
||||||
originalY: py,
|
originalY: py,
|
||||||
originalColor: new THREE.Color(r, g, b),
|
originalColor: new THREE.Color(r, g, b),
|
||||||
delay: normalizedDistance * 0.3 // 基于距离的延迟
|
delay: normalizedDistance * 0.3
|
||||||
});
|
});
|
||||||
|
|
||||||
// 随机初始位置(根据距离调整范围)
|
// 随机初始位置(根据距离调整范围)
|
||||||
@ -565,7 +564,7 @@ export const ParticleImage = ({
|
|||||||
const points = new THREE.Points(geometry, material);
|
const points = new THREE.Points(geometry, material);
|
||||||
scene.add(points);
|
scene.add(points);
|
||||||
|
|
||||||
// 动画
|
// 画
|
||||||
const positionAttribute = geometry.attributes.position;
|
const positionAttribute = geometry.attributes.position;
|
||||||
const colorAttribute = geometry.attributes.color;
|
const colorAttribute = geometry.attributes.color;
|
||||||
|
|
||||||
@ -693,7 +692,7 @@ export const ImageLoader = ({ src, alt, className }: {
|
|||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-[100px] md:w-[140px] h-[100px] md:h-[140px] shrink-0 overflow-hidden">
|
<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="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">
|
||||||
<ParticleImage
|
<ParticleImage
|
||||||
src={src}
|
src={src}
|
||||||
|
@ -1,13 +1,39 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import "styles/echoes.css"
|
import "styles/echoes.css";
|
||||||
|
|
||||||
export const Echoes: React.FC = () => {
|
export const Echoes: React.FC = () => {
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 优化动画性能
|
||||||
|
if (svgRef.current) {
|
||||||
|
svgRef.current.style.willChange = 'transform';
|
||||||
|
|
||||||
|
// 使用 requestAnimationFrame 来优化动画
|
||||||
|
const paths = svgRef.current.querySelectorAll('path');
|
||||||
|
paths.forEach(path => {
|
||||||
|
path.style.willChange = 'transform';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (svgRef.current) {
|
||||||
|
svgRef.current.style.willChange = 'auto';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
className="animated-text w-full h-full"
|
className="animated-text w-full h-full"
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
viewBox="50.4 44.600006 234.1 86"
|
viewBox="50.4 44.600006 234.1 86"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{
|
||||||
|
transform: 'translateZ(0)', // 启用硬件加速
|
||||||
|
backfaceVisibility: 'hidden'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M 77.6 116.50001 Q 75.5 118.30001 72.75 118.600006 Q 70 118.90001 67.4 117.75001 Q 64.8 116.600006 63.1 114.200005 Q 61.4 112.00001 60.9 109.15001 Q 60.4 106.30001 60.8 101.700005 Q 61.1 97.8 63.3 93.850006 Q 65.5 89.90001 68.65 86.8 Q 71.8 83.70001 74.9 82.50001 Q 76.4 81.50001 77.45 81.55 Q 78.5 81.600006 80.7 81.8 Q 83 82.100006 84.15 82.70001 Q 85.3 83.3 86 84.3 Q 87.5 85.8 88 87.25001 Q 88.5 88.700005 88.5 90.100006 Q 88.100006 92.3 85.55 95.450005 Q 83 98.600006 78.9 100.600006 Q 76.7 101.3 74.1 101.200005 Q 71.5 101.100006 69.8 99.90001 Q 67.9 99.100006 67.3 100.15001 Q 66.7 101.200005 66.2 105.200005 Q 65.8 109.00001 66.8 110.75001 Q 67.8 112.50001 69.5 112.80001 Q 71.5 113.200005 72.65 113.350006 Q 73.8 113.50001 75.5 112.50001 Q 77.1 111.50001 77.5 111.350006 Q 77.9 111.200005 78.15 110.950005 Q 78.4 110.700005 79.4 109.700005 Q 80.1 109.00001 80.5 108.65001 Q 80.9 108.30001 81.4 108.30001 Q 81.9 108.30001 82.3 108.75001 Q 82.7 109.200005 83.100006 109.40001 Q 84.100006 109.40001 83.55 110.600006 Q 83 111.80001 81.4 113.450005 Q 79.8 115.100006 77.6 116.50001 ZM 77.5 95.3 Q 78.6 94.8 79.8 93.850006 Q 81 92.90001 81.85 91.75001 Q 82.7 90.600006 82.7 89.8 Q 82.7 88.40001 81.4 87.45001 Q 80.1 86.50001 77.5 87.20001 Q 76.4 87.50001 75 88.65001 Q 73.6 89.8 72.35 91.200005 Q 71.1 92.600006 70.35 93.700005 Q 69.6 94.8 69.8 95.00001 Q 70.1 95.50001 71.5 95.75001 Q 72.9 96.00001 74.65 95.90001 Q 76.4 95.8 77.5 95.3 Z"
|
d="M 77.6 116.50001 Q 75.5 118.30001 72.75 118.600006 Q 70 118.90001 67.4 117.75001 Q 64.8 116.600006 63.1 114.200005 Q 61.4 112.00001 60.9 109.15001 Q 60.4 106.30001 60.8 101.700005 Q 61.1 97.8 63.3 93.850006 Q 65.5 89.90001 68.65 86.8 Q 71.8 83.70001 74.9 82.50001 Q 76.4 81.50001 77.45 81.55 Q 78.5 81.600006 80.7 81.8 Q 83 82.100006 84.15 82.70001 Q 85.3 83.3 86 84.3 Q 87.5 85.8 88 87.25001 Q 88.5 88.700005 88.5 90.100006 Q 88.100006 92.3 85.55 95.450005 Q 83 98.600006 78.9 100.600006 Q 76.7 101.3 74.1 101.200005 Q 71.5 101.100006 69.8 99.90001 Q 67.9 99.100006 67.3 100.15001 Q 66.7 101.200005 66.2 105.200005 Q 65.8 109.00001 66.8 110.75001 Q 67.8 112.50001 69.5 112.80001 Q 71.5 113.200005 72.65 113.350006 Q 73.8 113.50001 75.5 112.50001 Q 77.1 111.50001 77.5 111.350006 Q 77.9 111.200005 78.15 110.950005 Q 78.4 110.700005 79.4 109.700005 Q 80.1 109.00001 80.5 108.65001 Q 80.9 108.30001 81.4 108.30001 Q 81.9 108.30001 82.3 108.75001 Q 82.7 109.200005 83.100006 109.40001 Q 84.100006 109.40001 83.55 110.600006 Q 83 111.80001 81.4 113.450005 Q 79.8 115.100006 77.6 116.50001 ZM 77.5 95.3 Q 78.6 94.8 79.8 93.850006 Q 81 92.90001 81.85 91.75001 Q 82.7 90.600006 82.7 89.8 Q 82.7 88.40001 81.4 87.45001 Q 80.1 86.50001 77.5 87.20001 Q 76.4 87.50001 75 88.65001 Q 73.6 89.8 72.35 91.200005 Q 71.1 92.600006 70.35 93.700005 Q 69.6 94.8 69.8 95.00001 Q 70.1 95.50001 71.5 95.75001 Q 72.9 96.00001 74.65 95.90001 Q 76.4 95.8 77.5 95.3 Z"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Template } from "interface/template";
|
||||||
|
|
||||||
const ErrorPage = () => {
|
export default new Template({}, ({ args }) => {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const fullText = "404 - 页面不见了 :(";
|
const fullText = "404 - 页面不见了 :(";
|
||||||
const typingSpeed = 100;
|
const typingSpeed = 100;
|
||||||
@ -38,6 +39,4 @@ const ErrorPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default ErrorPage;
|
|
||||||
|
@ -4,29 +4,57 @@ import { Button } from "@radix-ui/themes";
|
|||||||
|
|
||||||
const THEME_KEY = "theme-preference";
|
const THEME_KEY = "theme-preference";
|
||||||
|
|
||||||
|
// 添加这个脚本来预先设置主题,避免闪烁
|
||||||
|
const themeScript = `
|
||||||
|
(function() {
|
||||||
|
function getInitialTheme() {
|
||||||
|
const savedTheme = localStorage.getItem("${THEME_KEY}");
|
||||||
|
if (savedTheme) return savedTheme;
|
||||||
|
|
||||||
|
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
const theme = isDark ? "dark" : "light";
|
||||||
|
localStorage.setItem("${THEME_KEY}", theme);
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
document.documentElement.className = getInitialTheme();
|
||||||
|
})()
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ThemeScript = () => {
|
||||||
|
return <script dangerouslySetInnerHTML={{ __html: themeScript }} />;
|
||||||
|
};
|
||||||
|
|
||||||
export const ThemeModeToggle: React.FC = () => {
|
export const ThemeModeToggle: React.FC = () => {
|
||||||
const [mounted, setMounted] = useState(false);
|
const [isDark, setIsDark] = useState<boolean | null>(null);
|
||||||
const [isDark, setIsDark] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
if (typeof window !== 'undefined') {
|
||||||
const saved = localStorage.getItem(THEME_KEY);
|
const savedTheme = localStorage.getItem(THEME_KEY);
|
||||||
const initialTheme =
|
const initialIsDark = savedTheme === 'dark' || document.documentElement.className === 'dark';
|
||||||
saved ||
|
setIsDark(initialIsDark);
|
||||||
(window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
||||||
? "dark"
|
|
||||||
: "light");
|
|
||||||
setIsDark(initialTheme === "dark");
|
|
||||||
if (saved) {
|
|
||||||
document.documentElement.className = saved;
|
|
||||||
} else {
|
|
||||||
document.documentElement.className = initialTheme;
|
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.attributeName === 'class') {
|
||||||
|
const isDarkTheme = document.documentElement.className === 'dark';
|
||||||
|
setIsDark(isDarkTheme);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class']
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
|
if (isDark === null) return;
|
||||||
const newIsDark = !isDark;
|
const newIsDark = !isDark;
|
||||||
setIsDark(newIsDark);
|
setIsDark(newIsDark);
|
||||||
const newTheme = newIsDark ? "dark" : "light";
|
const newTheme = newIsDark ? "dark" : "light";
|
||||||
@ -34,7 +62,17 @@ export const ThemeModeToggle: React.FC = () => {
|
|||||||
localStorage.setItem(THEME_KEY, newTheme);
|
localStorage.setItem(THEME_KEY, newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (isDark === null) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
|
||||||
|
aria-label="Loading theme"
|
||||||
|
>
|
||||||
|
<MoonIcon className="w-full h-full" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -44,7 +82,7 @@ export const ThemeModeToggle: React.FC = () => {
|
|||||||
aria-label="Toggle theme"
|
aria-label="Toggle theme"
|
||||||
>
|
>
|
||||||
{isDark ? (
|
{isDark ? (
|
||||||
<SunIcon className="w-full h-full"/>
|
<SunIcon className="w-full h-full" />
|
||||||
) : (
|
) : (
|
||||||
<MoonIcon className="w-full h-full" />
|
<MoonIcon className="w-full h-full" />
|
||||||
)}
|
)}
|
||||||
@ -52,6 +90,13 @@ export const ThemeModeToggle: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新类型定义
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__THEME__?: "light" | "dark";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useThemeMode = () => {
|
export const useThemeMode = () => {
|
||||||
const [mode, setMode] = useState<"light" | "dark">("light");
|
const [mode, setMode] = useState<"light" | "dark">("light");
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ export class Layout {
|
|||||||
public element: (props: {
|
public element: (props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
args?: Serializable;
|
args?: Serializable;
|
||||||
|
onTouchStart?: (e: TouchEvent) => void;
|
||||||
|
onTouchEnd?: (e: TouchEvent) => void;
|
||||||
}) => React.ReactNode,
|
}) => React.ReactNode,
|
||||||
services?: {
|
services?: {
|
||||||
http?: HttpClient;
|
http?: HttpClient;
|
||||||
@ -20,7 +22,16 @@ export class Layout {
|
|||||||
this.capability = services?.capability || CapabilityService.getInstance();
|
this.capability = services?.capability || CapabilityService.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
render(props: { children: React.ReactNode; args?: Serializable }) {
|
render(props: {
|
||||||
return this.element(props);
|
children: React.ReactNode;
|
||||||
|
args?: Serializable;
|
||||||
|
onTouchStart?: (e: TouchEvent) => void;
|
||||||
|
onTouchEnd?: (e: TouchEvent) => void;
|
||||||
|
}) {
|
||||||
|
return this.element({
|
||||||
|
...props,
|
||||||
|
onTouchStart: props.onTouchStart,
|
||||||
|
onTouchEnd: props.onTouchEnd
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ export class Template {
|
|||||||
services?: {
|
services?: {
|
||||||
http?: HttpClient;
|
http?: HttpClient;
|
||||||
capability?: CapabilityService;
|
capability?: CapabilityService;
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
this.http = services?.http || HttpClient.getInstance();
|
this.http = services?.http || HttpClient.getInstance();
|
||||||
this.capability = services?.capability || CapabilityService.getInstance();
|
this.capability = services?.capability || CapabilityService.getInstance();
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
.animated-text {
|
.animated-text {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
transform: translateZ(0);
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
will-change: transform;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animated-text path {
|
.animated-text path {
|
||||||
@ -13,6 +20,8 @@
|
|||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
|
animation-play-state: running !important;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes logo-anim {
|
@keyframes logo-anim {
|
||||||
@ -70,3 +79,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.animated-text {
|
||||||
|
touch-action: manipulation;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,8 +25,8 @@ export default {
|
|||||||
progress: "progress 3s linear",
|
progress: "progress 3s linear",
|
||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
'-10': '-10',
|
"-10": "-10",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { Template } from "interface/template";
|
import { Template } from "interface/template";
|
||||||
import { Container, Heading, Text, Box, Flex, Link } from "@radix-ui/themes";
|
import { Container, Heading, Text, Box, Flex, Link } from "@radix-ui/themes";
|
||||||
import { GitHubLogoIcon, TwitterLogoIcon, LinkedInLogoIcon, EnvelopeClosedIcon } from "@radix-ui/react-icons";
|
import {
|
||||||
|
GitHubLogoIcon,
|
||||||
|
TwitterLogoIcon,
|
||||||
|
LinkedInLogoIcon,
|
||||||
|
EnvelopeClosedIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
import { ParticleImage } from "hooks/ParticleImage";
|
import { ParticleImage } from "hooks/ParticleImage";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { gsap } from "gsap";
|
import { gsap } from "gsap";
|
||||||
@ -9,23 +14,23 @@ const socialLinks = [
|
|||||||
{
|
{
|
||||||
icon: <GitHubLogoIcon className="w-5 h-5" />,
|
icon: <GitHubLogoIcon className="w-5 h-5" />,
|
||||||
url: "https://github.com/yourusername",
|
url: "https://github.com/yourusername",
|
||||||
label: "GitHub"
|
label: "GitHub",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <TwitterLogoIcon className="w-5 h-5" />,
|
icon: <TwitterLogoIcon className="w-5 h-5" />,
|
||||||
url: "https://twitter.com/yourusername",
|
url: "https://twitter.com/yourusername",
|
||||||
label: "Twitter"
|
label: "Twitter",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <LinkedInLogoIcon className="w-5 h-5" />,
|
icon: <LinkedInLogoIcon className="w-5 h-5" />,
|
||||||
url: "https://linkedin.com/in/yourusername",
|
url: "https://linkedin.com/in/yourusername",
|
||||||
label: "LinkedIn"
|
label: "LinkedIn",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <EnvelopeClosedIcon className="w-5 h-5" />,
|
icon: <EnvelopeClosedIcon className="w-5 h-5" />,
|
||||||
url: "mailto:your.email@example.com",
|
url: "mailto:your.email@example.com",
|
||||||
label: "Email"
|
label: "Email",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const skills = [
|
const skills = [
|
||||||
@ -33,12 +38,10 @@ const skills = [
|
|||||||
{ name: "TypeScript", level: 85 },
|
{ name: "TypeScript", level: 85 },
|
||||||
{ name: "Node.js", level: 80 },
|
{ name: "Node.js", level: 80 },
|
||||||
{ name: "Three.js", level: 75 },
|
{ name: "Three.js", level: 75 },
|
||||||
{ name: "Python", level: 70 }
|
{ name: "Python", level: 70 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default new Template(
|
export default new Template({}, ({ http, args }) => {
|
||||||
{},
|
|
||||||
({ http, args }) => {
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
@ -52,7 +55,7 @@ export default new Template(
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 1,
|
duration: 1,
|
||||||
ease: "power3.out",
|
ease: "power3.out",
|
||||||
stagger: 0.2
|
stagger: 0.2,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 技能条动画
|
// 技能条动画
|
||||||
@ -64,7 +67,7 @@ export default new Template(
|
|||||||
scrollTrigger: {
|
scrollTrigger: {
|
||||||
trigger: ".skills-section",
|
trigger: ".skills-section",
|
||||||
start: "top center+=100",
|
start: "top center+=100",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 社交链接动画
|
// 社交链接动画
|
||||||
@ -73,7 +76,7 @@ export default new Template(
|
|||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 0.5,
|
duration: 0.5,
|
||||||
ease: "back.out(1.7)",
|
ease: "back.out(1.7)",
|
||||||
stagger: 0.1
|
stagger: 0.1,
|
||||||
});
|
});
|
||||||
}, containerRef);
|
}, containerRef);
|
||||||
|
|
||||||
@ -83,15 +86,11 @@ export default new Template(
|
|||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`transition-opacity duration-1000 ${isVisible ? 'opacity-100' : 'opacity-0'}`}
|
className={`transition-opacity duration-1000 ${isVisible ? "opacity-100" : "opacity-0"}`}
|
||||||
>
|
>
|
||||||
<Box className="max-w-4xl mx-auto px-4">
|
<Box className="max-w-4xl mx-auto px-4">
|
||||||
{/* 头部个人介绍 */}
|
{/* 头部个人介绍 */}
|
||||||
<Flex
|
<Flex direction="column" align="center" className="text-center mb-16">
|
||||||
direction="column"
|
|
||||||
align="center"
|
|
||||||
className="text-center mb-16"
|
|
||||||
>
|
|
||||||
<Box className="w-40 h-40 mb-8 relative">
|
<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">
|
<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" />
|
<ParticleImage src="/path/to/your/avatar.jpg" />
|
||||||
@ -112,7 +111,9 @@ export default new Template(
|
|||||||
|
|
||||||
{/* 技能展示 */}
|
{/* 技能展示 */}
|
||||||
<Box className="skills-section mb-16">
|
<Box className="skills-section mb-16">
|
||||||
<Heading size="4" className="mb-8">专业技能</Heading>
|
<Heading size="4" className="mb-8">
|
||||||
|
专业技能
|
||||||
|
</Heading>
|
||||||
<Flex direction="column" gap="4">
|
<Flex direction="column" gap="4">
|
||||||
{skills.map((skill) => (
|
{skills.map((skill) => (
|
||||||
<Box key={skill.name}>
|
<Box key={skill.name}>
|
||||||
@ -152,5 +153,4 @@ export default new Template(
|
|||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import { Template } from "interface/template";
|
import { Template } from "interface/template";
|
||||||
import { Container, Heading, Text, Flex, Card, Button } from "@radix-ui/themes";
|
import { Container, Heading, Text, Flex, Card, Button } from "@radix-ui/themes";
|
||||||
import { CalendarIcon, PersonIcon, ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
PersonIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
import { Post } from "interface/post";
|
import { Post } from "interface/post";
|
||||||
import { useMemo} from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { ImageLoader } from "hooks/ParticleImage";
|
import { ImageLoader } from "hooks/ParticleImage";
|
||||||
|
|
||||||
@ -20,64 +25,84 @@ const mockArticles: Post[] = [
|
|||||||
status: "published",
|
status: "published",
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
createdAt: new Date("2024-03-15"),
|
createdAt: new Date("2024-03-15"),
|
||||||
updatedAt: new Date("2024-03-15")
|
updatedAt: new Date("2024-03-15"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "React 18 新特性详解",
|
title: "React 18 新特性详解",
|
||||||
content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
|
content:
|
||||||
|
"React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...",
|
||||||
authorName: "李四",
|
authorName: "李四",
|
||||||
publishedAt: new Date("2024-03-14"),
|
publishedAt: new Date("2024-03-14"),
|
||||||
coverImage: "https://avatars.githubusercontent.com/u/2?v=4",
|
coverImage: "https://haowallpaper.com/link/common/file/previewFileIm",
|
||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
status: "published",
|
status: "published",
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
createdAt: new Date("2024-03-14"),
|
createdAt: new Date("2024-03-14"),
|
||||||
updatedAt: new Date("2024-03-14")
|
updatedAt: new Date("2024-03-14"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "JavaScript 性能优化技巧",
|
title: "JavaScript 性能优化技巧",
|
||||||
content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
content:
|
||||||
|
"在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
||||||
authorName: "王五",
|
authorName: "王五",
|
||||||
publishedAt: new Date("2024-03-13"),
|
publishedAt: new Date("2024-03-13"),
|
||||||
coverImage: "https://avatars.githubusercontent.com/u/",
|
coverImage: "https://haowallpaper.com/link/common/file/previewFileImg/15789130517090624",
|
||||||
metaKeywords: "",
|
metaKeywords: "",
|
||||||
metaDescription: "",
|
metaDescription: "",
|
||||||
status: "published",
|
status: "published",
|
||||||
isEditor: false,
|
isEditor: false,
|
||||||
createdAt: new Date("2024-03-13"),
|
createdAt: new Date("2024-03-13"),
|
||||||
updatedAt: new Date("2024-03-13")
|
updatedAt: new Date("2024-03-13"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "JavaScript 性能优化技巧",
|
||||||
|
content:
|
||||||
|
"在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...",
|
||||||
|
authorName: "田六",
|
||||||
|
publishedAt: new Date("2024-03-13"),
|
||||||
|
coverImage: "https://avatars.githubusercontent.com/u/2?v=4",
|
||||||
|
metaKeywords: "",
|
||||||
|
metaDescription: "",
|
||||||
|
status: "published",
|
||||||
|
isEditor: false,
|
||||||
|
createdAt: new Date("2024-03-13"),
|
||||||
|
updatedAt: new Date("2024-03-13"),
|
||||||
},
|
},
|
||||||
// 可以添加更多模拟文章
|
// 可以添加更多模拟文章
|
||||||
];
|
];
|
||||||
|
|
||||||
// 修改颜色组合数组,增加更多颜色选项
|
// 修改颜色组合数组,增加更多颜色选项
|
||||||
const colorSchemes = [
|
const colorSchemes = [
|
||||||
{ bg: 'bg-blue-100', text: 'text-blue-600' },
|
{ bg: "bg-blue-100", text: "text-blue-600" },
|
||||||
{ bg: 'bg-green-100', text: 'text-green-600' },
|
{ bg: "bg-green-100", text: "text-green-600" },
|
||||||
{ bg: 'bg-purple-100', text: 'text-purple-600' },
|
{ bg: "bg-purple-100", text: "text-purple-600" },
|
||||||
{ bg: 'bg-pink-100', text: 'text-pink-600' },
|
{ bg: "bg-pink-100", text: "text-pink-600" },
|
||||||
{ bg: 'bg-orange-100', text: 'text-orange-600' },
|
{ bg: "bg-orange-100", text: "text-orange-600" },
|
||||||
{ bg: 'bg-teal-100', text: 'text-teal-600' },
|
{ bg: "bg-teal-100", text: "text-teal-600" },
|
||||||
{ bg: 'bg-red-100', text: 'text-red-600' },
|
{ bg: "bg-red-100", text: "text-red-600" },
|
||||||
{ bg: 'bg-indigo-100', text: 'text-indigo-600' },
|
{ bg: "bg-indigo-100", text: "text-indigo-600" },
|
||||||
{ bg: 'bg-yellow-100', text: 'text-yellow-600' },
|
{ bg: "bg-yellow-100", text: "text-yellow-600" },
|
||||||
{ bg: 'bg-cyan-100', text: 'text-cyan-600' },
|
{ bg: "bg-cyan-100", text: "text-cyan-600" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const categories = ['前端开发', '后端开发', 'UI设计', '移动开发', '人工智能'];
|
const categories = ["前端开发", "后端开发", "UI设计", "移动开发", "人工智能"];
|
||||||
const tags = ['React', 'TypeScript', 'Vue', 'Node.js', 'Flutter', 'Python', 'Docker'];
|
const tags = [
|
||||||
|
"React",
|
||||||
|
"TypeScript",
|
||||||
|
"Vue",
|
||||||
|
"Node.js",
|
||||||
|
"Flutter",
|
||||||
|
"Python",
|
||||||
|
"Docker",
|
||||||
|
];
|
||||||
|
|
||||||
// 定义 SlideGeometry 类
|
// 定义 SlideGeometry 类
|
||||||
|
|
||||||
|
export default new Template({}, ({ http, args }) => {
|
||||||
|
|
||||||
export default new Template(
|
|
||||||
{
|
|
||||||
},
|
|
||||||
({ http, args }) => {
|
|
||||||
const articleData = useMemo(() => {
|
const articleData = useMemo(() => {
|
||||||
return mockArticles.map((article) => {
|
return mockArticles.map((article) => {
|
||||||
// 使用更复杂的散列函数来生成看起来更随机的索引
|
// 使用更复杂的散列函数来生成看起来更随机的索引
|
||||||
@ -85,51 +110,56 @@ export default new Template(
|
|||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const char = str.charCodeAt(i);
|
const char = str.charCodeAt(i);
|
||||||
hash = ((hash << 5) - hash) + char;
|
hash = (hash << 5) - hash + char;
|
||||||
hash = hash & hash;
|
hash = hash & hash;
|
||||||
}
|
}
|
||||||
return Math.abs(hash);
|
return Math.abs(hash);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用文章的不同属性来生成索引
|
// 使用文章的不同属性来生成索引
|
||||||
const categoryIndex = hash(article.title + article.id.toString()) % categories.length;
|
const categoryIndex =
|
||||||
const colorIndex = hash(article.authorName + article.id.toString()) % colorSchemes.length;
|
hash(article.title + article.id.toString()) % categories.length;
|
||||||
|
const colorIndex =
|
||||||
|
hash(article.authorName + article.id.toString()) % colorSchemes.length;
|
||||||
|
|
||||||
// 为标签生成不同的索引
|
// 为标签生成不同的索引
|
||||||
const tagIndices = tags
|
const tagIndices = tags
|
||||||
.map((_, index) => ({
|
.map((_, index) => ({
|
||||||
index,
|
index,
|
||||||
sort: hash(article.title + index.toString() + article.id.toString())
|
sort: hash(article.title + index.toString() + article.id.toString()),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.sort - b.sort)
|
.sort((a, b) => a.sort - b.sort)
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
.map(item => item.index);
|
.map((item) => item.index);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...article,
|
...article,
|
||||||
category: categories[categoryIndex],
|
category: categories[categoryIndex],
|
||||||
categoryColor: colorSchemes[colorIndex],
|
categoryColor: colorSchemes[colorIndex],
|
||||||
tags: tagIndices.map(index => ({
|
tags: tagIndices.map((index) => ({
|
||||||
name: tags[index],
|
name: tags[index],
|
||||||
color: colorSchemes[hash(tags[index] + article.id.toString()) % colorSchemes.length]
|
color:
|
||||||
}))
|
colorSchemes[
|
||||||
|
hash(tags[index] + article.id.toString()) % colorSchemes.length
|
||||||
|
],
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="3" className="pt-2 pb-4 md:pb-6">
|
<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-2 gap-4 md:gap-6 px-4 md:px-0">
|
||||||
{articleData.map(article => (
|
{articleData.map((article) => (
|
||||||
<Card
|
<Card
|
||||||
key={article.id}
|
key={article.id}
|
||||||
className="group cursor-pointer hover:shadow-lg transition-all duration-300 border border-[--gray-5] hover:border-[--accent-8] relative overflow-visible"
|
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-5`}>
|
<div className={`p-5 relative flex gap-4`}>
|
||||||
<ImageLoader
|
<ImageLoader
|
||||||
src={article.coverImage}
|
src={article.coverImage}
|
||||||
alt={article.title || ''}
|
alt={article.title || ""}
|
||||||
className="group-hover:scale-105 transition-transform duration-500 relative z-10"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<div className="flex-1 flex flex-col min-w-0">
|
||||||
@ -148,17 +178,23 @@ export default new Template(
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Flex gap="2" align="center" className="text-[--gray-11] mb-3 flex-wrap">
|
<Flex
|
||||||
|
gap="2"
|
||||||
|
align="center"
|
||||||
|
className="text-[--gray-11] mb-3 flex-wrap"
|
||||||
|
>
|
||||||
<CalendarIcon className="w-3 h-3" />
|
<CalendarIcon className="w-3 h-3" />
|
||||||
<Text size="1">
|
<Text size="1">
|
||||||
{article.publishedAt?.toLocaleDateString('zh-CN', {
|
{article.publishedAt?.toLocaleDateString("zh-CN", {
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: 'long',
|
month: "long",
|
||||||
day: 'numeric'
|
day: "numeric",
|
||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<span className="mx-1">·</span>
|
<span className="mx-1">·</span>
|
||||||
<Text size="1" weight="medium">{article.authorName}</Text>
|
<Text size="1" weight="medium">
|
||||||
|
{article.authorName}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Text className="text-[--gray-11] text-xs md:text-sm line-clamp-2 md:line-clamp-3 leading-relaxed">
|
<Text className="text-[--gray-11] text-xs md:text-sm line-clamp-2 md:line-clamp-3 leading-relaxed">
|
||||||
@ -166,7 +202,7 @@ export default new Template(
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Flex gap="2" className="mt-auto pt-3 flex-wrap">
|
<Flex gap="2" className="mt-auto pt-3 flex-wrap">
|
||||||
{article.tags.map(tag => (
|
{article.tags.map((tag) => (
|
||||||
<Text
|
<Text
|
||||||
key={tag.name}
|
key={tag.name}
|
||||||
size="1"
|
size="1"
|
||||||
@ -183,11 +219,7 @@ export default new Template(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Flex justify="center" align="center" gap="2" className="mt-8">
|
<Flex justify="center" align="center" gap="2" className="mt-8">
|
||||||
<Button
|
<Button variant="soft" className="group" disabled>
|
||||||
variant="soft"
|
|
||||||
className="group"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" />
|
<ChevronLeftIcon className="w-4 h-4 group-hover:-translate-x-0.5 transition-transform" />
|
||||||
上一页
|
上一页
|
||||||
</Button>
|
</Button>
|
||||||
@ -205,15 +237,11 @@ export default new Template(
|
|||||||
<Button variant="soft">10</Button>
|
<Button variant="soft">10</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Button
|
<Button variant="soft" className="group">
|
||||||
variant="soft"
|
|
||||||
className="group"
|
|
||||||
>
|
|
||||||
下一页
|
下一页
|
||||||
<ChevronRightIcon className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
<ChevronRightIcon className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Layout } from "interface/layout";
|
import { Layout } from "interface/layout";
|
||||||
import { ThemeModeToggle } from "hooks/themeMode";
|
import { ThemeModeToggle } from "hooks/themeMode";
|
||||||
import { Echoes } from "hooks/echoes";
|
import { Echoes } from "hooks/echoes";
|
||||||
import { Container, Flex, Box, Link, TextField } from "@radix-ui/themes";
|
import { Container, Flex, Box, Link, TextField, Button } from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
HamburgerMenuIcon,
|
HamburgerMenuIcon,
|
||||||
@ -59,13 +59,13 @@ export default new Layout(({ children, args }) => {
|
|||||||
panelBackground="solid"
|
panelBackground="solid"
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="min-h-screen flex flex-col"
|
className="h-screen flex flex-col overflow-hidden"
|
||||||
id="nav"
|
id="nav"
|
||||||
>
|
>
|
||||||
{/* 导航栏 */}
|
{/* 导航栏 */}
|
||||||
<Box
|
<Box
|
||||||
asChild
|
asChild
|
||||||
className="fixed top-0 w-full backdrop-blur-sm border-b border-[--gray-a5] z-60"
|
className="w-full backdrop-blur-sm border-b border-[--gray-a5] z-60"
|
||||||
>
|
>
|
||||||
<nav>
|
<nav>
|
||||||
<Container size="4">
|
<Container size="4">
|
||||||
@ -98,14 +98,14 @@ export default new Layout(({ children, args }) => {
|
|||||||
size="2"
|
size="2"
|
||||||
variant="surface"
|
variant="surface"
|
||||||
placeholder="搜索..."
|
placeholder="搜索..."
|
||||||
className="w-[240px] [&_input]:pl-3"
|
className="w-[240px] [&_input]:pl-3 hover:opacity-70 transition-opacity"
|
||||||
id="search"
|
id="search"
|
||||||
>
|
>
|
||||||
<TextField.Slot
|
<TextField.Slot
|
||||||
side="right"
|
side="right"
|
||||||
className="p-2"
|
className="p-2"
|
||||||
>
|
>
|
||||||
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-a12]" />
|
<MagnifyingGlassIcon className="h-4 w-4 text-[--gray-a11]" />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
@ -115,25 +115,22 @@ export default new Layout(({ children, args }) => {
|
|||||||
|
|
||||||
<DropdownMenuPrimitive.Root>
|
<DropdownMenuPrimitive.Root>
|
||||||
<DropdownMenuPrimitive.Trigger asChild>
|
<DropdownMenuPrimitive.Trigger asChild>
|
||||||
<button className="hover:opacity-70 transition-opacity cursor-pointer">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-10 h-10 p-0 hover:opacity-70 transition-opacity flex items-center justify-center"
|
||||||
|
>
|
||||||
{loginState ? (
|
{loginState ? (
|
||||||
<AvatarIcon className="w-6 h-6 text-[--gray-a12]" />
|
<AvatarIcon className="w-6 h-6 text-[--gray-a11]" />
|
||||||
) : (
|
) : (
|
||||||
<PersonIcon className="w-6 h-6 text-[--gray-a12]" />
|
<PersonIcon className="w-6 h-6 text-[--gray-a11]" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</DropdownMenuPrimitive.Trigger>
|
</DropdownMenuPrimitive.Trigger>
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<Theme
|
|
||||||
grayColor="gray"
|
|
||||||
accentColor="gray"
|
|
||||||
radius="large"
|
|
||||||
panelBackground="solid"
|
|
||||||
>
|
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
className="mt-3 p-1 min-w-[180px] rounded-md bg-[--color-background] 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-[--color-panel] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
|
||||||
>
|
>
|
||||||
{loginState ? (
|
{loginState ? (
|
||||||
<>
|
<>
|
||||||
@ -154,7 +151,6 @@ export default new Layout(({ children, args }) => {
|
|||||||
</DropdownMenuPrimitive.Item>
|
</DropdownMenuPrimitive.Item>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuPrimitive.Content>
|
</DropdownMenuPrimitive.Content>
|
||||||
</Theme>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
</DropdownMenuPrimitive.Root>
|
</DropdownMenuPrimitive.Root>
|
||||||
</Box>
|
</Box>
|
||||||
@ -168,13 +164,16 @@ export default new Layout(({ children, args }) => {
|
|||||||
onOpenChange={setMoreState}
|
onOpenChange={setMoreState}
|
||||||
>
|
>
|
||||||
<DropdownMenuPrimitive.Trigger asChild>
|
<DropdownMenuPrimitive.Trigger asChild>
|
||||||
<button className="hover:opacity-70 transition-opacity p-2">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-10 h-10 p-0 hover:opacity-70 transition-opacity flex items-center justify-center"
|
||||||
|
>
|
||||||
{moreState ? (
|
{moreState ? (
|
||||||
<Cross1Icon className="h-6 w-6 text-[--gray-a12]" />
|
<Cross1Icon className="h-5 w-5 text-[--gray-a11]" />
|
||||||
) : (
|
) : (
|
||||||
<HamburgerMenuIcon className="h-6 w-6 text-[--gray-a12]" />
|
<HamburgerMenuIcon className="h-5 w-5 text-[--gray-a11]" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</DropdownMenuPrimitive.Trigger>
|
</DropdownMenuPrimitive.Trigger>
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<Theme
|
<Theme
|
||||||
@ -186,7 +185,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
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"
|
className="mt-2 p-3 min-w-[280px] rounded-md bg-[--color-panel] border border-[--gray-a5] shadow-lg animate-in fade-in slide-in-from-top-2"
|
||||||
>
|
>
|
||||||
<Box className="flex flex-col gap-2">
|
<Box className="flex flex-col gap-2">
|
||||||
{parse(navString)}
|
{parse(navString)}
|
||||||
@ -215,9 +214,11 @@ export default new Layout(({ children, args }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 主题切换按钮 */}
|
{/* 主题切换按钮 */}
|
||||||
|
<Box className="flex items-center">
|
||||||
<Box className="w-6 h-6 flex items-center justify-center">
|
<Box className="w-6 h-6 flex items-center justify-center">
|
||||||
<ThemeModeToggle />
|
<ThemeModeToggle />
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
@ -225,7 +226,7 @@ export default new Layout(({ children, args }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 主要内容区域 */}
|
{/* 主要内容区域 */}
|
||||||
<Box className="flex-1 w-full mt-16">
|
<Box className="flex-1 w-full overflow-auto">
|
||||||
<Container
|
<Container
|
||||||
size="4"
|
size="4"
|
||||||
className="py-8"
|
className="py-8"
|
||||||
|
@ -7,10 +7,10 @@ const themeConfig: ThemeConfig = {
|
|||||||
description: "一个简约风格的博客主题",
|
description: "一个简约风格的博客主题",
|
||||||
author: "lsy",
|
author: "lsy",
|
||||||
configuration: {
|
configuration: {
|
||||||
"nav": {
|
nav: {
|
||||||
title: "导航配置",
|
title: "导航配置",
|
||||||
data: '<a href="h">你好</a> <a href="h">不好</a>'
|
data: '<a href="h">你好</a> <a href="h">不好</a>',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
layout: "layout.tsx",
|
layout: "layout.tsx",
|
||||||
templates: new Map([
|
templates: new Map([
|
||||||
@ -24,7 +24,7 @@ const themeConfig: ThemeConfig = {
|
|||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
|
|
||||||
routes: new Map<string, string>([])
|
routes: new Map<string, string>([]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default themeConfig;
|
export default themeConfig;
|
||||||
|
Loading…
Reference in New Issue
Block a user