前端重构实现

This commit is contained in:
2025-12-22 16:38:06 +08:00
parent cd6d995d5a
commit 26574e3db7
54 changed files with 8976 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
import { motion } from 'framer-motion';
/**
* Loader 组件
* 全屏加载动画,包含旋转圆环和文字
* @param {Object} props
* @param {string} props.text - 加载文字
*/
const Loader = ({ text = '载入生命序列...' }) => {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4 }}
className="fixed inset-0 flex flex-col items-center justify-center z-[200] bg-[#0a0c10]"
>
{/* 旋转圆环 */}
<div className="w-16 h-16 border-2 border-orange-200/20 border-t-orange-200 rounded-full animate-spin mb-4" />
{/* 加载文字 */}
<p className="text-xs tracking-[0.3em] text-white/40 uppercase">
{text}
</p>
</motion.div>
);
};
export default Loader;
+87
View File
@@ -0,0 +1,87 @@
import * as Dialog from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
/**
* Modal 组件
* 使用 Radix UI Dialog 实现的模态弹窗
* @param {Object} props
* @param {boolean} props.isOpen - 是否打开
* @param {Function} props.onClose - 关闭回调
* @param {React.ReactNode} props.children - 子元素
* @param {'sm'|'md'|'lg'} props.maxWidth - 最大宽度
* @param {string} props.title - 标题(可选)
*/
const Modal = ({
isOpen,
onClose,
children,
maxWidth = 'md',
title
}) => {
// 最大宽度映射
const maxWidthMap = {
sm: 'max-w-sm',
md: 'max-w-lg',
lg: 'max-w-2xl'
};
return (
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && onClose()}>
<AnimatePresence>
{isOpen && (
<Dialog.Portal forceMount>
{/* 遮罩层 */}
<Dialog.Overlay asChild>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-xl z-[100]"
/>
</Dialog.Overlay>
{/* 内容区 */}
<Dialog.Content asChild>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className={`
fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
glass-card ${maxWidthMap[maxWidth]} w-[calc(100%-2rem)] p-8
border border-white/10 shadow-2xl z-[101]
`}
>
{/* 关闭按钮 */}
<Dialog.Close asChild>
<button
className="absolute top-6 right-6 text-white/40 hover:text-white transition-colors"
aria-label="关闭"
>
<X className="w-5 h-5" />
</button>
</Dialog.Close>
{/* 标题 */}
{title && (
<Dialog.Title className="text-2xl font-serif mb-6">
{title}
</Dialog.Title>
)}
{/* 内容 */}
<div className="max-h-[70vh] overflow-y-auto pr-2 custom-scrollbar">
{children}
</div>
</motion.div>
</Dialog.Content>
</Dialog.Portal>
)}
</AnimatePresence>
</Dialog.Root>
);
};
export default Modal;
+40
View File
@@ -0,0 +1,40 @@
/**
* PromptTag 组件
* 灵感标签,点击后追加文本到目标输入框
* @param {Object} props
* @param {string} props.text - 标签文本
* @param {Function} props.onClick - 点击回调
*/
const PromptTag = ({ text, onClick }) => {
return (
<span
onClick={() => onClick(text)}
className="prompt-tag"
>
{text}
</span>
);
};
/**
* PromptTagGroup 组件
* 灵感标签组,用于批量渲染标签
* @param {Object} props
* @param {string[]} props.tags - 标签文本数组
* @param {Function} props.onTagClick - 标签点击回调
*/
export const PromptTagGroup = ({ tags, onTagClick }) => {
return (
<div className="flex flex-wrap gap-2 pt-2">
{tags.map((tag, index) => (
<PromptTag
key={index}
text={tag}
onClick={onTagClick}
/>
))}
</div>
);
};
export default PromptTag;
@@ -0,0 +1,27 @@
/**
* Background 组件
* 动态流体背景,包含渐变、浮动模糊圆和纹理叠加
*/
const Background = () => {
return (
<div id="app-bg" className="fixed inset-0 z-[-1]">
{/* 渐变底层 */}
<div className="absolute inset-0 bg-gradient-to-br from-[#1a1c2c] via-[#0a0c10] to-[#2d1b10] opacity-80" />
{/* 浮动模糊圆 - 蓝色 */}
<div className="absolute top-[-10%] left-[-10%] w-[60%] h-[60%] bg-blue-900/20 blur-[120px] rounded-full animate-float" />
{/* 浮动模糊圆 - 橙色 */}
<div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-orange-900/10 blur-[120px] rounded-full animate-float-delayed" />
{/* 纹理叠加层 */}
<img
src="https://r2-bucket.flowith.net/f/845b300ff0a2b36e/digital_healing_background_design_index_1%401024x1024.jpeg"
alt="texture"
className="w-full h-full object-cover mix-blend-overlay opacity-30"
/>
</div>
);
};
export default Background;
@@ -0,0 +1,52 @@
import { User } from 'lucide-react';
import GlassButton from '../ui/GlassButton';
/**
* Header 组件
* 固定定位头部,包含 logo 和用户按钮
* @param {Object} props
* @param {boolean} props.showNav - 是否显示导航按钮
* @param {Function} props.onProfileClick - 用户按钮点击回调
*/
const Header = ({ showNav = false, onProfileClick }) => {
/**
* 处理 logo 点击,刷新页面
*/
const handleLogoClick = () => {
window.location.reload();
};
return (
<header className="fixed top-0 left-0 p-6 z-50 w-full flex justify-between items-center pointer-events-none">
{/* Logo 区域 */}
<div
className="flex items-center gap-3 pointer-events-auto cursor-pointer"
onClick={handleLogoClick}
>
<img
src="https://r2-bucket.flowith.net/f/cf8c6e7c020409c9/lifeline_app_logo_design_index_0%401024x1024.jpeg"
alt="logo"
className="w-10 h-10 rounded-full shadow-2xl border border-white/10"
/>
<h1 className="text-xl font-serif tracking-[0.2em] bg-clip-text text-transparent bg-gradient-to-r from-white to-white/50">
人生轨迹
</h1>
</div>
{/* 导航区域 */}
{showNav && (
<nav className="pointer-events-auto">
<GlassButton
variant="icon"
onClick={onProfileClick}
className="hover:shadow-orange-200/10 shadow-lg"
>
<User className="w-5 h-5" />
</GlassButton>
</nav>
)}
</header>
);
};
export default Header;
@@ -0,0 +1,72 @@
import { History, Sparkles, Map } from 'lucide-react';
/**
* 导航项配置
*/
const navGroups = [
{
title: '回溯过去',
items: [
{ id: 'timeline', label: '生命长河', icon: History }
]
},
{
title: '创造未来',
items: [
{ id: 'script', label: '爽文剧本', icon: Sparkles },
{ id: 'path', label: '实现路径', icon: Map }
]
}
];
/**
* Sidebar 组件
* 仪表盘侧边栏导航
* @param {Object} props
* @param {'timeline'|'script'|'path'} props.activeView - 当前激活的视图
* @param {Function} props.onViewChange - 视图切换回调
*/
const Sidebar = ({ activeView, onViewChange }) => {
return (
<aside className="md:col-span-3 border-r border-white/5 p-6 flex flex-col gap-6 bg-black/20">
{/* 导航分组 */}
{navGroups.map((group) => (
<div key={group.title} className="space-y-2">
{/* 分组标题 */}
<div className="px-3 py-2 text-[10px] text-white/30 uppercase tracking-[0.2em] font-bold">
{group.title}
</div>
{/* 导航项 */}
{group.items.map((item) => {
const Icon = item.icon;
const isActive = activeView === item.id;
return (
<button
key={item.id}
onClick={() => onViewChange(item.id)}
className={`
nav-item w-full flex items-center gap-3 p-4 rounded-2xl glass-btn text-white/50
${isActive ? 'active' : ''}
`}
>
<Icon className="w-5 h-5" />
<span>{item.label}</span>
</button>
);
})}
</div>
))}
{/* 底部引用文字 */}
<div className="mt-auto p-4 bg-white/[0.02] rounded-2xl border border-white/5">
<p className="text-[10px] text-white/20 italic leading-relaxed">
"回溯过去、记录当下、创造未来。"
</p>
</div>
</aside>
);
};
export default Sidebar;
@@ -0,0 +1,6 @@
/**
* 布局组件统一导出
*/
export { default as Background } from './Background';
export { default as Header } from './Header';
export { default as Sidebar } from './Sidebar';
@@ -0,0 +1,54 @@
import { Loader2 } from 'lucide-react';
/**
* GlassButton 组件
* 毛玻璃按钮样式,支持多种变体和加载状态
* @param {Object} props
* @param {React.ReactNode} props.children - 子元素
* @param {Function} props.onClick - 点击事件处理函数
* @param {'default'|'primary'|'icon'} props.variant - 按钮变体
* @param {boolean} props.disabled - 是否禁用
* @param {boolean} props.loading - 是否显示加载状态
* @param {string} props.className - 额外的CSS类名
* @param {string} props.type - 按钮类型
*/
const GlassButton = ({
children,
onClick,
variant = 'default',
disabled = false,
loading = false,
className = '',
type = 'button'
}) => {
// 变体样式映射
const variantMap = {
default: 'px-6 py-3 rounded-2xl text-white/70',
primary: 'px-8 py-4 rounded-2xl bg-orange-200/5 text-orange-200 font-bold tracking-[0.2em] border-orange-200/20 shadow-lg shadow-orange-900/10',
icon: 'p-2.5 rounded-full'
};
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className={`
glass-btn
${variantMap[variant]}
${className}
`}
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
<span className="animate-pulse">处理中...</span>
</>
) : (
children
)}
</button>
);
};
export default GlassButton;
@@ -0,0 +1,44 @@
/**
* GlassCard 组件
* 毛玻璃卡片样式,支持多种变体
* @param {Object} props
* @param {React.ReactNode} props.children - 子元素
* @param {string} props.className - 额外的CSS类名
* @param {'default'|'highlight'|'ai'} props.variant - 卡片变体
* @param {'sm'|'md'|'lg'} props.padding - 内边距大小
*/
const GlassCard = ({
children,
className = '',
variant = 'default',
padding = 'md'
}) => {
// 内边距映射
const paddingMap = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8'
};
// 变体样式映射
const variantMap = {
default: 'border-white/5',
highlight: 'border-orange-200/20 shadow-orange-900/10',
ai: 'border-orange-200/5 bg-orange-200/[0.02]'
};
return (
<div
className={`
glass-card
${paddingMap[padding]}
${variantMap[variant]}
${className}
`}
>
{children}
</div>
);
};
export default GlassCard;
@@ -0,0 +1,47 @@
/**
* GlassInput 组件
* 毛玻璃输入框样式
* @param {Object} props
* @param {string} props.label - 标签文本
* @param {string} props.type - 输入类型
* @param {string} props.placeholder - 占位符文本
* @param {string} props.value - 输入值
* @param {Function} props.onChange - 值变化处理函数
* @param {number} props.maxLength - 最大长度
* @param {string} props.className - 额外的CSS类名
* @param {string} props.id - 输入框ID
*/
const GlassInput = ({
label,
type = 'text',
placeholder = '',
value,
onChange,
maxLength,
className = '',
id
}) => {
return (
<div className={`flex flex-col gap-2 ${className}`}>
{label && (
<label
htmlFor={id}
className="text-[10px] text-white/30 uppercase tracking-widest font-bold"
>
{label}
</label>
)}
<input
id={id}
type={type}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
maxLength={maxLength}
className="glass-input w-full focus:ring-2 focus:ring-orange-200/50"
/>
</div>
);
};
export default GlassInput;
@@ -0,0 +1,50 @@
/**
* GlassSelect 组件
* 毛玻璃下拉选择框样式
* @param {Object} props
* @param {string} props.label - 标签文本
* @param {Array<{value: string, label: string}>} props.options - 选项数组
* @param {string} props.value - 当前选中值
* @param {Function} props.onChange - 值变化处理函数
* @param {string} props.className - 额外的CSS类名
* @param {string} props.id - 选择框ID
*/
const GlassSelect = ({
label,
options = [],
value,
onChange,
className = '',
id
}) => {
return (
<div className={`flex flex-col gap-2 ${className}`}>
{label && (
<label
htmlFor={id}
className="text-[10px] text-white/30 uppercase tracking-widest font-bold"
>
{label}
</label>
)}
<select
id={id}
value={value}
onChange={(e) => onChange(e.target.value)}
className="glass-input w-full appearance-none cursor-pointer"
>
{options.map((option) => (
<option
key={option.value}
value={option.value}
className="bg-[#0a0c10] text-white"
>
{option.label}
</option>
))}
</select>
</div>
);
};
export default GlassSelect;
@@ -0,0 +1,44 @@
/**
* GlassTextarea 组件
* 毛玻璃文本区域样式
* @param {Object} props
* @param {string} props.label - 标签文本
* @param {string} props.placeholder - 占位符文本
* @param {string} props.value - 输入值
* @param {Function} props.onChange - 值变化处理函数
* @param {number} props.rows - 行数
* @param {string} props.className - 额外的CSS类名
* @param {string} props.id - 文本区域ID
*/
const GlassTextarea = ({
label,
placeholder = '',
value,
onChange,
rows = 4,
className = '',
id
}) => {
return (
<div className={`flex flex-col gap-2 ${className}`}>
{label && (
<label
htmlFor={id}
className="text-[10px] text-white/30 uppercase tracking-widest font-bold"
>
{label}
</label>
)}
<textarea
id={id}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
rows={rows}
className="glass-input w-full resize-none focus:ring-2 focus:ring-orange-200/50 text-sm"
/>
</div>
);
};
export default GlassTextarea;
+8
View File
@@ -0,0 +1,8 @@
/**
* UI组件统一导出
*/
export { default as GlassCard } from './GlassCard';
export { default as GlassButton } from './GlassButton';
export { default as GlassInput } from './GlassInput';
export { default as GlassTextarea } from './GlassTextarea';
export { default as GlassSelect } from './GlassSelect';