722 lines
18 KiB
Vue
722 lines
18 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, onMounted, onUnmounted } from "vue";
|
|||
|
|
import { invoke } from "@tauri-apps/api/core";
|
|||
|
|
import Sidebar from "./components/Sidebar.vue";
|
|||
|
|
import MonacoEditor from "./components/MonacoEditor.vue";
|
|||
|
|
import CommandCreator from "./components/CommandCreator.vue";
|
|||
|
|
import EventCreator from "./components/EventCreator.vue";
|
|||
|
|
import ProjectSelector from "./components/ProjectSelector.vue";
|
|||
|
|
import CommandPalette from "./components/CommandPalette.vue";
|
|||
|
|
import SkeletonLoader from "./components/SkeletonLoader.vue";
|
|||
|
|
import DatabaseViewer from "./components/DatabaseViewer.vue";
|
|||
|
|
import EnvManager from "./components/EnvManager.vue";
|
|||
|
|
import type { ProjectStats, FileInfo, Command, Event } from "./types/bot";
|
|||
|
|
|
|||
|
|
// Estado de la aplicación
|
|||
|
|
const projectRoot = ref<string>("");
|
|||
|
|
const showProjectSelector = ref(false);
|
|||
|
|
const showCommandPalette = ref(false);
|
|||
|
|
const devUltraMode = ref(false);
|
|||
|
|
const initialLoading = ref(true);
|
|||
|
|
const stats = ref<ProjectStats>({
|
|||
|
|
messageCommands: 0,
|
|||
|
|
slashCommands: 0,
|
|||
|
|
standardEvents: 0,
|
|||
|
|
customEvents: 0,
|
|||
|
|
totalCommands: 0,
|
|||
|
|
totalEvents: 0,
|
|||
|
|
});
|
|||
|
|
const commands = ref<FileInfo[]>([]);
|
|||
|
|
const events = ref<FileInfo[]>([]);
|
|||
|
|
const allFiles = ref<FileInfo[]>([]);
|
|||
|
|
const selectedFile = ref<FileInfo | null>(null);
|
|||
|
|
const fileContent = ref<string>("");
|
|||
|
|
const currentView = ref<"editor" | "command-creator" | "event-creator" | "database" | "env-manager">("editor");
|
|||
|
|
const loading = ref(false);
|
|||
|
|
const errorMsg = ref<string>("");
|
|||
|
|
const schemaContent = ref<string>("");
|
|||
|
|
|
|||
|
|
// Deshabilitar F12 y habilitar Ctrl+Q
|
|||
|
|
const handleF12 = (e: KeyboardEvent) => {
|
|||
|
|
if (e.key === 'F12') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
e.stopPropagation();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleCtrlQ = (e: KeyboardEvent) => {
|
|||
|
|
if (e.ctrlKey && e.key === 'q') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
e.stopPropagation();
|
|||
|
|
showCommandPalette.value = !showCommandPalette.value;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
window.addEventListener('keydown', handleF12);
|
|||
|
|
window.addEventListener('keydown', handleCtrlQ);
|
|||
|
|
|
|||
|
|
// Inicializar Discord RPC
|
|||
|
|
initDiscordRPC();
|
|||
|
|
|
|||
|
|
// Cargar proyecto inicial
|
|||
|
|
loadProjectData();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
window.removeEventListener('keydown', handleF12);
|
|||
|
|
window.removeEventListener('keydown', handleCtrlQ);
|
|||
|
|
|
|||
|
|
// Desconectar Discord RPC al cerrar
|
|||
|
|
disconnectDiscordRPC();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Funciones Discord RPC
|
|||
|
|
async function initDiscordRPC() {
|
|||
|
|
try {
|
|||
|
|
await invoke('init_discord_rpc');
|
|||
|
|
console.log('✅ Discord RPC inicializado');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.warn('⚠️ No se pudo inicializar Discord RPC:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function updateDiscordRPC(details: string, state: string, fileName?: string) {
|
|||
|
|
try {
|
|||
|
|
await invoke('update_discord_rpc', { details, state, fileName });
|
|||
|
|
} catch (error) {
|
|||
|
|
console.warn('⚠️ Error actualizando Discord RPC:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function disconnectDiscordRPC() {
|
|||
|
|
try {
|
|||
|
|
await invoke('disconnect_discord_rpc');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.warn('⚠️ Error desconectando Discord RPC:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cargar datos del proyecto
|
|||
|
|
async function loadProjectData() {
|
|||
|
|
loading.value = true;
|
|||
|
|
errorMsg.value = "";
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Si no hay projectRoot, intentar cargar desde localStorage o mostrar selector
|
|||
|
|
if (!projectRoot.value) {
|
|||
|
|
const savedPath = localStorage.getItem('amayo-project-path');
|
|||
|
|
if (savedPath) {
|
|||
|
|
// Validar ruta guardada
|
|||
|
|
const isValid = await invoke<boolean>('validate_project_path', {
|
|||
|
|
path: savedPath,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (isValid) {
|
|||
|
|
projectRoot.value = savedPath;
|
|||
|
|
} else {
|
|||
|
|
// Ruta inválida, mostrar selector
|
|||
|
|
showProjectSelector.value = true;
|
|||
|
|
initialLoading.value = false;
|
|||
|
|
loading.value = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// No hay ruta guardada, mostrar selector
|
|||
|
|
showProjectSelector.value = true;
|
|||
|
|
initialLoading.value = false;
|
|||
|
|
loading.value = false;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cargar estadísticas
|
|||
|
|
stats.value = await invoke<ProjectStats>("get_project_stats", {
|
|||
|
|
projectRoot: projectRoot.value
|
|||
|
|
});
|
|||
|
|
console.log("📊 Stats cargadas:", stats.value);
|
|||
|
|
|
|||
|
|
// Cargar comandos
|
|||
|
|
commands.value = await invoke<FileInfo[]>("scan_commands", {
|
|||
|
|
projectRoot: projectRoot.value
|
|||
|
|
});
|
|||
|
|
console.log("💬 Comandos cargados:", commands.value.length, commands.value);
|
|||
|
|
|
|||
|
|
// Cargar eventos
|
|||
|
|
events.value = await invoke<FileInfo[]>("scan_events", {
|
|||
|
|
projectRoot: projectRoot.value
|
|||
|
|
});
|
|||
|
|
console.log("📡 Eventos cargados:", events.value.length, events.value);
|
|||
|
|
|
|||
|
|
// Cargar schema.prisma si existe
|
|||
|
|
try {
|
|||
|
|
schemaContent.value = await invoke<string>("read_file_content", {
|
|||
|
|
filePath: `${projectRoot.value}/prisma/schema.prisma`
|
|||
|
|
});
|
|||
|
|
} catch {
|
|||
|
|
schemaContent.value = "// Schema no encontrado";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Actualizar Discord RPC
|
|||
|
|
updateDiscordRPC(
|
|||
|
|
"Navegando proyecto",
|
|||
|
|
`${stats.value.totalCommands} comandos | ${stats.value.totalEvents} eventos`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
} catch (error: any) {
|
|||
|
|
errorMsg.value = `Error cargando proyecto: ${error}`;
|
|||
|
|
console.error("Error:", error);
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
// Pequeño delay para mostrar el skeleton
|
|||
|
|
setTimeout(() => {
|
|||
|
|
initialLoading.value = false;
|
|||
|
|
}, 800);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Seleccionar archivo
|
|||
|
|
async function selectFile(file: FileInfo) {
|
|||
|
|
try {
|
|||
|
|
selectedFile.value = file;
|
|||
|
|
fileContent.value = await invoke<string>("read_file_content", {
|
|||
|
|
filePath: file.path
|
|||
|
|
});
|
|||
|
|
currentView.value = "editor";
|
|||
|
|
|
|||
|
|
// Actualizar Discord RPC
|
|||
|
|
const fileType = file.commandType
|
|||
|
|
? `Comando ${file.commandType}`
|
|||
|
|
: file.eventType
|
|||
|
|
? `Evento ${file.eventType}`
|
|||
|
|
: "Archivo";
|
|||
|
|
updateDiscordRPC(`Editando ${fileType}`, file.name);
|
|||
|
|
} catch (error: any) {
|
|||
|
|
errorMsg.value = `Error leyendo archivo: ${error}`;
|
|||
|
|
console.error("Error:", error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Guardar archivo
|
|||
|
|
async function saveFile(content: string) {
|
|||
|
|
if (!selectedFile.value) return;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await invoke("write_file_content", {
|
|||
|
|
filePath: selectedFile.value.path,
|
|||
|
|
content: content
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Mostrar notificación de éxito
|
|||
|
|
showNotification("✅ Archivo guardado correctamente");
|
|||
|
|
|
|||
|
|
// Recargar estadísticas
|
|||
|
|
await loadProjectData();
|
|||
|
|
} catch (error: any) {
|
|||
|
|
errorMsg.value = `Error guardando archivo: ${error}`;
|
|||
|
|
console.error("Error:", error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Crear nuevo comando
|
|||
|
|
function showCommandCreator() {
|
|||
|
|
currentView.value = "command-creator";
|
|||
|
|
updateDiscordRPC("Creando comando nuevo", "En el wizard de comandos");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Crear nuevo evento
|
|||
|
|
function showEventCreator() {
|
|||
|
|
currentView.value = "event-creator";
|
|||
|
|
updateDiscordRPC("Creando evento nuevo", "En el wizard de eventos");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Guardar nuevo comando
|
|||
|
|
async function saveCommand(_command: Command, code: string, savePath: string) {
|
|||
|
|
try {
|
|||
|
|
const fullPath = `${projectRoot.value}/${savePath}`;
|
|||
|
|
await invoke("write_file_content", {
|
|||
|
|
filePath: fullPath,
|
|||
|
|
content: code
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
showNotification("✅ Comando creado correctamente");
|
|||
|
|
currentView.value = "editor";
|
|||
|
|
await loadProjectData();
|
|||
|
|
|
|||
|
|
} catch (error: any) {
|
|||
|
|
errorMsg.value = `Error creando comando: ${error}`;
|
|||
|
|
console.error("Error:", error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Guardar nuevo evento
|
|||
|
|
async function saveEvent(_event: Event, code: string, savePath: string) {
|
|||
|
|
try {
|
|||
|
|
const fullPath = `${projectRoot.value}/${savePath}`;
|
|||
|
|
await invoke("write_file_content", {
|
|||
|
|
filePath: fullPath,
|
|||
|
|
content: code
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
showNotification("✅ Evento creado correctamente");
|
|||
|
|
currentView.value = "editor";
|
|||
|
|
await loadProjectData();
|
|||
|
|
|
|||
|
|
} catch (error: any) {
|
|||
|
|
errorMsg.value = `Error creando evento: ${error}`;
|
|||
|
|
console.error("Error:", error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cerrar creadores
|
|||
|
|
function closeCreator() {
|
|||
|
|
currentView.value = "editor";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Mostrar notificación temporal
|
|||
|
|
function showNotification(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
|||
|
|
const notification = document.createElement("div");
|
|||
|
|
notification.className = `notification notification-${type}`;
|
|||
|
|
notification.textContent = message;
|
|||
|
|
document.body.appendChild(notification);
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
notification.remove();
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Manejar selección de proyecto
|
|||
|
|
function handleProjectPathSelected(path: string) {
|
|||
|
|
projectRoot.value = path;
|
|||
|
|
showProjectSelector.value = false;
|
|||
|
|
loadProjectData();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cambiar directorio del proyecto
|
|||
|
|
function changeProjectDirectory() {
|
|||
|
|
showProjectSelector.value = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Toggle modo Dev Ultra
|
|||
|
|
async function toggleDevUltra() {
|
|||
|
|
devUltraMode.value = !devUltraMode.value;
|
|||
|
|
if (devUltraMode.value) {
|
|||
|
|
showNotification("⚡ Modo Dev Ultra Activado - Cargando archivos...");
|
|||
|
|
// Cargar todos los archivos del proyecto
|
|||
|
|
try {
|
|||
|
|
allFiles.value = await invoke<FileInfo[]>("scan_all_files", {
|
|||
|
|
projectRoot: projectRoot.value
|
|||
|
|
});
|
|||
|
|
} catch (error: any) {
|
|||
|
|
errorMsg.value = `Error cargando archivos: ${error}`;
|
|||
|
|
console.error("Error:", error);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
showNotification("🔒 Modo Dev Ultra Desactivado");
|
|||
|
|
allFiles.value = [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Toggle Database Viewer
|
|||
|
|
function toggleDatabase() {
|
|||
|
|
if (currentView.value === 'database') {
|
|||
|
|
currentView.value = 'editor';
|
|||
|
|
updateDiscordRPC("Navegando proyecto", "En el editor");
|
|||
|
|
} else {
|
|||
|
|
currentView.value = 'database';
|
|||
|
|
updateDiscordRPC("Editando base de datos", "Visualizando schema.prisma");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Toggle Env Manager
|
|||
|
|
function toggleEnvManager() {
|
|||
|
|
if (currentView.value === 'env-manager') {
|
|||
|
|
currentView.value = 'editor';
|
|||
|
|
updateDiscordRPC("Navegando proyecto", "En el editor");
|
|||
|
|
} else {
|
|||
|
|
currentView.value = 'env-manager';
|
|||
|
|
updateDiscordRPC("Configurando variables", "Gestionando .env");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Guardar schema de base de datos
|
|||
|
|
async function saveSchema(content: string) {
|
|||
|
|
try {
|
|||
|
|
await invoke("write_file_content", {
|
|||
|
|
filePath: `${projectRoot.value}/prisma/schema.prisma`,
|
|||
|
|
content: content
|
|||
|
|
});
|
|||
|
|
schemaContent.value = content;
|
|||
|
|
showNotification("✅ Schema guardado correctamente");
|
|||
|
|
} catch (error: any) {
|
|||
|
|
errorMsg.value = `Error guardando schema: ${error}`;
|
|||
|
|
console.error("Error:", error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Manejar comandos del palette
|
|||
|
|
function handlePaletteCommand(commandId: string) {
|
|||
|
|
switch (commandId) {
|
|||
|
|
case 'new-command':
|
|||
|
|
showCommandCreator();
|
|||
|
|
break;
|
|||
|
|
case 'new-event':
|
|||
|
|
showEventCreator();
|
|||
|
|
break;
|
|||
|
|
case 'refresh':
|
|||
|
|
loadProjectData();
|
|||
|
|
break;
|
|||
|
|
case 'change-project':
|
|||
|
|
changeProjectDirectory();
|
|||
|
|
break;
|
|||
|
|
case 'database':
|
|||
|
|
toggleDatabase();
|
|||
|
|
break;
|
|||
|
|
case 'env-manager':
|
|||
|
|
toggleEnvManager();
|
|||
|
|
break;
|
|||
|
|
case 'toggle-dev-ultra':
|
|||
|
|
toggleDevUltra();
|
|||
|
|
break;
|
|||
|
|
case 'save':
|
|||
|
|
if (selectedFile.value && fileContent.value) {
|
|||
|
|
saveFile(fileContent.value);
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="app-container">
|
|||
|
|
<!-- Skeleton Loader -->
|
|||
|
|
<SkeletonLoader v-if="initialLoading" />
|
|||
|
|
|
|||
|
|
<!-- Command Palette -->
|
|||
|
|
<CommandPalette
|
|||
|
|
:isOpen="showCommandPalette"
|
|||
|
|
@close="showCommandPalette = false"
|
|||
|
|
@command="handlePaletteCommand"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Selector de proyecto -->
|
|||
|
|
<ProjectSelector
|
|||
|
|
v-if="showProjectSelector && !initialLoading"
|
|||
|
|
@path-selected="handleProjectPathSelected"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<template v-if="!showProjectSelector && !initialLoading">
|
|||
|
|
<Sidebar
|
|||
|
|
:stats="stats"
|
|||
|
|
:commands="commands"
|
|||
|
|
:events="events"
|
|||
|
|
:allFiles="allFiles"
|
|||
|
|
:selectedFile="selectedFile"
|
|||
|
|
:projectRoot="projectRoot"
|
|||
|
|
:devUltraMode="devUltraMode"
|
|||
|
|
@new-command="showCommandCreator"
|
|||
|
|
@new-event="showEventCreator"
|
|||
|
|
@refresh="loadProjectData"
|
|||
|
|
@select-file="selectFile"
|
|||
|
|
@change-directory="changeProjectDirectory"
|
|||
|
|
@toggle-dev-ultra="toggleDevUltra"
|
|||
|
|
@toggle-database="toggleDatabase"
|
|||
|
|
@toggle-env-manager="toggleEnvManager"
|
|||
|
|
@notify="showNotification"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div class="main-content">
|
|||
|
|
<div v-if="loading" class="loading-overlay">
|
|||
|
|
<div class="loading-spinner">⏳ Cargando proyecto...</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-else-if="errorMsg" class="error-banner">
|
|||
|
|
❌ {{ errorMsg }}
|
|||
|
|
<button @click="errorMsg = ''" class="close-error">✕</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Editor Monaco -->
|
|||
|
|
<MonacoEditor
|
|||
|
|
v-if="currentView === 'editor' && selectedFile"
|
|||
|
|
:fileInfo="selectedFile"
|
|||
|
|
:content="fileContent"
|
|||
|
|
@save="saveFile"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Command Creator -->
|
|||
|
|
<CommandCreator
|
|||
|
|
v-if="currentView === 'command-creator'"
|
|||
|
|
@save="saveCommand"
|
|||
|
|
@close="closeCreator"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Event Creator -->
|
|||
|
|
<EventCreator
|
|||
|
|
v-if="currentView === 'event-creator'"
|
|||
|
|
@save="saveEvent"
|
|||
|
|
@close="closeCreator"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Database Viewer -->
|
|||
|
|
<DatabaseViewer
|
|||
|
|
v-if="currentView === 'database'"
|
|||
|
|
:schemaContent="schemaContent"
|
|||
|
|
:projectRoot="projectRoot"
|
|||
|
|
@save="saveSchema"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Environment Manager -->
|
|||
|
|
<EnvManager
|
|||
|
|
v-if="currentView === 'env-manager'"
|
|||
|
|
:projectRoot="projectRoot"
|
|||
|
|
@close="() => currentView = 'editor'"
|
|||
|
|
@notify="showNotification"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- Welcome Screen -->
|
|||
|
|
<div v-if="currentView === 'editor' && !selectedFile" class="welcome-screen">
|
|||
|
|
<div class="welcome-content">
|
|||
|
|
<h1>🤖 Amayo Bot Editor</h1>
|
|||
|
|
<p>Editor estilo VS Code para tu bot de Discord</p>
|
|||
|
|
<div class="welcome-stats">
|
|||
|
|
<div class="welcome-stat">
|
|||
|
|
<div class="stat-number">{{ stats.totalCommands }}</div>
|
|||
|
|
<div class="stat-label">Comandos Totales</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="welcome-stat">
|
|||
|
|
<div class="stat-number">{{ stats.totalEvents }}</div>
|
|||
|
|
<div class="stat-label">Eventos Totales</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="welcome-actions">
|
|||
|
|
<button @click="showCommandCreator" class="welcome-btn primary">
|
|||
|
|
➕ Crear Comando
|
|||
|
|
</button>
|
|||
|
|
<button @click="showEventCreator" class="welcome-btn primary">
|
|||
|
|
➕ Crear Evento
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<p class="welcome-hint">
|
|||
|
|
💡 <strong>Tip:</strong> Selecciona un archivo del panel izquierdo para editarlo
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.app-container {
|
|||
|
|
display: flex;
|
|||
|
|
height: 100vh;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background-color: #1e1e1e;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.main-content {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-overlay {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
background-color: rgba(30, 30, 30, 0.95);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-spinner {
|
|||
|
|
color: #ffffff;
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error-banner {
|
|||
|
|
background-color: #f14c4c;
|
|||
|
|
color: #ffffff;
|
|||
|
|
padding: 12px 20px;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.close-error {
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: #ffffff;
|
|||
|
|
font-size: 18px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
line-height: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.close-error:hover {
|
|||
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-screen {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background-color: #1e1e1e;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-content {
|
|||
|
|
text-align: center;
|
|||
|
|
max-width: 600px;
|
|||
|
|
padding: 40px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-content h1 {
|
|||
|
|
color: #ffffff;
|
|||
|
|
font-size: 48px;
|
|||
|
|
margin: 0 0 16px 0;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-content > p {
|
|||
|
|
color: #cccccc;
|
|||
|
|
font-size: 18px;
|
|||
|
|
margin: 0 0 40px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-stats {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 40px;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-bottom: 40px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-stat {
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-number {
|
|||
|
|
font-size: 48px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #4ec9b0;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stat-label {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #cccccc;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 16px;
|
|||
|
|
justify-content: center;
|
|||
|
|
margin-bottom: 40px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-btn {
|
|||
|
|
padding: 12px 24px;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 15px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-btn.primary {
|
|||
|
|
background-color: #0e639c;
|
|||
|
|
color: #ffffff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-btn.primary:hover {
|
|||
|
|
background-color: #1177bb;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-hint {
|
|||
|
|
color: #858585;
|
|||
|
|
font-size: 14px;
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 16px;
|
|||
|
|
background-color: #252526;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
border-left: 3px solid #0e639c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-hint strong {
|
|||
|
|
color: #4ec9b0;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
* {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
|||
|
|
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
|||
|
|
-webkit-font-smoothing: antialiased;
|
|||
|
|
-moz-osx-font-smoothing: grayscale;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#app {
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Notificaciones */
|
|||
|
|
.notification {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 20px;
|
|||
|
|
right: 20px;
|
|||
|
|
color: #ffffff;
|
|||
|
|
padding: 12px 20px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|||
|
|
z-index: 10000;
|
|||
|
|
animation: slideIn 0.3s ease-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-success {
|
|||
|
|
background-color: #4ec9b0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-error {
|
|||
|
|
background-color: #f48771;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-info {
|
|||
|
|
background-color: #007acc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes slideIn {
|
|||
|
|
from {
|
|||
|
|
transform: translateX(400px);
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
transform: translateX(0);
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|