319 lines
9.0 KiB
Vue
319 lines
9.0 KiB
Vue
<template>
|
|
<div class="app-wrapper">
|
|
<header class="app-header">
|
|
<div class="header-left">
|
|
<div class="logo">
|
|
<div class="logo-icon">
|
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</div>
|
|
<span class="logo-text">AI Robot</span>
|
|
</div>
|
|
<nav class="main-nav">
|
|
<div class="nav-row">
|
|
<router-link to="/dashboard" class="nav-item" :class="{ active: isActive('/dashboard') }">
|
|
<el-icon><Odometer /></el-icon>
|
|
<span>控制台</span>
|
|
</router-link>
|
|
<router-link to="/admin/knowledge-bases" class="nav-item" :class="{ active: isActive('/admin/knowledge-bases') }">
|
|
<el-icon><FolderOpened /></el-icon>
|
|
<span>知识库</span>
|
|
</router-link>
|
|
<router-link to="/rag-lab" class="nav-item" :class="{ active: isActive('/rag-lab') }">
|
|
<el-icon><Cpu /></el-icon>
|
|
<span>RAG 实验室</span>
|
|
</router-link>
|
|
<router-link to="/monitoring" class="nav-item" :class="{ active: isActive('/monitoring') }">
|
|
<el-icon><Monitor /></el-icon>
|
|
<span>会话监控</span>
|
|
</router-link>
|
|
</div>
|
|
<div class="nav-row">
|
|
<router-link to="/admin/embedding" class="nav-item" :class="{ active: isActive('/admin/embedding') }">
|
|
<el-icon><Connection /></el-icon>
|
|
<span>嵌入模型</span>
|
|
</router-link>
|
|
<router-link to="/admin/llm" class="nav-item" :class="{ active: isActive('/admin/llm') }">
|
|
<el-icon><ChatDotSquare /></el-icon>
|
|
<span>LLM 配置</span>
|
|
</router-link>
|
|
<router-link to="/admin/prompt-templates" class="nav-item" :class="{ active: isActive('/admin/prompt-templates') }">
|
|
<el-icon><Document /></el-icon>
|
|
<span>Prompt 模板</span>
|
|
</router-link>
|
|
<router-link to="/admin/intent-rules" class="nav-item" :class="{ active: isActive('/admin/intent-rules') }">
|
|
<el-icon><Aim /></el-icon>
|
|
<span>意图规则</span>
|
|
</router-link>
|
|
<router-link to="/admin/script-flows" class="nav-item" :class="{ active: isActive('/admin/script-flows') }">
|
|
<el-icon><Share /></el-icon>
|
|
<span>话术流程</span>
|
|
</router-link>
|
|
<router-link to="/admin/guardrails" class="nav-item" :class="{ active: isActive('/admin/guardrails') }">
|
|
<el-icon><Warning /></el-icon>
|
|
<span>输出护栏</span>
|
|
</router-link>
|
|
<router-link to="/admin/mid-platform-playground" class="nav-item" :class="{ active: isActive('/admin/mid-platform-playground') }">
|
|
<el-icon><ChatLineRound /></el-icon>
|
|
<span>中台联调</span>
|
|
</router-link>
|
|
<router-link to="/admin/metadata-schemas" class="nav-item" :class="{ active: isActive('/admin/metadata-schemas') }">
|
|
<el-icon><Setting /></el-icon>
|
|
<span>元数据配置</span>
|
|
</router-link>
|
|
<router-link to="/admin/slot-definitions" class="nav-item" :class="{ active: isActive('/admin/slot-definitions') }">
|
|
<el-icon><Grid /></el-icon>
|
|
<span>槽位定义</span>
|
|
</router-link>
|
|
<router-link to="/admin/scene-slot-bundles" class="nav-item" :class="{ active: isActive('/admin/scene-slot-bundles') }">
|
|
<el-icon><Collection /></el-icon>
|
|
<span>场景槽位包</span>
|
|
</router-link>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="tenant-selector">
|
|
<el-select
|
|
v-model="currentTenantId"
|
|
placeholder="选择租户"
|
|
size="default"
|
|
:loading="loading"
|
|
@change="handleTenantChange"
|
|
>
|
|
<el-option
|
|
v-for="tenant in tenantList"
|
|
:key="tenant.id"
|
|
:label="tenant.name"
|
|
:value="tenant.id"
|
|
/>
|
|
</el-select>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main class="app-main">
|
|
<router-view />
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { useTenantStore } from '@/stores/tenant'
|
|
import { getTenantList, type Tenant } from '@/api/tenant'
|
|
import { Odometer, FolderOpened, Cpu, Monitor, Connection, ChatDotSquare, Document, Aim, Share, Warning, Setting, ChatLineRound, Grid, Collection } from '@element-plus/icons-vue'
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
const route = useRoute()
|
|
const tenantStore = useTenantStore()
|
|
|
|
const currentTenantId = ref(tenantStore.currentTenantId)
|
|
const tenantList = ref<Tenant[]>([])
|
|
const loading = ref(false)
|
|
|
|
const isActive = (path: string) => {
|
|
return route.path === path || route.path.startsWith(path + '/')
|
|
}
|
|
|
|
const handleTenantChange = (val: string) => {
|
|
tenantStore.setTenant(val)
|
|
// 刷新页面以加载新租户的数据
|
|
window.location.reload()
|
|
}
|
|
|
|
// Validate tenant ID format: name@ash@year
|
|
const isValidTenantId = (tenantId: string): boolean => {
|
|
return /^[^@]+@ash@\d{4}$/.test(tenantId)
|
|
}
|
|
|
|
const fetchTenantList = async () => {
|
|
loading.value = true
|
|
try {
|
|
if (!isValidTenantId(currentTenantId.value)) {
|
|
console.warn('Invalid tenant ID format, resetting to default:', currentTenantId.value)
|
|
currentTenantId.value = 'default@ash@2026'
|
|
tenantStore.setTenant(currentTenantId.value)
|
|
}
|
|
|
|
const response = await getTenantList()
|
|
tenantList.value = response.tenants || []
|
|
|
|
if (tenantList.value.length > 0 && !tenantList.value.find(t => t.id === currentTenantId.value)) {
|
|
const firstTenant = tenantList.value[0]
|
|
currentTenantId.value = firstTenant.id
|
|
tenantStore.setTenant(firstTenant.id)
|
|
}
|
|
} catch (error) {
|
|
ElMessage.error('获取租户列表失败')
|
|
console.error('Failed to fetch tenant list:', error)
|
|
tenantList.value = [{ id: 'default@ash@2026', name: 'default (2026)', displayName: 'default', year: '2026', createdAt: new Date().toISOString() }]
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchTenantList()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.app-wrapper {
|
|
min-height: 100vh;
|
|
background-color: var(--bg-primary, #F8FAFC);
|
|
}
|
|
|
|
.app-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 24px;
|
|
height: auto;
|
|
min-height: 60px;
|
|
background-color: var(--bg-secondary, #FFFFFF);
|
|
border-bottom: 1px solid var(--border-color, #E2E8F0);
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 32px;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.logo-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--primary-color, #4F7CFF);
|
|
}
|
|
|
|
.logo-icon svg {
|
|
width: 28px;
|
|
height: 28px;
|
|
}
|
|
|
|
.logo-text {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text-primary, #1E293B);
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.main-nav {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.nav-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 14px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary, #64748B);
|
|
text-decoration: none;
|
|
border-radius: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
color: var(--primary-color, #4F7CFF);
|
|
background-color: var(--primary-lighter, #E8EEFF);
|
|
}
|
|
|
|
.nav-item.active {
|
|
color: var(--primary-color, #4F7CFF);
|
|
background-color: var(--primary-lighter, #E8EEFF);
|
|
}
|
|
|
|
.nav-item .el-icon {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.nav-divider {
|
|
width: 1px;
|
|
height: 20px;
|
|
margin: 0 8px;
|
|
background-color: var(--border-color, #E2E8F0);
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.tenant-selector {
|
|
min-width: 140px;
|
|
}
|
|
|
|
.tenant-selector :deep(.el-input__wrapper) {
|
|
background-color: var(--bg-tertiary, #F1F5F9);
|
|
border-color: transparent;
|
|
}
|
|
|
|
.tenant-selector :deep(.el-input__wrapper:hover) {
|
|
background-color: var(--bg-hover, #E2E8F0);
|
|
}
|
|
|
|
.app-main {
|
|
min-height: calc(100vh - 60px);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.app-header {
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.header-left {
|
|
gap: 16px;
|
|
}
|
|
|
|
.main-nav {
|
|
gap: 2px;
|
|
}
|
|
|
|
.nav-item {
|
|
padding: 8px 10px;
|
|
}
|
|
|
|
.nav-item span {
|
|
display: none;
|
|
}
|
|
|
|
.nav-divider {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.logo-text {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|