Procházet zdrojové kódy

feat: 添加统计柱形图

wuwenyi před 2 týdny
rodič
revize
e590287a1b
2 změnil soubory, kde provedl 73 přidání a 6 odebrání
  1. 4 0
      src/api/analysis.ts
  2. 69 6
      src/views/analysis/index.vue

+ 4 - 0
src/api/analysis.ts

@@ -60,3 +60,7 @@ export function getStuckInstances(
 ): Promise<PageResult<StuckInstance>> {
   return request.get('/analysis/stuck-instances', { params, ...config })
 }
+
+export function getNodeCount(processDefinitionId: number): Promise<{ nodeName: string; count: number }[]> {
+  return request.get('/analysis/node-count', { params: { processDefinitionId } })
+}

+ 69 - 6
src/views/analysis/index.vue

@@ -136,12 +136,29 @@
       </el-col>
     </el-row>
 
+    <el-row :gutter="16" style="margin-bottom: 16px;">
+      <el-col :span="12">
+        <el-card shadow="hover" v-loading="loadingNodeCount">
+          <template #header>
+            <div class="detail-header">
+              <span>各节点统计</span>
+              <el-select v-model="nodeCountDefId" placeholder="请选择流程" style="width: 260px;" @change="loadNodeCount">
+                <el-option v-for="def in definitionList" :key="def.id" :label="def.name" :value="def.id" />
+              </el-select>
+            </div>
+          </template>
+          <v-chart v-if="nodeCountData.length > 0" :option="nodeCountOption" autoresize style="height: 60px;" :style="{ height: Math.max(nodeCountData.length * 36, 200) + 'px' }" />
+          <el-empty v-else description="请选择流程查看各节点统计" />
+        </el-card>
+      </el-col>
+    </el-row>
+
     <el-card shadow="hover" class="detail-card" v-loading="loadingInstanceList">
       <template #header>
         <div class="detail-header">
           <span>流程明细</span>
           <div class="detail-actions">
-            <el-select v-model="instanceQuery.processName" placeholder="流程名称" clearable style="width: 160px;" @change="loadInstanceList">
+            <el-select v-model="instanceQuery.processName" placeholder="流程名称" clearable style="width: 160px;" @change="onInstanceProcessChange">
               <el-option v-for="def in definitionList" :key="def.id" :label="def.name" :value="def.name" />
             </el-select>
             <el-input v-model="instanceQuery.applicantName" placeholder="归属用户" clearable style="width: 140px; margin-left: 8px;" @change="loadInstanceList" />
@@ -211,7 +228,8 @@ import {
   getStuckInstances,
   getOverview,
   getStatusDistribution,
-  getTrend
+  getTrend,
+  getNodeCount
 } from '@/api/analysis'
 import { listEnabled } from '@/api/flow/definition'
 import { listInstance } from '@/api/flow/instance'
@@ -636,14 +654,25 @@ const loadingInstanceList = ref(false)
 const definitionList = ref<FlowDefinition[]>([])
 const currentNodeOptions = ref<string[]>([])
 
-async function loadDefinitionsFull() {
-  try { definitionList.value = await listEnabled() } catch { /* ignore */ }
-}
-
 const instanceDetailVisible = ref(false)
 const instanceDetailId = ref(0)
 function openInstanceDetail(id: number) { instanceDetailId.value = id; instanceDetailVisible.value = true }
 
+function onInstanceProcessChange() {
+  instanceQuery.currentNodeName = ''
+  const def = definitionList.value.find(d => d.name === instanceQuery.processName)
+  if (def?.flowJson) {
+    try {
+      currentNodeOptions.value = JSON.parse(def.flowJson).nodes
+        .filter((n: any) => n.type !== 'start' && n.type !== 'end' && n.type !== 'condition' && n.type !== 'cc')
+        .map((n: any) => n.name)
+    } catch { currentNodeOptions.value = [] }
+  } else {
+    currentNodeOptions.value = []
+  }
+  loadInstanceList()
+}
+
 async function loadInstanceList() {
   loadingInstanceList.value = true
   try {
@@ -658,6 +687,40 @@ async function loadInstanceList() {
   } finally { loadingInstanceList.value = false }
 }
 
+// 各节点统计图
+const nodeCountDefId = ref<number | null>(null)
+const nodeCountData = ref<{ nodeName: string; count: number }[]>([])
+const loadingNodeCount = ref(false)
+const nodeCountOption = computed(() => ({
+  tooltip: { trigger: 'axis' as const, axisPointer: { type: 'shadow' as const } },
+  grid: { left: 120, right: 40, top: 10, bottom: 10 },
+  xAxis: { type: 'value' as const, minInterval: 1 },
+  yAxis: { type: 'category' as const, data: nodeCountData.value.map(d => d.nodeName), inverse: true },
+  series: [{
+    type: 'bar',
+    data: nodeCountData.value.map((d, i) => ({ value: d.count, itemStyle: { color: colorPalette[i % colorPalette.length] } })),
+    barMaxWidth: 30,
+    label: { show: true, position: 'right' as const }
+  }]
+}))
+
+async function loadNodeCount() {
+  if (!nodeCountDefId.value) return
+  loadingNodeCount.value = true
+  try { nodeCountData.value = await getNodeCount(nodeCountDefId.value) } catch { nodeCountData.value = [] }
+  finally { loadingNodeCount.value = false }
+}
+
+async function loadDefinitionsFull() {
+  try {
+    definitionList.value = await listEnabled()
+    if (definitionList.value.length > 0 && !nodeCountDefId.value) {
+      nodeCountDefId.value = definitionList.value[definitionList.value.length - 1].id
+      loadNodeCount()
+    }
+  } catch { /* ignore */ }
+}
+
 onMounted(() => {
   loadDefinitions()
   loadAll()