This commit is contained in:
star7th
2025-04-10 18:06:45 +08:00
commit 54650cd944
154 changed files with 29090 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
'use client';
import React, { useState, useEffect } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowUp } from '@fortawesome/free-solid-svg-icons';
interface BackToTopProps {
scrollThreshold?: number; // 显示按钮的滚动阈值(像素)
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; // 按钮位置
offset?: number; // 与屏幕边缘的距离(像素)
containerRef?: React.RefObject<HTMLElement>; // 滚动容器引用,默认为window
zIndex?: number; // 自定义z-index
size?: 'small' | 'medium' | 'large'; // 按钮大小
}
/**
* 回到顶部组件
*
* 用法:
* 1. 基本用法: <BackToTop />
* 2. 自定义: <BackToTop position="bottom-left" offset={30} size="large" />
* 3. 对特定容器:
* const containerRef = useRef<HTMLDivElement>(null);
* <div ref={containerRef} style={{height: '500px', overflow: 'auto'}}>
* 内容
* <BackToTop containerRef={containerRef} />
* </div>
*/
const BackToTop: React.FC<BackToTopProps> = ({
scrollThreshold = 300,
position = 'bottom-right',
offset = 20,
containerRef,
zIndex = 40,
size = 'medium'
}) => {
const [isVisible, setIsVisible] = useState(false);
// 生成位置样式
const getPositionStyle = () => {
const positionStyle: React.CSSProperties = {};
if (position.includes('bottom')) {
positionStyle.bottom = offset;
} else {
positionStyle.top = offset;
}
if (position.includes('right')) {
positionStyle.right = offset;
} else {
positionStyle.left = offset;
}
return positionStyle;
};
// 根据大小获取样式类名
const getSizeClassName = () => {
switch (size) {
case 'small':
return 'w-8 h-8';
case 'large':
return 'w-12 h-12';
case 'medium':
default:
return 'w-10 h-10';
}
};
// 处理滚动事件
const handleScroll = () => {
if (containerRef && containerRef.current) {
setIsVisible(containerRef.current.scrollTop > scrollThreshold);
} else {
setIsVisible(window.scrollY > scrollThreshold);
}
};
// 回到顶部
const scrollToTop = () => {
if (containerRef && containerRef.current) {
containerRef.current.scrollTo({
top: 0,
behavior: 'smooth'
});
} else {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
};
// 设置滚动监听
useEffect(() => {
const scrollElement = containerRef?.current || window;
scrollElement.addEventListener('scroll', handleScroll);
// 初始检查
handleScroll();
return () => {
scrollElement.removeEventListener('scroll', handleScroll);
};
}, [containerRef, scrollThreshold]);
// 如果不可见,不渲染
if (!isVisible) return null;
const positionStyle = getPositionStyle();
return (
<button
onClick={scrollToTop}
className={`
fixed
${getSizeClassName()}
bg-gradient-to-r from-[#6366F1] to-[#8B5CF6]
text-white
rounded-full
flex
items-center
justify-center
shadow-lg
hover:shadow-xl
transition-all
duration-300
hover:scale-110
focus:outline-none
focus:ring-2
focus:ring-[#6366F1]
focus:ring-opacity-50
`}
style={{
...positionStyle,
zIndex
}}
aria-label="回到顶部"
title="回到顶部"
>
<FontAwesomeIcon icon={faArrowUp} />
</button>
);
};
export default BackToTop;
+45
View File
@@ -0,0 +1,45 @@
'use client';
import { useEffect, useRef } from 'react';
import { createJSONEditor } from 'vanilla-jsoneditor';
import type { JSONEditorPropsOptional } from 'vanilla-jsoneditor';
interface JsonEditorProps extends JSONEditorPropsOptional {
className?: string;
}
const JsonEditor = (props: JsonEditorProps) => {
const { className, ...editorProps } = props;
const refContainer = useRef<HTMLDivElement>(null);
const refEditor = useRef<ReturnType<typeof createJSONEditor> | null>(null);
useEffect(() => {
// 确保只在客户端渲染
if (typeof window !== 'undefined' && refContainer.current && !refEditor.current) {
// 创建编辑器
refEditor.current = createJSONEditor({
target: refContainer.current,
props: editorProps
});
}
return () => {
// 销毁编辑器
if (refEditor.current) {
refEditor.current.destroy();
refEditor.current = null;
}
};
}, []);
// 更新props
useEffect(() => {
if (refEditor.current) {
refEditor.current.updateProps(editorProps);
}
}, [editorProps]);
return <div ref={refContainer} className={className}></div>;
};
export default JsonEditor;
+70
View File
@@ -0,0 +1,70 @@
'use client';
import React, { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLanguage } from '@fortawesome/free-solid-svg-icons';
import { useLanguage } from '@/context/LanguageContext';
import { Language } from '@/config/i18n';
export default function LanguageToggle() {
const { language, changeLanguage, t } = useLanguage();
const [showDropdown, setShowDropdown] = useState(false);
// 切换下拉菜单显示状态
const toggleDropdown = () => {
setShowDropdown(prev => !prev);
};
// 选择语言
const selectLanguage = (lang: Language) => {
changeLanguage(lang);
setShowDropdown(false);
};
return (
<div className="relative">
<button
className="btn-secondary w-10 h-10 rounded-full flex items-center justify-center group relative"
onClick={toggleDropdown}
aria-label={t('common.language.title')}
title={t('common.language.title')}
>
<FontAwesomeIcon
icon={faLanguage}
className="text-[rgb(var(--color-primary))] text-xl"
/>
<span className="absolute left-1/2 -translate-x-1/2 top-full mt-2 px-2 py-1 rounded-md whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity duration-300 text-sm z-10"
style={{
backgroundColor: 'rgb(var(--color-bg-secondary))',
color: 'rgb(var(--color-text-primary))'
}}>
{t('common.language.title')}
</span>
</button>
{/* 语言选择下拉菜单 */}
{showDropdown && (
<div
className="absolute right-0 top-full mt-2 py-2 w-32 bg-[rgb(var(--color-bg-card))] shadow-lg rounded-lg border border-[rgba(var(--color-primary),0.2)] z-50"
style={{
backgroundColor: 'rgb(var(--color-bg-card))',
border: '1px solid rgba(var(--color-primary), 0.2)'
}}
>
<button
className={`w-full text-left px-4 py-2 hover:bg-[rgba(var(--color-primary),0.1)] transition-colors duration-200 ${language === 'zh' ? 'text-[rgb(var(--color-primary))] font-medium' : ''}`}
onClick={() => selectLanguage('zh')}
>
{t('common.language.zh')}
</button>
<button
className={`w-full text-left px-4 py-2 hover:bg-[rgba(var(--color-primary),0.1)] transition-colors duration-200 ${language === 'en' ? 'text-[rgb(var(--color-primary))] font-medium' : ''}`}
onClick={() => selectLanguage('en')}
>
{t('common.language.en')}
</button>
</div>
)}
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
'use client';
import React, { useRef, useEffect } from 'react';
interface MarkdownPreviewProps {
htmlContent: string;
}
export default function MarkdownPreview({ htmlContent }: MarkdownPreviewProps) {
const previewRef = useRef<HTMLDivElement>(null);
// 在渲染后处理所有可能的脚本和危险内容
useEffect(() => {
if (!previewRef.current) return;
const container = previewRef.current;
// 禁用所有链接默认行为
const links = container.querySelectorAll('a');
links.forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
// 阻止链接点击默认行为,除非特别需要允许
link.addEventListener('click', (e) => {
e.preventDefault();
});
});
// 移除所有脚本标签
const scripts = container.querySelectorAll('script');
scripts.forEach(script => script.remove());
// 处理所有iframe,确保它们有sandbox属性
const iframes = container.querySelectorAll('iframe');
iframes.forEach(iframe => {
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.setAttribute('loading', 'lazy');
});
}, [htmlContent]);
return (
<div
className="markdown-preview-container prose prose-invert"
ref={previewRef}
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
);
}
+66
View File
@@ -0,0 +1,66 @@
'use client';
import React, { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLightbulb } from '@fortawesome/free-solid-svg-icons';
import { faLightbulb as farLightbulb } from '@fortawesome/free-regular-svg-icons';
import { useLanguage } from '@/context/LanguageContext';
export default function ThemeToggle() {
const [theme, setTheme] = useState('dark');
const [mounted, setMounted] = useState(false);
const { t } = useLanguage();
// 组件挂载后执行
useEffect(() => {
setMounted(true);
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}
}, []);
// 切换主题
const toggleTheme = () => {
const newTheme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
// 保存到本地存储并设置 data-theme 属性
localStorage.setItem('theme', newTheme);
document.documentElement.setAttribute('data-theme', newTheme);
};
// 如果组件尚未挂载,返回空白以避免服务器/客户端不匹配
if (!mounted) return null;
return (
<button
className="btn-secondary w-10 h-10 rounded-full flex items-center justify-center group relative overflow-hidden"
onClick={toggleTheme}
aria-label={theme === 'dark' ? t('common.theme.light') : t('common.theme.dark')}
title={theme === 'dark' ? t('common.theme.light') : t('common.theme.dark')}
>
{theme === 'dark' ? (
// 灯泡图标 - 深色模式下显示(实心灯泡表示可以"点亮")
<FontAwesomeIcon
icon={faLightbulb}
className="text-[rgb(var(--color-warning))] text-xl"
/>
) : (
// 灯泡轮廓图标 - 浅色模式下显示(空心灯泡表示可以"关闭")
<FontAwesomeIcon
icon={farLightbulb}
className="text-[rgb(var(--color-primary-light))] text-xl"
/>
)}
<span className="absolute left-1/2 -translate-x-1/2 top-full mt-2 px-2 py-1 rounded-md whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity duration-300 text-sm z-10"
style={{
backgroundColor: 'rgb(var(--color-bg-secondary))',
color: 'rgb(var(--color-text-primary))'
}}>
{theme === 'dark' ? t('common.theme.light') : t('common.theme.dark')}
</span>
</button>
);
}
+46
View File
@@ -0,0 +1,46 @@
'use client';
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import { useRouter } from 'next/navigation';
import { useLanguage } from '@/context/LanguageContext';
interface ToolHeaderProps {
title: string;
description: string;
icon: IconDefinition;
toolCode: string;
}
export default function ToolHeader({ title, description, icon, toolCode }: ToolHeaderProps) {
const router = useRouter();
const { t } = useLanguage();
// 返回首页
const goBack = () => {
router.push('/');
};
return (
<header className="flex items-center gap-4 mb-8 rounded-lg p-4 shadow-md border">
<button
className="btn-secondary px-3 py-2"
onClick={goBack}
>
{t('common.backToHome')}
</button>
<div className="flex items-center gap-2">
<div className="icon-container w-10 h-10 flex-shrink-0">
<FontAwesomeIcon icon={icon} className="icon" />
</div>
<div>
<h1 className="text-xl font-bold">{title || t(`tools.${toolCode}.title`)}</h1>
<p className="text-sm"
style={{color: 'rgb(var(--color-text-secondary))'}}
>{description || t(`tools.${toolCode}.description`)}</p>
</div>
</div>
</header>
);
}