diff --git a/README.md b/README.md index 8be0179..da4f18b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ -# uitocode +# 产品功能 +2026.4.23 +1、设计师视角需要增加一个原型库的按钮 +2、5月将流程跑通 + +2026.4.21 +1、从getdesign.md中选取风格,毛玻璃浅色等(已完成、确定meta-light) +2、提供切换产品、设计视角的能力 +3、精简功能:标签(整个页面或某个选中元素)(优先级1)、设计规范已元素的形式呈现 +4、可将工具栏分别收起至左、右两个图片按钮;左侧为根据角色变化的工具,右侧为通用工具(已完成) +5、可扩展功能:全屏预览、颜色/字体等的设置(要直观,可参考浏览器自带的拾色器) +6、线框图 -》设计规范 -》 设计稿 +7、语音图标,放到AI对话框的底部上传文件的图标旁边 -》 语音转文字(已完成) +8、opencode的开源代码 diff --git a/src/components/DesignFrame.tsx b/src/components/DesignFrame.tsx index 4ec1fd3..de17206 100644 --- a/src/components/DesignFrame.tsx +++ b/src/components/DesignFrame.tsx @@ -221,6 +221,7 @@ const DesignFrame: React.FC = ({ const renderContent = () => { switch (renderMode) { case 'iframe': + console.log('iframe', file.content); if (file.fileType === 'svg') { const svgHtml = ` @@ -250,6 +251,13 @@ const DesignFrame: React.FC = ({ max-width: 100%; height: auto; } + .clickable { + cursor: pointer; + } + .clickable.highlighted { + outline: 2px solid #47A5FA; + outline-offset: 4px; + } @@ -266,6 +274,11 @@ const DesignFrame: React.FC = ({ } }); }); + document.querySelectorAll('.clickable').forEach(function(element) { + element.addEventListener('click', function() { + this.classList.toggle('highlighted'); + }); + }); @@ -444,6 +457,7 @@ const DesignFrame: React.FC = ({ ); case 'html': + console.log('html',file.content); if (file.fileType === 'svg') { return (
= ({ case 'placeholder': default: + console.log('placeholder default',file.content); const placeholderIcon = file.fileType === 'svg' ? '🎨' : '🌐'; const placeholderHint = file.fileType === 'svg' ? t('designFrame.placeholder.svg') : t('designFrame.placeholder.html'); diff --git a/src/components/SideBar.css b/src/components/SideBar.css index bf940be..db011dc 100644 --- a/src/components/SideBar.css +++ b/src/components/SideBar.css @@ -1,6 +1,6 @@ .side-bar { position: absolute; - top: 100px; + top: 20px; left: 20px; width: 48px; background-color: var(--color-white); @@ -473,3 +473,171 @@ transform: scale(0.9); opacity: 0.5; } + +.toolbar-panel { + display: flex; + flex-direction: column; + gap: var(--space-4); + padding: var(--space-5); + background-color: var(--color-white); + border-top: 1px solid var(--color-border-subtle); + min-width: 280px; +} + +.toolbar-section { + display: flex; + align-items: center; +} + +.control-group { + display: flex; + align-items: center; + gap: var(--space-4); + flex-wrap: wrap; +} + +.toolbar-btn { + padding: var(--space-3) var(--space-5); + border: 1px solid var(--color-border-subtle); + background-color: var(--color-soft-gray); + color: var(--color-text-primary); + border-radius: var(--radius-pill); + cursor: pointer; + transition: background 200ms ease, transform 150ms ease; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-family-primary); + font-size: var(--font-size-button); + font-weight: var(--font-weight-regular); + letter-spacing: var(--letter-spacing-tighter); +} + +.toolbar-btn:hover { + background-color: var(--color-button-hover); + color: var(--color-white); + border-color: var(--color-meta-blue); + transform: scale(1.05); +} + +.toolbar-btn:focus-visible { + outline: 3px ring var(--color-meta-blue); + outline: auto 2px var(--color-meta-blue); + outline-offset: 2px; +} + +.toolbar-btn.active { + background-color: var(--color-meta-blue); + border-color: var(--color-meta-blue); + color: var(--color-white); +} + +.toolbar-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.zoom-display { + padding: 0 var(--space-3); + font-family: var(--font-family-secondary); + font-size: var(--font-size-small); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); + opacity: 0.8; + min-width: 50px; + text-align: center; + letter-spacing: var(--letter-spacing-normal); +} + +.zoom-value { + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); +} + +.layout-toggle { + display: flex; + background-color: var(--color-soft-gray); + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-pill); + overflow: hidden; +} + +.toggle-btn { + padding: var(--space-3) var(--space-7); + border: none; + background-color: transparent; + color: var(--color-text-primary); + opacity: 0.8; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-family-primary); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-regular); + letter-spacing: var(--letter-spacing-tighter); +} + +.toggle-btn:hover { + background-color: var(--color-button-hover); + color: var(--color-white); + opacity: 1; +} + +.toggle-btn:focus-visible { + outline: 3px ring var(--color-meta-blue); + outline: auto 2px var(--color-meta-blue); + outline-offset: 2px; +} + +.toggle-btn.active { + background-color: var(--color-meta-blue); + color: var(--color-white); +} + +.viewport-selector { + display: flex; + gap: var(--space-3); +} + +.viewport-btn { + padding: var(--space-3) var(--space-5); + border: 1px solid var(--color-border-subtle); + background-color: var(--color-soft-gray); + color: var(--color-text-primary); + opacity: 0.8; + border-radius: var(--radius-circle); + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-family-primary); + font-size: var(--font-size-caption); + letter-spacing: var(--letter-spacing-tighter); +} + +.viewport-btn:hover { + background-color: var(--color-button-hover); + color: var(--color-white); + opacity: 1; + border-color: var(--color-meta-blue); +} + +.viewport-btn:focus-visible { + outline: 3px ring var(--color-meta-blue); + outline: auto 2px var(--color-meta-blue); + outline-offset: 2px; +} + +.viewport-btn.active { + background-color: var(--color-meta-blue); + color: var(--color-white); + border-color: var(--color-meta-blue); +} + +.viewport-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index d5c25f4..29bcbce 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -1,16 +1,59 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import './SideBar.css' -import { ArrowIcon, NoteIcon, CopyIcon, PaletteIcon, PushPinIcon, PlusIcon } from './Icons' +import { + ArrowIcon, + NoteIcon, + CopyIcon, + PaletteIcon, + PushPinIcon, + PlusIcon, + HomeIcon, + RefreshIcon, + GlobeIcon, + MobileIcon, + TabletIcon, + DesktopIcon, + TreeIcon, + LinkIcon +} from './Icons' import ContextModal from './ContextModal' +import { ViewportMode, LayoutMode } from '../types/canvas.types' interface SideBarProps { selectedFrames: string[] onSelectNote: (frameName: string, note: string) => void onCreateContext: (title: string, description: string) => void + currentZoom?: number + onZoomChange?: (zoom: number) => void + onResetZoom?: () => void + onResetPositions?: () => void + layoutMode?: LayoutMode + onLayoutModeChange?: (mode: LayoutMode) => void + showConnections?: boolean + onToggleConnections?: () => void + useGlobalViewport?: boolean + onToggleGlobalViewport?: () => void + globalViewportMode?: ViewportMode + onViewportModeChange?: (mode: ViewportMode) => void } -const SideBar: React.FC = ({ selectedFrames, onSelectNote, onCreateContext }) => { +const SideBar: React.FC = ({ + selectedFrames, + onSelectNote, + onCreateContext, + currentZoom = 1, + onResetZoom, + onResetPositions, + layoutMode = 'grid', + onLayoutModeChange, + showConnections = true, + onToggleConnections, + useGlobalViewport = true, + onToggleGlobalViewport, + globalViewportMode = 'tablet', + onViewportModeChange +}) => { const { t } = useTranslation() const [activeItem, setActiveItem] = useState('select') const [showNoteModal, setShowNoteModal] = useState(false) @@ -19,12 +62,26 @@ const SideBar: React.FC = ({ selectedFrames, onSelectNote, onCreat const [noteModalPosition, setNoteModalPosition] = useState({ x: 0, y: 0 }) const [showResourceModal, setShowResourceModal] = useState(false) const [showContextModal, setShowContextModal] = useState(false) + const [currentViewportMode, setCurrentViewportMode] = useState(globalViewportMode) + + // 当globalViewportMode变化时更新currentViewportMode + useEffect(() => { + setCurrentViewportMode(globalViewportMode) + }, [globalViewportMode]) const menuItems = [ { id: 'select', icon: , label: t('sideBar.menu.select') }, { id: 'note', icon: , label: t('sideBar.menu.note') }, - { id: 'copy', icon: , label: t('sideBar.menu.copy') }, + // { id: 'copy', icon: , label: t('sideBar.menu.copy') }, { id: 'customize', icon: , label: t('sideBar.menu.customize') }, + // Canvas controls + { id: 'resetZoom', icon: , label: t('canvasArea.toolbar.resetZoom') }, + { id: 'resetPositions', icon: , label: t('canvasArea.toolbar.resetPositions') }, + { id: 'layoutGrid', icon: , label: t('canvasArea.toolbar.gridLayout') }, + { id: 'layoutHierarchy', icon: , label: t('canvasArea.toolbar.hierarchyLayout') }, + { id: 'toggleConnections', icon: , label: t('canvasArea.toolbar.toggleConnections') }, + { id: 'toggleGlobalViewport', icon: , label: t('canvasArea.toolbar.toggleGlobalViewport') }, + { id: 'viewportToggle', icon: currentViewportMode === 'mobile' ? : currentViewportMode === 'tablet' ? : , label: t(`canvasArea.toolbar.${currentViewportMode}View`) }, // { id: 'fix', icon: , label: t('sideBar.menu.fix') }, // { id: 'add', icon: , label: t('sideBar.menu.add') }, ] @@ -43,20 +100,18 @@ const SideBar: React.FC = ({ selectedFrames, onSelectNote, onCreat setShowNoSelectionMessage(false) setShowNoteModal(true) - // 计算弹窗位置,跟随选中的DesignFrame const selectedFrame = document.querySelector(`[data-frame-name="${selectedFrames[0]}"]`) if (selectedFrame) { const rect = selectedFrame.getBoundingClientRect() setNoteModalPosition({ - x: rect.left + rect.width / 2 - 200, // 居中 - y: rect.top + rect.height / 2 - 150 // 居中 + x: rect.left + rect.width / 2 - 200, + y: rect.top + rect.height / 2 - 150 }) } } else { setShowNoSelectionMessage(true) setShowNoteModal(false) - // 3秒后自动隐藏提示 setTimeout(() => { setShowNoSelectionMessage(false) }, 3000) @@ -65,6 +120,23 @@ const SideBar: React.FC = ({ selectedFrames, onSelectNote, onCreat setShowResourceModal(true) } else if (item.id === 'fix') { setShowContextModal(true) + } else if (item.id === 'resetZoom') { + onResetZoom?.() + } else if (item.id === 'resetPositions') { + onResetPositions?.() + } else if (item.id === 'layoutGrid') { + onLayoutModeChange?.('grid') + } else if (item.id === 'layoutHierarchy') { + onLayoutModeChange?.('hierarchy') + } else if (item.id === 'toggleConnections') { + onToggleConnections?.() + } else if (item.id === 'toggleGlobalViewport') { + onToggleGlobalViewport?.() + } else if (item.id === 'viewportToggle') { + // 循环切换viewport模式 + const nextMode = currentViewportMode === 'mobile' ? 'tablet' : currentViewportMode === 'tablet' ? 'desktop' : 'mobile' + setCurrentViewportMode(nextMode) + onViewportModeChange?.(nextMode) } }} title={item.label} diff --git a/src/pages/app/CanvasArea.tsx b/src/pages/app/CanvasArea.tsx index d12d51b..b2acd21 100644 --- a/src/pages/app/CanvasArea.tsx +++ b/src/pages/app/CanvasArea.tsx @@ -112,19 +112,46 @@ interface CanvasAreaProps { onFrameSelect?: (frames: string[]) => void designFiles?: DesignFile[] setDesignFiles?: (files: DesignFile[]) => void + currentZoom?: number + onZoomChange?: (zoom: number) => void + onResetZoom?: () => void + onResetPositions?: () => void + layoutMode?: LayoutMode + onLayoutModeChange?: (mode: LayoutMode) => void + showConnections?: boolean + onToggleConnections?: () => void + useGlobalViewport?: boolean + onToggleGlobalViewport?: () => void + globalViewportMode?: ViewportMode + onViewportModeChange?: (mode: ViewportMode) => void } -const CanvasArea: React.FC = ({ onFrameSelect, designFiles: externalDesignFiles }) => { +const CanvasArea: React.FC = ({ + onFrameSelect, + designFiles: externalDesignFiles, + currentZoom: externalZoom, + onZoomChange, + onResetZoom, + onResetPositions, + layoutMode: externalLayoutMode, + onLayoutModeChange, + showConnections: externalShowConnections, + onToggleConnections, + useGlobalViewport: externalUseGlobalViewport, + onToggleGlobalViewport, + globalViewportMode: externalViewportMode, + onViewportModeChange +}) => { const { t } = useTranslation() const [designFiles, setDesignFiles] = useState([]) const [selectedFrames, setSelectedFrames] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) - const [currentZoom, setCurrentZoom] = useState(1) + const [currentZoom, setCurrentZoom] = useState(externalZoom ?? 1) const [currentConfig, setCurrentConfig] = useState(CANVAS_CONFIG) - const [globalViewportMode, setGlobalViewportMode] = useState('tablet') + const [globalViewportMode, setGlobalViewportMode] = useState(externalViewportMode ?? 'tablet') const [frameViewports, setFrameViewports] = useState({}) - const [useGlobalViewport, setUseGlobalViewport] = useState(true) + const [useGlobalViewport, setUseGlobalViewport] = useState(externalUseGlobalViewport ?? true) const [customPositions, setCustomPositions] = useState({}) const [dragState, setDragState] = useState({ isDragging: false, @@ -133,12 +160,45 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext currentPosition: { x: 0, y: 0 }, offset: { x: 0, y: 0 } }) - const [layoutMode, setLayoutMode] = useState('grid') + const [layoutMode, setLayoutMode] = useState(externalLayoutMode ?? 'grid') const [hierarchyTree, setHierarchyTree] = useState(null) - const [showConnections, setShowConnections] = useState(true) + const [showConnections, setShowConnections] = useState(externalShowConnections ?? true) const transformRef = useRef(null) const canvasContainerRef = useRef(null) + useEffect(() => { + if (externalZoom !== undefined) { + setCurrentZoom(externalZoom) + if (externalZoom === 1 && transformRef.current) { + transformRef.current.resetTransform() + } + } + }, [externalZoom]) + + useEffect(() => { + if (externalLayoutMode !== undefined) { + setLayoutMode(externalLayoutMode) + } + }, [externalLayoutMode]) + + useEffect(() => { + if (externalShowConnections !== undefined) { + setShowConnections(externalShowConnections) + } + }, [externalShowConnections]) + + useEffect(() => { + if (externalUseGlobalViewport !== undefined) { + setUseGlobalViewport(externalUseGlobalViewport) + } + }, [externalUseGlobalViewport]) + + useEffect(() => { + if (externalViewportMode !== undefined) { + setGlobalViewportMode(externalViewportMode) + } + }, [externalViewportMode]) + // 性能优化:根据缩放级别切换渲染模式 const getOptimalRenderMode = useCallback((zoom: number): 'placeholder' | 'iframe' => { return zoom < 0.3 ? 'placeholder' : 'iframe' @@ -176,6 +236,7 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext const handleGlobalViewportChange = useCallback((viewport: ViewportMode) => { setGlobalViewportMode(viewport) + onViewportModeChange?.(viewport) if (useGlobalViewport) { const newFrameViewports: FrameViewportState = {} designFiles.forEach(f => { @@ -216,6 +277,7 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext const toggleGlobalViewport = useCallback(() => { const newUseGlobal = !useGlobalViewport setUseGlobalViewport(newUseGlobal) + onToggleGlobalViewport?.() if (newUseGlobal) { const newFrameViewports: FrameViewportState = {} @@ -224,7 +286,7 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext }) setFrameViewports(newFrameViewports) } - }, [useGlobalViewport, designFiles, globalViewportMode]) + }, [useGlobalViewport, designFiles, globalViewportMode, onToggleGlobalViewport]) // 响应式配置更新 useEffect(() => { @@ -378,7 +440,8 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext if (transformRef.current) { transformRef.current.resetTransform() } - }, []) + onResetZoom?.() + }, [onResetZoom]) const handleTransformChange = useCallback((ref: any) => { const state = ref.state @@ -389,7 +452,8 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext } setCurrentZoom(state.scale) - }, []) + onZoomChange?.(state.scale) + }, [onZoomChange]) // 获取帧位置(自定义、层级或默认网格位置) const getFramePosition = useCallback((fileName: string, index: number): GridPosition => { @@ -479,7 +543,8 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext // 重置位置到网格 const handleResetPositions = useCallback(() => { setCustomPositions({}) - }, []) + onResetPositions?.() + }, [onResetPositions]) // 根据当前帧位置更新连接线位置 const updateConnectionPositions = useCallback((connections: ConnectionLine[], files: DesignFile[]): ConnectionLine[] => { @@ -594,98 +659,6 @@ const CanvasArea: React.FC = ({ onFrameSelect, designFiles: ext return (
-
-
-
- {/* */} -
- {Math.round(currentZoom * 100)}% -
- {/* */} -
- - -
-
- -
-
-
- - -
- {layoutMode === 'hierarchy' && ( - - )} -
-
- -
-
- -
- - - -
-
-
-
-

Hello World

This is a test design file.

', + content: '

Hello World

This is a test design file.

', fileType: 'html', relationships: { parent: undefined, @@ -58,6 +59,11 @@ const MainApp: React.FC = () => { const [isChatVisible, setIsChatVisible] = useState(true) const [selectedFrames, setSelectedFrames] = useState([]) const [designFiles, setDesignFiles] = useState(MOCK_DESIGN_FILES) + const [currentZoom, setCurrentZoom] = useState(1) + const [layoutMode, setLayoutMode] = useState('grid') + const [showConnections, setShowConnections] = useState(true) + const [useGlobalViewport, setUseGlobalViewport] = useState(true) + const [globalViewportMode, setGlobalViewportMode] = useState('tablet') const toggleChat = () => { setIsChatVisible(!isChatVisible) @@ -93,11 +99,49 @@ const MainApp: React.FC = () => { setDesignFiles([...designFiles, newFrame]) } + const handleResetZoom = useCallback(() => { + setCurrentZoom(1) + }, []) + + const handleResetPositions = useCallback(() => { + }, []) + return (
- + setShowConnections(!showConnections)} + useGlobalViewport={useGlobalViewport} + onToggleGlobalViewport={() => setUseGlobalViewport(!useGlobalViewport)} + globalViewportMode={globalViewportMode} + onViewportModeChange={setGlobalViewportMode} + /> - + setShowConnections(!showConnections)} + useGlobalViewport={useGlobalViewport} + onToggleGlobalViewport={() => setUseGlobalViewport(!useGlobalViewport)} + globalViewportMode={globalViewportMode} + onViewportModeChange={setGlobalViewportMode} + /> {isChatVisible && }
)