Initial commit: Nexus Mission Control Platform
- ASP.NET Core 10 Backend (JWT Auth, Agent config API) - Vue 3 Frontend (Dashboard, Team, Agents, Config Editor) - PostgreSQL Database - Docker Compose setup - Mission Control Dashboard redesign
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import { Loader2, Save, Check, AlertCircle } from '@lucide/vue'
|
||||
|
||||
defineProps<{
|
||||
fileName: string | null
|
||||
fileSize: string
|
||||
fileModified: string
|
||||
content: string
|
||||
dirty: boolean
|
||||
saving: boolean
|
||||
saveStatus: 'idle' | 'saved' | 'error'
|
||||
saveMessage: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
updateContent: [value: string]
|
||||
save: []
|
||||
}>()
|
||||
|
||||
function onInput(event: Event) {
|
||||
const textarea = event.target as HTMLTextAreaElement
|
||||
// Pass content change up, parent handles dirty detection
|
||||
;(event.target as HTMLTextAreaElement).dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="editor-panel">
|
||||
<!-- File info header -->
|
||||
<div class="editor-header">
|
||||
<div class="editor-file-info">
|
||||
<span class="editor-filename">{{ fileName || '—' }}</span>
|
||||
<span v-if="fileName" class="editor-file-meta">
|
||||
{{ fileSize }}
|
||||
<span class="meta-sep">·</span>
|
||||
{{ fileModified }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Save button & status -->
|
||||
<div class="editor-actions">
|
||||
<span v-if="saveStatus === 'saved'" class="save-indicator success">
|
||||
<Check :size="14" />
|
||||
{{ saveMessage }}
|
||||
</span>
|
||||
<span v-if="saveStatus === 'error'" class="save-indicator error">
|
||||
<AlertCircle :size="14" />
|
||||
{{ saveMessage }}
|
||||
</span>
|
||||
<button
|
||||
class="save-btn"
|
||||
:class="{ dirty, saving }"
|
||||
:disabled="!dirty || saving"
|
||||
@click="$emit('save')"
|
||||
>
|
||||
<Loader2 v-if="saving" :size="14" class="spin" />
|
||||
<Save v-else :size="14" />
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text editor -->
|
||||
<textarea
|
||||
class="config-editor"
|
||||
:value="content"
|
||||
@input="$emit('updateContent', ($event.target as HTMLTextAreaElement).value)"
|
||||
spellcheck="false"
|
||||
wrap="off"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.editor-panel {
|
||||
border: 1px solid var(--line, #1e2030);
|
||||
border-top: none;
|
||||
border-radius: 0 0 10px 10px;
|
||||
overflow: hidden;
|
||||
background: var(--panel, #13141f);
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background: rgba(255,255,255,.02);
|
||||
border-bottom: 1px solid var(--line, #1e2030);
|
||||
gap: 12px;
|
||||
}
|
||||
.editor-file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.editor-filename {
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
color: #d0d4dd;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.editor-file-meta {
|
||||
font-size: 10px;
|
||||
color: #6b7385;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.meta-sep {
|
||||
margin: 0 4px;
|
||||
color: #3d4152;
|
||||
}
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.save-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 10.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.save-indicator.success {
|
||||
color: #51d49a;
|
||||
}
|
||||
.save-indicator.error {
|
||||
color: #e16e75;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--line, #1e2030);
|
||||
border-radius: 7px;
|
||||
background: rgba(139,124,246,.08);
|
||||
color: #8b7cf6;
|
||||
font-size: 10.5px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
.save-btn:hover:not(:disabled) {
|
||||
background: rgba(139,124,246,.14);
|
||||
border-color: #443d7c;
|
||||
}
|
||||
.save-btn.dirty {
|
||||
background: rgba(139,124,246,.18);
|
||||
border-color: #5c4ed6;
|
||||
color: #a99cff;
|
||||
}
|
||||
.save-btn.saving {
|
||||
opacity: 0.7;
|
||||
cursor: wait;
|
||||
}
|
||||
.save-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.config-editor {
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
background: #0d0e17;
|
||||
color: #c8cbe0;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
tab-size: 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.config-editor:focus {
|
||||
background: #0f101b;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.editor-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.editor-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
tabs: string[]
|
||||
activeTab: number
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
switchTab: [index: number]
|
||||
}>()
|
||||
|
||||
function tabLabel(tab: string): string {
|
||||
return tab.replace('.md', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="config-tabs">
|
||||
<button
|
||||
v-for="(tab, idx) in tabs"
|
||||
:key="tab"
|
||||
class="config-tab"
|
||||
:class="{ active: activeTab === idx }"
|
||||
@click="$emit('switchTab', idx)"
|
||||
>
|
||||
{{ tabLabel(tab) }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.config-tabs {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
background: var(--line, #1e2030);
|
||||
border-radius: 10px 10px 0 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line, #1e2030);
|
||||
border-bottom: none;
|
||||
}
|
||||
.config-tab {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
background: var(--panel, #13141f);
|
||||
border: none;
|
||||
color: #6b7385;
|
||||
font-size: 10.5px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
letter-spacing: 0.04em;
|
||||
font-family: inherit;
|
||||
}
|
||||
.config-tab:hover {
|
||||
background: rgba(139,124,246,.06);
|
||||
color: #a0a8b8;
|
||||
}
|
||||
.config-tab.active {
|
||||
background: rgba(139,124,246,.1);
|
||||
color: #c8cbe0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.config-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.config-tab {
|
||||
flex: 1 0 auto;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user