AppNav.vue 12.6 KB
<template>
  <nav class="app-nav">
    <div class="nav-brand" @click="$router.push('/')">
      <span class="nav-logo">🧬</span>
      <span class="nav-title">LemonBoss</span>
    </div>
    <div class="nav-links">
      <a href="/agent/master" target="_blank" class="nav-link" :class="{ active: $route.path.startsWith('/agent') }">
        <span>🤖</span> System Agents
      </a>

      <a href="/templates" target="_blank" class="nav-link" :class="{ active: $route.path.startsWith('/templates') }">
        <span>📦</span> Templates
      </a>

      <a href="/tasks" target="_blank" class="nav-link" :class="{ active: $route.path.startsWith('/tasks') }">
        <span>📜</span> Task Logs
      </a>

      <a href="/quota" target="_blank" class="nav-link" :class="{ active: $route.path === '/quota' }">
        <span>📊</span> Quota
      </a>

      <div class="nav-divider"></div>

      <!-- Company Switcher -->
      <div class="nav-switcher-wrapper" ref="switcherRef">
        <div class="nav-switcher" @click.stop="showDropdown = !showDropdown">
          <span class="switcher-icon">🏢</span>
          <span class="switcher-label">{{ currentCompany ? currentCompany.name : 'Companies' }}</span>
          <span class="switcher-arrow">▼</span>
        </div>
        <div class="nav-dropdown" v-if="showDropdown">
          <div class="nav-dropdown-scroll">
            <div 
              v-for="c in companies" :key="c.id" 
              class="nav-dropdown-item" :class="{ active: currentCompany?.id === c.id }"
              @click="switchCompany(c.id)">
              {{ c.name }}
            </div>
            <div v-if="companies.length === 0" class="nav-dropdown-item text-muted">暂无公司</div>
          </div>
          <div class="nav-dropdown-divider"></div>
          <div class="nav-dropdown-item create-action clickable" @click="showNewCompanyModal = true; showDropdown = false">
             + 新增公司
          </div>
        </div>
      </div>

      <!-- Contextual Company Navigation -->
      <template v-if="currentCompany">
        <a :href="`/${currentCompany.id}/overview`" target="_blank" class="nav-link" :class="{ active: $route.params.tab === 'overview' }">
          📊 Overview
        </a>
        <a :href="`/${currentCompany.id}/agents`" target="_blank" class="nav-link" :class="{ active: $route.params.tab === 'agents' }">
          👥 Team
        </a>
        <a :href="`/${currentCompany.id}/workspace`" target="_blank" class="nav-link" :class="{ active: $route.params.tab === 'workspace' }">
          💼 Workspace
        </a>
        <a :href="`/${currentCompany.id}/pipelines`" target="_blank" class="nav-link" :class="{ active: $route.params.tab === 'pipelines' || $route.params.tab === 'sop' }">
          ⚙️ SOP
        </a>
        <a :href="`/${currentCompany.id}/goals`" target="_blank" class="nav-link" :class="{ active: $route.params.tab === 'goals' || $route.params.tab === 'okr' }">
          🎯 OKR
        </a>
        <a :href="`/${currentCompany.id}/files`" target="_blank" class="nav-link" :class="{ active: $route.params.tab === 'files' }">
          📁 Files
        </a>
        <a :href="`/${currentCompany.id}/messages`" target="_blank" class="nav-link" :class="{ active: $route.params.tab === 'messages' }">
          💬 Messages
        </a>
      </template>
    </div>
    
    <div class="nav-right">
      <button class="theme-btn" @click="cycleTheme" :title="`主题: ${themeLabel}`">
        {{ themeIcon }}
      </button>
      <span class="nav-version">v5.0</span>
    </div>

    <!-- New Company Modal -->
    <div v-if="showNewCompanyModal" class="nav-modal-overlay" @click="showNewCompanyModal = false">
      <div class="nav-modal-content" @click.stop>
        <div class="nav-modal-header">
          <h3>新增空壳公司</h3>
          <button class="nav-modal-close" @click="showNewCompanyModal = false">✕</button>
        </div>
        <div class="nav-modal-body">
          <div class="form-group">
            <label>公司 ID (小写字母/数字)</label>
            <input v-model="newCompanyForm.id" placeholder="例如: my-company" />
          </div>
          <div class="form-group">
            <label>公司名称</label>
            <input v-model="newCompanyForm.name" placeholder="例如: 柠檬科技有限公司" />
          </div>
          <div class="form-group">
            <label>公司简介</label>
            <textarea v-model="newCompanyForm.description" placeholder="简要描述公司的主要业务..." rows="3"></textarea>
          </div>
          <p class="nav-modal-tip">创建成功后将是空壳公司,需唤醒 MasterBot 招聘员工。</p>
        </div>
        <div class="nav-modal-footer">
          <button class="nav-btn-cancel" @click="showNewCompanyModal = false">取消</button>
          <button class="nav-btn-submit" :disabled="isCreatingCompany" @click="submitNewCompany">
            {{ isCreatingCompany ? '⏳ 创建中...' : '🎉 确认创建' }}
          </button>
        </div>
      </div>
    </div>
  </nav>
</template>

<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import axios from 'axios'
import { useTheme } from '@/composables/useTheme'
import { useCompanies } from '@/composables/useCompanies'

const route = useRoute()
const router = useRouter()
const { themeMode, cycleTheme } = useTheme()
const { companies, loadCompanies, activeCompanyId, setActiveCompany } = useCompanies()

const showDropdown = ref(false)
const showNewCompanyModal = ref(false)
const isCreatingCompany = ref(false)
const newCompanyForm = ref({ id: '', name: '', description: '' })

const themeIcon = computed(() => {
  if (themeMode.value === 'light') return '☀️'
  if (themeMode.value === 'dark') return '🌙'
  return '🌗'
})
const themeLabel = computed(() => {
  if (themeMode.value === 'light') return '浅色'
  if (themeMode.value === 'dark') return '深色'
  return '跟随系统'
})

const currentCompany = computed(() => {
  const cid = route.params.id || activeCompanyId.value
  if (!cid) return null
  return companies.value.find(c => c.id === cid) || companies.value[0] || null
})

watch(() => route.params.id, (newId) => {
  if (newId) setActiveCompany(newId)
}, { immediate: true })

function switchCompany(id) {
  showDropdown.value = false
  setActiveCompany(id)
  const activeTab = route.params.tab || 'agents'
  router.push(`/${id}/${activeTab}`)
}

function closeDropdown(e) {
  showDropdown.value = false
}

onMounted(async () => {
  await loadCompanies()
  document.addEventListener('click', closeDropdown)
})

onUnmounted(() => {
  document.removeEventListener('click', closeDropdown)
})

async function submitNewCompany() {
  if (!newCompanyForm.value.id || !newCompanyForm.value.name) return alert('请填写公司ID和名称')
  isCreatingCompany.value = true
  try {
    const res = await axios.post('/api/company/create', newCompanyForm.value)
    if (res.data.success) {
      alert('创建成功')
      showNewCompanyModal.value = false
      await loadCompanies()
      router.push(`/${res.data.company.id}/overview`)
      newCompanyForm.value = { id: '', name: '', description: '' }
    }
  } catch(e) {
    alert('创建失败: ' + (e.response?.data?.error || e.message))
  }
  isCreatingCompany.value = false
}
</script>

<style scoped>
.app-nav {
  display: flex; align-items: center; height: 44px; padding: 0 16px;
  background: var(--bg-secondary); backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border-primary);
  position: sticky; top: 0; z-index: 100; gap: 20px;
  transition: background 0.3s;
}
.nav-brand { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.nav-logo { font-size: 16px; }
.nav-title {
  font-size: 14px; font-weight: 800;
  background: linear-gradient(135deg, var(--accent), var(--accent-light));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.nav-links { display: flex; align-items: center; gap: 6px; flex: 1; }
.nav-divider { width: 1px; height: 20px; background: var(--border-primary); margin: 0 8px; }

.nav-link {
  display: flex; align-items: center; gap: 6px; padding: 6px 12px;
  border-radius: 8px; text-decoration: none; color: var(--text-tertiary);
  font-size: 13px; font-weight: 600; transition: all 0.2s;
}
.nav-link:hover { background: var(--bg-surface-hover); color: var(--text-secondary); }
.nav-link.active { background: var(--bg-surface); color: var(--accent); box-shadow: 0 2px 8px rgba(0,0,0,0.05); }

/* Switcher */
.nav-switcher-wrapper { position: relative; margin-right: 8px; }
.nav-switcher {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 8px; cursor: pointer;
  background: var(--bg-surface); border: 1px solid var(--border-primary);
  transition: all 0.2s; font-size: 13px; font-weight: 700; color: var(--text-primary);
}
.nav-switcher:hover { background: var(--bg-surface-hover); border-color: var(--border-active); }
.switcher-arrow { font-size: 9px; color: var(--text-muted); padding-top:2px; }

.nav-dropdown {
  position: absolute; top: calc(100% + 6px); left: 0; width: 200px;
  background: var(--bg-secondary); border: 1px solid var(--border-primary);
  border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 1000;
}
.nav-dropdown-scroll { max-height: 200px; overflow-y: auto; padding: 6px; }
.nav-dropdown-item {
  padding: 8px 12px; font-size: 13px; font-weight: 600; color: var(--text-secondary);
  border-radius: 6px; cursor: pointer; transition: all 0.1s;
}
.nav-dropdown-item:hover { background: var(--bg-surface-hover); color: var(--text-primary); }
.nav-dropdown-item.active { background: var(--accent-bg); color: var(--accent-light); }
.nav-dropdown-divider { height: 1px; background: var(--border-primary); margin: 0; }
.create-action { color: var(--accent-light); text-align: center; margin: 6px; border: 1px dashed var(--border-active); }
.create-action:hover { background: var(--accent-bg); color: var(--accent-light); }

.nav-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.theme-btn {
  width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--border-primary);
  background: var(--bg-surface); cursor: pointer; font-size: 14px;
  display: flex; align-items: center; justify-content: center; transition: all 0.2s;
}
.theme-btn:hover { background: var(--bg-surface-hover); transform: scale(1.1); }
.nav-version { font-size: 10px; color: var(--text-faint); padding: 2px 6px; border: 1px solid var(--border-primary); border-radius: 4px; font-weight:600;}

/* Modal */
.nav-modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2000; backdrop-filter: blur(4px); }
.nav-modal-content { background: var(--bg-primary); border-radius: 12px; width: 400px; padding: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); border: 1px solid var(--border-primary); }
.nav-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.nav-modal-header h3 { margin: 0; font-size: 16px; color: var(--text-primary); }
.nav-modal-close { background: transparent; border: none; font-size: 16px; cursor: pointer; color: var(--text-secondary); }
.nav-modal-body .form-group { margin-bottom: 12px; }
.nav-modal-body label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
.nav-modal-body input, .nav-modal-body textarea { width: 100%; padding: 8px 10px; border: 1px solid var(--border-primary); border-radius: 6px; background: var(--bg-surface); color: var(--text-primary); font-size: 13px; box-sizing: border-box; }
.nav-modal-body input:focus, .nav-modal-body textarea:focus { outline: none; border-color: var(--accent); }
.nav-modal-tip { font-size: 12px; color: var(--text-muted); margin-top: 12px; }
.nav-modal-footer { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; }
.nav-btn-cancel { padding: 6px 16px; border-radius: 6px; border: 1px solid var(--border-primary); background: var(--bg-surface); cursor: pointer; color: var(--text-secondary); font-size: 13px; font-weight: 600; transition: all 0.2s; }
.nav-btn-cancel:hover { background: var(--bg-surface-hover); }
.nav-btn-submit { padding: 6px 16px; border-radius: 6px; border: none; background: var(--accent); cursor: pointer; color: white; font-size: 13px; font-weight: 600; transition: all 0.2s; }
.nav-btn-submit:hover { background: var(--accent-hover); }
.nav-btn-submit:disabled { opacity: 0.6; cursor: not-allowed; }
.create-action.clickable { opacity: 1; cursor: pointer; border: 1px dashed var(--border-active); }
</style>