feat(ctrt): 完成 CTRT-01~03 响应契约统一 - 三端拦截器+portal_token清理+调用点适配

This commit is contained in:
Simon
2026-07-07 22:56:49 +08:00
parent d56a9a6079
commit 6f0fbbb066
48 changed files with 6183 additions and 613 deletions
+28
View File
@@ -0,0 +1,28 @@
# =============================================================================
# 企微IT智能服务台 — 管理前端开发版 Docker 镜像
# =============================================================================
# 说明:基于 node:20 开发模式,支持代码热更新(volume mount 源码)
# 用途:本地开发,代码修改自动生效
# =============================================================================
FROM node:20-slim
# 安装 pnpm
RUN npm install -g pnpm
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY package.json pnpm-lock.yaml* ./
# 安装依赖
RUN pnpm install --frozen-lockfile
# 复制源码(后续通过 volume mount 更新)
COPY . .
# 暴露端口
EXPOSE 5175
# 启动开发服务器(热更新)
CMD ["pnpm", "dev", "--host"]
+105
View File
@@ -0,0 +1,105 @@
// =============================================================================
// 企微IT智能服务台 — 阶段5 自动化闭环 API 调用模块(管理后台)
// =============================================================================
// 说明:封装管理端自动化配置 / 规则版本 / 指标相关 HTTP 请求。
// 约定:与 admin 其它 api 模块一致 —— 返回「包装对象」{ data: { code, data, message } }
// 调用方通过 res.data.data 取业务数据(响应拦截器已做统一错误处理)。
// 路径:与后端契约 §15.3 一致,前缀 /itportal/automation。
// =============================================================================
import apiClient from './index'
// --------------------------------------------------------------------------
// 类型定义(与坐席端保持一致)
// --------------------------------------------------------------------------
export interface ScenarioConfig {
id: string
scenario_key: string
name: string
description: string
enabled: boolean
trigger_conditions: Record<string, any> | null
actions: any[] | null
approval_strategy: Record<string, any> | null
current_version_id: string | null
}
export interface RuleVersion {
id: string
scenario_key: string
version: number
content: Record<string, any> | null
status: string
canary_percent: number
created_by: string | null
remark: string
created_at: string | null
}
export interface AutoMetrics {
total_sessions: number
resolved_sessions: number
handoff_sessions: number
error_sessions: number
auto_executed_actions: number
approval_required_actions: number
by_scenario: Record<string, number>
}
export interface UpdateScenarioPayload {
name?: string
description?: string
enabled?: boolean
trigger_conditions?: Record<string, any>
actions?: any[]
approval_strategy?: Record<string, any>
}
export interface PublishVersionPayload {
content?: Record<string, any>
canary_percent?: number
remark?: string
}
/** 统一包装返回类型(与 admin 约定一致) */
type ApiResp<T> = { data: { code: number; data: T; message: string } }
// --------------------------------------------------------------------------
// 管理端接口(§15.3 端点)
// --------------------------------------------------------------------------
/** 场景配置列表(对应后端 /admin/scenarios */
export function getAutomationConfigs(): Promise<ApiResp<ScenarioConfig[]>> {
return apiClient.get('/itportal/automation/admin/scenarios')
}
/** 新建场景配置(对应后端 POST /admin/scenarios */
export function createAutomationConfig(payload: UpdateScenarioPayload): Promise<ApiResp<ScenarioConfig>> {
return apiClient.post('/itportal/automation/admin/scenarios', payload)
}
/** 更新场景配置(对应后端 PUT /admin/scenarios/{scenario_key} */
export function updateAutomationConfig(
scenario_key: string,
payload: UpdateScenarioPayload,
): Promise<ApiResp<ScenarioConfig>> {
return apiClient.put(`/itportal/automation/admin/scenarios/${scenario_key}`, payload)
}
/** 发布规则版本(含灰度比例,对应后端 POST /admin/scenarios/{scenario_key}/version */
export function publishConfigVersion(
scenario_key: string,
payload: PublishVersionPayload,
): Promise<ApiResp<RuleVersion>> {
return apiClient.post(`/itportal/automation/admin/scenarios/${scenario_key}/version`, payload)
}
/** 规则版本列表(对应后端 GET /admin/rule-versions */
export function getConfigVersions(scenario_key: string): Promise<ApiResp<RuleVersion[]>> {
return apiClient.get('/itportal/automation/admin/rule-versions', { params: { scenario_key } })
}
/** 自动化指标看板(对应后端 GET /admin/metrics */
export function getAutomationMetrics(): Promise<ApiResp<AutoMetrics>> {
return apiClient.get('/itportal/automation/admin/metrics')
}
+8 -24
View File
@@ -60,12 +60,9 @@ interface ApiResponse<T> {
}
// -----------------------------------------------------------------------------
// Axios 实例(继承全局 baseURL)
// 复用全局 apiClientCTRT-01 统一契约)
// -----------------------------------------------------------------------------
const http = axios.create({
baseURL: '/api',
timeout: 30000,
})
import apiClient from './index'
// -----------------------------------------------------------------------------
// 5 个端点
@@ -73,29 +70,20 @@ const http = axios.create({
/** GET /api/troubleshooting-templates — 获取模板列表 */
export async function listTemplates(): Promise<TroubleshootingTemplate[]> {
const res = await http.get<ApiResponse<TroubleshootingTemplate[]>>(
'/troubleshooting-templates'
)
return res.data.data || []
const res = await apiClient.get<TroubleshootingTemplate[]>('/troubleshooting-templates')
return res || []
}
/** GET /api/troubleshooting-templates/{id} — 获取模板详情 */
export async function getTemplate(id: string): Promise<TroubleshootingTemplate> {
const res = await http.get<ApiResponse<TroubleshootingTemplate>>(
`/troubleshooting-templates/${id}`
)
return res.data.data
return await apiClient.get<TroubleshootingTemplate>(`/troubleshooting-templates/${id}`)
}
/** POST /api/troubleshooting-templates — 新建模板 */
export async function createTemplate(
data: TroubleshootingTemplate
): Promise<TroubleshootingTemplate> {
const res = await http.post<ApiResponse<TroubleshootingTemplate>>(
'/troubleshooting-templates',
data
)
return res.data.data
return await apiClient.post<TroubleshootingTemplate>('/troubleshooting-templates', data)
}
/** PUT /api/troubleshooting-templates/{id} — 更新模板 */
@@ -103,16 +91,12 @@ export async function updateTemplate(
id: string,
data: TroubleshootingTemplate
): Promise<TroubleshootingTemplate> {
const res = await http.put<ApiResponse<TroubleshootingTemplate>>(
`/troubleshooting-templates/${id}`,
data
)
return res.data.data
return await apiClient.put<TroubleshootingTemplate>(`/troubleshooting-templates/${id}`, data)
}
/** DELETE /api/troubleshooting-templates/{id} — 删除模板 */
export async function deleteTemplate(id: string): Promise<void> {
await http.delete(`/troubleshooting-templates/${id}`)
await apiClient.delete(`/troubleshooting-templates/${id}`)
}
/** 工具:把对象格式化成 JSON 字符串(带缩进) */
+16 -1
View File
@@ -104,6 +104,21 @@
<span>知识库建议</span>
</el-menu-item>
<!-- 🤖 自动化闭环阶段5 -->
<div class="menu-section-title">🤖 自动化闭环</div>
<el-menu-item index="/automation-scenarios">
<el-icon><MagicStick /></el-icon>
<span>自动化场景</span>
</el-menu-item>
<el-menu-item index="/automation-versions">
<el-icon><Files /></el-icon>
<span>规则版本</span>
</el-menu-item>
<el-menu-item index="/automation-metrics">
<el-icon><TrendCharts /></el-icon>
<span>自动化指标</span>
</el-menu-item>
<!-- 🔒 开发中P2占位 -->
<div class="menu-section-title">🔒 开发中</div>
<el-menu-item index="/themes" class="locked-menu-item">
@@ -131,7 +146,7 @@
// ==========================================================================
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { Headset, PieChart, Switch, UserFilled, Connection, Warning, ChatLineSquare, Sort, Share, Monitor, Brush, DataAnalysis, Reading, Lock, Key, Document, TrendCharts, Notebook, Star } from '@element-plus/icons-vue'
import { Headset, PieChart, Switch, UserFilled, Connection, Warning, ChatLineSquare, Sort, Share, Monitor, Brush, DataAnalysis, Reading, Lock, Key, Document, TrendCharts, Notebook, Star, MagicStick, Files } from '@element-plus/icons-vue'
// ==========================================================================
// 当前激活菜单
+21
View File
@@ -160,6 +160,27 @@ const routes = [
component: () => import('@/views/KnowledgeSuggestions.vue'),
meta: { title: '知识库建议', requiresAuth: true },
},
{
// 阶段5 自动化闭环 — 场景配置
path: 'automation-scenarios',
name: 'AutomationScenarios',
component: () => import('@/views/automation/ScenarioConfig.vue'),
meta: { title: '自动化场景', requiresAuth: true },
},
{
// 阶段5 自动化闭环 — 规则版本/灰度
path: 'automation-versions',
name: 'AutomationVersions',
component: () => import('@/views/automation/RuleVersion.vue'),
meta: { title: '规则版本', requiresAuth: true },
},
{
// 阶段5 自动化闭环 — 指标看板
path: 'automation-metrics',
name: 'AutomationMetrics',
component: () => import('@/views/dashboard/AutoMetrics.vue'),
meta: { title: '自动化指标', requiresAuth: true },
},
],
},
{
+99
View File
@@ -0,0 +1,99 @@
// =============================================================================
// 企微IT智能服务台 — 阶段5 自动化闭环 状态管理(Pinia Store,管理后台)
// =============================================================================
// 说明:管理自动化场景配置、规则版本、运营指标。
// 约定:api 返回包装对象,调用方统一从 res.data.data 取业务数据。
// =============================================================================
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type {
ScenarioConfig,
RuleVersion,
AutoMetrics,
UpdateScenarioPayload,
PublishVersionPayload,
} from '@/api/automation'
import {
getAutomationConfigs,
updateAutomationConfig,
publishConfigVersion,
getConfigVersions,
getAutomationMetrics,
} from '@/api/automation'
export const useAutomationStore = defineStore('automation', () => {
// ------------------------------------------------------------------------
// 状态
// ------------------------------------------------------------------------
const scenarios = ref<ScenarioConfig[]>([])
const versions = ref<RuleVersion[]>([])
const metrics = ref<AutoMetrics | null>(null)
const loading = ref(false)
const currentScenarioKey = ref<string>('')
// ------------------------------------------------------------------------
// 配置(场景开关)
// ------------------------------------------------------------------------
async function loadScenarios(): Promise<void> {
loading.value = true
try {
const res = await getAutomationConfigs()
scenarios.value = res.data.data
} finally {
loading.value = false
}
}
async function updateScenario(
id: string,
payload: UpdateScenarioPayload,
): Promise<ScenarioConfig> {
const res = await updateAutomationConfig(id, payload)
const updated = res.data.data
// 本地同步,避免整表刷新
const idx = scenarios.value.findIndex((s) => s.id === id)
if (idx >= 0) scenarios.value[idx] = updated
return updated
}
// ------------------------------------------------------------------------
// 规则版本(灰度)
// ------------------------------------------------------------------------
async function loadVersions(scenarioKey: string): Promise<void> {
currentScenarioKey.value = scenarioKey
const res = await getConfigVersions(scenarioKey)
versions.value = res.data.data
}
async function publishVersion(
id: string,
payload: PublishVersionPayload,
): Promise<RuleVersion> {
const res = await publishConfigVersion(id, payload)
const created = res.data.data
versions.value.unshift(created)
return created
}
// ------------------------------------------------------------------------
// 指标
// ------------------------------------------------------------------------
async function loadMetrics(): Promise<void> {
const res = await getAutomationMetrics()
metrics.value = res.data.data
}
return {
scenarios,
versions,
metrics,
loading,
currentScenarioKey,
loadScenarios,
updateScenario,
loadVersions,
publishVersion,
loadMetrics,
}
})
@@ -0,0 +1,193 @@
<!--
=============================================================================
企微IT智能服务台 自动化规则版本管理 / 灰度管理后台
=============================================================================
说明查看某场景的规则版本列表版本号 / 状态 / 灰度比例并发布新版本
- 灰度发布时可设置 canary_percent灰度放量比例
- 风格Element Plus 组件 + Tailwind 布局
-->
<template>
<div class="rule-version">
<div class="page-title">规则版本管理 / 灰度</div>
<div class="page-desc">管理各场景自动化规则的版本与灰度放量新版本先小流量验证再全量</div>
<!-- 场景选择 -->
<div class="toolbar">
<el-select
v-model="selectedKey"
placeholder="选择场景"
filterable
style="width: 260px"
@change="onSelectScenario"
>
<el-option
v-for="sc in store.scenarios"
:key="sc.scenario_key"
:label="sc.name"
:value="sc.scenario_key"
/>
</el-select>
<el-button type="primary" :disabled="!selectedKey" @click="openPublish">
发布新版本
</el-button>
</div>
<div v-if="store.loading" class="loading-state">
<el-skeleton :rows="6" animated />
</div>
<el-table v-else :data="store.versions" class="ver-table" stripe>
<el-table-column prop="version" label="版本" width="90" />
<el-table-column label="状态" width="110">
<template #default="{ row }">
<el-tag :type="statusTag(row.status)" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="灰度比例" width="120">
<template #default="{ row }">{{ row.canary_percent }}%</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
<el-table-column prop="created_by" label="发布人" width="120" />
<el-table-column label="发布时间" width="180">
<template #default="{ row }">{{ formatTime(row.created_at) }}</template>
</el-table-column>
</el-table>
<!-- 发布新版本弹窗 -->
<el-dialog v-model="publishVisible" title="发布新版本(灰度)" width="520px" destroy-on-close>
<el-form label-position="top">
<el-form-item label="灰度放量比例:{{ canaryPercent }}%">
<el-slider v-model="canaryPercent" :min="0" :max="100" :step="5" show-input />
</el-form-item>
<el-form-item label="规则内容(JSON">
<el-input v-model="contentText" type="textarea" :rows="6" placeholder="可选,留空沿用上一版本" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="remark" placeholder="如:修复 VPN 重置流程" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="publishVisible = false">取消</el-button>
<el-button type="primary" :loading="publishing" @click="onPublish">发布</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAutomationStore } from '@/stores/automation'
import type { ScenarioConfig } from '@/api/automation'
const store = useAutomationStore()
const route = useRoute()
const selectedKey = ref<string>('')
const publishVisible = ref(false)
const publishing = ref(false)
const canaryPercent = ref(10)
const contentText = ref('')
const remark = ref('')
onMounted(async () => {
// 先加载场景列表,再决定默认选中项(支持 ?scenario=KEY
await store.loadScenarios()
const fromQuery = (route.query.scenario as string) || ''
const firstKey = store.scenarios[0]?.scenario_key || ''
selectedKey.value = fromQuery && store.scenarios.some((s) => s.scenario_key === fromQuery)
? fromQuery
: firstKey
if (selectedKey.value) await store.loadVersions(selectedKey.value)
})
async function onSelectScenario(key: string): Promise<void> {
if (key) await store.loadVersions(key)
}
function statusTag(status: string): 'success' | 'warning' | 'info' | 'danger' {
switch (status) {
case 'active':
case 'full':
return 'success'
case 'canary':
return 'warning'
case 'rollback':
return 'danger'
default:
return 'info'
}
}
function openPublish(): void {
if (!selectedKey.value) return
canaryPercent.value = 10
contentText.value = ''
remark.value = ''
publishVisible.value = true
}
async function onPublish(): Promise<void> {
if (!selectedKey.value) return
const sc = store.scenarios.find((s: ScenarioConfig) => s.scenario_key === selectedKey.value)
if (!sc) return
let content: Record<string, any> | undefined
if (contentText.value.trim()) {
try {
content = JSON.parse(contentText.value)
} catch {
ElMessage.error('规则内容 JSON 格式有误')
return
}
}
publishing.value = true
try {
await store.publishVersion(sc.id, {
content,
canary_percent: canaryPercent.value,
remark: remark.value,
})
ElMessage.success('新版本已发布')
publishVisible.value = false
} catch {
ElMessage.error('发布失败')
} finally {
publishing.value = false
}
}
function formatTime(t: string | null): string {
if (!t) return '-'
try {
return new Date(t).toLocaleString('zh-CN', { hour12: false })
} catch {
return t
}
}
</script>
<style scoped>
.rule-version {
padding: 4px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.page-desc {
color: var(--text-secondary);
font-size: 13px;
margin: 6px 0 16px;
}
.toolbar {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.ver-table {
margin-top: 8px;
}
</style>
@@ -0,0 +1,239 @@
<!--
=============================================================================
企微IT智能服务台 自动化场景配置页管理后台
=============================================================================
说明按场景展示自动化开关触发条件动作与审批策略
- 开关el-switch 即时切换 enabled
- 编辑弹窗编辑触发条件 / 动作 / 审批策略JSON
- 风格Element Plus 组件 + Tailwind 布局 Configs.vue 一致
-->
<template>
<div class="automation-config">
<div class="page-title">自动化场景配置</div>
<div class="page-desc">
按场景控制自动化处置的开关与策略运行时切换即时生效高危动作需员工/H5 二次确认
</div>
<div v-if="store.loading" class="loading-state">
<el-skeleton :rows="8" animated />
</div>
<div v-else class="feature-grid">
<el-card
v-for="sc in store.scenarios"
:key="sc.id"
class="scenario-card"
shadow="hover"
>
<div class="sc-header">
<div>
<div class="sc-name">{{ sc.name }}</div>
<div class="sc-key">{{ sc.scenario_key }}</div>
</div>
<el-switch
:model-value="sc.enabled"
@change="(v: boolean) => onToggle(sc, v)"
/>
</div>
<div class="sc-desc">{{ sc.description || '暂无描述' }}</div>
<el-descriptions :column="1" size="small" class="sc-meta" border>
<el-descriptions-item label="触发条件">
{{ sc.trigger_conditions ? '已配置' : '默认' }}
</el-descriptions-item>
<el-descriptions-item label="动作数">
{{ Array.isArray(sc.actions) ? sc.actions.length : 0 }}
</el-descriptions-item>
<el-descriptions-item label="审批策略">
{{ sc.approval_strategy ? '已配置' : '默认' }}
</el-descriptions-item>
</el-descriptions>
<div class="sc-actions">
<el-button size="small" @click="openEdit(sc)">编辑策略</el-button>
<el-button size="small" type="primary" @click="goVersions(sc)">版本管理</el-button>
</div>
</el-card>
</div>
<!-- 编辑弹窗 -->
<el-dialog
v-model="editVisible"
:title="'编辑场景 — ' + (editing?.name || '')"
width="640px"
destroy-on-close
>
<el-form v-if="editing" label-position="top">
<el-form-item label="场景名称">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="form.enabled" />
</el-form-item>
<el-form-item label="触发条件(JSON">
<el-input v-model="form.trigger_conditions" type="textarea" :rows="4" />
</el-form-item>
<el-form-item label="动作(JSON 数组)">
<el-input v-model="form.actions" type="textarea" :rows="4" />
</el-form-item>
<el-form-item label="审批策略(JSON">
<el-input v-model="form.approval_strategy" type="textarea" :rows="4" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="onSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAutomationStore } from '@/stores/automation'
import type { ScenarioConfig } from '@/api/automation'
const store = useAutomationStore()
const router = useRouter()
const editVisible = ref(false)
const saving = ref(false)
const editing = ref<ScenarioConfig | null>(null)
const form = reactive({
name: '',
description: '',
enabled: true,
trigger_conditions: '',
actions: '',
approval_strategy: '',
})
onMounted(() => {
store.loadScenarios()
})
// --------------------------------------------------------------------------
// 开关切换:只提交 enabled 字段
// --------------------------------------------------------------------------
async function onToggle(sc: ScenarioConfig, val: boolean): Promise<void> {
try {
await store.updateScenario(sc.id, { enabled: val })
ElMessage.success(val ? '已启用' : '已停用')
} catch {
ElMessage.error('切换失败')
}
}
// --------------------------------------------------------------------------
// 打开编辑:把对象序列化为 JSON 文本填入表单
// --------------------------------------------------------------------------
function openEdit(sc: ScenarioConfig): void {
editing.value = sc
form.name = sc.name
form.description = sc.description
form.enabled = sc.enabled
form.trigger_conditions = sc.trigger_conditions ? JSON.stringify(sc.trigger_conditions, null, 2) : ''
form.actions = sc.actions ? JSON.stringify(sc.actions, null, 2) : '[]'
form.approval_strategy = sc.approval_strategy ? JSON.stringify(sc.approval_strategy, null, 2) : ''
editVisible.value = true
}
// --------------------------------------------------------------------------
// 保存:将 JSON 文本解析回对象,提交更新
// --------------------------------------------------------------------------
async function onSave(): Promise<void> {
if (!editing.value) return
let trigger_conditions: Record<string, any> | undefined
let actions: any[] | undefined
let approval_strategy: Record<string, any> | undefined
try {
trigger_conditions = form.trigger_conditions ? JSON.parse(form.trigger_conditions) : undefined
actions = form.actions ? JSON.parse(form.actions) : undefined
approval_strategy = form.approval_strategy ? JSON.parse(form.approval_strategy) : undefined
} catch {
ElMessage.error('JSON 格式有误,请检查后重试')
return
}
saving.value = true
try {
await store.updateScenario(editing.value.id, {
name: form.name,
description: form.description,
enabled: form.enabled,
trigger_conditions,
actions,
approval_strategy,
})
ElMessage.success('配置已保存')
editVisible.value = false
} catch {
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
// 跳转到该场景的版本管理页
function goVersions(sc: ScenarioConfig): void {
router.push({ name: 'AutomationVersions', query: { scenario: sc.scenario_key } })
}
</script>
<style scoped>
.automation-config {
padding: 4px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.page-desc {
color: var(--text-secondary);
font-size: 13px;
margin: 6px 0 16px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.scenario-card {
display: flex;
flex-direction: column;
}
.sc-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.sc-name {
font-weight: 600;
font-size: 15px;
}
.sc-key {
font-size: 12px;
color: var(--text-muted);
margin-top: 2px;
}
.sc-desc {
font-size: 13px;
color: var(--text-secondary);
margin: 10px 0;
min-height: 36px;
}
.sc-meta {
margin-bottom: 10px;
}
.sc-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
</style>
@@ -0,0 +1,96 @@
<!--
=============================================================================
企微IT智能服务台 自动化指标看板管理后台扩展阶段4
=============================================================================
说明展示阶段5 自动化闭环的运营指标会话量解决率转人工异常
自动执行动作数需审批动作数以及按场景分布
- 复用现有 StatCard 组件深色统计卡片
- 风格Element Plus + Tailwind Dashboard.vue 一致
-->
<template>
<div class="auto-metrics">
<div class="page-title">自动化运营指标</div>
<div class="page-desc">阶段5 自动化闭环核心指标看板数据实时反映自动化处置成效</div>
<div v-if="!store.metrics" class="loading-state">
<el-skeleton :rows="6" animated />
</div>
<template v-else>
<!-- 核心指标卡片 -->
<div class="stat-grid">
<StatCard
label="自动化会话总数"
:value="m.total_sessions"
:subtitle="resolveRate"
value-color="var(--accent)"
/>
<StatCard label="已解决" :value="m.resolved_sessions" value-color="var(--success)" />
<StatCard label="转人工" :value="m.handoff_sessions" value-color="var(--warning)" />
<StatCard label="异常会话" :value="m.error_sessions" value-color="var(--danger)" />
<StatCard label="自动执行动作" :value="m.auto_executed_actions" />
<StatCard label="需审批动作" :value="m.approval_required_actions" value-color="var(--warning)" />
</div>
<!-- 按场景分布 -->
<el-card class="breakdown" shadow="never">
<template #header>按场景分布会话数</template>
<el-table :data="scenarioRows" stripe>
<el-table-column prop="scenario" label="场景" />
<el-table-column prop="count" label="会话数" width="140" />
</el-table>
</el-card>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import StatCard from '@/components/StatCard.vue'
import { useAutomationStore } from '@/stores/automation'
const store = useAutomationStore()
const m = computed(() => store.metrics as NonNullable<typeof store.metrics>)
// 解决率(核心健康度指标)
const resolveRate = computed(() => {
if (!m.value || m.value.total_sessions === 0) return '解决率 -'
const rate = Math.round((m.value.resolved_sessions / m.value.total_sessions) * 100)
return `解决率 ${rate}%`
})
// 按场景分布行
const scenarioRows = computed(() => {
const by = m.value?.by_scenario || {}
return Object.entries(by).map(([scenario, count]) => ({ scenario, count }))
})
onMounted(() => {
store.loadMetrics()
})
</script>
<style scoped>
.auto-metrics {
padding: 4px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.page-desc {
color: var(--text-secondary);
font-size: 13px;
margin: 6px 0 16px;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.breakdown {
margin-top: 8px;
}
</style>