ai-robot-core/ai-service-admin/src/App.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>