Trae Assistant
initial commit: clean history and remove node_modules
76127be
import { useState, useMemo } from 'react';
import {
Plus,
Trash2,
Settings2,
Layout,
Type,
Square,
CheckSquare,
Smartphone,
Monitor,
Eye,
Code,
Save,
Download
} from 'lucide-react';
import { v4 as uuidv4 } from 'uuid';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// 样式合并工具函数
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// 组件类型定义
type ComponentType = 'text' | 'button' | 'input' | 'card' | 'checkbox';
interface VisualComponent {
id: string;
type: ComponentType;
props: Record<string, any>;
}
// 可选组件库
const COMPONENT_LIBRARY = [
{ type: 'text' as const, label: '文本', icon: Type, defaultProps: { text: '点击编辑文本', fontSize: '16px', color: '#000000' } },
{ type: 'button' as const, label: '按钮', icon: Square, defaultProps: { label: '点击按钮', variant: 'primary', width: 'auto' } },
{ type: 'input' as const, label: '输入框', icon: Type, defaultProps: { placeholder: '请输入内容...', label: '表单字段' } },
{ type: 'card' as const, label: '卡片容器', icon: Layout, defaultProps: { title: '卡片标题', content: '这里是卡片内容区域' } },
{ type: 'checkbox' as const, label: '复选框', icon: CheckSquare, defaultProps: { label: '选项名称', checked: false } },
];
/**
* 极简可视化低代码平台 - 核心入口组件
* 支持组件库拖拽、画布渲染、属性实时编辑及多端预览
*/
export default function App() {
// --- 状态管理 ---
const [components, setComponents] = useState<VisualComponent[]>([]); // 画布上的组件列表
const [selectedId, setSelectedId] = useState<string | null>(null); // 当前选中的组件 ID
const [viewMode, setViewMode] = useState<'desktop' | 'mobile'>('desktop'); // 预览模式:桌面/移动端
const [isPreview, setIsPreview] = useState(false); // 是否处于预览模式
// 获取当前选中的组件对象
const selectedComponent = useMemo(() =>
components.find(c => c.id === selectedId),
[components, selectedId]
);
// 添加新组件到画布
const addComponent = (type: ComponentType, defaultProps: any) => {
const newComp: VisualComponent = {
id: uuidv4(),
type,
props: { ...defaultProps }
};
setComponents([...components, newComp]);
setSelectedId(newComp.id);
};
// 从画布中移除组件
const removeComponent = (id: string) => {
setComponents(components.filter(c => c.id !== id));
if (selectedId === id) setSelectedId(null);
};
// 更新选中组件的属性
const updateProps = (id: string, newProps: any) => {
setComponents(components.map(c =>
c.id === id ? { ...c, props: { ...c.props, ...newProps } } : c
));
};
// 渲染单个可视化组件
const renderComponent = (comp: VisualComponent, index: number) => {
const isSelected = selectedId === comp.id;
const wrapperClass = cn(
"relative group cursor-pointer border-2 border-transparent transition-all",
!isPreview && isSelected && "border-blue-500 rounded-lg",
!isPreview && !isSelected && "hover:border-dashed hover:border-gray-300 rounded-lg"
);
const commonProps = {
onClick: (e: React.MouseEvent) => {
if (isPreview) return;
e.stopPropagation();
setSelectedId(comp.id);
}
};
let element;
switch (comp.type) {
case 'text':
element = (
<div style={{ fontSize: comp.props.fontSize, color: comp.props.color }}>
{comp.props.text}
</div>
);
break;
case 'button':
element = (
<button className={cn(
"px-4 py-2 rounded font-medium transition-colors",
comp.props.variant === 'primary' ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-800",
comp.props.width === 'full' ? "w-full" : "w-auto"
)}>
{comp.props.label}
</button>
);
break;
case 'input':
element = (
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-gray-700">{comp.props.label}</label>
<input
type="text"
placeholder={comp.props.placeholder}
className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
disabled={!isPreview}
/>
</div>
);
break;
case 'card':
element = (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-100">
<h3 className="text-lg font-bold mb-2">{comp.props.title}</h3>
<p className="text-gray-600">{comp.props.content}</p>
</div>
);
break;
case 'checkbox':
element = (
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={comp.props.checked} readOnly={!isPreview} className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<span className="text-gray-700">{comp.props.label}</span>
</label>
);
break;
}
return (
<div key={comp.id} className={wrapperClass} {...commonProps}>
{element}
{!isPreview && isSelected && (
<button
onClick={(e) => { e.stopPropagation(); removeComponent(comp.id); }}
className="absolute -top-3 -right-3 bg-red-500 text-white p-1 rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-opacity"
>
<Trash2 size={14} />
</button>
)}
</div>
);
};
return (
<div className="flex h-screen bg-gray-50 text-gray-900 font-sans overflow-hidden">
{/* 顶部工具栏 */}
<div className="absolute top-0 left-0 right-0 h-14 bg-white border-b border-gray-200 flex items-center justify-between px-6 z-50 shadow-sm">
<div className="flex items-center gap-2">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Code className="text-white" size={20} />
</div>
<span className="font-bold text-lg tracking-tight">极简低代码平台</span>
</div>
<div className="flex items-center gap-4 bg-gray-100 p-1 rounded-lg">
<button
onClick={() => setViewMode('desktop')}
className={cn("p-1.5 rounded-md transition-all", viewMode === 'desktop' ? "bg-white shadow-sm text-blue-600" : "text-gray-500")}
>
<Monitor size={18} />
</button>
<button
onClick={() => setViewMode('mobile')}
className={cn("p-1.5 rounded-md transition-all", viewMode === 'mobile' ? "bg-white shadow-sm text-blue-600" : "text-gray-500")}
>
<Smartphone size={18} />
</button>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setIsPreview(!isPreview)}
className={cn(
"flex items-center gap-2 px-4 py-1.5 rounded-lg font-medium transition-all",
isPreview ? "bg-orange-100 text-orange-600" : "bg-blue-50 text-blue-600 hover:bg-blue-100"
)}
>
{isPreview ? <Save size={18} /> : <Eye size={18} />}
{isPreview ? "退出预览" : "预览"}
</button>
<button className="bg-blue-600 text-white px-4 py-1.5 rounded-lg font-medium hover:bg-blue-700 transition-all flex items-center gap-2">
<Download size={18} />
发布
</button>
</div>
</div>
{/* 左侧组件库 */}
{!isPreview && (
<aside className="w-72 bg-white border-r border-gray-200 mt-14 flex flex-col z-40">
<div className="p-4 border-b border-gray-100">
<h2 className="text-sm font-semibold text-gray-400 uppercase tracking-wider">组件库</h2>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{COMPONENT_LIBRARY.map((item) => (
<button
key={item.type}
onClick={() => addComponent(item.type, item.defaultProps)}
className="w-full flex items-center gap-3 p-3 bg-gray-50 hover:bg-blue-50 border border-gray-100 hover:border-blue-200 rounded-xl transition-all group text-left"
>
<div className="p-2 bg-white rounded-lg shadow-sm group-hover:text-blue-600 transition-colors">
<item.icon size={18} />
</div>
<span className="font-medium text-gray-700 group-hover:text-blue-600">{item.label}</span>
<Plus size={16} className="ml-auto text-gray-300 group-hover:text-blue-400" />
</button>
))}
</div>
</aside>
)}
{/* 中间画布 */}
<main className={cn(
"flex-1 mt-14 overflow-auto p-8 flex justify-center transition-all duration-300 bg-gray-50",
isPreview && "p-0 bg-white"
)}>
<div
className={cn(
"bg-white shadow-2xl transition-all duration-300 border border-gray-200 min-h-full",
viewMode === 'mobile' ? "w-[375px] rounded-[40px] border-[12px] border-gray-800 p-8 my-4" : "w-full max-w-4xl rounded-xl p-10",
isPreview && viewMode === 'desktop' && "max-w-none w-full border-none shadow-none rounded-none",
isPreview && viewMode === 'mobile' && "my-8"
)}
onClick={() => setSelectedId(null)}
>
{components.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-gray-400 gap-4 mt-20">
<div className="bg-gray-100 p-6 rounded-full">
<Layout size={48} />
</div>
<p className="text-lg">从左侧拖拽或点击组件开始设计</p>
</div>
) : (
<div className="space-y-6">
{components.map((comp, index) => renderComponent(comp, index))}
</div>
)}
</div>
</main>
{/* 右侧属性面板 */}
{!isPreview && (
<aside className="w-80 bg-white border-l border-gray-200 mt-14 flex flex-col z-40">
<div className="p-4 border-b border-gray-100 flex items-center gap-2">
<Settings2 size={18} className="text-blue-600" />
<h2 className="text-sm font-semibold text-gray-700">属性配置</h2>
</div>
<div className="flex-1 overflow-y-auto p-5">
{selectedComponent ? (
<div className="space-y-6">
<div className="bg-blue-50 p-3 rounded-lg border border-blue-100">
<span className="text-xs font-bold text-blue-600 uppercase">当前组件: {selectedComponent.type}</span>
</div>
{/* 根据组件类型显示不同的编辑项 */}
<div className="space-y-4">
{Object.entries(selectedComponent.props).map(([key, value]) => (
<div key={key} className="space-y-1.5">
<label className="text-xs font-medium text-gray-500 uppercase">{key === 'text' ? '文本内容' : key === 'label' ? '标签' : key === 'fontSize' ? '字号' : key === 'color' ? '颜色' : key === 'placeholder' ? '提示词' : key === 'title' ? '标题' : key === 'content' ? '正文' : key === 'variant' ? '样式' : key === 'width' ? '宽度' : key}</label>
{typeof value === 'string' && (key !== 'variant' && key !== 'width') ? (
<input
type={key === 'color' ? 'color' : 'text'}
value={value}
onChange={(e) => updateProps(selectedComponent.id, { [key]: e.target.value })}
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm"
/>
) : key === 'variant' ? (
<select
value={value}
onChange={(e) => updateProps(selectedComponent.id, { [key]: e.target.value })}
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg outline-none text-sm"
>
<option value="primary">主按钮</option>
<option value="secondary">次按钮</option>
</select>
) : key === 'width' ? (
<select
value={value}
onChange={(e) => updateProps(selectedComponent.id, { [key]: e.target.value })}
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg outline-none text-sm"
>
<option value="auto">自适应</option>
<option value="full">撑满</option>
</select>
) : null}
</div>
))}
</div>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-gray-400 text-sm gap-2">
<Smartphone size={32} strokeWidth={1.5} />
<p>选中画布上的组件进行编辑</p>
</div>
)}
</div>
</aside>
)}
</div>
);
}