feat(management): 管理驾驶舱页——KPI/双趋势/生命周期/维度分布/异常两次点击到事实
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<div class="mgmt-dash">
|
||||
<!-- 筛选栏:渠道/时间(URL query 真源)+口径入口;aside=数据截至角标 -->
|
||||
<ManagementFilterBar
|
||||
:channel="channel"
|
||||
:start="start"
|
||||
:end="end"
|
||||
:compare="compare"
|
||||
@apply="apply"
|
||||
@open-defs="defsVisible = true"
|
||||
>
|
||||
<template #aside>
|
||||
<!-- trial 常显:dashboard 载荷无 trial 键(评分配置层字段),批次C/D
|
||||
评分处于试算期(v2 §0 全默认+试算),批次E 转正时随配置联动 -->
|
||||
<DataAsOfBadge
|
||||
:data-as-of="resp?.data_as_of ?? null"
|
||||
:metric-version="resp?.metric_version"
|
||||
:trial="true"
|
||||
:config-source="resp?.config_source"
|
||||
/>
|
||||
</template>
|
||||
</ManagementFilterBar>
|
||||
|
||||
<!-- 错误态:alert+重试;resp 不清空,保留上次成功数据 -->
|
||||
<el-alert v-if="loadError" type="error" :closable="false" class="dash-alert">
|
||||
<template #title>
|
||||
<span>{{ loadError }}</span>
|
||||
<el-button size="small" type="primary" class="retry-btn" @click="load">重试</el-button>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 空态:窗口内无任何业务数据(数据截至角标同步显「无数据」) -->
|
||||
<el-alert
|
||||
v-if="noData"
|
||||
type="info"
|
||||
title="该窗口无数据——试试调整时间范围"
|
||||
:closable="false"
|
||||
class="dash-alert"
|
||||
/>
|
||||
|
||||
<!-- 六 KPI 卡:两行三卡(≥768px 三列/窄屏两列);后三卡行动入口直达处置页 -->
|
||||
<el-row :gutter="12" class="block-row kpi-row" v-loading="loading">
|
||||
<el-col :xs="12" :sm="8">
|
||||
<ManagementKpiCard
|
||||
label="活跃客户"
|
||||
:value="kpis?.active_customers ?? null"
|
||||
unit="人"
|
||||
:hint="threadsHint"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8">
|
||||
<ManagementKpiCard
|
||||
label="活跃员工"
|
||||
:value="kpis?.active_employees ?? null"
|
||||
unit="人"
|
||||
hint="期内有数据天数>0 口径"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8">
|
||||
<!-- tooltip 说明混合 vs 1:1 口径差异,hover 才展开,卡面 hint 保持一行 -->
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
content="管理域消息量仅统计 1:1 私聊消息;「数据分析·数据看板」为私聊+群聊混合口径,两者数值不可直接对比"
|
||||
>
|
||||
<ManagementKpiCard
|
||||
label="消息量"
|
||||
:value="kpis?.msg_total ?? null"
|
||||
unit="条"
|
||||
hint="1:1口径·与数据看板口径不同"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8">
|
||||
<ManagementKpiCard
|
||||
label="待回复"
|
||||
:value="actions?.unreplied_2h ?? null"
|
||||
unit="人"
|
||||
:danger="(actions?.unreplied_2h ?? 0) > 0"
|
||||
to="/analytics/unreplied"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8">
|
||||
<ManagementKpiCard
|
||||
label="即将流失"
|
||||
:value="actions?.churn_real_30d ?? null"
|
||||
unit="人"
|
||||
to="/analytics/churn"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="8">
|
||||
<ManagementKpiCard
|
||||
label="风险待处理"
|
||||
:value="actions?.sensitive_pending ?? null"
|
||||
unit="条"
|
||||
to="/analytics/sensitive"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 双趋势图:左沟通量(消息线+通话柱双 y 轴),右服务质量(首响分位线+回复率右轴) -->
|
||||
<el-row :gutter="12" class="block-row">
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card" v-loading="loading">
|
||||
<template #header>客户沟通趋势</template>
|
||||
<div ref="commEl" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card" v-loading="loading">
|
||||
<template #header>服务质量趋势</template>
|
||||
<div ref="svcEl" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 双条形图:左生命周期四态(横向),右员工维度均分(0-100,无样本灰) -->
|
||||
<el-row :gutter="12" class="block-row">
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card" v-loading="loading">
|
||||
<template #header>生命周期分布</template>
|
||||
<div ref="lifeEl" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card" v-loading="loading">
|
||||
<template #header>员工维度分布</template>
|
||||
<div ref="dimsEl" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 异常榜:critical 在前(后端已排);点行开下钻抽屉,两次点击到事实 -->
|
||||
<el-card shadow="never" class="block-row anomaly-card" v-loading="loading">
|
||||
<template #header>
|
||||
异常榜
|
||||
<span class="anomaly-sub">按严重度排序 · 点行查看明细</span>
|
||||
</template>
|
||||
<AnomalyList :anomalies="resp?.anomalies ?? []" :loading="loading" @drill="onDrill" />
|
||||
</el-card>
|
||||
|
||||
<!-- 下钻抽屉:metric 类异常逐串/逐行明细,行点击跳聊天;employee 类批次D 占位 -->
|
||||
<MetricDrilldownDrawer
|
||||
v-model="drillVisible"
|
||||
:card="drillCard"
|
||||
:user-id="drillCard?.employee_user_id ?? ''"
|
||||
:start="start"
|
||||
:end="end"
|
||||
:channel="channel"
|
||||
/>
|
||||
|
||||
<!-- 指标口径抽屉:19 项定义+评分配置,自缓存 -->
|
||||
<MetricDefsDrawer v-model="defsVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 管理驾驶舱(经营管理板块批次C首页):筛选(URL query 真源)→ 六 KPI →
|
||||
// 双趋势(沟通量/服务质量)→ 生命周期四态 + 员工维度均分 → 异常榜(下钻
|
||||
// 抽屉两次点击到事实)。管理者 5 分钟看整体:先 KPI 判断总体,异常卡
|
||||
// 直达事实。载荷 /management/dashboard 单往返;compare 仅回显(对比值
|
||||
// 批次D 员工表格呈现)。seq 守卫/双图 useChart/格式化均沿用项目惯例。
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import {
|
||||
getDashboard,
|
||||
type AnomalyCard,
|
||||
type DashboardResp,
|
||||
} from '@/api/management'
|
||||
import { useManagementFilters } from '@/composables/useManagementFilters'
|
||||
import { useChart } from '@/composables/useChart'
|
||||
import { formatMsDuration, formatRatio } from '@/utils/format'
|
||||
import ManagementFilterBar from '@/components/management/ManagementFilterBar.vue'
|
||||
import ManagementKpiCard from '@/components/management/ManagementKpiCard.vue'
|
||||
import DataAsOfBadge from '@/components/management/DataAsOfBadge.vue'
|
||||
import AnomalyList from '@/components/management/AnomalyList.vue'
|
||||
import MetricDrilldownDrawer from '@/components/management/MetricDrilldownDrawer.vue'
|
||||
import MetricDefsDrawer from '@/components/management/MetricDefsDrawer.vue'
|
||||
|
||||
// ── 筛选(URL query 为真源,刷新/分享保持)与加载 ──
|
||||
const { channel, start, end, compare, apply } = useManagementFilters()
|
||||
|
||||
const resp = ref<DashboardResp | null>(null)
|
||||
const loading = ref(false)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
let seq = 0 // 竞态守卫:筛选快速切换时旧响应作废(项目惯例)
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const my = ++seq
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
const r = await getDashboard(channel.value, start.value, end.value, compare.value)
|
||||
if (my !== seq) return
|
||||
resp.value = r
|
||||
} catch (e) {
|
||||
// 失败保留上次成功数据(resp 不清空),仅标记 error 供 alert+重试
|
||||
if (my === seq) loadError.value = e instanceof Error ? e.message : String(e)
|
||||
} finally {
|
||||
// 过期请求不清 loading(新一轮还在飞)也不覆盖 error
|
||||
if (my === seq) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// immediate 首帧即取数,替代 onMounted;筛选任一变化重查
|
||||
watch([channel, start, end, compare], load, { immediate: true })
|
||||
|
||||
const kpis = computed(() => resp.value?.kpis ?? null)
|
||||
const actions = computed(() => resp.value?.action_summary ?? null)
|
||||
|
||||
// 活跃客户辅助行:串/被回(口径与 KPI 同载荷,管理域跨日精确串)
|
||||
const threadsHint = computed(() => {
|
||||
const k = kpis.value
|
||||
if (!k) return undefined
|
||||
return `串 ${k.cust_threads.toLocaleString()} · 被回 ${k.threads_answered.toLocaleString()}`
|
||||
})
|
||||
|
||||
// 空态:核心 KPI 全 0(趋势按日脚手架恒非空,以 KPI 判空才可靠)
|
||||
const noData = computed(() => {
|
||||
const k = kpis.value
|
||||
if (!k || loadError.value) return false
|
||||
return !k.active_customers && !k.active_employees && !k.msg_total && !k.call_count
|
||||
})
|
||||
|
||||
// ── 异常下钻 ──
|
||||
const drillVisible = ref(false)
|
||||
const drillCard = ref<AnomalyCard | null>(null)
|
||||
const defsVisible = ref(false)
|
||||
|
||||
function onDrill(card: AnomalyCard): void {
|
||||
drillCard.value = card
|
||||
drillVisible.value = true
|
||||
}
|
||||
|
||||
// ── 图表(四张,容器恒在 DOM——useChart 只 init 一次,不 v-if) ──
|
||||
const commEl = ref<HTMLElement | null>(null)
|
||||
const svcEl = ref<HTMLElement | null>(null)
|
||||
const lifeEl = ref<HTMLElement | null>(null)
|
||||
const dimsEl = ref<HTMLElement | null>(null)
|
||||
const { render: renderComm } = useChart(commEl)
|
||||
const { render: renderSvc } = useChart(svcEl)
|
||||
const { render: renderLife } = useChart(lifeEl)
|
||||
const { render: renderDims } = useChart(dimsEl)
|
||||
|
||||
// yyyymmdd(20260810)→ 08-10 短格式(轴/tooltip 共用)
|
||||
const shortDay = (d: number): string => `${String(d).slice(4, 6)}-${String(d).slice(6)}`
|
||||
// 大数压缩:轴刻度统一 1.1万 口径(与 DashboardView 一致)
|
||||
const compact = (n: number): string =>
|
||||
n >= 10000 ? `${(n / 10000).toFixed(1)}万` : n.toLocaleString()
|
||||
// 服务质量左轴刻度:ms → 紧凑时长(轴上省秒,tooltip 仍用 formatMsDuration)
|
||||
const fmtMsAxis = (v: number): string =>
|
||||
v >= 3_600_000 ? `${(v / 3_600_000).toFixed(1)}时` : `${Math.round(v / 60_000)}分`
|
||||
// X 轴 ~6 个刻度(30 个日期全排会挤成一串不可读)
|
||||
const axisInterval = (n: number): number => Math.max(0, Math.ceil(n / 6) - 1)
|
||||
|
||||
const AXIS_LABEL = { color: '#909399' }
|
||||
const SPLIT_LINE = { lineStyle: { type: 'solid' as const, color: '#f0f1f3' } }
|
||||
|
||||
function drawComm(): void {
|
||||
const t = resp.value?.communication_trend
|
||||
if (!t || !t.length) return
|
||||
const labels = t.map((p) => shortDay(p.day))
|
||||
renderComm({
|
||||
grid: { left: 56, right: 56, top: 40, bottom: 28 },
|
||||
tooltip: { trigger: 'axis', valueFormatter: (v: unknown) => compact(Number(v)) },
|
||||
legend: { data: ['消息量', '通话量'], top: 4, itemWidth: 14, textStyle: { color: '#606266' } },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: labels,
|
||||
axisLabel: { interval: axisInterval(labels.length), hideOverlap: true, ...AXIS_LABEL },
|
||||
axisLine: { lineStyle: { color: '#dcdfe6' } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '消息量',
|
||||
axisLabel: { formatter: (v: number) => compact(v), ...AXIS_LABEL },
|
||||
splitLine: SPLIT_LINE,
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '通话量',
|
||||
axisLabel: { formatter: (v: number) => compact(v), ...AXIS_LABEL },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '消息量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
yAxisIndex: 0,
|
||||
lineStyle: { width: 2, color: '#409EFF' },
|
||||
itemStyle: { color: '#409EFF' },
|
||||
areaStyle: { opacity: 0.1, color: '#409EFF' },
|
||||
showSymbol: false,
|
||||
data: t.map((p) => p.msg_total),
|
||||
},
|
||||
{
|
||||
name: '通话量',
|
||||
type: 'bar',
|
||||
yAxisIndex: 1,
|
||||
barMaxWidth: 12,
|
||||
itemStyle: { color: '#67C23A', borderRadius: [3, 3, 0, 0] },
|
||||
data: t.map((p) => p.call_cnt),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// 服务质量 tooltip:ms→时长/率→百分比(null 显 '-',与断点语义一致)
|
||||
interface TipParam { marker?: string; seriesName?: string; value?: number | null; axisValueLabel?: string }
|
||||
function svcTooltip(ps: unknown): string {
|
||||
const arr = (Array.isArray(ps) ? ps : [ps]) as TipParam[]
|
||||
if (!arr.length) return ''
|
||||
const rows = arr.map((p) => {
|
||||
const v = typeof p.value === 'number' ? p.value : null
|
||||
const text = v === null ? '-' : p.seriesName === '2h回复率' ? formatRatio(v) : formatMsDuration(v)
|
||||
return `<div>${p.marker ?? ''}${p.seriesName}: ${text}</div>`
|
||||
})
|
||||
return `<div>${arr[0].axisValueLabel ?? ''}</div>${rows.join('')}`
|
||||
}
|
||||
|
||||
function drawSvc(): void {
|
||||
const t = resp.value?.service_trend
|
||||
if (!t || !t.length) return
|
||||
const labels = t.map((p) => shortDay(p.day))
|
||||
renderSvc({
|
||||
grid: { left: 56, right: 56, top: 40, bottom: 28 },
|
||||
tooltip: { trigger: 'axis', formatter: svcTooltip },
|
||||
legend: {
|
||||
data: ['首响中位', '首响九分位', '2h回复率'],
|
||||
top: 4, itemWidth: 14, textStyle: { color: '#606266' },
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: labels,
|
||||
axisLabel: { interval: axisInterval(labels.length), hideOverlap: true, ...AXIS_LABEL },
|
||||
axisLine: { lineStyle: { color: '#dcdfe6' } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '首响',
|
||||
axisLabel: { formatter: fmtMsAxis, ...AXIS_LABEL },
|
||||
splitLine: SPLIT_LINE,
|
||||
},
|
||||
{
|
||||
// 右轴 0-1 率:不单列双轴则率线贴底不可读
|
||||
type: 'value',
|
||||
name: '2h回复率',
|
||||
min: 0,
|
||||
max: 1,
|
||||
axisLabel: { formatter: (v: number) => formatRatio(v), ...AXIS_LABEL },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
// null(无样本日)=断点:connectNulls:false 显式钉死,不跨缺口连线
|
||||
{
|
||||
name: '首响中位',
|
||||
type: 'line', smooth: true, showSymbol: false, connectNulls: false,
|
||||
lineStyle: { width: 2 },
|
||||
data: t.map((p) => p.fr_p50_ms),
|
||||
},
|
||||
{
|
||||
name: '首响九分位',
|
||||
type: 'line', smooth: true, showSymbol: false, connectNulls: false,
|
||||
lineStyle: { width: 2 },
|
||||
data: t.map((p) => p.fr_p90_ms),
|
||||
},
|
||||
{
|
||||
name: '2h回复率',
|
||||
type: 'line', smooth: true, showSymbol: false, connectNulls: false,
|
||||
yAxisIndex: 1,
|
||||
lineStyle: { width: 2, color: '#67C23A' },
|
||||
itemStyle: { color: '#67C23A' },
|
||||
data: t.map((p) => p.reply_2h_rate),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期四态:横向条形(名义类别,状态色——新客蓝/降温橙/沉睡红/回流绿)
|
||||
const LIFE_META = [
|
||||
{ key: 'new', label: '新客', color: '#409EFF' },
|
||||
{ key: 'cooling', label: '降温', color: '#E6A23C' },
|
||||
{ key: 'dormant', label: '沉睡', color: '#F56C6C' },
|
||||
{ key: 'reactivated', label: '回流', color: '#67C23A' },
|
||||
] as const
|
||||
|
||||
function drawLife(): void {
|
||||
const lc = resp.value?.lifecycle
|
||||
if (!lc) return
|
||||
renderLife({
|
||||
grid: { left: 8, right: 48, top: 8, bottom: 8, containLabel: true },
|
||||
tooltip: { trigger: 'item', valueFormatter: (v: unknown) => compact(Number(v)) },
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { formatter: (v: number) => compact(v), ...AXIS_LABEL },
|
||||
splitLine: SPLIT_LINE,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
inverse: true, // 新客在顶部(生命周期顺序)
|
||||
data: LIFE_META.map((m) => m.label),
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { color: '#606266' },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 14,
|
||||
itemStyle: { borderRadius: [0, 4, 4, 0] },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
formatter: (p: { value: unknown }) => compact(Number(p.value)),
|
||||
color: '#606266',
|
||||
fontSize: 11,
|
||||
},
|
||||
data: LIFE_META.map((m) => ({ value: lc[m.key], itemStyle: { color: m.color } })),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// 员工维度分布:pending_data 维(action/risk)恒无 scored/insufficient 计数,
|
||||
// 以计数>0 过滤,只留真实参与评分的维度(customer/service/workload)
|
||||
const dimRows = computed(() =>
|
||||
Object.entries(resp.value?.dimension_summary ?? {})
|
||||
.filter(([, d]) => d.scored_count > 0 || d.insufficient_count > 0)
|
||||
.map(([key, d]) => ({ key, ...d })),
|
||||
)
|
||||
|
||||
function dimsTooltip(p: { dataIndex?: number }): string {
|
||||
const r = p.dataIndex != null ? dimRows.value[p.dataIndex] : undefined
|
||||
if (!r) return ''
|
||||
const avg = r.avg_score === null ? '-' : Math.round(r.avg_score)
|
||||
return (
|
||||
`<div>${r.name}</div>` +
|
||||
`<div>均分 ${avg}(权重 ${r.weight}%)</div>` +
|
||||
`<div>已评分 ${r.scored_count} · 样本不足 ${r.insufficient_count} · 待数据 ${r.pending_count}</div>`
|
||||
)
|
||||
}
|
||||
|
||||
function drawDims(): void {
|
||||
const rows = dimRows.value
|
||||
if (!rows.length) return
|
||||
renderDims({
|
||||
grid: { left: 40, right: 16, top: 24, bottom: 28, containLabel: true },
|
||||
tooltip: { trigger: 'item', formatter: dimsTooltip },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: rows.map((r) => r.name),
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: '#dcdfe6' } },
|
||||
axisLabel: { color: '#606266' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 100, // 维度分定义域 0-100
|
||||
axisLabel: AXIS_LABEL,
|
||||
splitLine: SPLIT_LINE,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 28,
|
||||
itemStyle: { borderRadius: [3, 3, 0, 0] },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter: (p: { dataIndex: number; value: unknown }) =>
|
||||
(rows[p.dataIndex]?.avg_score ?? null) === null ? '-' : compact(Number(p.value)),
|
||||
color: '#606266',
|
||||
fontSize: 11,
|
||||
},
|
||||
data: rows.map((r) =>
|
||||
r.avg_score === null
|
||||
// 无样本维度(全员未评):0 值灰条——有位置但不冒充分数
|
||||
? { value: 0, itemStyle: { color: '#c0c4cc' } }
|
||||
: { value: Math.round(r.avg_score), itemStyle: { color: '#409EFF' } },
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// post:等 DOM 更新后再画,保证首帧容器已挂载;resp 单源,四图同刷
|
||||
watch(resp, () => { drawComm(); drawSvc(); drawLife(); drawDims() }, { flush: 'post' })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dash-alert {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.retry-btn {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.block-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.kpi-row {
|
||||
min-height: 100px;
|
||||
}
|
||||
.chart-card :deep(.el-card__body) {
|
||||
padding: 12px 16px 8px;
|
||||
}
|
||||
.chart {
|
||||
height: 300px;
|
||||
}
|
||||
.anomaly-sub {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user