|
|
@@ -0,0 +1,253 @@
|
|
|
+<template>
|
|
|
+ <div class="s8-supply-trend">
|
|
|
+ <div class="s8-supply-trend__header">
|
|
|
+ <span class="s8-supply-trend__title">近7日供应异常趋势</span>
|
|
|
+ <span class="s8-supply-trend__subtitle">统计近7天供应相关异常数量变化,辅助识别供应风险波动。</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="isEmpty" class="s8-supply-trend__empty">
|
|
|
+ <div class="s8-supply-trend__empty-title">暂无趋势数据</div>
|
|
|
+ <div class="s8-supply-trend__empty-desc">最近7天未统计到供应异常记录</div>
|
|
|
+ </div>
|
|
|
+ <scEcharts
|
|
|
+ v-else
|
|
|
+ ref="chartRef"
|
|
|
+ height="240px"
|
|
|
+ width="100%"
|
|
|
+ :option="chartOption"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="s8-supply-trend__summary">
|
|
|
+ <div class="s8-supply-trend__summary-card">
|
|
|
+ <div class="s8-supply-trend__summary-label">7日峰值</div>
|
|
|
+ <div class="s8-supply-trend__summary-value">{{ peakText }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="s8-supply-trend__summary-card">
|
|
|
+ <div class="s8-supply-trend__summary-label">7日均值</div>
|
|
|
+ <div class="s8-supply-trend__summary-value">{{ avgText }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="s8-supply-trend__summary-card">
|
|
|
+ <div class="s8-supply-trend__summary-label">今日异常</div>
|
|
|
+ <div class="s8-supply-trend__summary-value">{{ todayText }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, ref } from 'vue';
|
|
|
+import scEcharts from '/@/components/scEcharts/index.vue';
|
|
|
+import type { S8SupplyTrendData, S8SupplyTrendDay } from '../../api/s8MonitoringApi';
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ data: S8SupplyTrendData | null;
|
|
|
+}>();
|
|
|
+
|
|
|
+const chartRef = ref();
|
|
|
+
|
|
|
+const days = computed<S8SupplyTrendDay[]>(() => props.data?.days ?? []);
|
|
|
+const summary = computed(() => props.data?.summary ?? null);
|
|
|
+const isEmpty = computed(() => days.value.length === 0 || days.value.every((d) => d.total === 0));
|
|
|
+
|
|
|
+const peakText = computed(() => {
|
|
|
+ const s = summary.value;
|
|
|
+ if (!s || s.peakValue <= 0) return '--';
|
|
|
+ return String(s.peakValue);
|
|
|
+});
|
|
|
+const avgText = computed(() => {
|
|
|
+ const s = summary.value;
|
|
|
+ if (!s || s.avgValue <= 0) return '--';
|
|
|
+ return s.avgValue.toFixed(1);
|
|
|
+});
|
|
|
+const todayText = computed(() => String(summary.value?.todayValue ?? 0));
|
|
|
+
|
|
|
+// 7 类完整元数据;按近 7 日总量选 Top5 非零展示,避免视觉拥挤。
|
|
|
+const ALL_SERIES_META = [
|
|
|
+ { key: 'supplierEtaIssue', name: '供应商交期异常', color: '#7bd0ff' },
|
|
|
+ { key: 'supplierShipIssue', name: '供应商发货异常', color: '#ffc107' },
|
|
|
+ { key: 'warehouseReceiptAbnormal', name: '仓库收货异常', color: '#ff9d6c' },
|
|
|
+ { key: 'iqcIssue', name: 'IQC 检验异常', color: '#88fd54' },
|
|
|
+ { key: 'warehousePutawayIssue', name: '仓库上架异常', color: '#c084fc' },
|
|
|
+ { key: 'workOrderKittingAbnormal', name: '工单备料异常', color: '#f87171' },
|
|
|
+ { key: 'workOrderIssueAbnormal', name: '工单发料异常', color: '#34d399' },
|
|
|
+] as const;
|
|
|
+
|
|
|
+type SeriesMeta = typeof ALL_SERIES_META[number];
|
|
|
+
|
|
|
+const visibleSeries = computed<SeriesMeta[]>(() => {
|
|
|
+ const list = days.value;
|
|
|
+ if (list.length === 0) return [];
|
|
|
+ const totals = ALL_SERIES_META.map((m) => ({
|
|
|
+ meta: m,
|
|
|
+ sum: list.reduce((s, d) => s + (d[m.key as keyof S8SupplyTrendDay] as number), 0),
|
|
|
+ }));
|
|
|
+ const nonZero = totals.filter((t) => t.sum > 0).sort((a, b) => b.sum - a.sum);
|
|
|
+ return nonZero.slice(0, 5).map((t) => t.meta);
|
|
|
+});
|
|
|
+
|
|
|
+const chartOption = computed(() => {
|
|
|
+ const list = days.value;
|
|
|
+ const labels = list.map((d) => d.date);
|
|
|
+ const series = visibleSeries.value;
|
|
|
+
|
|
|
+ return {
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ animation: true,
|
|
|
+ animationDuration: 600,
|
|
|
+ animationEasing: 'cubicOut',
|
|
|
+ legend: {
|
|
|
+ show: true,
|
|
|
+ top: 0,
|
|
|
+ right: 8,
|
|
|
+ textStyle: { color: '#c6c6cd', fontSize: 11 },
|
|
|
+ itemWidth: 14,
|
|
|
+ itemHeight: 8,
|
|
|
+ data: series.map((m) => m.name),
|
|
|
+ },
|
|
|
+ grid: { left: 12, right: 18, top: 32, bottom: 8, containLabel: true },
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(15,23,42,0.92)',
|
|
|
+ borderColor: 'rgba(109,224,57,0.24)',
|
|
|
+ borderWidth: 1,
|
|
|
+ textStyle: { color: '#dbe5f1', fontSize: 12 },
|
|
|
+ formatter: (params: any[]) => {
|
|
|
+ if (!params?.length) return '';
|
|
|
+ const idx = params[0].dataIndex;
|
|
|
+ const day = list[idx];
|
|
|
+ if (!day) return '';
|
|
|
+ const lines = series.map((m) => `<div>· ${m.name}:${day[m.key as keyof S8SupplyTrendDay]}</div>`).join('');
|
|
|
+ return `<div style="font-weight:600;margin-bottom:4px;">${day.rawDate}</div>`
|
|
|
+ + lines
|
|
|
+ + `<div style="margin-top:4px;color:#88fd54;">合计:${day.total}</div>`;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: labels,
|
|
|
+ boundaryGap: false,
|
|
|
+ axisLine: { lineStyle: { color: 'rgba(255,255,255,0.12)' } },
|
|
|
+ axisTick: { show: false },
|
|
|
+ axisLabel: { color: '#9ca3af', fontSize: 11 },
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ minInterval: 1,
|
|
|
+ axisLine: { show: false },
|
|
|
+ axisTick: { show: false },
|
|
|
+ axisLabel: { color: '#9ca3af', fontSize: 11 },
|
|
|
+ splitLine: { lineStyle: { color: 'rgba(255,255,255,0.06)' } },
|
|
|
+ },
|
|
|
+ series: series.map((meta) => ({
|
|
|
+ name: meta.name,
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 6,
|
|
|
+ lineStyle: { width: 2, color: meta.color },
|
|
|
+ itemStyle: { color: meta.color, borderColor: meta.color, borderWidth: 2 },
|
|
|
+ areaStyle: {
|
|
|
+ color: {
|
|
|
+ type: 'linear',
|
|
|
+ x: 0, y: 0, x2: 0, y2: 1,
|
|
|
+ colorStops: [
|
|
|
+ { offset: 0, color: hexToRgba(meta.color, 0.32) },
|
|
|
+ { offset: 1, color: hexToRgba(meta.color, 0.02) },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data: list.map((d) => d[meta.key as keyof S8SupplyTrendDay] as number),
|
|
|
+ })),
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+function hexToRgba(hex: string, alpha: number) {
|
|
|
+ const h = hex.replace('#', '');
|
|
|
+ const r = parseInt(h.slice(0, 2), 16);
|
|
|
+ const g = parseInt(h.slice(2, 4), 16);
|
|
|
+ const b = parseInt(h.slice(4, 6), 16);
|
|
|
+ return `rgba(${r},${g},${b},${alpha})`;
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.s8-supply-trend {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ padding: 18px 18px 16px;
|
|
|
+ gap: 12px;
|
|
|
+ background: rgba(15, 19, 26, 0.55);
|
|
|
+ border: 1px solid rgba(109, 224, 57, 0.12);
|
|
|
+ border-radius: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__header {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ letter-spacing: 0.06em;
|
|
|
+ color: #e1e6ee;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__subtitle {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__empty {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 200px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 6px;
|
|
|
+ border: 1px dashed rgba(255, 255, 255, 0.08);
|
|
|
+ border-radius: 10px;
|
|
|
+ background: rgba(11, 14, 20, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__empty-title {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #c6c6cd;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__empty-desc {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #6b7280;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__summary {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__summary-card {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: rgba(11, 14, 20, 0.55);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__summary-label {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.s8-supply-trend__summary-value {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #e1e6ee;
|
|
|
+ font-family: 'Roboto Mono', monospace;
|
|
|
+}
|
|
|
+</style>
|