项目启动流程和资产浏览器功能
This commit is contained in:
@@ -2,8 +2,10 @@ import { useState, useEffect } from 'react';
|
||||
import { Core, Scene } from '@esengine/ecs-framework';
|
||||
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, PropertyMetadataService, ProjectService, ComponentDiscoveryService, ComponentLoaderService } from '@esengine/editor-core';
|
||||
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
||||
import { StartupPage } from './components/StartupPage';
|
||||
import { SceneHierarchy } from './components/SceneHierarchy';
|
||||
import { EntityInspector } from './components/EntityInspector';
|
||||
import { AssetBrowser } from './components/AssetBrowser';
|
||||
import { TauriAPI } from './api/tauri';
|
||||
import { TransformComponent } from './example-components/TransformComponent';
|
||||
import { SpriteComponent } from './example-components/SpriteComponent';
|
||||
@@ -12,7 +14,6 @@ import { useLocale } from './hooks/useLocale';
|
||||
import { en, zh } from './locales';
|
||||
import './styles/App.css';
|
||||
|
||||
// 在 App 组件外部初始化 Core 和基础服务
|
||||
const coreInstance = Core.create({ debug: true });
|
||||
|
||||
const localeService = new LocaleService();
|
||||
@@ -52,6 +53,8 @@ propertyMetadata.register(RigidBodyComponent, {
|
||||
|
||||
function App() {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [projectLoaded, setProjectLoaded] = useState(false);
|
||||
const [currentProjectPath, setCurrentProjectPath] = useState<string | null>(null);
|
||||
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
||||
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
||||
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
||||
@@ -125,29 +128,6 @@ function App() {
|
||||
initializeEditor();
|
||||
}, []);
|
||||
|
||||
const handleCreateEntity = () => {
|
||||
if (!initialized || !entityStore) return;
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
const entity = scene.createEntity('Entity');
|
||||
entityStore.addEntity(entity);
|
||||
};
|
||||
|
||||
const handleDeleteEntity = () => {
|
||||
if (!entityStore) return;
|
||||
const selected = entityStore.getSelectedEntity();
|
||||
if (selected) {
|
||||
selected.destroy();
|
||||
entityStore.removeEntity(selected);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocaleChange = () => {
|
||||
const newLocale = locale === 'en' ? 'zh' : 'en';
|
||||
changeLocale(newLocale);
|
||||
};
|
||||
|
||||
const handleOpenProject = async () => {
|
||||
try {
|
||||
const projectPath = await TauriAPI.openProjectDialog();
|
||||
@@ -182,20 +162,66 @@ function App() {
|
||||
} else {
|
||||
setStatus(t('header.status.projectOpened'));
|
||||
}
|
||||
|
||||
setCurrentProjectPath(projectPath);
|
||||
setProjectLoaded(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to open project:', error);
|
||||
setStatus(t('header.status.failed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateProject = async () => {
|
||||
console.log('Create project not implemented yet');
|
||||
};
|
||||
|
||||
const handleCreateEntity = () => {
|
||||
if (!initialized || !entityStore) return;
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
const entity = scene.createEntity('Entity');
|
||||
entityStore.addEntity(entity);
|
||||
};
|
||||
|
||||
const handleDeleteEntity = () => {
|
||||
if (!entityStore) return;
|
||||
const selected = entityStore.getSelectedEntity();
|
||||
if (selected) {
|
||||
selected.destroy();
|
||||
entityStore.removeEntity(selected);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocaleChange = () => {
|
||||
const newLocale = locale === 'en' ? 'zh' : 'en';
|
||||
changeLocale(newLocale);
|
||||
};
|
||||
|
||||
if (!initialized) {
|
||||
return (
|
||||
<div className="editor-loading">
|
||||
<h2>Loading Editor...</h2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!projectLoaded) {
|
||||
return (
|
||||
<StartupPage
|
||||
onOpenProject={handleOpenProject}
|
||||
onCreateProject={handleCreateProject}
|
||||
recentProjects={[]}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-container">
|
||||
<div className="editor-header">
|
||||
<h1>{t('app.title')}</h1>
|
||||
<div className="header-toolbar">
|
||||
<button onClick={handleOpenProject} disabled={!initialized} className="toolbar-btn">
|
||||
{t('header.toolbar.openProject')}
|
||||
</button>
|
||||
<button onClick={handleCreateEntity} disabled={!initialized} className="toolbar-btn">
|
||||
{t('header.toolbar.createEntity')}
|
||||
</button>
|
||||
@@ -225,8 +251,7 @@ function App() {
|
||||
</div>
|
||||
|
||||
<div className="bottom-panel">
|
||||
<h4>{t('console.title')}</h4>
|
||||
<p>{t('console.placeholder')}</p>
|
||||
<AssetBrowser projectPath={currentProjectPath} locale={locale} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
202
packages/editor-app/src/components/AssetBrowser.tsx
Normal file
202
packages/editor-app/src/components/AssetBrowser.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
import '../styles/AssetBrowser.css';
|
||||
|
||||
interface AssetItem {
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'folder';
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
interface AssetBrowserProps {
|
||||
projectPath: string | null;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
||||
const [currentPath, setCurrentPath] = useState<string>('');
|
||||
const [assets, setAssets] = useState<AssetItem[]>([]);
|
||||
const [selectedAsset, setSelectedAsset] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
title: 'Assets',
|
||||
noProject: 'No project loaded',
|
||||
loading: 'Loading...',
|
||||
empty: 'No assets found',
|
||||
name: 'Name',
|
||||
type: 'Type',
|
||||
file: 'File',
|
||||
folder: 'Folder'
|
||||
},
|
||||
zh: {
|
||||
title: '资产',
|
||||
noProject: '没有加载项目',
|
||||
loading: '加载中...',
|
||||
empty: '没有找到资产',
|
||||
name: '名称',
|
||||
type: '类型',
|
||||
file: '文件',
|
||||
folder: '文件夹'
|
||||
}
|
||||
};
|
||||
|
||||
const t = translations[locale as keyof typeof translations] || translations.en;
|
||||
|
||||
useEffect(() => {
|
||||
if (projectPath) {
|
||||
setCurrentPath(projectPath);
|
||||
loadAssets(projectPath);
|
||||
} else {
|
||||
setAssets([]);
|
||||
setCurrentPath('');
|
||||
}
|
||||
}, [projectPath]);
|
||||
|
||||
const loadAssets = async (path: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const files = await TauriAPI.scanDirectory(path, '*');
|
||||
|
||||
const assetItems: AssetItem[] = files.map(filePath => {
|
||||
const name = filePath.split(/[\\/]/).pop() || '';
|
||||
const extension = name.includes('.') ? name.split('.').pop() : undefined;
|
||||
|
||||
return {
|
||||
name,
|
||||
path: filePath,
|
||||
type: 'file' as const,
|
||||
extension
|
||||
};
|
||||
});
|
||||
|
||||
assetItems.sort((a, b) => {
|
||||
if (a.type === b.type) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
return a.type === 'folder' ? -1 : 1;
|
||||
});
|
||||
|
||||
setAssets(assetItems);
|
||||
} catch (error) {
|
||||
console.error('Failed to load assets:', error);
|
||||
setAssets([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssetClick = (asset: AssetItem) => {
|
||||
setSelectedAsset(asset.path);
|
||||
if (asset.type === 'folder') {
|
||||
setCurrentPath(asset.path);
|
||||
loadAssets(asset.path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssetDoubleClick = (asset: AssetItem) => {
|
||||
console.log('Open asset:', asset);
|
||||
};
|
||||
|
||||
const getFileIcon = (extension?: string) => {
|
||||
switch (extension?.toLowerCase()) {
|
||||
case 'ts':
|
||||
case 'tsx':
|
||||
case 'js':
|
||||
case 'jsx':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="asset-icon">
|
||||
<path d="M14 2H6C4.89543 2 4 2.89543 4 4V20C4 21.1046 4.89543 22 6 22H18C19.1046 22 20 21.1046 20 20V8L14 2Z" strokeWidth="2"/>
|
||||
<path d="M14 2V8H20" strokeWidth="2"/>
|
||||
<path d="M12 18L12 14M12 10L12 12" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
case 'json':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="asset-icon">
|
||||
<path d="M14 2H6C4.89543 2 4 2.89543 4 4V20C4 21.1046 4.89543 22 6 22H18C19.1046 22 20 21.1046 20 20V8L14 2Z" strokeWidth="2"/>
|
||||
<path d="M14 2V8H20" strokeWidth="2"/>
|
||||
</svg>
|
||||
);
|
||||
case 'png':
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'gif':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="asset-icon">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" strokeWidth="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"/>
|
||||
<path d="M21 15L16 10L5 21" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="asset-icon">
|
||||
<path d="M14 2H6C4.89543 2 4 2.89543 4 4V20C4 21.1046 4.89543 22 6 22H18C19.1046 22 20 21.1046 20 20V8L14 2Z" strokeWidth="2"/>
|
||||
<path d="M14 2V8H20" strokeWidth="2"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getFolderIcon = () => (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="asset-icon folder">
|
||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
if (!projectPath) {
|
||||
return (
|
||||
<div className="asset-browser">
|
||||
<div className="asset-browser-header">
|
||||
<h3>{t.title}</h3>
|
||||
</div>
|
||||
<div className="asset-browser-empty">
|
||||
<p>{t.noProject}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="asset-browser">
|
||||
<div className="asset-browser-header">
|
||||
<h3>{t.title}</h3>
|
||||
<div className="asset-path">{currentPath}</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="asset-browser-loading">
|
||||
<p>{t.loading}</p>
|
||||
</div>
|
||||
) : assets.length === 0 ? (
|
||||
<div className="asset-browser-empty">
|
||||
<p>{t.empty}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="asset-browser-content">
|
||||
<div className="asset-list">
|
||||
{assets.map((asset, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`asset-item ${selectedAsset === asset.path ? 'selected' : ''}`}
|
||||
onClick={() => handleAssetClick(asset)}
|
||||
onDoubleClick={() => handleAssetDoubleClick(asset)}
|
||||
>
|
||||
{asset.type === 'folder' ? getFolderIcon() : getFileIcon(asset.extension)}
|
||||
<div className="asset-name" title={asset.name}>
|
||||
{asset.name}
|
||||
</div>
|
||||
<div className="asset-type">
|
||||
{asset.type === 'folder' ? t.folder : asset.extension || t.file}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ interface EntityInspectorProps {
|
||||
messageHub: MessageHub;
|
||||
}
|
||||
|
||||
export function EntityInspector({ entityStore, messageHub }: EntityInspectorProps) {
|
||||
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
|
||||
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
|
||||
const [showAddComponent, setShowAddComponent] = useState(false);
|
||||
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
|
||||
|
||||
93
packages/editor-app/src/components/StartupPage.tsx
Normal file
93
packages/editor-app/src/components/StartupPage.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import '../styles/StartupPage.css';
|
||||
|
||||
interface StartupPageProps {
|
||||
onOpenProject: () => void;
|
||||
onCreateProject: () => void;
|
||||
recentProjects?: string[];
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [], locale }: StartupPageProps) {
|
||||
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
title: 'ECS Framework Editor',
|
||||
subtitle: 'Professional Game Development Tool',
|
||||
openProject: 'Open Project',
|
||||
createProject: 'Create New Project',
|
||||
recentProjects: 'Recent Projects',
|
||||
noRecentProjects: 'No recent projects',
|
||||
version: 'Version 1.0.0'
|
||||
},
|
||||
zh: {
|
||||
title: 'ECS 框架编辑器',
|
||||
subtitle: '专业游戏开发工具',
|
||||
openProject: '打开项目',
|
||||
createProject: '创建新项目',
|
||||
recentProjects: '最近的项目',
|
||||
noRecentProjects: '没有最近的项目',
|
||||
version: '版本 1.0.0'
|
||||
}
|
||||
};
|
||||
|
||||
const t = translations[locale as keyof typeof translations] || translations.en;
|
||||
|
||||
return (
|
||||
<div className="startup-page">
|
||||
<div className="startup-header">
|
||||
<h1 className="startup-title">{t.title}</h1>
|
||||
<p className="startup-subtitle">{t.subtitle}</p>
|
||||
</div>
|
||||
|
||||
<div className="startup-content">
|
||||
<div className="startup-actions">
|
||||
<button className="startup-action-btn primary" onClick={onOpenProject}>
|
||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span>{t.openProject}</span>
|
||||
</button>
|
||||
|
||||
<button className="startup-action-btn" onClick={onCreateProject}>
|
||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 5V19M5 12H19" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span>{t.createProject}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="startup-recent">
|
||||
<h2 className="recent-title">{t.recentProjects}</h2>
|
||||
{recentProjects.length === 0 ? (
|
||||
<p className="recent-empty">{t.noRecentProjects}</p>
|
||||
) : (
|
||||
<ul className="recent-list">
|
||||
{recentProjects.map((project, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={`recent-item ${hoveredProject === project ? 'hovered' : ''}`}
|
||||
onMouseEnter={() => setHoveredProject(project)}
|
||||
onMouseLeave={() => setHoveredProject(null)}
|
||||
>
|
||||
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
|
||||
</svg>
|
||||
<div className="recent-info">
|
||||
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
|
||||
<div className="recent-path">{project}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="startup-footer">
|
||||
<span className="startup-version">{t.version}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
130
packages/editor-app/src/styles/AssetBrowser.css
Normal file
130
packages/editor-app/src/styles/AssetBrowser.css
Normal file
@@ -0,0 +1,130 @@
|
||||
.asset-browser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1e1e1e;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.asset-browser-header {
|
||||
padding: 12px 15px;
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.asset-browser-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.asset-path {
|
||||
font-size: 11px;
|
||||
color: #858585;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.asset-browser-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.asset-browser-loading,
|
||||
.asset-browser-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #858585;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.asset-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.asset-item:hover {
|
||||
background: #2a2d2e;
|
||||
}
|
||||
|
||||
.asset-item.selected {
|
||||
background: #094771;
|
||||
}
|
||||
|
||||
.asset-item.selected:hover {
|
||||
background: #0e6caa;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: #cccccc;
|
||||
margin-bottom: 8px;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.asset-icon.folder {
|
||||
color: #dcb67a;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-size: 12px;
|
||||
color: #cccccc;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.asset-type {
|
||||
font-size: 10px;
|
||||
color: #858585;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.asset-browser-content::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.asset-browser-content::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.asset-browser-content::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.asset-browser-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e4e;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.asset-list {
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
183
packages/editor-app/src/styles/StartupPage.css
Normal file
183
packages/editor-app/src/styles/StartupPage.css
Normal file
@@ -0,0 +1,183 @@
|
||||
.startup-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.startup-header {
|
||||
text-align: center;
|
||||
padding: 60px 20px 40px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0) 100%);
|
||||
}
|
||||
|
||||
.startup-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.startup-subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: #a0a0a0;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.startup-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.startup-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.startup-action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 40px 60px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: #e0e0e0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.startup-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.startup-action-btn.primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.startup-action-btn.primary:hover {
|
||||
background: linear-gradient(135deg, #7a8ef0 0%, #8a5cb2 100%);
|
||||
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.startup-recent {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.recent-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 20px;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.recent-empty {
|
||||
text-align: center;
|
||||
color: #808080;
|
||||
padding: 40px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.recent-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.recent-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.recent-item:hover,
|
||||
.recent-item.hovered {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(102, 126, 234, 0.5);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.recent-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: #667eea;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recent-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recent-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: #f0f0f0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.recent-path {
|
||||
font-size: 0.9rem;
|
||||
color: #808080;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.startup-footer {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.startup-version {
|
||||
color: #606060;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.startup-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.startup-action-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.startup-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user