|
|
@@ -13,12 +13,15 @@
|
|
|
<S8MonitoringResizableShell
|
|
|
:shell="draftLayout.shell"
|
|
|
:editable="editMode"
|
|
|
+ :hide-sidebar="true"
|
|
|
+ :hide-analysis="true"
|
|
|
@update:shell="onShellUpdate"
|
|
|
>
|
|
|
<template #modules>
|
|
|
<section class="s8-monitor__modules glass-panel">
|
|
|
<S8MonitoringModulesGrid
|
|
|
:cards="allStageCards"
|
|
|
+ :summary-cards="allSummaryCards"
|
|
|
:layout="draftLayout.modules"
|
|
|
:editable="editMode"
|
|
|
:row-height="60"
|
|
|
@@ -29,94 +32,6 @@
|
|
|
/>
|
|
|
</section>
|
|
|
</template>
|
|
|
-
|
|
|
- <template #sidebar>
|
|
|
- <aside class="s8-monitor__sidebar glass-panel">
|
|
|
- <template v-if="pageConfig.hasDeptCluster.value">
|
|
|
- <div class="section-title">
|
|
|
- <div class="section-title__bar" />
|
|
|
- <h2 class="section-title__text">S9 部门效率分析</h2>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="s8-monitor__dept-list">
|
|
|
- <article
|
|
|
- v-for="dept in configuredDeptCards"
|
|
|
- :key="dept.deptName"
|
|
|
- class="dept-card"
|
|
|
- :class="[`dept-card--${dept.tone}`, { 'dept-card--dragging': deptDraggingKey === dept.deptName, 'dept-card--dragover': deptDragOverKey === dept.deptName }]"
|
|
|
- draggable="true"
|
|
|
- @dragstart="onDeptDragStart(dept.deptName)"
|
|
|
- @dragover.prevent="onDeptDragOver(dept.deptName)"
|
|
|
- @drop.prevent="onDeptDrop(dept.deptName)"
|
|
|
- @dragend="onDeptDragEnd"
|
|
|
- >
|
|
|
- <div class="dept-card__head">
|
|
|
- <span class="dept-card__name">{{ dept.deptName }}</span>
|
|
|
- <span class="dept-card__badge" :class="`dept-card__badge--${dept.tone}`">
|
|
|
- {{ dept.levelLabel }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="dept-card__metrics">
|
|
|
- <div class="dept-card__metric">
|
|
|
- <div class="dept-card__metric-label">异常数</div>
|
|
|
- <div class="dept-card__metric-value">{{ dept.totalText }}</div>
|
|
|
- </div>
|
|
|
- <div class="dept-card__metric">
|
|
|
- <div class="dept-card__metric-label">均时</div>
|
|
|
- <div class="dept-card__metric-value">{{ dept.avgHoursText }}</div>
|
|
|
- </div>
|
|
|
- <div class="dept-card__metric">
|
|
|
- <div class="dept-card__metric-label">关闭率</div>
|
|
|
- <div class="dept-card__metric-value dept-card__metric-value--accent">{{ dept.closeRateText }}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </article>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
-
|
|
|
- <div class="s8-monitor__efficiency">
|
|
|
- <div class="s8-monitor__efficiency-head">
|
|
|
- <span>整体响应效能</span>
|
|
|
- <span :class="`s8-monitor__efficiency-label s8-monitor__efficiency-label--${overallEfficiency.tone}`">
|
|
|
- {{ overallEfficiency.label }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div class="s8-monitor__efficiency-track">
|
|
|
- <div class="s8-monitor__efficiency-fill" :style="{ width: `${overallEfficiency.score}%` }" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </aside>
|
|
|
- </template>
|
|
|
-
|
|
|
- <template #analysis>
|
|
|
- <section class="s8-monitor__analysis glass-panel">
|
|
|
- <div class="s8-monitor__analysis-head">
|
|
|
- <div class="section-title">
|
|
|
- <div class="section-title__bar section-title__bar--dim" />
|
|
|
- <h2 class="section-title__text">S8 功能异常多维分析</h2>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="s8-monitor__tabs" aria-hidden="true">
|
|
|
- <!-- S8-MONITORING-DEMO-SEMANTICS-CLOSURE-1:原「最近24小时 / 实时快照」装饰文案与实际全量口径不符,改为「累计统计」。 -->
|
|
|
- <span class="s8-monitor__tab s8-monitor__tab--active">累计统计</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="s8-monitor__analysis-grid">
|
|
|
- <S8MonitoringCategoryGrid
|
|
|
- :cards="configuredCategoryCards"
|
|
|
- :layout="draftLayout.analysis"
|
|
|
- :editable="editMode"
|
|
|
- :row-height="56"
|
|
|
- :gap="12"
|
|
|
- @update:layout="onAnalysisLayoutUpdate"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- S8-MONITORING-TOP-STATUS-STRIP-REMOVE-1:移除底部全局状态条(与模块卡内三色重复且口径不清晰)。 -->
|
|
|
- </section>
|
|
|
- </template>
|
|
|
</S8MonitoringResizableShell>
|
|
|
|
|
|
<S8MonitoringStageConfigDrawer
|
|
|
@@ -151,7 +66,6 @@ import {
|
|
|
} from '@element-plus/icons-vue';
|
|
|
import {
|
|
|
s8MonitoringApi,
|
|
|
- type S8CategorySummary,
|
|
|
type S8DeptSummary,
|
|
|
type S8ModuleOrderSummary,
|
|
|
type S8ModuleSummaryItem,
|
|
|
@@ -162,11 +76,10 @@ import { deepClone, useS8Layout } from './useS8Layout';
|
|
|
import type { S8LayoutSchema } from './useS8Layout';
|
|
|
import { DEMO_LAYOUT } from './useS8Layout';
|
|
|
import S8MonitoringResizableShell from './components/S8MonitoringResizableShell.vue';
|
|
|
-import S8MonitoringModulesGrid from './components/S8MonitoringModulesGrid.vue';
|
|
|
-import S8MonitoringCategoryGrid, { type CategoryGridCardData } from './components/S8MonitoringCategoryGrid.vue';
|
|
|
+import S8MonitoringModulesGrid, { type SummaryCardData } from './components/S8MonitoringModulesGrid.vue';
|
|
|
import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
|
|
|
import S8MonitoringStageConfigDrawer from './components/S8MonitoringStageConfigDrawer.vue';
|
|
|
-import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT } from './useS8StageConfig';
|
|
|
+import { useS8StageConfig, buildModuleTriColorStats, DEFAULT_NORMAL_WORK_ORDER_COUNT, ORDER_STATUS_MOCK } from './useS8StageConfig';
|
|
|
import { useS8CategoryConfig } from './useS8CategoryConfig';
|
|
|
import { useS9DeptConfig } from './useS9DeptConfig';
|
|
|
import { useS8UnsavedLayoutEditGuard } from './useS8UnsavedLayoutEditGuard';
|
|
|
@@ -249,7 +162,7 @@ async function loadPageConfig() {
|
|
|
const { layout: persistedLayout, save, resetToDefault, restoreDemo } = useS8Layout();
|
|
|
const { stageConfigState, initializeFromCards, applyConfig, reset: resetStageConfigState } = useS8StageConfig();
|
|
|
const { categoryConfigState, initializeFromCategories, applyConfig: applyCategoryConfig, reset: resetCategoryConfigState } = useS8CategoryConfig();
|
|
|
-const { deptConfigState, initializeFromDepts, applyConfig: applyDeptConfig, reorder: reorderDept, reset: resetDeptConfigState } = useS9DeptConfig();
|
|
|
+const { deptConfigState, initializeFromDepts, applyConfig: applyDeptConfig, reset: resetDeptConfigState } = useS9DeptConfig();
|
|
|
|
|
|
const editMode = shallowRef(false);
|
|
|
const configDrawerVisible = shallowRef(false);
|
|
|
@@ -354,9 +267,6 @@ function onModulesLayoutUpdate(modules: S8LayoutSchema['modules']) {
|
|
|
draftLayout.modules.splice(0, draftLayout.modules.length, ...modules);
|
|
|
}
|
|
|
|
|
|
-function onAnalysisLayoutUpdate(analysis: S8LayoutSchema['analysis']) {
|
|
|
- draftLayout.analysis.splice(0, draftLayout.analysis.length, ...analysis);
|
|
|
-}
|
|
|
|
|
|
const layoutVars = computed<CSSProperties>(() => ({
|
|
|
'--grid-gap': '24px',
|
|
|
@@ -394,6 +304,66 @@ const stageCards = computed(() => {
|
|
|
|
|
|
const allStageCards = computed(() => stageCards.value.map((card) => applyConfig(card)));
|
|
|
|
|
|
+// ─── S8/S9 同级摘要卡 ───────────────────────────────────────────────────────
|
|
|
+
|
|
|
+const s8SummaryCard = computed<SummaryCardData>(() => {
|
|
|
+ const cats = gridData.byCategory;
|
|
|
+ const totalAnomalies = cats.reduce((sum, c) => sum + Math.max(c.total, 0), 0);
|
|
|
+ const topCat = cats.length ? [...cats].sort((a, b) => b.total - a.total)[0] : null;
|
|
|
+ const avgCloseRate = cats.length
|
|
|
+ ? cats.reduce((sum, c) => sum + clampPercent(c.closeRate), 0) / cats.length
|
|
|
+ : 0;
|
|
|
+ const avgHours = cats.length
|
|
|
+ ? cats.reduce((sum, c) => sum + Math.max(c.avgProcessHours ?? 0, 0), 0) / cats.length
|
|
|
+ : 0;
|
|
|
+ const hasData = totalAnomalies > 0 || cats.length > 0;
|
|
|
+ const tone: SummaryCardData['tone'] = avgCloseRate >= 90 ? 'good' : avgCloseRate >= 70 ? 'warn' : 'danger';
|
|
|
+ return {
|
|
|
+ code: 'S8',
|
|
|
+ title: '功能异常多维分析',
|
|
|
+ subtitle: '按异常类型汇总',
|
|
|
+ icon: DataAnalysis,
|
|
|
+ tone: hasData ? tone : 'warn',
|
|
|
+ mainLabel: '累计异常数',
|
|
|
+ mainValue: formatInteger(totalAnomalies),
|
|
|
+ metrics: [
|
|
|
+ { label: '异常类型数', value: String(cats.length || '--') },
|
|
|
+ { label: '平均关闭率', value: hasData ? formatPercent(avgCloseRate) : '--', accent: true },
|
|
|
+ { label: '平均处理时长', value: hasData ? formatHours(avgHours) : '--' },
|
|
|
+ { label: '最高风险类别', value: topCat?.category ?? '--' },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const s9SummaryCard = computed<SummaryCardData>(() => {
|
|
|
+ const depts = deptCards.value;
|
|
|
+ const pressedCount = depts.filter((d) => d.tone === 'danger').length;
|
|
|
+ const topDept = depts.length ? depts[0] : null;
|
|
|
+ const avgCloseRate = depts.length
|
|
|
+ ? depts.reduce((sum, d) => sum + clampPercent(parseFloat(d.closeRateText) || 0), 0) / depts.length
|
|
|
+ : 0;
|
|
|
+ const eff = overallEfficiency.value;
|
|
|
+ const tone: SummaryCardData['tone'] = eff.tone === 'good' ? 'good' : eff.tone === 'warn' ? 'warn' : 'danger';
|
|
|
+ return {
|
|
|
+ code: 'S9',
|
|
|
+ title: '部门效率分析',
|
|
|
+ subtitle: '按部门响应效率汇总',
|
|
|
+ icon: TrendCharts,
|
|
|
+ tone,
|
|
|
+ mainLabel: '整体响应效能',
|
|
|
+ mainValue: String(eff.score),
|
|
|
+ mainSuffix: '%',
|
|
|
+ metrics: [
|
|
|
+ { label: '部门数', value: String(depts.length || '--') },
|
|
|
+ { label: '承压部门数', value: String(pressedCount), accent: pressedCount > 0 },
|
|
|
+ { label: '平均关闭率', value: depts.length ? formatPercent(avgCloseRate) : '--' },
|
|
|
+ { label: '异常最多部门', value: topDept?.deptName ?? '--' },
|
|
|
+ ],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const allSummaryCards = computed<SummaryCardData[]>(() => [s8SummaryCard.value, s9SummaryCard.value]);
|
|
|
+
|
|
|
const deptCards = computed<DeptDisplay[]>(() => {
|
|
|
const source = gridData.byDept.length
|
|
|
? [...gridData.byDept].sort((a, b) => b.total - a.total)
|
|
|
@@ -447,7 +417,7 @@ const overallEfficiency = computed(() => {
|
|
|
return { score, label: '承压', tone: 'danger' as const };
|
|
|
});
|
|
|
|
|
|
-const categoryCards = computed<CategoryGridCardData[]>(() => {
|
|
|
+const categoryCards = computed(() => {
|
|
|
const sourceMap = new Map(gridData.byCategory.map((item) => [item.category, item]));
|
|
|
|
|
|
return effectiveCategoryDefs.value.map((category, index) => {
|
|
|
@@ -523,6 +493,8 @@ function buildStageCard(
|
|
|
wideRate: closeRate,
|
|
|
wideRateLabel: `闭环率 ${closeRateText} / 均时 ${avgHoursText}`,
|
|
|
avgProcessHours,
|
|
|
+ // TASK-006 [MOCK]:S1-S7 全部卡片注入订单状态辅助行
|
|
|
+ orderStatus: ORDER_STATUS_MOCK,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -574,7 +546,7 @@ function resolveDeptLevelLabel(item: S8DeptSummary, tone: 'good' | 'warn' | 'dan
|
|
|
return '滞后';
|
|
|
}
|
|
|
|
|
|
-function resolveCategoryTone(item: S8CategorySummary): 'good' | 'warn' | 'danger' {
|
|
|
+function resolveCategoryTone(item: { closeRate: number }): 'good' | 'warn' | 'danger' {
|
|
|
if (item.closeRate >= 95) return 'good';
|
|
|
if (item.closeRate >= 80) return 'warn';
|
|
|
return 'danger';
|
|
|
@@ -644,32 +616,6 @@ function resetDeptConfig() {
|
|
|
resetDeptConfigState(deptCards.value);
|
|
|
}
|
|
|
|
|
|
-// ── S9 拖拽排序 ──────────────────────────────────────────────────────────────
|
|
|
-const deptDraggingKey = shallowRef('');
|
|
|
-const deptDragOverKey = shallowRef('');
|
|
|
-
|
|
|
-function onDeptDragStart(deptName: string) {
|
|
|
- deptDraggingKey.value = deptName;
|
|
|
-}
|
|
|
-
|
|
|
-function onDeptDragOver(deptName: string) {
|
|
|
- if (deptDraggingKey.value && deptDraggingKey.value !== deptName) {
|
|
|
- deptDragOverKey.value = deptName;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function onDeptDrop(targetName: string) {
|
|
|
- if (deptDraggingKey.value && deptDraggingKey.value !== targetName) {
|
|
|
- reorderDept(deptDraggingKey.value, targetName);
|
|
|
- }
|
|
|
- deptDraggingKey.value = '';
|
|
|
- deptDragOverKey.value = '';
|
|
|
-}
|
|
|
-
|
|
|
-function onDeptDragEnd() {
|
|
|
- deptDraggingKey.value = '';
|
|
|
- deptDragOverKey.value = '';
|
|
|
-}
|
|
|
|
|
|
function onBlockOrderUpdate(code: string, order: string[]) {
|
|
|
if (stageConfigState.items[code]) {
|