Add-lightweight-control-center-home

This commit is contained in:
Codex
2026-06-08 20:58:11 +08:00
parent 4bb3593fc8
commit 9ecffde2ed
5 changed files with 750 additions and 2 deletions
+1
View File
@@ -22,6 +22,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
'../views/_core/announcement/index.vue',
'../views/_core/announcement/list.vue',
'../views/_core/authentication/**/*.vue',
'../views/_core/control-center/index.vue',
'../views/_core/fallback/**/*.vue',
'../views/_core/login-log/index.vue',
'../views/_core/menu/index.vue',
@@ -41,6 +41,7 @@ const componentKeys: string[] = Object.keys(
'../../views/_core/announcement/index.vue',
'../../views/_core/announcement/list.vue',
'../../views/_core/authentication/**/*.vue',
'../../views/_core/control-center/index.vue',
'../../views/_core/fallback/**/*.vue',
'../../views/_core/login-log/index.vue',
'../../views/_core/menu/index.vue',
@@ -0,0 +1,734 @@
<script setup lang="ts">
import type {
AgentListItem,
KnowledgeBaseListItem,
WorkflowListItem,
WorkflowRunListItem,
} from '#/api/ai-platform/ai-platform';
import { computed, onMounted, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import {
Bot,
BookOpen,
CircleCheckBig,
Clock3,
GitBranch,
MessageSquare,
Play,
RefreshCw,
Route,
Settings,
Shield,
Sparkles,
Users,
Workflow,
} from '@vben/icons';
import {
ElButton,
ElCard,
ElEmpty,
ElSkeleton,
ElTag,
ElTimeline,
ElTimelineItem,
} from 'element-plus';
import {
getAgentListApi,
getAllWorkflowRunsApi,
getKnowledgeBaseListApi,
getWorkflowListApi,
} from '#/api/ai-platform/ai-platform';
defineOptions({ name: 'ControlCenter' });
const router = useRouter();
const loading = ref(false);
const loaded = ref(false);
const agents = ref<AgentListItem[]>([]);
const workflows = ref<WorkflowListItem[]>([]);
const knowledgeBases = ref<KnowledgeBaseListItem[]>([]);
const runs = ref<WorkflowRunListItem[]>([]);
const stats = reactive({
agentTotal: 0,
workflowTotal: 0,
knowledgeTotal: 0,
runTotal: 0,
});
const publishedAgents = computed(
() => agents.value.filter((item) => item.status === 'published').length,
);
const publishedWorkflows = computed(
() => workflows.value.filter((item) => item.status === 'published').length,
);
const successfulRuns = computed(
() => runs.value.filter((item) => item.status === 'success').length,
);
const failedRuns = computed(
() => runs.value.filter((item) => item.status === 'failed').length,
);
const quickEntries = [
{
desc: '沉淀角色能力、提示词和工具调用策略',
icon: Bot,
path: '/ai-platform/agent',
title: '智能体',
},
{
desc: '编排多 Agent 协作、人工确认和系统调用',
icon: Workflow,
path: '/ai-platform/workflow',
title: '流程编排',
},
{
desc: '管理文档、分段、向量索引和检索日志',
icon: BookOpen,
path: '/ai-platform/knowledge-base',
title: '知识库',
},
{
desc: '进入 Codex 协作入口,串联开发任务',
icon: MessageSquare,
path: '/agent-chat/codex',
title: 'Codex',
},
];
const adminEntries = [
{ icon: Users, path: '/system/user', title: '用户管理' },
{ icon: Shield, path: '/system/role', title: '角色权限' },
{ icon: Route, path: '/system/menu', title: '菜单管理' },
{ icon: Settings, path: '/system-config', title: '系统配置' },
];
function go(path: string) {
router.push(path);
}
function formatTime(value?: string) {
if (!value) return '-';
return value.replace('T', ' ').slice(0, 19);
}
async function loadDashboard() {
loading.value = true;
try {
const [agentRes, workflowRes, knowledgeRes, runRes] = await Promise.all([
getAgentListApi({ page: 1, pageSize: 6 }),
getWorkflowListApi({ page: 1, pageSize: 6 }),
getKnowledgeBaseListApi({ page: 1, pageSize: 6 }),
getAllWorkflowRunsApi({ page: 1, pageSize: 6 }),
]);
agents.value = agentRes.items || [];
workflows.value = workflowRes.items || [];
knowledgeBases.value = knowledgeRes.items || [];
runs.value = runRes.items || [];
stats.agentTotal = agentRes.total || agents.value.length;
stats.workflowTotal = workflowRes.total || workflows.value.length;
stats.knowledgeTotal = knowledgeRes.total || knowledgeBases.value.length;
stats.runTotal = runRes.total || runs.value.length;
loaded.value = true;
} finally {
loading.value = false;
}
}
onMounted(loadDashboard);
</script>
<template>
<Page auto-content-height title="控制中心">
<div class="control-center">
<section class="hero-panel">
<div>
<div class="eyebrow">
<Sparkles :size="16" />
ZQ-AI Agent Admin
</div>
<h1>控制中心</h1>
<p>
{{ stats.agentTotal }} 个智能体{{ stats.workflowTotal }} 条流程{{ stats.runTotal }} 次执行记录已汇总
</p>
</div>
<div class="hero-actions">
<ElButton :icon="RefreshCw" :loading="loading" @click="loadDashboard">
刷新
</ElButton>
<ElButton type="primary" :icon="Bot" @click="go('/ai-platform/agent')">
创建智能体
</ElButton>
</div>
</section>
<section class="metric-grid">
<ElCard shadow="never" class="metric-card">
<div class="metric-icon agent"><Bot :size="22" /></div>
<div>
<span>智能体</span>
<strong>{{ stats.agentTotal }}</strong>
<small>已发布 {{ publishedAgents }}</small>
</div>
</ElCard>
<ElCard shadow="never" class="metric-card">
<div class="metric-icon workflow"><Workflow :size="22" /></div>
<div>
<span>流程</span>
<strong>{{ stats.workflowTotal }}</strong>
<small>已发布 {{ publishedWorkflows }}</small>
</div>
</ElCard>
<ElCard shadow="never" class="metric-card">
<div class="metric-icon knowledge"><BookOpen :size="22" /></div>
<div>
<span>知识库</span>
<strong>{{ stats.knowledgeTotal }}</strong>
<small>{{ knowledgeBases.length }} 个最近维护</small>
</div>
</ElCard>
<ElCard shadow="never" class="metric-card">
<div class="metric-icon run"><Play :size="22" /></div>
<div>
<span>执行记录</span>
<strong>{{ stats.runTotal }}</strong>
<small>成功 {{ successfulRuns }} / 失败 {{ failedRuns }}</small>
</div>
</ElCard>
</section>
<section class="content-grid">
<ElCard shadow="never" class="panel-card">
<template #header>
<div class="card-header">
<span>AI 工作入口</span>
<ElButton link type="primary" @click="go('/ai-platform/agent')">
查看全部
</ElButton>
</div>
</template>
<div class="quick-grid">
<button
v-for="entry in quickEntries"
:key="entry.path"
class="quick-entry"
type="button"
@click="go(entry.path)"
>
<component :is="entry.icon" :size="22" />
<span>{{ entry.title }}</span>
<small>{{ entry.desc }}</small>
</button>
</div>
</ElCard>
<ElCard shadow="never" class="panel-card">
<template #header>
<div class="card-header">
<span>基础后台</span>
<ElButton link type="primary" @click="go('/system/user')">
用户权限
</ElButton>
</div>
</template>
<div class="admin-grid">
<button
v-for="entry in adminEntries"
:key="entry.path"
class="admin-entry"
type="button"
@click="go(entry.path)"
>
<component :is="entry.icon" :size="18" />
<span>{{ entry.title }}</span>
</button>
</div>
</ElCard>
</section>
<section class="content-grid lower">
<ElCard shadow="never" class="panel-card">
<template #header>
<div class="card-header">
<span>智能体协作</span>
<ElButton link type="primary" @click="go('/ai-platform/agent')">
管理智能体
</ElButton>
</div>
</template>
<ElSkeleton v-if="loading && !loaded" :rows="5" animated />
<div v-else-if="agents.length" class="list-stack">
<button
v-for="agent in agents"
:key="agent.id"
class="list-row"
type="button"
@click="go(`/ai-platform/agent/editor/${agent.id}`)"
>
<span>
<strong>{{ agent.name }}</strong>
<small>{{ agent.description || agent.code }}</small>
</span>
<ElTag :type="agent.status === 'published' ? 'success' : 'info'">
{{ agent.status === 'published' ? '已发布' : '草稿' }}
</ElTag>
</button>
</div>
<ElEmpty v-else description="暂无智能体" />
</ElCard>
<ElCard shadow="never" class="panel-card">
<template #header>
<div class="card-header">
<span>最近执行</span>
<ElButton
link
type="primary"
@click="go('/ai-platform/workflow-runs')"
>
执行历史
</ElButton>
</div>
</template>
<ElTimeline v-if="runs.length" class="run-timeline">
<ElTimelineItem
v-for="run in runs"
:key="run.id"
:timestamp="formatTime(run.started_at)"
:type="run.status === 'success' ? 'success' : run.status === 'failed' ? 'danger' : 'primary'"
>
<button
class="run-item"
type="button"
@click="go('/ai-platform/workflow-runs')"
>
<span>
<strong>{{ run.workflow_name }}</strong>
<small>
{{ run.total_steps }} · {{ run.total_tokens }} tokens
</small>
</span>
<ElTag size="small">
{{ run.status }}
</ElTag>
</button>
</ElTimelineItem>
</ElTimeline>
<ElEmpty v-else description="暂无执行记录" />
</ElCard>
</section>
<section class="content-grid lower">
<ElCard shadow="never" class="panel-card">
<template #header>
<div class="card-header">
<span>流程编排</span>
<ElButton link type="primary" @click="go('/ai-platform/workflow')">
查看流程
</ElButton>
</div>
</template>
<div v-if="workflows.length" class="list-stack compact">
<button
v-for="workflow in workflows"
:key="workflow.id"
class="list-row"
type="button"
@click="go(`/ai-platform/workflow/editor/${workflow.id}`)"
>
<span>
<strong>{{ workflow.name }}</strong>
<small>{{ workflow.description || workflow.code }}</small>
</span>
<ElTag type="info">{{ workflow.workflow_type }}</ElTag>
</button>
</div>
<ElEmpty v-else description="暂无流程" />
</ElCard>
<ElCard shadow="never" class="panel-card">
<template #header>
<div class="card-header">
<span>知识资产</span>
<ElButton
link
type="primary"
@click="go('/ai-platform/knowledge-base')"
>
查看知识库
</ElButton>
</div>
</template>
<div v-if="knowledgeBases.length" class="knowledge-strip">
<button
v-for="kb in knowledgeBases"
:key="kb.id"
class="knowledge-item"
type="button"
@click="go(`/ai-platform/knowledge/detail/${kb.id}`)"
>
<BookOpen :size="18" />
<span>
<strong>{{ kb.name }}</strong>
<small>
{{ kb.document_count }} 文档 · {{ kb.segment_count }} 分段
</small>
</span>
</button>
</div>
<ElEmpty v-else description="暂无知识库" />
</ElCard>
</section>
<section class="health-band">
<div>
<CircleCheckBig :size="18" />
<span>基础后台状态正常用户角色菜单和公告入口可用</span>
</div>
<div>
<GitBranch :size="18" />
<span>AI 协作资源已加载智能体流程和 Codex 入口可用</span>
</div>
<div>
<Clock3 :size="18" />
<span>最近执行记录已同步刷新后可查看最新运行状态</span>
</div>
</section>
</div>
</Page>
</template>
<style scoped>
.control-center {
display: flex;
flex-direction: column;
gap: 16px;
}
.hero-panel {
align-items: center;
background:
linear-gradient(135deg, rgb(15 23 42 / 92%), rgb(32 41 64 / 88%)),
radial-gradient(circle at 72% 24%, rgb(20 184 166 / 38%), transparent 30%);
border-radius: 8px;
color: #fff;
display: flex;
justify-content: space-between;
min-height: 178px;
overflow: hidden;
padding: 28px 32px;
}
.eyebrow,
.hero-actions,
.card-header,
.health-band div {
align-items: center;
display: flex;
}
.eyebrow {
color: rgb(204 251 241);
font-size: 13px;
gap: 8px;
margin-bottom: 14px;
}
.hero-panel h1 {
font-size: 28px;
font-weight: 650;
line-height: 1.25;
margin: 0 0 10px;
}
.hero-panel p {
color: rgb(226 232 240);
line-height: 1.7;
margin: 0;
max-width: 680px;
}
.hero-actions {
flex-shrink: 0;
gap: 10px;
}
.metric-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.metric-card :deep(.el-card__body) {
align-items: center;
display: flex;
gap: 14px;
padding: 18px;
}
.metric-icon {
align-items: center;
border-radius: 8px;
display: flex;
height: 46px;
justify-content: center;
width: 46px;
}
.metric-icon.agent {
background: rgb(219 234 254);
color: rgb(37 99 235);
}
.metric-icon.workflow {
background: rgb(204 251 241);
color: rgb(13 148 136);
}
.metric-icon.knowledge {
background: rgb(254 243 199);
color: rgb(180 83 9);
}
.metric-icon.run {
background: rgb(220 252 231);
color: rgb(22 163 74);
}
.metric-card span,
.list-row small,
.knowledge-item small {
color: hsl(var(--muted-foreground));
}
.metric-card strong {
display: block;
font-size: 28px;
line-height: 1.1;
margin: 4px 0;
}
.metric-card small {
color: hsl(var(--muted-foreground));
}
.content-grid {
display: grid;
gap: 16px;
grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.65fr);
}
.content-grid.lower {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.panel-card {
border-radius: 8px;
}
.card-header {
justify-content: space-between;
}
.card-header span {
font-weight: 650;
}
.quick-grid {
display: grid;
gap: 12px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.quick-entry,
.admin-entry,
.list-row,
.run-item,
.knowledge-item {
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 8px;
color: inherit;
cursor: pointer;
text-align: left;
transition:
border-color 0.18s ease,
box-shadow 0.18s ease,
transform 0.18s ease;
}
.quick-entry:hover,
.admin-entry:hover,
.list-row:hover,
.run-item:hover,
.knowledge-item:hover {
border-color: hsl(var(--primary) / 55%);
box-shadow: 0 8px 24px rgb(15 23 42 / 8%);
transform: translateY(-1px);
}
.quick-entry {
display: grid;
gap: 8px;
min-height: 126px;
padding: 16px;
}
.quick-entry svg {
color: hsl(var(--primary));
}
.quick-entry span,
.admin-entry span,
.knowledge-item strong {
font-weight: 650;
}
.quick-entry small {
color: hsl(var(--muted-foreground));
line-height: 1.6;
}
.admin-grid {
display: grid;
gap: 10px;
}
.admin-entry {
align-items: center;
display: flex;
gap: 10px;
height: 48px;
padding: 0 14px;
}
.admin-entry svg {
color: hsl(var(--primary));
}
.list-stack {
display: grid;
gap: 10px;
}
.list-stack.compact {
gap: 8px;
}
.list-row,
.run-item {
align-items: center;
display: flex;
justify-content: space-between;
min-height: 62px;
padding: 10px 12px;
width: 100%;
}
.list-row span,
.run-item span,
.knowledge-item span {
display: grid;
gap: 4px;
min-width: 0;
}
.list-row strong,
.run-item strong,
.knowledge-item strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.run-timeline {
padding-top: 4px;
}
.run-item {
border: 0;
min-height: 50px;
padding: 4px 0;
}
.knowledge-strip {
display: grid;
gap: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.knowledge-item {
align-items: center;
display: flex;
gap: 10px;
min-height: 72px;
padding: 12px;
}
.knowledge-item svg {
color: hsl(var(--primary));
flex-shrink: 0;
}
.health-band {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
display: grid;
gap: 10px;
grid-template-columns: repeat(3, minmax(0, 1fr));
padding: 14px 16px;
}
.health-band div {
color: hsl(var(--muted-foreground));
gap: 8px;
min-width: 0;
}
.health-band svg {
color: hsl(var(--primary));
flex-shrink: 0;
}
@media (max-width: 1180px) {
.metric-grid,
.content-grid,
.content-grid.lower,
.health-band {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.hero-panel {
align-items: flex-start;
flex-direction: column;
gap: 18px;
padding: 22px;
}
.hero-panel h1 {
font-size: 24px;
}
.hero-actions,
.hero-actions .el-button {
width: 100%;
}
.metric-grid,
.content-grid,
.content-grid.lower,
.quick-grid,
.knowledge-strip,
.health-band {
grid-template-columns: 1fr;
}
}
</style>
+1 -1
View File
@@ -17,7 +17,7 @@ const defaultPreferences: Preferences = {
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/ai-platform/agent',
defaultHomePath: '/control-center',
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,