|
|
@@ -0,0 +1,179 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed } from 'vue';
|
|
|
+import type { S8ResultKpiItem } from '../../api/s8MonitoringApi';
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ items: S8ResultKpiItem[];
|
|
|
+ source: string;
|
|
|
+ loading?: boolean;
|
|
|
+ error?: string | null;
|
|
|
+ minHeight?: number;
|
|
|
+}>();
|
|
|
+
|
|
|
+const cardStyle = computed(() => ({
|
|
|
+ minHeight: `${Math.max(140, props.minHeight ?? 180)}px`,
|
|
|
+}));
|
|
|
+
|
|
|
+const isMock = computed(() => props.source === 'DICTIONARY_MOCK');
|
|
|
+
|
|
|
+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-result-kpi" :style="cardStyle">
|
|
|
+ <div class="s9-result-kpi__header">
|
|
|
+ <span class="s9-result-kpi__code">S9</span>
|
|
|
+ <span class="s9-result-kpi__title">结果 KPI</span>
|
|
|
+ <span v-if="isMock" class="s9-result-kpi__sub">字典占位 · 待接入真实计算</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="loading" class="s9-result-kpi__placeholder">加载中…</div>
|
|
|
+ <div v-else-if="error" class="s9-result-kpi__placeholder s9-result-kpi__placeholder--err">
|
|
|
+ 加载失败:{{ error }}
|
|
|
+ </div>
|
|
|
+ <div v-else-if="!items.length" class="s9-result-kpi__placeholder">暂无 result KPI 字典数据</div>
|
|
|
+ <div v-else class="s9-result-kpi__tracks">
|
|
|
+ <div v-for="item in items" :key="item.metricCode" class="s9-result-kpi__track">
|
|
|
+ <div class="s9-result-kpi__track-head">
|
|
|
+ <span class="s9-result-kpi__track-name">{{ item.metricName }}</span>
|
|
|
+ <span v-if="!item.dictionaryEnabled" class="s9-result-kpi__badge">未启用</span>
|
|
|
+ </div>
|
|
|
+ <div class="s9-result-kpi__track-body">
|
|
|
+ <span
|
|
|
+ class="s9-result-kpi__track-val"
|
|
|
+ :class="{ 's9-result-kpi__track-val--mock': item.currentValue === null }"
|
|
|
+ >{{ fmtCurrent(item) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="s9-result-kpi__track-foot">{{ fmtTarget(item) }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.s9-result-kpi {
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 18px;
|
|
|
+ width: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 14px 16px 12px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ background: rgba(22, 26, 36, 0.85);
|
|
|
+ border: 1px solid rgba(123, 208, 255, 0.14);
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__header {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__code {
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: rgba(123, 208, 255, 0.7);
|
|
|
+ letter-spacing: 0.08em;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e1e2eb;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__sub {
|
|
|
+ font-size: 10px;
|
|
|
+ color: rgba(255, 193, 7, 0.75);
|
|
|
+ margin-left: 4px;
|
|
|
+ letter-spacing: 0.02em;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__placeholder {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #909097;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__placeholder--err {
|
|
|
+ color: #ffb4ab;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__tracks {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 8px;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__track {
|
|
|
+ background: rgba(255, 255, 255, 0.03);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__track-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__track-name {
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: rgba(123, 208, 255, 0.75);
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__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-result-kpi__track-body {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__track-val {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #c6c6cd;
|
|
|
+ line-height: 1.2;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__track-val--mock {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909097;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.s9-result-kpi__track-foot {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #909097;
|
|
|
+}
|
|
|
+</style>
|