前端重构实现
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user