|
|
@@ -1,16 +1,28 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { computed } from 'vue';
|
|
|
-import type { S8QdcSummaryItem } from '../../api/s8MonitoringApi';
|
|
|
+import { computed, ref } from 'vue';
|
|
|
+import type { S8QdcSummaryItem, S8ResultKpiItem } from '../../api/s8MonitoringApi';
|
|
|
|
|
|
const props = defineProps<{
|
|
|
items: S8QdcSummaryItem[];
|
|
|
+ resultKpiItems?: S8ResultKpiItem[];
|
|
|
+ resultKpiSource?: string;
|
|
|
+ resultKpiLoading?: boolean;
|
|
|
+ resultKpiError?: string | null;
|
|
|
minHeight?: number;
|
|
|
}>();
|
|
|
|
|
|
+type ViewKey = 'resultKpi' | 'exception';
|
|
|
+const activeView = ref<ViewKey>('resultKpi');
|
|
|
+
|
|
|
const cardStyle = computed(() => ({
|
|
|
minHeight: `${Math.max(120, props.minHeight ?? 160)}px`,
|
|
|
}));
|
|
|
|
|
|
+const isMock = computed(() => props.resultKpiSource === 'DICTIONARY_MOCK');
|
|
|
+const resultKpiList = computed(() => props.resultKpiItems ?? []);
|
|
|
+const hasResultKpi = computed(() => resultKpiList.value.length > 0);
|
|
|
+const hasException = computed(() => props.items.length > 0);
|
|
|
+
|
|
|
function fmtHours(val: number | null | undefined): string {
|
|
|
if (val === null || val === undefined) return '--';
|
|
|
return `${val.toFixed(1)}h`;
|
|
|
@@ -20,133 +32,259 @@ function fmtRate(val: number | null | undefined): string {
|
|
|
if (val === null || val === undefined) return '--';
|
|
|
return `${val.toFixed(0)}%`;
|
|
|
}
|
|
|
+
|
|
|
+function fmtCurrent(item: S8ResultKpiItem): string {
|
|
|
+ if (item.currentValue === null || item.currentValue === undefined) return '待接入';
|
|
|
+ const unit = item.unit || '%';
|
|
|
+ return `${item.currentValue.toFixed(1)}${unit}`;
|
|
|
+}
|
|
|
+
|
|
|
+function fmtTarget(item: S8ResultKpiItem): string {
|
|
|
+ if (item.targetRatio === null || item.targetRatio === undefined) return '目标 待配置';
|
|
|
+ const unit = item.unit || '%';
|
|
|
+ return `目标 ${item.targetRatio.toFixed(1)}${unit}`;
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <article class="s9-qdc-card" :style="cardStyle">
|
|
|
- <div class="s9-qdc-card__header">
|
|
|
- <span class="s9-qdc-card__code">S9</span>
|
|
|
- <span class="s9-qdc-card__title">运营指标</span>
|
|
|
- </div>
|
|
|
- <div class="s9-qdc-card__tracks">
|
|
|
- <div v-for="item in items" :key="item.code" class="s9-qdc-track">
|
|
|
- <div class="s9-qdc-track__name">{{ item.title }}</div>
|
|
|
- <div class="s9-qdc-track__metrics">
|
|
|
- <div class="s9-qdc-track__metric">
|
|
|
- <span class="s9-qdc-track__metric-val">{{ item.total }}</span>
|
|
|
- <span class="s9-qdc-track__metric-lbl">异常总数</span>
|
|
|
- </div>
|
|
|
- <div class="s9-qdc-track__metric">
|
|
|
- <span class="s9-qdc-track__metric-val">{{ fmtHours(item.avgProcessHours) }}</span>
|
|
|
- <span class="s9-qdc-track__metric-lbl">平均处理</span>
|
|
|
- </div>
|
|
|
- <div class="s9-qdc-track__metric">
|
|
|
- <span class="s9-qdc-track__metric-val">{{ fmtRate(item.onTimeCloseRate) }}</span>
|
|
|
- <span class="s9-qdc-track__metric-lbl">及时关闭率</span>
|
|
|
- </div>
|
|
|
- <div class="s9-qdc-track__metric">
|
|
|
- <span class="s9-qdc-track__metric-val">{{ fmtRate(item.closeRate) }}</span>
|
|
|
- <span class="s9-qdc-track__metric-lbl">总关闭率</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </article>
|
|
|
+ <article class="s9-qdc-card" :style="cardStyle">
|
|
|
+ <div class="s9-qdc-card__header">
|
|
|
+ <span class="s9-qdc-card__code">S9</span>
|
|
|
+ <span class="s9-qdc-card__title">运营指标</span>
|
|
|
+ <span
|
|
|
+ v-if="activeView === 'resultKpi' && isMock"
|
|
|
+ class="s9-qdc-card__sub"
|
|
|
+ >字典占位 · 待接入真实计算</span>
|
|
|
+ <div class="s9-qdc-card__tabs">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="s9-qdc-card__tab"
|
|
|
+ :class="{ 's9-qdc-card__tab--active': activeView === 'resultKpi' }"
|
|
|
+ @click="activeView = 'resultKpi'"
|
|
|
+ >结果 KPI</button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="s9-qdc-card__tab"
|
|
|
+ :class="{ 's9-qdc-card__tab--active': activeView === 'exception' }"
|
|
|
+ @click="activeView = 'exception'"
|
|
|
+ >异常态势</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-if="activeView === 'resultKpi'">
|
|
|
+ <div v-if="resultKpiLoading" class="s9-qdc-card__placeholder">加载中…</div>
|
|
|
+ <div
|
|
|
+ v-else-if="resultKpiError"
|
|
|
+ class="s9-qdc-card__placeholder s9-qdc-card__placeholder--err"
|
|
|
+ >加载失败:{{ resultKpiError }}</div>
|
|
|
+ <div v-else-if="!hasResultKpi" class="s9-qdc-card__placeholder">暂无 result KPI 字典数据</div>
|
|
|
+ <div v-else class="s9-qdc-card__tracks">
|
|
|
+ <div v-for="item in resultKpiList" :key="item.metricCode" class="s9-qdc-track s9-qdc-track--kpi">
|
|
|
+ <div class="s9-qdc-track__kpi-head">
|
|
|
+ <span class="s9-qdc-track__name">{{ item.metricName }}</span>
|
|
|
+ <span v-if="!item.dictionaryEnabled" class="s9-qdc-card__badge">未启用</span>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="s9-qdc-track__main-val"
|
|
|
+ :class="{ 's9-qdc-track__main-val--mock': item.currentValue === null }"
|
|
|
+ >{{ fmtCurrent(item) }}</div>
|
|
|
+ <div class="s9-qdc-track__foot">{{ fmtTarget(item) }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <div v-if="!hasException" class="s9-qdc-card__placeholder">暂无异常态势数据</div>
|
|
|
+ <div v-else class="s9-qdc-card__tracks">
|
|
|
+ <div v-for="item in items" :key="item.code" class="s9-qdc-track">
|
|
|
+ <div class="s9-qdc-track__name">{{ item.title }}</div>
|
|
|
+ <div class="s9-qdc-track__metrics">
|
|
|
+ <div class="s9-qdc-track__metric">
|
|
|
+ <span class="s9-qdc-track__metric-val">{{ item.total }}</span>
|
|
|
+ <span class="s9-qdc-track__metric-lbl">异常总数</span>
|
|
|
+ </div>
|
|
|
+ <div class="s9-qdc-track__metric">
|
|
|
+ <span class="s9-qdc-track__metric-val">{{ fmtHours(item.avgProcessHours) }}</span>
|
|
|
+ <span class="s9-qdc-track__metric-lbl">平均处理</span>
|
|
|
+ </div>
|
|
|
+ <div class="s9-qdc-track__metric">
|
|
|
+ <span class="s9-qdc-track__metric-val">{{ fmtRate(item.onTimeCloseRate) }}</span>
|
|
|
+ <span class="s9-qdc-track__metric-lbl">及时关闭率</span>
|
|
|
+ </div>
|
|
|
+ <div class="s9-qdc-track__metric">
|
|
|
+ <span class="s9-qdc-track__metric-val">{{ fmtRate(item.closeRate) }}</span>
|
|
|
+ <span class="s9-qdc-track__metric-lbl">总关闭率</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </article>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
.s9-qdc-card {
|
|
|
- position: relative;
|
|
|
- overflow: hidden;
|
|
|
- border-radius: 18px;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- box-sizing: border-box;
|
|
|
- padding: 14px 16px 12px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
- background: rgba(22, 26, 36, 0.85);
|
|
|
- border: 1px solid rgba(123, 208, 255, 0.14);
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 18px;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 14px 16px 12px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ background: rgba(22, 26, 36, 0.85);
|
|
|
+ border: 1px solid rgba(123, 208, 255, 0.14);
|
|
|
}
|
|
|
|
|
|
.s9-qdc-card__header {
|
|
|
- display: flex;
|
|
|
- align-items: baseline;
|
|
|
- gap: 6px;
|
|
|
- flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-card__code {
|
|
|
- font-size: 11px;
|
|
|
- font-weight: 700;
|
|
|
- color: rgba(123, 208, 255, 0.7);
|
|
|
- letter-spacing: 0.08em;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: rgba(123, 208, 255, 0.7);
|
|
|
+ letter-spacing: 0.08em;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-card__title {
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 700;
|
|
|
- color: #e1e2eb;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e1e2eb;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-card__sub {
|
|
|
- font-size: 10px;
|
|
|
- color: #909097;
|
|
|
- margin-left: 2px;
|
|
|
+ font-size: 10px;
|
|
|
+ color: rgba(255, 193, 7, 0.75);
|
|
|
+ margin-left: 2px;
|
|
|
+ letter-spacing: 0.02em;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-card__tabs {
|
|
|
+ margin-left: auto;
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-card__tab {
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-size: 10px;
|
|
|
+ color: #909097;
|
|
|
+ background: rgba(255, 255, 255, 0.04);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
+ cursor: pointer;
|
|
|
+ line-height: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-card__tab:hover {
|
|
|
+ color: #c6c6cd;
|
|
|
+ background: rgba(255, 255, 255, 0.07);
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-card__tab--active {
|
|
|
+ color: rgba(123, 208, 255, 0.95);
|
|
|
+ background: rgba(123, 208, 255, 0.12);
|
|
|
+ border-color: rgba(123, 208, 255, 0.32);
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-card__placeholder {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #909097;
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-card__placeholder--err {
|
|
|
+ color: #ffb4ab;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-card__badge {
|
|
|
+ padding: 1px 6px;
|
|
|
+ border-radius: 999px;
|
|
|
+ font-size: 9px;
|
|
|
+ color: rgba(255, 193, 7, 0.85);
|
|
|
+ background: rgba(255, 193, 7, 0.1);
|
|
|
+ border: 1px solid rgba(255, 193, 7, 0.2);
|
|
|
}
|
|
|
|
|
|
.s9-qdc-card__tracks {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 1fr 1fr;
|
|
|
- gap: 6px;
|
|
|
- flex: 1;
|
|
|
- min-height: 0;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 6px;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-track {
|
|
|
- background: rgba(255, 255, 255, 0.03);
|
|
|
- border: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
- border-radius: 10px;
|
|
|
- padding: 7px 8px 6px;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
+ background: rgba(255, 255, 255, 0.03);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 7px 8px 6px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-track__kpi-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 6px;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-track__name {
|
|
|
- font-size: 11px;
|
|
|
- font-weight: 600;
|
|
|
- color: rgba(123, 208, 255, 0.75);
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: rgba(123, 208, 255, 0.75);
|
|
|
}
|
|
|
|
|
|
.s9-qdc-track__metrics {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: 1fr 1fr;
|
|
|
- gap: 3px 6px;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 3px 6px;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-track__metric {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 1px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 1px;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-track__metric-val {
|
|
|
- font-size: 13px;
|
|
|
- font-weight: 700;
|
|
|
- color: #c6c6cd;
|
|
|
- line-height: 1.2;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #c6c6cd;
|
|
|
+ line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
.s9-qdc-track__metric-lbl {
|
|
|
- font-size: 9px;
|
|
|
- color: #909097;
|
|
|
+ font-size: 9px;
|
|
|
+ color: #909097;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-track__main-val {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #c6c6cd;
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-qdc-track__main-val--mock {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #909097;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
|
|
|
-.s9-qdc-track__remark {
|
|
|
- font-size: 9px;
|
|
|
- color: rgba(144, 144, 151, 0.55);
|
|
|
- margin-top: 2px;
|
|
|
+.s9-qdc-track__foot {
|
|
|
+ font-size: 9px;
|
|
|
+ color: #909097;
|
|
|
}
|
|
|
</style>
|