按钮合并
This commit is contained in:
parent
bdd11126e6
commit
9f5056f554
15
README.md
15
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的开源代码
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
|
|||
const renderContent = () => {
|
||||
switch (renderMode) {
|
||||
case 'iframe':
|
||||
console.log('iframe', file.content);
|
||||
if (file.fileType === 'svg') {
|
||||
const svgHtml = `
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -250,6 +251,13 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
|
|||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.clickable.highlighted {
|
||||
outline: 2px solid #47A5FA;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -266,6 +274,11 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
|
|||
}
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.clickable').forEach(function(element) {
|
||||
element.addEventListener('click', function() {
|
||||
this.classList.toggle('highlighted');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -444,6 +457,7 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
|
|||
);
|
||||
|
||||
case 'html':
|
||||
console.log('html',file.content);
|
||||
if (file.fileType === 'svg') {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -464,6 +478,7 @@ const DesignFrame: React.FC<DesignFrameProps> = ({
|
|||
|
||||
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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SideBarProps> = ({ selectedFrames, onSelectNote, onCreateContext }) => {
|
||||
const SideBar: React.FC<SideBarProps> = ({
|
||||
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<SideBarProps> = ({ 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<ViewportMode>(globalViewportMode)
|
||||
|
||||
// 当globalViewportMode变化时更新currentViewportMode
|
||||
useEffect(() => {
|
||||
setCurrentViewportMode(globalViewportMode)
|
||||
}, [globalViewportMode])
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'select', icon: <ArrowIcon />, label: t('sideBar.menu.select') },
|
||||
{ id: 'note', icon: <NoteIcon />, label: t('sideBar.menu.note') },
|
||||
{ id: 'copy', icon: <CopyIcon />, label: t('sideBar.menu.copy') },
|
||||
// { id: 'copy', icon: <CopyIcon />, label: t('sideBar.menu.copy') },
|
||||
{ id: 'customize', icon: <PaletteIcon />, label: t('sideBar.menu.customize') },
|
||||
// Canvas controls
|
||||
{ id: 'resetZoom', icon: <HomeIcon />, label: t('canvasArea.toolbar.resetZoom') },
|
||||
{ id: 'resetPositions', icon: <RefreshIcon />, label: t('canvasArea.toolbar.resetPositions') },
|
||||
{ id: 'layoutGrid', icon: <ArrowIcon />, label: t('canvasArea.toolbar.gridLayout') },
|
||||
{ id: 'layoutHierarchy', icon: <TreeIcon />, label: t('canvasArea.toolbar.hierarchyLayout') },
|
||||
{ id: 'toggleConnections', icon: <LinkIcon />, label: t('canvasArea.toolbar.toggleConnections') },
|
||||
{ id: 'toggleGlobalViewport', icon: <GlobeIcon />, label: t('canvasArea.toolbar.toggleGlobalViewport') },
|
||||
{ id: 'viewportToggle', icon: currentViewportMode === 'mobile' ? <MobileIcon /> : currentViewportMode === 'tablet' ? <TabletIcon /> : <DesktopIcon />, label: t(`canvasArea.toolbar.${currentViewportMode}View`) },
|
||||
// { id: 'fix', icon: <PushPinIcon />, label: t('sideBar.menu.fix') },
|
||||
// { id: 'add', icon: <PlusIcon />, label: t('sideBar.menu.add') },
|
||||
]
|
||||
|
|
@ -43,20 +100,18 @@ const SideBar: React.FC<SideBarProps> = ({ 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<SideBarProps> = ({ 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}
|
||||
|
|
|
|||
|
|
@ -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<CanvasAreaProps> = ({ onFrameSelect, designFiles: externalDesignFiles }) => {
|
||||
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||
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<DesignFile[]>([])
|
||||
const [selectedFrames, setSelectedFrames] = useState<string[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [currentZoom, setCurrentZoom] = useState(1)
|
||||
const [currentZoom, setCurrentZoom] = useState(externalZoom ?? 1)
|
||||
const [currentConfig, setCurrentConfig] = useState<CanvasConfig>(CANVAS_CONFIG)
|
||||
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>('tablet')
|
||||
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>(externalViewportMode ?? 'tablet')
|
||||
const [frameViewports, setFrameViewports] = useState<FrameViewportState>({})
|
||||
const [useGlobalViewport, setUseGlobalViewport] = useState(true)
|
||||
const [useGlobalViewport, setUseGlobalViewport] = useState(externalUseGlobalViewport ?? true)
|
||||
const [customPositions, setCustomPositions] = useState<FramePositionState>({})
|
||||
const [dragState, setDragState] = useState<DragState>({
|
||||
isDragging: false,
|
||||
|
|
@ -133,12 +160,45 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
|
|||
currentPosition: { x: 0, y: 0 },
|
||||
offset: { x: 0, y: 0 }
|
||||
})
|
||||
const [layoutMode, setLayoutMode] = useState<LayoutMode>('grid')
|
||||
const [layoutMode, setLayoutMode] = useState<LayoutMode>(externalLayoutMode ?? 'grid')
|
||||
const [hierarchyTree, setHierarchyTree] = useState<HierarchyTree | null>(null)
|
||||
const [showConnections, setShowConnections] = useState(true)
|
||||
const [showConnections, setShowConnections] = useState(externalShowConnections ?? true)
|
||||
const transformRef = useRef<ReactZoomPanPinchRef>(null)
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(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<CanvasAreaProps> = ({ 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<CanvasAreaProps> = ({ 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<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
|
|||
})
|
||||
setFrameViewports(newFrameViewports)
|
||||
}
|
||||
}, [useGlobalViewport, designFiles, globalViewportMode])
|
||||
}, [useGlobalViewport, designFiles, globalViewportMode, onToggleGlobalViewport])
|
||||
|
||||
// 响应式配置更新
|
||||
useEffect(() => {
|
||||
|
|
@ -378,7 +440,8 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({ 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<CanvasAreaProps> = ({ 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<CanvasAreaProps> = ({ 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<CanvasAreaProps> = ({ onFrameSelect, designFiles: ext
|
|||
|
||||
return (
|
||||
<div className="canvas-area" ref={canvasContainerRef}>
|
||||
<div className="canvas-toolbar">
|
||||
<div className="toolbar-section">
|
||||
<div className="control-group">
|
||||
{/* <button className="toolbar-btn zoom-btn" onClick={handleZoomOut} title={t('canvasArea.toolbar.zoomOut')}>
|
||||
<ZoomOutIcon />
|
||||
</button> */}
|
||||
<div className="zoom-display">
|
||||
<span className="zoom-value">{Math.round(currentZoom * 100)}%</span>
|
||||
</div>
|
||||
{/* <button className="toolbar-btn zoom-btn" onClick={handleZoomIn} title={t('canvasArea.toolbar.zoomIn')}>
|
||||
<ZoomInIcon />
|
||||
</button> */}
|
||||
<div className="toolbar-divider"></div>
|
||||
<button className="toolbar-btn" onClick={handleResetZoom} title={t('canvasArea.toolbar.resetZoom')}>
|
||||
<HomeIcon />
|
||||
</button>
|
||||
<button className="toolbar-btn" onClick={handleResetPositions} title={t('canvasArea.toolbar.resetPositions')}>
|
||||
<RefreshIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-section">
|
||||
<div className="control-group">
|
||||
<div className="layout-toggle">
|
||||
<button
|
||||
className={`toggle-btn ${layoutMode === 'grid' ? 'active' : ''}`}
|
||||
onClick={() => setLayoutMode('grid')}
|
||||
title={t('canvasArea.toolbar.gridLayout')}
|
||||
>
|
||||
<span>{t('canvasArea.toolbar.grid')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`toggle-btn ${layoutMode === 'hierarchy' ? 'active' : ''}`}
|
||||
onClick={() => setLayoutMode('hierarchy')}
|
||||
title={t('canvasArea.toolbar.hierarchyLayout')}
|
||||
disabled={!hierarchyTree || hierarchyTree.nodes.size === 0}
|
||||
>
|
||||
<TreeIcon />
|
||||
</button>
|
||||
</div>
|
||||
{layoutMode === 'hierarchy' && (
|
||||
<button
|
||||
className={`toolbar-btn connection-btn ${showConnections ? 'active' : ''}`}
|
||||
onClick={() => setShowConnections(!showConnections)}
|
||||
title={t('canvasArea.toolbar.toggleConnections')}
|
||||
>
|
||||
<LinkIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-section">
|
||||
<div className="control-group">
|
||||
<button
|
||||
className={`toolbar-btn viewport-mode-btn ${useGlobalViewport ? 'active' : ''}`}
|
||||
onClick={toggleGlobalViewport}
|
||||
title={t('canvasArea.toolbar.toggleGlobalViewport')}
|
||||
>
|
||||
<GlobeIcon />
|
||||
</button>
|
||||
<div className="viewport-selector">
|
||||
<button
|
||||
className={`viewport-btn ${globalViewportMode === 'mobile' && useGlobalViewport ? 'active' : ''}`}
|
||||
onClick={() => handleGlobalViewportChange('mobile')}
|
||||
title={t('canvasArea.toolbar.mobileView')}
|
||||
disabled={!useGlobalViewport}
|
||||
>
|
||||
<MobileIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${globalViewportMode === 'tablet' && useGlobalViewport ? 'active' : ''}`}
|
||||
onClick={() => handleGlobalViewportChange('tablet')}
|
||||
title={t('canvasArea.toolbar.tabletView')}
|
||||
disabled={!useGlobalViewport}
|
||||
>
|
||||
<TabletIcon />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${globalViewportMode === 'desktop' && useGlobalViewport ? 'active' : ''}`}
|
||||
onClick={() => handleGlobalViewportChange('desktop')}
|
||||
title={t('canvasArea.toolbar.desktopView')}
|
||||
disabled={!useGlobalViewport}
|
||||
>
|
||||
<DesktopIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TransformWrapper
|
||||
ref={transformRef}
|
||||
initialScale={1}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useState } from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import CanvasArea from './CanvasArea'
|
||||
import TopBar from '../../components/TopBar'
|
||||
import SideBar from '../../components/SideBar'
|
||||
import AIChatPanel from '../../components/AIChatPanel'
|
||||
import { ViewportMode, LayoutMode } from '../../types/canvas.types'
|
||||
import '../../App.css'
|
||||
|
||||
// 模拟设计文件数据
|
||||
|
|
@ -11,7 +12,7 @@ const MOCK_DESIGN_FILES = [
|
|||
name: 'text_1.html',
|
||||
path: '/designs/text_1.html',
|
||||
modified: new Date(),
|
||||
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World</h1><p>This is a test design file.</p></div>',
|
||||
content: '<div className="clickable" style="padding: 20px; font-family: Arial, sans-serif;"><h1 className="clickable">Hello World</h1><p className="clickable">This is a test design file.</p></div>',
|
||||
fileType: 'html',
|
||||
relationships: {
|
||||
parent: undefined,
|
||||
|
|
@ -58,6 +59,11 @@ const MainApp: React.FC = () => {
|
|||
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||
const [selectedFrames, setSelectedFrames] = useState<string[]>([])
|
||||
const [designFiles, setDesignFiles] = useState<any[]>(MOCK_DESIGN_FILES)
|
||||
const [currentZoom, setCurrentZoom] = useState(1)
|
||||
const [layoutMode, setLayoutMode] = useState<LayoutMode>('grid')
|
||||
const [showConnections, setShowConnections] = useState(true)
|
||||
const [useGlobalViewport, setUseGlobalViewport] = useState(true)
|
||||
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>('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 (
|
||||
<div className="app-container">
|
||||
<CanvasArea onFrameSelect={handleFrameSelect} designFiles={designFiles} setDesignFiles={setDesignFiles} />
|
||||
<CanvasArea
|
||||
onFrameSelect={handleFrameSelect}
|
||||
designFiles={designFiles}
|
||||
setDesignFiles={setDesignFiles}
|
||||
currentZoom={currentZoom}
|
||||
onZoomChange={setCurrentZoom}
|
||||
onResetZoom={handleResetZoom}
|
||||
onResetPositions={handleResetPositions}
|
||||
layoutMode={layoutMode}
|
||||
onLayoutModeChange={setLayoutMode}
|
||||
showConnections={showConnections}
|
||||
onToggleConnections={() => setShowConnections(!showConnections)}
|
||||
useGlobalViewport={useGlobalViewport}
|
||||
onToggleGlobalViewport={() => setUseGlobalViewport(!useGlobalViewport)}
|
||||
globalViewportMode={globalViewportMode}
|
||||
onViewportModeChange={setGlobalViewportMode}
|
||||
/>
|
||||
<TopBar onToggleChat={toggleChat} isChatVisible={isChatVisible} />
|
||||
<SideBar selectedFrames={selectedFrames} onSelectNote={handleSelectNote} onCreateContext={handleCreateContext} />
|
||||
<SideBar
|
||||
selectedFrames={selectedFrames}
|
||||
onSelectNote={handleSelectNote}
|
||||
onCreateContext={handleCreateContext}
|
||||
currentZoom={currentZoom}
|
||||
onResetZoom={handleResetZoom}
|
||||
onResetPositions={handleResetPositions}
|
||||
layoutMode={layoutMode}
|
||||
onLayoutModeChange={setLayoutMode}
|
||||
showConnections={showConnections}
|
||||
onToggleConnections={() => setShowConnections(!showConnections)}
|
||||
useGlobalViewport={useGlobalViewport}
|
||||
onToggleGlobalViewport={() => setUseGlobalViewport(!useGlobalViewport)}
|
||||
globalViewportMode={globalViewportMode}
|
||||
onViewportModeChange={setGlobalViewportMode}
|
||||
/>
|
||||
{isChatVisible && <AIChatPanel />}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue