"添加前端模板和运行代码模块"
This commit is contained in:
@@ -0,0 +1,711 @@
|
||||
/**
|
||||
* @fileoverview 组件编辑器页面
|
||||
* @description 提供可视化的组件编辑功能,支持拖拽、属性编辑和实时预览
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Plus,
|
||||
Play,
|
||||
Square,
|
||||
Save,
|
||||
Download,
|
||||
Upload,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Settings,
|
||||
Layers,
|
||||
Code,
|
||||
Smartphone,
|
||||
Monitor,
|
||||
Tablet
|
||||
} from 'lucide-react';
|
||||
import { Card, CardBody, CardHeader } from '../components/ui/Card';
|
||||
import { Button, IconButton } from '../components/ui/Button';
|
||||
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
|
||||
import { useDesignModeStore } from '../main';
|
||||
|
||||
// 编辑模式类型
|
||||
type EditMode = 'design' | 'code' | 'preview';
|
||||
|
||||
// 设备类型
|
||||
type DeviceType = 'desktop' | 'tablet' | 'mobile';
|
||||
|
||||
// 组件库数据
|
||||
const COMPONENT_LIBRARY = [
|
||||
{
|
||||
id: 'button',
|
||||
name: 'Button',
|
||||
category: 'Input',
|
||||
icon: '🔘',
|
||||
description: 'Interactive button component',
|
||||
props: {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
disabled: false,
|
||||
loading: false
|
||||
},
|
||||
variants: ['primary', 'secondary', 'outline', 'ghost', 'danger'],
|
||||
sizes: ['sm', 'md', 'lg', 'xl']
|
||||
},
|
||||
{
|
||||
id: 'card',
|
||||
name: 'Card',
|
||||
category: 'Layout',
|
||||
icon: '🃏',
|
||||
description: 'Container component with header, body, and footer',
|
||||
props: {
|
||||
variant: 'default',
|
||||
size: 'md',
|
||||
hover: false,
|
||||
interactive: false
|
||||
},
|
||||
variants: ['default', 'elevated', 'outlined', 'ghost', 'filled'],
|
||||
sizes: ['sm', 'md', 'lg', 'xl']
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
name: 'Input',
|
||||
category: 'Form',
|
||||
icon: '📝',
|
||||
description: 'Text input field component',
|
||||
props: {
|
||||
type: 'text',
|
||||
placeholder: 'Enter text...',
|
||||
disabled: false,
|
||||
required: false,
|
||||
error: false
|
||||
},
|
||||
variants: ['default', 'filled', 'underlined'],
|
||||
sizes: ['sm', 'md', 'lg']
|
||||
},
|
||||
{
|
||||
id: 'modal',
|
||||
name: 'Modal',
|
||||
category: 'Overlay',
|
||||
icon: '🪟',
|
||||
description: 'Overlay modal dialog',
|
||||
props: {
|
||||
open: false,
|
||||
size: 'md',
|
||||
closable: true,
|
||||
maskClosable: true
|
||||
},
|
||||
variants: ['default', 'fullscreen', 'drawer'],
|
||||
sizes: ['sm', 'md', 'lg', 'xl']
|
||||
}
|
||||
];
|
||||
|
||||
// 组件预览组件
|
||||
const ComponentPreview: React.FC<{
|
||||
component: any;
|
||||
deviceType: DeviceType;
|
||||
isPlaying: boolean;
|
||||
}> = ({ component, deviceType, isPlaying }) => {
|
||||
const renderComponent = () => {
|
||||
switch (component.id) {
|
||||
case 'button':
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
px-4 py-2 rounded-md font-medium transition-colors
|
||||
${component.props.variant === 'primary' ? 'bg-primary-600 text-white hover:bg-primary-700' :
|
||||
component.props.variant === 'secondary' ? 'bg-neutral-200 text-neutral-900 hover:bg-neutral-300' :
|
||||
component.props.variant === 'outline' ? 'border border-primary-600 text-primary-600 hover:bg-primary-600 hover:text-white' :
|
||||
component.props.variant === 'danger' ? 'bg-error-600 text-white hover:bg-error-700' :
|
||||
'bg-transparent text-neutral-700 hover:bg-neutral-100'
|
||||
}
|
||||
${component.props.size === 'sm' ? 'px-3 py-1.5 text-sm' :
|
||||
component.props.size === 'lg' ? 'px-6 py-3 text-lg' :
|
||||
component.props.size === 'xl' ? 'px-8 py-4 text-xl' :
|
||||
'px-4 py-2 text-sm'
|
||||
}
|
||||
${component.props.disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
disabled={component.props.disabled}
|
||||
data-component="Button"
|
||||
data-element="preview-button"
|
||||
data-variant={component.props.variant}
|
||||
data-size={component.props.size}
|
||||
>
|
||||
{isPlaying ? 'Loading...' : 'Button Component'}
|
||||
</button>
|
||||
);
|
||||
|
||||
case 'card':
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
bg-white dark:bg-neutral-900 rounded-lg overflow-hidden
|
||||
${component.props.variant === 'elevated' ? 'shadow-lg' :
|
||||
component.props.variant === 'outlined' ? 'border-2 border-primary-200 dark:border-primary-800' :
|
||||
component.props.variant === 'filled' ? 'bg-neutral-50 dark:bg-neutral-800' :
|
||||
'border border-neutral-200 dark:border-neutral-800'
|
||||
}
|
||||
${component.props.hover ? 'hover:shadow-md transition-shadow' : ''}
|
||||
${component.props.size === 'sm' ? 'p-3' :
|
||||
component.props.size === 'lg' ? 'p-6' :
|
||||
component.props.size === 'xl' ? 'p-8' :
|
||||
'p-4'
|
||||
}
|
||||
`}
|
||||
data-component="Card"
|
||||
data-element="preview-card"
|
||||
data-variant={component.props.variant}
|
||||
data-size={component.props.size}
|
||||
>
|
||||
<div className="font-semibold text-neutral-900 dark:text-neutral-100 mb-2">
|
||||
Card Header
|
||||
</div>
|
||||
<div className="text-neutral-600 dark:text-neutral-400 text-sm mb-4">
|
||||
This is a preview of the {component.name} component.
|
||||
</div>
|
||||
<div className="text-xs text-neutral-500">
|
||||
Card content goes here...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'input':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Input Label
|
||||
</label>
|
||||
<input
|
||||
type={component.props.type}
|
||||
placeholder={component.props.placeholder}
|
||||
disabled={component.props.disabled}
|
||||
required={component.props.required}
|
||||
className={`
|
||||
w-full px-3 py-2 border rounded-md transition-colors
|
||||
${component.props.error ? 'border-error-300 focus:border-error-500 focus:ring-error-500' :
|
||||
'border-neutral-300 dark:border-neutral-600 focus:border-primary-500 focus:ring-primary-500'
|
||||
}
|
||||
${component.props.variant === 'filled' ? 'bg-neutral-50 dark:bg-neutral-800' : 'bg-white dark:bg-neutral-900'}
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
`}
|
||||
data-component="Input"
|
||||
data-element="preview-input"
|
||||
data-variant={component.props.variant}
|
||||
data-error={component.props.error ? 'true' : 'false'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'modal':
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="p-8 bg-neutral-100 dark:bg-neutral-800 rounded-lg border-2 border-dashed border-neutral-300 dark:border-neutral-600">
|
||||
<div className="text-center text-neutral-500 dark:text-neutral-400">
|
||||
<div className="w-16 h-16 bg-neutral-200 dark:bg-neutral-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
🪟
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
{component.props.open ? 'Modal is open' : 'Click to open modal'}
|
||||
</p>
|
||||
<p className="text-xs mt-2">
|
||||
Size: {component.props.size} • Closable: {component.props.closable ? 'Yes' : 'No'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="p-8 bg-neutral-100 dark:bg-neutral-800 rounded-lg border-2 border-dashed border-neutral-300 dark:border-neutral-600 text-center">
|
||||
<p className="text-neutral-500 dark:text-neutral-400">
|
||||
Component preview not available
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getDeviceClasses = () => {
|
||||
switch (deviceType) {
|
||||
case 'mobile':
|
||||
return 'max-w-sm mx-auto';
|
||||
case 'tablet':
|
||||
return 'max-w-2xl mx-auto';
|
||||
default:
|
||||
return 'w-full';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`component-preview ${getDeviceClasses()}`}
|
||||
data-component="ComponentPreview"
|
||||
data-element="preview"
|
||||
data-device={deviceType}
|
||||
data-component-id={component.id}
|
||||
>
|
||||
{renderComponent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 属性面板组件
|
||||
const PropertyPanel: React.FC<{
|
||||
component: any;
|
||||
onUpdate: (props: any) => void;
|
||||
}> = ({ component, onUpdate }) => {
|
||||
const updateProp = (key: string, value: any) => {
|
||||
onUpdate({
|
||||
...component.props,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
title={`${component.name} Properties`}
|
||||
subtitle={`${component.category} • ${component.id}`}
|
||||
/>
|
||||
<CardBody className="space-y-6">
|
||||
{/* 变体选择 */}
|
||||
{component.variants && (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Variant
|
||||
</label>
|
||||
<select
|
||||
value={component.props.variant}
|
||||
onChange={(e) => updateProp('variant', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
data-component="Select"
|
||||
data-element="variant-select"
|
||||
data-component-prop="variant"
|
||||
>
|
||||
{component.variants.map((variant: string) => (
|
||||
<option key={variant} value={variant}>
|
||||
{variant.charAt(0).toUpperCase() + variant.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 尺寸选择 */}
|
||||
{component.sizes && (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Size
|
||||
</label>
|
||||
<select
|
||||
value={component.props.size}
|
||||
onChange={(e) => updateProp('size', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
data-component="Select"
|
||||
data-element="size-select"
|
||||
data-component-prop="size"
|
||||
>
|
||||
{component.sizes.map((size: string) => (
|
||||
<option key={size} value={size}>
|
||||
{size.toUpperCase()}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 布尔属性 */}
|
||||
{['disabled', 'loading', 'hover', 'interactive', 'required', 'error'].map((prop) => {
|
||||
if (!(prop in component.props)) return null;
|
||||
|
||||
return (
|
||||
<div key={prop} className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
{prop.charAt(0).toUpperCase() + prop.slice(1)}
|
||||
</label>
|
||||
<button
|
||||
onClick={() => updateProp(prop, !component.props[prop])}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
|
||||
${component.props[prop] ? 'bg-primary-600' : 'bg-neutral-200 dark:bg-neutral-700'}
|
||||
`}
|
||||
data-component="Toggle"
|
||||
data-element="boolean-toggle"
|
||||
data-prop={prop}
|
||||
data-value={component.props[prop] ? 'true' : 'false'}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
||||
${component.props[prop] ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 文本属性 */}
|
||||
{['placeholder', 'type'].map((prop) => {
|
||||
if (!(prop in component.props)) return null;
|
||||
|
||||
return (
|
||||
<div key={prop} className="space-y-2">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
{prop.charAt(0).toUpperCase() + prop.slice(1)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={component.props[prop]}
|
||||
onChange={(e) => updateProp(prop, e.target.value)}
|
||||
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
data-component="Input"
|
||||
data-element="text-input"
|
||||
data-component-prop={prop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件编辑器主页面
|
||||
*/
|
||||
const ComponentEditor: React.FC = () => {
|
||||
const [selectedComponent, setSelectedComponent] = useState(COMPONENT_LIBRARY[0]);
|
||||
const [editMode, setEditMode] = useState<EditMode>('design');
|
||||
const [deviceType, setDeviceType] = useState<DeviceType>('desktop');
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const { isDesignModeEnabled } = useDesignModeStore();
|
||||
|
||||
// 组件属性更新处理
|
||||
const handleComponentUpdate = useCallback((newProps: any) => {
|
||||
setSelectedComponent((prev: any) => ({
|
||||
...prev,
|
||||
props: newProps
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 开始/停止播放状态
|
||||
const togglePlayState = () => {
|
||||
setIsPlaying(!isPlaying);
|
||||
};
|
||||
|
||||
// 生成代码
|
||||
const generateCode = () => {
|
||||
const propsString = Object.entries(selectedComponent.props)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ');
|
||||
|
||||
const codeString = `<${selectedComponent.id}${propsString ? ' ' + propsString : ''} />`;
|
||||
setCode(codeString);
|
||||
setEditMode('code');
|
||||
};
|
||||
|
||||
// 复制代码
|
||||
const copyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
// 可以添加成功提示
|
||||
} catch (error) {
|
||||
console.error('Failed to copy code:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="component-editor h-full flex flex-col"
|
||||
data-component="ComponentEditor"
|
||||
data-design-mode={isDesignModeEnabled ? 'true' : 'false'}
|
||||
data-edit-mode={editMode}
|
||||
data-device={deviceType}
|
||||
>
|
||||
{/* 编辑器工具栏 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="editor-toolbar border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4"
|
||||
data-element="toolbar"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 左侧:组件库和模式切换 */}
|
||||
<div className="flex items-center space-x-4" data-element="left-controls">
|
||||
{/* 组件库选择 */}
|
||||
<div className="flex items-center space-x-2" data-element="component-selector">
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Component:
|
||||
</label>
|
||||
<select
|
||||
value={selectedComponent.id}
|
||||
onChange={(e) => {
|
||||
const component = COMPONENT_LIBRARY.find(c => c.id === e.target.value);
|
||||
if (component) setSelectedComponent(component);
|
||||
}}
|
||||
className="px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
data-component="Select"
|
||||
data-element="component-select"
|
||||
>
|
||||
{COMPONENT_LIBRARY.map((component) => (
|
||||
<option key={component.id} value={component.id}>
|
||||
{component.icon} {component.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 模式切换 */}
|
||||
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden" data-element="mode-toggle">
|
||||
{(['design', 'code', 'preview'] as EditMode[]).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => setEditMode(mode)}
|
||||
className={`
|
||||
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
|
||||
${editMode === mode
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
}
|
||||
`}
|
||||
data-component="ModeButton"
|
||||
data-element="mode-button"
|
||||
data-mode={mode}
|
||||
>
|
||||
{mode === 'design' && <Settings className="w-4 h-4" />}
|
||||
{mode === 'code' && <Code className="w-4 h-4" />}
|
||||
{mode === 'preview' && <Eye className="w-4 h-4" />}
|
||||
<span className="capitalize">{mode}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮 */}
|
||||
<div className="flex items-center space-x-3" data-element="right-controls">
|
||||
{/* 设备切换 */}
|
||||
{editMode === 'preview' && (
|
||||
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden" data-element="device-toggle">
|
||||
{[
|
||||
{ type: 'desktop', icon: Monitor, label: 'Desktop' },
|
||||
{ type: 'tablet', icon: Tablet, label: 'Tablet' },
|
||||
{ type: 'mobile', icon: Smartphone, label: 'Mobile' }
|
||||
].map(({ type, icon: Icon, label }) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setDeviceType(type as DeviceType)}
|
||||
className={`
|
||||
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
|
||||
${deviceType === type
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
}
|
||||
`}
|
||||
data-component="DeviceButton"
|
||||
data-element="device-button"
|
||||
data-device={type}
|
||||
title={label}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 播放按钮 */}
|
||||
{selectedComponent.props.hasOwnProperty('loading') && (
|
||||
<IconButton
|
||||
variant={isPlaying ? 'primary' : 'ghost'}
|
||||
onClick={togglePlayState}
|
||||
data-component="IconButton"
|
||||
data-element="play-button"
|
||||
data-action="toggle-play-state"
|
||||
>
|
||||
{isPlaying ? <Square className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* 生成代码 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={generateCode}
|
||||
data-component="Button"
|
||||
data-element="generate-code-button"
|
||||
data-action="generate-code"
|
||||
>
|
||||
<Code className="w-4 h-4 mr-2" />
|
||||
Generate Code
|
||||
</Button>
|
||||
|
||||
{/* 保存 */}
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
data-component="IconButton"
|
||||
data-element="save-button"
|
||||
data-action="save-component"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 编辑器主要内容 */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* 组件库面板 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="component-library w-64 border-r border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800 overflow-y-auto"
|
||||
data-element="library-panel"
|
||||
>
|
||||
<div className="p-4" data-element="library-content">
|
||||
<h3 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-4">
|
||||
Component Library
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
{COMPONENT_LIBRARY.map((component) => (
|
||||
<button
|
||||
key={component.id}
|
||||
onClick={() => setSelectedComponent(component)}
|
||||
className={`
|
||||
w-full text-left p-3 rounded-lg border transition-colors
|
||||
${selectedComponent.id === component.id
|
||||
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-200 dark:border-primary-800'
|
||||
: 'bg-white dark:bg-neutral-900 border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
}
|
||||
`}
|
||||
data-component="LibraryItem"
|
||||
data-element="library-item"
|
||||
data-component-id={component.id}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-lg">{component.icon}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-neutral-900 dark:text-neutral-100 truncate">
|
||||
{component.name}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-500 truncate">
|
||||
{component.category}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 主编辑区域 */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{editMode === 'design' && (
|
||||
<motion.div
|
||||
key="design"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
className="flex-1 flex"
|
||||
data-element="design-mode"
|
||||
>
|
||||
{/* 设计画布 */}
|
||||
<div className="flex-1 bg-neutral-100 dark:bg-neutral-900 p-8 overflow-auto">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-lg p-8 min-h-96">
|
||||
<ComponentPreview
|
||||
component={selectedComponent}
|
||||
deviceType={deviceType}
|
||||
isPlaying={isPlaying}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 属性面板 */}
|
||||
<div className="w-80 border-l border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 overflow-y-auto">
|
||||
<div className="p-4">
|
||||
<PropertyPanel
|
||||
component={selectedComponent}
|
||||
onUpdate={handleComponentUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{editMode === 'code' && (
|
||||
<motion.div
|
||||
key="code"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
className="flex-1 flex flex-col"
|
||||
data-element="code-mode"
|
||||
>
|
||||
{/* 代码编辑器 */}
|
||||
<div className="flex-1 p-6 bg-neutral-900 text-green-400 font-mono text-sm overflow-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-white font-semibold">Generated Code</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={copyCode}
|
||||
data-component="Button"
|
||||
data-element="copy-code-button"
|
||||
data-action="copy-code"
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="bg-neutral-800 p-4 rounded-lg overflow-x-auto">
|
||||
<code data-element="code-content">
|
||||
{code || '// Click "Generate Code" to create component code'}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* 代码预览 */}
|
||||
<div className="h-64 border-t border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
|
||||
<ComponentPreview
|
||||
component={selectedComponent}
|
||||
deviceType={deviceType}
|
||||
isPlaying={isPlaying}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{editMode === 'preview' && (
|
||||
<motion.div
|
||||
key="preview"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
className="flex-1 bg-neutral-100 dark:bg-neutral-900 overflow-auto"
|
||||
data-element="preview-mode"
|
||||
>
|
||||
<div className="min-h-full p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-lg overflow-hidden">
|
||||
<div className="p-8">
|
||||
<ComponentPreview
|
||||
component={selectedComponent}
|
||||
deviceType={deviceType}
|
||||
isPlaying={isPlaying}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComponentEditor;
|
||||
@@ -0,0 +1,527 @@
|
||||
/**
|
||||
* @fileoverview 仪表盘页面组件
|
||||
* @description 设计系统的概览仪表盘,展示系统状态、统计信息和快速入口
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Palette,
|
||||
Type,
|
||||
Layout,
|
||||
Zap,
|
||||
ArrowRight,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Activity,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
import { Card, CardBody, CardHeader, CardGroup } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
|
||||
import { useDesignTokens, useColors, useTypography, useSpacing, useEvents } from '../store/design-system-store';
|
||||
|
||||
// 简化统计卡片组件
|
||||
interface StatsCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon: React.ReactNode;
|
||||
trend?: {
|
||||
value: number;
|
||||
label: string;
|
||||
};
|
||||
color: 'primary' | 'secondary' | 'success' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
// 统计卡片组件
|
||||
const StatsCard: React.FC<StatsCardProps> = ({ title, value, icon, trend, color }) => {
|
||||
const colorClasses = {
|
||||
primary: 'from-primary-500 to-primary-600',
|
||||
secondary: 'from-secondary-500 to-secondary-600',
|
||||
success: 'from-success-500 to-success-600',
|
||||
warning: 'from-warning-500 to-warning-600',
|
||||
error: 'from-error-500 to-error-600',
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
whileHover={{ y: -2 }}
|
||||
className="stats-card"
|
||||
data-component="StatsCard"
|
||||
data-element="card"
|
||||
data-color={color}
|
||||
>
|
||||
<Card interactive className="h-full">
|
||||
<CardBody className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className={`
|
||||
w-12 h-12 rounded-lg bg-gradient-to-br ${colorClasses[color]}
|
||||
flex items-center justify-center text-white shadow-lg
|
||||
`} data-element="icon">
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{trend && (
|
||||
<div className="flex items-center text-sm" data-element="trend">
|
||||
<TrendingUp className="w-4 h-4 text-success-500 mr-1" />
|
||||
<span className="text-success-600 dark:text-success-400">
|
||||
+{trend.value}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2" data-element="content">
|
||||
<h3 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100" data-element="value">
|
||||
{value}
|
||||
</h3>
|
||||
<p className="text-neutral-600 dark:text-neutral-400 text-sm" data-element="label">
|
||||
{title}
|
||||
</p>
|
||||
{trend && (
|
||||
<p className="text-xs text-neutral-500" data-element="trend-label">
|
||||
{trend.label}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
// 活动项组件
|
||||
interface ActivityItemProps {
|
||||
type: 'token' | 'component' | 'library' | 'theme';
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: string;
|
||||
status: 'success' | 'pending' | 'error';
|
||||
}
|
||||
|
||||
// 活动项组件
|
||||
const ActivityItem: React.FC<ActivityItemProps> = ({ type, title, description, timestamp, status }) => {
|
||||
const getTypeIcon = () => {
|
||||
switch (type) {
|
||||
case 'token':
|
||||
return <Palette className="w-4 h-4" />;
|
||||
case 'component':
|
||||
return <Layout className="w-4 h-4" />;
|
||||
case 'library':
|
||||
return <Type className="w-4 h-4" />;
|
||||
case 'theme':
|
||||
return <Zap className="w-4 h-4" />;
|
||||
default:
|
||||
return <Activity className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'text-success-500';
|
||||
case 'pending':
|
||||
return 'text-warning-500';
|
||||
case 'error':
|
||||
return 'text-error-500';
|
||||
default:
|
||||
return 'text-neutral-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircle className="w-4 h-4" />;
|
||||
case 'pending':
|
||||
return <Clock className="w-4 h-4" />;
|
||||
case 'error':
|
||||
return <AlertCircle className="w-4 h-4" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="activity-item flex items-start space-x-3 p-3 hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg transition-colors" data-component="ActivityItem" data-element="item" data-type={type} data-status={status}>
|
||||
<div className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0
|
||||
${status === 'success' ? 'bg-success-100 dark:bg-success-900/20' :
|
||||
status === 'pending' ? 'bg-warning-100 dark:bg-warning-900/20' :
|
||||
'bg-error-100 dark:bg-error-900/20'
|
||||
}
|
||||
`} data-element="icon-container">
|
||||
{getTypeIcon()}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0" data-element="content">
|
||||
<div className="flex items-center justify-between mb-1" data-element="header">
|
||||
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100 truncate" data-element="title">
|
||||
{title}
|
||||
</p>
|
||||
<div className={`flex items-center ${getStatusColor()}`} data-element="status">
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-600 dark:text-neutral-400 mb-2" data-element="description">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-xs text-neutral-500" data-element="timestamp">
|
||||
{timestamp}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 仪表盘主组件
|
||||
*/
|
||||
const Dashboard: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 获取设计系统数据
|
||||
const colors = useColors();
|
||||
const typography = useTypography();
|
||||
const spacing = useSpacing();
|
||||
const events = useEvents();
|
||||
|
||||
// 模拟数据加载
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// 统计数据
|
||||
const stats = {
|
||||
totalTokens: colors.length + typography.length + spacing.length,
|
||||
totalColors: colors.length,
|
||||
totalTypography: typography.length,
|
||||
totalSpacing: spacing.length,
|
||||
totalComponents: 24, // 模拟数据
|
||||
totalLibraries: 3, // 模拟数据
|
||||
designScore: 94, // 模拟数据
|
||||
};
|
||||
|
||||
// 最近活动(基于真实事件)
|
||||
const recentActivities = events.slice(0, 5).map((event, index) => ({
|
||||
type: event.type.includes('token') ? 'token' : event.type.includes('component') ? 'component' : 'theme',
|
||||
title: `System ${event.type}`,
|
||||
description: `System ${event.type} was updated`,
|
||||
timestamp: new Date(event.timestamp).toLocaleTimeString(),
|
||||
status: 'success' as const
|
||||
}));
|
||||
|
||||
// 如果加载中,显示加载状态
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="dashboard-loading" data-component="DashboardLoading" data-element="loading">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<LoadingSpinner size="lg" />
|
||||
<span className="ml-3 text-neutral-600 dark:text-neutral-400">Loading dashboard...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard space-y-8" data-component="Dashboard" data-element="dashboard">
|
||||
{/* 欢迎区域 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="welcome-section" data-element="welcome-section"
|
||||
>
|
||||
<div className="bg-gradient-to-r from-primary-500 to-secondary-500 rounded-xl p-8 text-white" data-element="hero">
|
||||
<div className="max-w-3xl" data-element="content">
|
||||
<h1 className="text-3xl font-bold mb-4" data-element="title">
|
||||
Welcome to Design System Studio
|
||||
</h1>
|
||||
<p className="text-primary-100 mb-6 text-lg" data-element="subtitle">
|
||||
Manage your design tokens, components, and build consistent user interfaces.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3" data-element="actions">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-white/20 border-white/30 text-white hover:bg-white/30"
|
||||
data-component="Button"
|
||||
data-element="get-started-button"
|
||||
data-action="navigate-editor"
|
||||
>
|
||||
Get Started
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-white border-white/30 hover:bg-white/10"
|
||||
data-component="Button"
|
||||
data-element="learn-more-button"
|
||||
data-action="navigate-docs"
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="stats-section" data-element="stats-section"
|
||||
>
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100 mb-6" data-element="section-title">
|
||||
System Overview
|
||||
</h2>
|
||||
<CardGroup columns={4} gap="lg" data-element="stats-grid">
|
||||
<StatsCard
|
||||
title="Total Tokens"
|
||||
value={stats.totalTokens}
|
||||
icon={<span className="text-2xl">🎨</span>}
|
||||
trend={{ value: 12, label: 'from last month' }}
|
||||
color="primary"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Design Score"
|
||||
value={`${stats.designScore}%`}
|
||||
icon={<span className="text-2xl">✅</span>}
|
||||
trend={{ value: 5, label: 'improvement' }}
|
||||
color="success"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Components"
|
||||
value={stats.totalComponents}
|
||||
icon={<span className="text-2xl">🧩</span>}
|
||||
trend={{ value: 8, label: 'new this month' }}
|
||||
color="secondary"
|
||||
/>
|
||||
<StatsCard
|
||||
title="Libraries"
|
||||
value={stats.totalLibraries}
|
||||
icon={<span className="text-2xl">📚</span>}
|
||||
color="warning"
|
||||
/>
|
||||
</CardGroup>
|
||||
</motion.div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8" data-element="content-grid">
|
||||
{/* 左侧列 */}
|
||||
<div className="lg:col-span-2 space-y-8" data-element="left-column">
|
||||
{/* 快速访问 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="quick-access" data-element="quick-access"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader title="Quick Access" />
|
||||
<CardBody>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4" data-element="access-grid">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="justify-start h-auto p-4"
|
||||
data-component="Button"
|
||||
data-element="design-system-button"
|
||||
data-action="navigate-design-system"
|
||||
>
|
||||
<div className="flex items-center space-x-3" data-element="content">
|
||||
<Palette className="w-5 h-5 text-primary-500" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Design System</div>
|
||||
<div className="text-sm text-neutral-500">Manage tokens & themes</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="justify-start h-auto p-4"
|
||||
data-component="Button"
|
||||
data-element="component-editor-button"
|
||||
data-action="navigate-editor"
|
||||
>
|
||||
<div className="flex items-center space-x-3" data-element="content">
|
||||
<Layout className="w-5 h-5 text-secondary-500" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Component Editor</div>
|
||||
<div className="text-sm text-neutral-500">Build & customize</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="justify-start h-auto p-4"
|
||||
data-component="Button"
|
||||
data-element="live-preview-button"
|
||||
data-action="navigate-preview"
|
||||
>
|
||||
<div className="flex items-center space-x-3" data-element="content">
|
||||
<Zap className="w-5 h-5 text-success-500" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Live Preview</div>
|
||||
<div className="text-sm text-neutral-500">Real-time testing</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="justify-start h-auto p-4"
|
||||
data-component="Button"
|
||||
data-element="libraries-button"
|
||||
data-action="navigate-libraries"
|
||||
>
|
||||
<div className="flex items-center space-x-3" data-element="content">
|
||||
<Users className="w-5 h-5 text-warning-500" />
|
||||
<div className="text-left">
|
||||
<div className="font-medium">Libraries</div>
|
||||
<div className="text-sm text-neutral-500">Component collections</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* 令牌统计 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="token-stats" data-element="token-stats"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader title="Token Distribution" />
|
||||
<CardBody>
|
||||
<div className="space-y-4" data-element="token-grid">
|
||||
<div className="flex items-center justify-between p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-3 h-3 bg-primary-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium">Colors</span>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">{stats.totalColors} tokens</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-3 h-3 bg-secondary-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium">Typography</span>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">{stats.totalTypography} tokens</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-3 h-3 bg-success-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium">Spacing</span>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">{stats.totalSpacing} tokens</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 右侧列 */}
|
||||
<div className="space-y-8" data-element="right-column">
|
||||
{/* 最近活动 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="recent-activity" data-element="recent-activity"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader title="Recent Activity" />
|
||||
<CardBody>
|
||||
<div className="space-y-3" data-element="activity-list">
|
||||
{recentActivities.length > 0 ? (
|
||||
recentActivities.map((activity, index) => (
|
||||
<ActivityItem key={index} {...activity} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-8 text-neutral-500 dark:text-neutral-400">
|
||||
<Activity className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No recent activity</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{events.length > 5 && (
|
||||
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-800">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
data-component="Button"
|
||||
data-element="view-all-activity-button"
|
||||
data-action="navigate-activity"
|
||||
>
|
||||
View All Activity
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* 系统状态 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="system-status" data-element="system-status"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader title="System Status" />
|
||||
<CardBody>
|
||||
<div className="space-y-4" data-element="status-items">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<CheckCircle className="w-5 h-5 text-success-500" />
|
||||
<span className="text-sm font-medium">Design Mode</span>
|
||||
</div>
|
||||
<span className="text-sm text-success-600 dark:text-success-400">Active</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<CheckCircle className="w-5 h-5 text-success-500" />
|
||||
<span className="text-sm font-medium">Token Sync</span>
|
||||
</div>
|
||||
<span className="text-sm text-success-600 dark:text-success-400">Synced</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<CheckCircle className="w-5 h-5 text-success-500" />
|
||||
<span className="text-sm font-medium">Component Library</span>
|
||||
</div>
|
||||
<span className="text-sm text-success-600 dark:text-success-400">Updated</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* @fileoverview 设计系统管理页面
|
||||
* @description 展示和管理设计系统的所有令牌,包括颜色、字体、间距等
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Palette,
|
||||
Type,
|
||||
Ruler,
|
||||
Shadow,
|
||||
Circle,
|
||||
Zap,
|
||||
Search,
|
||||
Filter,
|
||||
Grid,
|
||||
List,
|
||||
Download,
|
||||
Upload,
|
||||
Settings,
|
||||
Plus,
|
||||
Eye,
|
||||
EyeOff
|
||||
} from 'lucide-react';
|
||||
import { Card, CardBody, CardHeader } from '../components/ui/Card';
|
||||
import { Button, IconButton } from '../components/ui/Button';
|
||||
import { TokenCard } from '../components/design-system/TokenCard';
|
||||
import { ColorPalette } from '../components/design-system/ColorPalette';
|
||||
import {
|
||||
useColors,
|
||||
useTypography,
|
||||
useSpacing,
|
||||
useShadows,
|
||||
useBorderRadius,
|
||||
useAnimations,
|
||||
useSelectedToken,
|
||||
useDesignSystemStore
|
||||
} from '../store/design-system-store';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
// 令牌类型定义
|
||||
type TokenType = 'all' | 'color' | 'typography' | 'spacing' | 'shadow' | 'border-radius' | 'animation';
|
||||
|
||||
// 令牌类别配置
|
||||
const TOKEN_CATEGORIES = {
|
||||
all: { label: 'All Tokens', icon: Settings, color: 'neutral' },
|
||||
color: { label: 'Colors', icon: Palette, color: 'primary' },
|
||||
typography: { label: 'Typography', icon: Type, color: 'secondary' },
|
||||
spacing: { label: 'Spacing', icon: Ruler, color: 'success' },
|
||||
shadow: { label: 'Shadows', icon: Shadow, color: 'warning' },
|
||||
'border-radius': { label: 'Border Radius', icon: Circle, color: 'error' },
|
||||
animation: { label: 'Animation', icon: Zap, color: 'info' },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 设计系统主页面组件
|
||||
*/
|
||||
const DesignSystem: React.FC = () => {
|
||||
const [activeCategory, setActiveCategory] = useState<TokenType>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [showDeprecated, setShowDeprecated] = useState(true);
|
||||
const [selectedToken, setSelectedToken] = useState(null);
|
||||
|
||||
// 获取所有令牌数据
|
||||
const colors = useColors();
|
||||
const typography = useTypography();
|
||||
const spacing = useSpacing();
|
||||
const shadows = useShadows();
|
||||
const borderRadius = useBorderRadius();
|
||||
const animations = useAnimations();
|
||||
|
||||
// 合并所有令牌
|
||||
const allTokens = useMemo(() => {
|
||||
return [
|
||||
...colors,
|
||||
...typography,
|
||||
...spacing,
|
||||
...shadows,
|
||||
...borderRadius,
|
||||
...animations,
|
||||
];
|
||||
}, [colors, typography, spacing, shadows, borderRadius, animations]);
|
||||
|
||||
// 过滤令牌
|
||||
const filteredTokens = useMemo(() => {
|
||||
return allTokens.filter(token => {
|
||||
// 类别过滤
|
||||
const matchesCategory = activeCategory === 'all' || token.category === activeCategory;
|
||||
|
||||
// 搜索过滤
|
||||
const matchesSearch = searchQuery === '' ||
|
||||
token.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
token.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
token.value.toString().toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
// 废弃状态过滤
|
||||
const matchesDeprecated = showDeprecated || !token.deprecated;
|
||||
|
||||
return matchesCategory && matchesSearch && matchesDeprecated;
|
||||
});
|
||||
}, [allTokens, activeCategory, searchQuery, showDeprecated]);
|
||||
|
||||
// 渲染特定类别的内容
|
||||
const renderCategoryContent = () => {
|
||||
switch (activeCategory) {
|
||||
case 'color':
|
||||
return (
|
||||
<ColorPalette
|
||||
variant="grid"
|
||||
showControls={false}
|
||||
interactive={true}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'typography':
|
||||
return (
|
||||
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{typography.map((token, index) => (
|
||||
<motion.div
|
||||
key={token.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<TokenCard token={token} interactive={true} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'spacing':
|
||||
return (
|
||||
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{spacing.map((token, index) => (
|
||||
<motion.div
|
||||
key={token.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<TokenCard token={token} variant="compact" interactive={true} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'shadow':
|
||||
return (
|
||||
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{shadows.map((token, index) => (
|
||||
<motion.div
|
||||
key={token.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<TokenCard token={token} interactive={true} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'border-radius':
|
||||
return (
|
||||
<div className="token-grid grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{borderRadius.map((token, index) => (
|
||||
<motion.div
|
||||
key={token.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<TokenCard token={token} variant="compact" interactive={true} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'animation':
|
||||
return (
|
||||
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{animations.map((token, index) => (
|
||||
<motion.div
|
||||
key={token.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
>
|
||||
<TokenCard token={token} interactive={true} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<AnimatePresence>
|
||||
{filteredTokens.map((token, index) => (
|
||||
<motion.div
|
||||
key={token.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ delay: index * 0.02 }}
|
||||
>
|
||||
<TokenCard token={token} interactive={true} />
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取类别统计
|
||||
const getCategoryStats = (type: TokenType) => {
|
||||
switch (type) {
|
||||
case 'color': return colors.length;
|
||||
case 'typography': return typography.length;
|
||||
case 'spacing': return spacing.length;
|
||||
case 'shadow': return shadows.length;
|
||||
case 'border-radius': return borderRadius.length;
|
||||
case 'animation': return animations.length;
|
||||
default: return allTokens.length;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="design-system-page" data-component="DesignSystemPage">
|
||||
{/* 页面头部 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="page-header mb-8" data-element="header"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
|
||||
<div className="space-y-2" data-element="title-section">
|
||||
<h1 className="text-3xl font-bold text-neutral-900 dark:text-neutral-100">
|
||||
Design System
|
||||
</h1>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
Manage your design tokens, typography, colors, and component guidelines
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3" data-element="actions">
|
||||
<Button
|
||||
variant="outline"
|
||||
icon={<Upload className="w-4 h-4" />}
|
||||
data-component="Button"
|
||||
data-element="import-button"
|
||||
data-action="import-tokens"
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
icon={<Download className="w-4 h-4" />}
|
||||
data-component="Button"
|
||||
data-element="export-button"
|
||||
data-action="export-tokens"
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
data-component="Button"
|
||||
data-element="add-token-button"
|
||||
data-action="add-token"
|
||||
>
|
||||
Add Token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 控制面板 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="control-panel mb-8" data-element="controls"
|
||||
>
|
||||
<Card>
|
||||
<CardBody className="p-4">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center space-y-4 lg:space-y-0 lg:space-x-6">
|
||||
{/* 搜索框 */}
|
||||
<div className="flex-1 max-w-md" data-element="search">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 w-4 h-4" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tokens..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 pr-4 py-2 w-full border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
data-component="Input"
|
||||
data-element="search-input"
|
||||
data-placeholder="search-tokens"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 视图模式切换 */}
|
||||
<div className="flex items-center space-x-2" data-element="view-mode">
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">View:</span>
|
||||
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 text-sm transition-colors',
|
||||
viewMode === 'grid'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
)}
|
||||
data-component="ToggleButton"
|
||||
data-element="grid-view-button"
|
||||
data-action="set-view-grid"
|
||||
>
|
||||
<Grid className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 text-sm transition-colors',
|
||||
viewMode === 'list'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
)}
|
||||
data-component="ToggleButton"
|
||||
data-element="list-view-button"
|
||||
data-action="set-view-list"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 废弃令牌切换 */}
|
||||
<div className="flex items-center space-x-2" data-element="deprecated-filter">
|
||||
<IconButton
|
||||
variant={showDeprecated ? 'primary' : 'ghost'}
|
||||
onClick={() => setShowDeprecated(!showDeprecated)}
|
||||
size="sm"
|
||||
data-component="IconButton"
|
||||
data-element="deprecated-toggle"
|
||||
data-action="toggle-deprecated"
|
||||
>
|
||||
{showDeprecated ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||
</IconButton>
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Show deprecated
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* 类别选择器 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="category-tabs mb-8" data-element="categories"
|
||||
>
|
||||
<div className="flex flex-wrap gap-2" data-element="tab-list">
|
||||
{Object.entries(TOKEN_CATEGORIES).map(([key, config]) => {
|
||||
const Icon = config.icon;
|
||||
const count = getCategoryStats(key as TokenType);
|
||||
const isActive = activeCategory === key;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveCategory(key as TokenType)}
|
||||
className={clsx(
|
||||
'flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all duration-200',
|
||||
isActive
|
||||
? 'bg-primary-500 text-white border-primary-500 shadow-md'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 border-neutral-300 dark:border-neutral-600 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
)}
|
||||
data-component="CategoryTab"
|
||||
data-element="category-tab"
|
||||
data-category={key}
|
||||
data-active={isActive ? 'true' : 'false'}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="font-medium">{config.label}</span>
|
||||
<span className={clsx(
|
||||
'px-2 py-0.5 text-xs rounded-full',
|
||||
isActive
|
||||
? 'bg-white/20 text-white'
|
||||
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400'
|
||||
)}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="main-content" data-element="content"
|
||||
>
|
||||
{/* 空状态 */}
|
||||
{filteredTokens.length === 0 && activeCategory !== 'color' ? (
|
||||
<Card>
|
||||
<CardBody className="p-12 text-center">
|
||||
<div className="w-16 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Search className="w-8 h-8 text-neutral-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">
|
||||
No tokens found
|
||||
</h3>
|
||||
<p className="text-neutral-600 dark:text-neutral-400 mb-6">
|
||||
Try adjusting your search criteria or browse different categories.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setActiveCategory('all');
|
||||
}}
|
||||
data-component="Button"
|
||||
data-element="clear-filters-button"
|
||||
data-action="clear-filters"
|
||||
>
|
||||
Clear Filters
|
||||
</Button>
|
||||
</CardBody>
|
||||
</Card>
|
||||
) : (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeCategory}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="category-content" data-element="category-content"
|
||||
>
|
||||
{renderCategoryContent()}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="stats-footer mt-12 pt-8 border-t border-neutral-200 dark:border-neutral-800" data-element="stats"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 text-center" data-element="stats-grid">
|
||||
{Object.entries(TOKEN_CATEGORIES).slice(1).map(([key, config]) => {
|
||||
const Icon = config.icon;
|
||||
const count = getCategoryStats(key as TokenType);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="stat-item p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg"
|
||||
data-component="StatItem"
|
||||
data-element="stat-item"
|
||||
data-category={key}
|
||||
>
|
||||
<Icon className={clsx(
|
||||
'w-6 h-6 mx-auto mb-2',
|
||||
`text-${config.color}-500`
|
||||
)} />
|
||||
<div className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
|
||||
{count}
|
||||
</div>
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{config.label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesignSystem;
|
||||
@@ -0,0 +1,566 @@
|
||||
/**
|
||||
* @fileoverview 实时预览页面
|
||||
* @description 展示组件的实时预览效果,支持不同设备和主题切换
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
RefreshCw,
|
||||
Maximize,
|
||||
Smartphone,
|
||||
Tablet,
|
||||
Monitor,
|
||||
Moon,
|
||||
Sun,
|
||||
Settings,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Play,
|
||||
Pause,
|
||||
Download,
|
||||
Share2
|
||||
} from 'lucide-react';
|
||||
import { Card, CardBody, CardHeader } from '../components/ui/Card';
|
||||
import { Button, IconButton } from '../components/ui/Button';
|
||||
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
|
||||
import { useDesignModeStore } from '../main';
|
||||
|
||||
// 设备类型
|
||||
type DeviceType = 'desktop' | 'tablet' | 'mobile';
|
||||
|
||||
// 主题类型
|
||||
type ThemeType = 'light' | 'dark' | 'system';
|
||||
|
||||
// 预览模式
|
||||
type PreviewMode = 'component' | 'page' | 'interaction';
|
||||
|
||||
// 示例组件数据
|
||||
const PREVIEW_COMPONENTS = [
|
||||
{
|
||||
id: 'hero-section',
|
||||
name: 'Hero Section',
|
||||
description: 'Landing page hero with call-to-action',
|
||||
category: 'Layout',
|
||||
component: (
|
||||
<div className="bg-gradient-to-r from-primary-600 to-secondary-600 text-white py-20 px-8 text-center">
|
||||
<h1 className="text-5xl font-bold mb-6">Design System Studio</h1>
|
||||
<p className="text-xl mb-8 max-w-2xl mx-auto">
|
||||
Build consistent and beautiful user interfaces with our comprehensive design system toolkit.
|
||||
</p>
|
||||
<div className="flex justify-center space-x-4">
|
||||
<button className="bg-white text-primary-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors">
|
||||
Get Started
|
||||
</button>
|
||||
<button className="border-2 border-white text-white px-8 py-3 rounded-lg font-semibold hover:bg-white hover:text-primary-600 transition-colors">
|
||||
Learn More
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'feature-grid',
|
||||
name: 'Feature Grid',
|
||||
description: 'Grid of features with icons and descriptions',
|
||||
category: 'Content',
|
||||
component: (
|
||||
<div className="py-16 px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">Powerful Features</h2>
|
||||
<p className="text-gray-600 max-w-2xl mx-auto">
|
||||
Everything you need to build and maintain a consistent design system.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{[
|
||||
{ icon: '🎨', title: 'Design Tokens', desc: 'Centralized design decisions' },
|
||||
{ icon: '🔧', title: 'Component Library', desc: 'Reusable UI components' },
|
||||
{ icon: '📱', title: 'Responsive Design', desc: 'Mobile-first approach' },
|
||||
{ icon: '⚡', title: 'Fast Performance', desc: 'Optimized for speed' },
|
||||
{ icon: '🎯', title: 'Accessibility', desc: 'WCAG compliant design' },
|
||||
{ icon: '🔄', title: 'Real-time Sync', desc: 'Instant updates across team' }
|
||||
].map((feature, index) => (
|
||||
<div key={index} className="text-center p-6 bg-gray-50 rounded-lg">
|
||||
<div className="text-4xl mb-4">{feature.icon}</div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-2">{feature.title}</h3>
|
||||
<p className="text-gray-600">{feature.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'form-example',
|
||||
name: 'Contact Form',
|
||||
description: 'Complete contact form with validation',
|
||||
category: 'Form',
|
||||
component: (
|
||||
<div className="max-w-2xl mx-auto py-16 px-8">
|
||||
<div className="text-center mb-8">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">Contact Us</h2>
|
||||
<p className="text-gray-600">We'd love to hear from you. Send us a message!</p>
|
||||
</div>
|
||||
<form className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="John"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Message</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Tell us more about your project..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 text-sm text-gray-600">
|
||||
I agree to the <a href="#" className="text-primary-600 hover:underline">Terms of Service</a> and <a href="#" className="text-primary-600 hover:underline">Privacy Policy</a>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-primary-600 text-white py-3 px-6 rounded-lg font-semibold hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
// 设备预览组件
|
||||
const DevicePreview: React.FC<{
|
||||
component: React.ReactNode;
|
||||
deviceType: DeviceType;
|
||||
theme: ThemeType;
|
||||
isLoading?: boolean;
|
||||
}> = ({ component, deviceType, theme, isLoading = false }) => {
|
||||
const getDeviceStyles = () => {
|
||||
switch (deviceType) {
|
||||
case 'mobile':
|
||||
return {
|
||||
width: '375px',
|
||||
height: '667px',
|
||||
maxWidth: '100%',
|
||||
borderRadius: '25px',
|
||||
border: '1px solid #e5e5e5'
|
||||
};
|
||||
case 'tablet':
|
||||
return {
|
||||
width: '768px',
|
||||
height: '1024px',
|
||||
maxWidth: '100%',
|
||||
borderRadius: '15px',
|
||||
border: '1px solid #e5e5e5'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
width: '100%',
|
||||
height: '600px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e5e5e5'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const deviceStyles = getDeviceStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="device-preview flex justify-center items-center bg-gray-100 dark:bg-gray-900 p-8"
|
||||
data-component="DevicePreview"
|
||||
data-element="preview-container"
|
||||
data-device={deviceType}
|
||||
data-theme={theme}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="device-frame bg-white dark:bg-gray-800 overflow-hidden shadow-2xl"
|
||||
style={deviceStyles}
|
||||
data-element="device-frame"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="preview-content h-full overflow-auto"
|
||||
data-element="preview-content"
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 实时预览主页面
|
||||
*/
|
||||
const LivePreview: React.FC = () => {
|
||||
const [selectedComponent, setSelectedComponent] = useState(PREVIEW_COMPONENTS[0]);
|
||||
const [deviceType, setDeviceType] = useState<DeviceType>('desktop');
|
||||
const [theme, setTheme] = useState<ThemeType>('light');
|
||||
const [previewMode, setPreviewMode] = useState<PreviewMode>('component');
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [isAutoRefresh, setIsAutoRefresh] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [lastUpdate, setLastUpdate] = useState(new Date());
|
||||
|
||||
const { isDesignModeEnabled } = useDesignModeStore();
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 刷新预览
|
||||
const refreshPreview = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
// 模拟加载延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setIsLoading(false);
|
||||
setLastUpdate(new Date());
|
||||
}, []);
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
if (!document.fullscreenElement) {
|
||||
previewRef.current?.requestFullscreen();
|
||||
setIsFullscreen(true);
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
setIsFullscreen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 应用主题
|
||||
const applyTheme = useCallback((newTheme: ThemeType) => {
|
||||
setTheme(newTheme);
|
||||
const root = document.documentElement;
|
||||
|
||||
switch (newTheme) {
|
||||
case 'dark':
|
||||
root.classList.add('dark');
|
||||
break;
|
||||
case 'light':
|
||||
root.classList.remove('dark');
|
||||
break;
|
||||
case 'system':
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
root.classList.toggle('dark', prefersDark);
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化主题
|
||||
React.useEffect(() => {
|
||||
applyTheme(theme);
|
||||
}, [theme, applyTheme]);
|
||||
|
||||
// 设备切换时刷新
|
||||
React.useEffect(() => {
|
||||
if (isAutoRefresh) {
|
||||
refreshPreview();
|
||||
}
|
||||
}, [deviceType, selectedComponent, isAutoRefresh, refreshPreview]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="live-preview h-full flex flex-col"
|
||||
data-component="LivePreview"
|
||||
data-design-mode={isDesignModeEnabled ? 'true' : 'false'}
|
||||
data-preview-mode={previewMode}
|
||||
data-device={deviceType}
|
||||
data-theme={theme}
|
||||
>
|
||||
{/* 预览控制栏 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="preview-toolbar border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4"
|
||||
data-element="toolbar"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 左侧:组件选择和模式 */}
|
||||
<div className="flex items-center space-x-4" data-element="left-controls">
|
||||
{/* 组件选择 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
Preview:
|
||||
</label>
|
||||
<select
|
||||
value={selectedComponent.id}
|
||||
onChange={(e) => {
|
||||
const component = PREVIEW_COMPONENTS.find(c => c.id === e.target.value);
|
||||
if (component) setSelectedComponent(component);
|
||||
}}
|
||||
className="px-3 py-1.5 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
data-component="Select"
|
||||
data-element="component-select"
|
||||
>
|
||||
{PREVIEW_COMPONENTS.map((component) => (
|
||||
<option key={component.id} value={component.id}>
|
||||
{component.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 预览模式切换 */}
|
||||
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden">
|
||||
{[
|
||||
{ mode: 'component' as PreviewMode, icon: Settings, label: 'Component' },
|
||||
{ mode: 'page' as PreviewMode, icon: Monitor, label: 'Page' },
|
||||
{ mode: 'interaction' as PreviewMode, icon: Play, label: 'Interaction' }
|
||||
].map(({ mode, icon: Icon, label }) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => setPreviewMode(mode)}
|
||||
className={`
|
||||
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
|
||||
${previewMode === mode
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
}
|
||||
`}
|
||||
data-component="ModeButton"
|
||||
data-element="mode-button"
|
||||
data-mode={mode}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:控制按钮 */}
|
||||
<div className="flex items-center space-x-3" data-element="right-controls">
|
||||
{/* 设备切换 */}
|
||||
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden" data-element="device-toggle">
|
||||
{[
|
||||
{ type: 'mobile' as DeviceType, icon: Smartphone, label: 'Mobile' },
|
||||
{ type: 'tablet' as DeviceType, icon: Tablet, label: 'Tablet' },
|
||||
{ type: 'desktop' as DeviceType, icon: Monitor, label: 'Desktop' }
|
||||
].map(({ type, icon: Icon, label }) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setDeviceType(type)}
|
||||
className={`
|
||||
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
|
||||
${deviceType === type
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
}
|
||||
`}
|
||||
data-component="DeviceButton"
|
||||
data-element="device-button"
|
||||
data-device={type}
|
||||
title={label}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 主题切换 */}
|
||||
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden">
|
||||
{[
|
||||
{ type: 'light' as ThemeType, icon: Sun, label: 'Light' },
|
||||
{ type: 'dark' as ThemeType, icon: Moon, label: 'Dark' },
|
||||
{ type: 'system' as ThemeType, icon: Settings, label: 'System' }
|
||||
].map(({ type, icon: Icon, label }) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => applyTheme(type)}
|
||||
className={`
|
||||
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
|
||||
${theme === type
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
|
||||
}
|
||||
`}
|
||||
data-component="ThemeButton"
|
||||
data-element="theme-button"
|
||||
data-theme={type}
|
||||
title={label}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 自动刷新切换 */}
|
||||
<IconButton
|
||||
variant={isAutoRefresh ? 'primary' : 'ghost'}
|
||||
onClick={() => setIsAutoRefresh(!isAutoRefresh)}
|
||||
title="Auto Refresh"
|
||||
data-component="IconButton"
|
||||
data-element="auto-refresh-toggle"
|
||||
data-action="toggle-auto-refresh"
|
||||
>
|
||||
{isAutoRefresh ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
|
||||
</IconButton>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={refreshPreview}
|
||||
disabled={isLoading}
|
||||
title="Refresh Preview"
|
||||
data-component="IconButton"
|
||||
data-element="refresh-button"
|
||||
data-action="refresh-preview"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
</IconButton>
|
||||
|
||||
{/* 全屏按钮 */}
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={toggleFullscreen}
|
||||
title="Fullscreen"
|
||||
data-component="IconButton"
|
||||
data-element="fullscreen-button"
|
||||
data-action="toggle-fullscreen"
|
||||
>
|
||||
<Maximize className="w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 预览信息栏 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="preview-info bg-neutral-50 dark:bg-neutral-800 px-4 py-2 text-sm text-neutral-600 dark:text-neutral-400 border-b border-neutral-200 dark:border-neutral-700"
|
||||
data-element="info-bar"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4" data-element="preview-info-left">
|
||||
<span data-element="component-name">
|
||||
{selectedComponent.name} • {selectedComponent.category}
|
||||
</span>
|
||||
<span data-element="device-info">
|
||||
{deviceType.charAt(0).toUpperCase() + deviceType.slice(1)} • {theme.charAt(0).toUpperCase() + theme.slice(1)} Mode
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4" data-element="preview-info-right">
|
||||
<span data-element="last-update">
|
||||
Last updated: {lastUpdate.toLocaleTimeString()}
|
||||
</span>
|
||||
{isAutoRefresh && (
|
||||
<span className="text-success-600 dark:text-success-400" data-element="auto-refresh-status">
|
||||
● Auto-refresh ON
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 主要预览区域 */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={`${selectedComponent.id}-${deviceType}-${theme}`}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="preview-area h-full"
|
||||
data-element="preview-area"
|
||||
>
|
||||
<DevicePreview
|
||||
component={selectedComponent.component}
|
||||
deviceType={deviceType}
|
||||
theme={theme}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 覆盖层指示器 */}
|
||||
<div className="absolute top-4 right-4 bg-black bg-opacity-50 text-white px-3 py-1 rounded text-sm">
|
||||
{selectedComponent.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="preview-actions border-t border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4"
|
||||
data-element="actions-bar"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3" data-element="left-actions">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon={<Download className="w-4 h-4" />}
|
||||
data-component="Button"
|
||||
data-element="download-button"
|
||||
data-action="download-preview"
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon={<Share2 className="w-4 h-4" />}
|
||||
data-component="Button"
|
||||
data-element="share-button"
|
||||
data-action="share-preview"
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3" data-element="right-actions">
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Preview Mode: {previewMode.charAt(0).toUpperCase() + previewMode.slice(1)}
|
||||
</div>
|
||||
<div className="w-3 h-3 bg-success-500 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LivePreview;
|
||||
Reference in New Issue
Block a user