feat(ctrt): 完成 CTRT-01~03 响应契约统一 - 三端拦截器+portal_token清理+调用点适配
This commit is contained in:
@@ -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"]
|
||||
@@ -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')
|
||||
}
|
||||
@@ -60,12 +60,9 @@ interface ApiResponse<T> {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Axios 实例(继承全局 baseURL)
|
||||
// 复用全局 apiClient(CTRT-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 字符串(带缩进) */
|
||||
|
||||
@@ -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'
|
||||
|
||||
// ==========================================================================
|
||||
// 当前激活菜单
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user