|
|
@@ -1159,9 +1159,10 @@ const statusCounts = computed(() => {
|
|
|
})
|
|
|
|
|
|
const mainProblem = computed(() => {
|
|
|
- let best = {
|
|
|
+ const empty = {
|
|
|
score: -1,
|
|
|
severity: -1,
|
|
|
+ deviation: 0,
|
|
|
level: 0,
|
|
|
stageKey: '',
|
|
|
tier1Key: '',
|
|
|
@@ -1172,20 +1173,16 @@ const mainProblem = computed(() => {
|
|
|
status: 'green',
|
|
|
dept: '',
|
|
|
}
|
|
|
- const accept = (candidate) => {
|
|
|
- const score = candidate.severity * 10 + candidate.level
|
|
|
- if (score > best.score) best = { ...candidate, score }
|
|
|
- }
|
|
|
if (!stages.length) {
|
|
|
const status = diagnosisContext.status || 'green'
|
|
|
const severity = statusSeverity(status)
|
|
|
return {
|
|
|
- score: severity * 10 + 1,
|
|
|
+ ...empty,
|
|
|
+ score: severity,
|
|
|
severity,
|
|
|
+ deviation: problemDeviation(diagnosisContext.actual, diagnosisContext.target, status),
|
|
|
level: 1,
|
|
|
stageKey: diagnosisContext.metricCode || '',
|
|
|
- tier1Key: '',
|
|
|
- tier2Key: '',
|
|
|
name: diagnosisContext.metricName || '',
|
|
|
actual: diagnosisContext.actual || '-',
|
|
|
plan: diagnosisContext.target || '-',
|
|
|
@@ -1193,15 +1190,65 @@ const mainProblem = computed(() => {
|
|
|
dept: diagnosisContext.department || '未配置',
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ const toCandidate = (partial) => ({
|
|
|
+ ...empty,
|
|
|
+ ...partial,
|
|
|
+ deviation: problemDeviation(partial.actual, partial.plan, partial.status),
|
|
|
+ score: partial.severity,
|
|
|
+ })
|
|
|
+
|
|
|
+ let best = empty
|
|
|
+ const accept = (candidate) => {
|
|
|
+ if (isWorseProblem(candidate, best)) best = candidate
|
|
|
+ }
|
|
|
+
|
|
|
+ const stageCandidates = stages.map((s) =>
|
|
|
+ toCandidate({
|
|
|
+ severity: statusSeverity(s.status),
|
|
|
+ level: 1,
|
|
|
+ stageKey: s.key,
|
|
|
+ name: s.name,
|
|
|
+ actual: s.actualDays,
|
|
|
+ plan: s.kpi,
|
|
|
+ status: s.status,
|
|
|
+ dept: s.dept || '',
|
|
|
+ })
|
|
|
+ )
|
|
|
+ for (const c of stageCandidates) accept(c)
|
|
|
+ // 这一级(当前诊断根的直接下级)已有红/黄时,用该级最差项,不把绿灯分支下的深层小红灯当成主要问题。
|
|
|
+ if (best.severity >= 2) return best
|
|
|
+
|
|
|
for (const s of stages) {
|
|
|
- const severity = statusSeverity(s.status)
|
|
|
- accept({ severity, level: 1, stageKey: s.key, tier1Key: '', tier2Key: '', name: s.name, actual: s.actualDays, plan: s.kpi, status: s.status, dept: s.dept || '' })
|
|
|
for (const node of drillTier1ByStage[s.key] || []) {
|
|
|
- const nodeSeverity = statusSeverity(node.status)
|
|
|
- accept({ severity: nodeSeverity, level: 2, stageKey: s.key, tier1Key: node.key, tier2Key: '', name: node.name, actual: node.actual, plan: node.plan, status: node.status, dept: node.dept || s.dept || '' })
|
|
|
+ accept(
|
|
|
+ toCandidate({
|
|
|
+ severity: statusSeverity(node.status),
|
|
|
+ level: 2,
|
|
|
+ stageKey: s.key,
|
|
|
+ tier1Key: node.key,
|
|
|
+ name: node.name,
|
|
|
+ actual: node.actual,
|
|
|
+ plan: node.plan,
|
|
|
+ status: node.status,
|
|
|
+ dept: node.dept || s.dept || '',
|
|
|
+ })
|
|
|
+ )
|
|
|
for (const child of problemTier2Rows(s.key, node.key)) {
|
|
|
- const childSeverity = statusSeverity(child.status)
|
|
|
- accept({ severity: childSeverity, level: 3, stageKey: s.key, tier1Key: node.key, tier2Key: child.key, name: child.name, actual: child.actual, plan: child.plan, status: child.status, dept: child.dept || node.dept || s.dept || '' })
|
|
|
+ accept(
|
|
|
+ toCandidate({
|
|
|
+ severity: statusSeverity(child.status),
|
|
|
+ level: 3,
|
|
|
+ stageKey: s.key,
|
|
|
+ tier1Key: node.key,
|
|
|
+ tier2Key: child.key,
|
|
|
+ name: child.name,
|
|
|
+ actual: child.actual,
|
|
|
+ plan: child.plan,
|
|
|
+ status: child.status,
|
|
|
+ dept: child.dept || node.dept || s.dept || '',
|
|
|
+ })
|
|
|
+ )
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1350,6 +1397,28 @@ function statusSeverity(status) {
|
|
|
return map[status] || 0
|
|
|
}
|
|
|
|
|
|
+function parseMetricNumber(raw) {
|
|
|
+ if (raw == null) return null
|
|
|
+ const match = String(raw).replace(/,/g, '').match(/-?\d+(?:\.\d+)?/)
|
|
|
+ return match ? Number(match[0]) : null
|
|
|
+}
|
|
|
+
|
|
|
+/** 相对目标的偏离幅度;绿灯为 0。单位不同时用 |实际-目标|/|目标|,避免「更深一层」压过「缺口更大」。 */
|
|
|
+function problemDeviation(actual, plan, status) {
|
|
|
+ if (statusSeverity(status) <= 1) return 0
|
|
|
+ const a = parseMetricNumber(actual)
|
|
|
+ const p = parseMetricNumber(plan)
|
|
|
+ if (a == null || p == null || p === 0) return 0
|
|
|
+ return Math.abs(a - p) / Math.abs(p)
|
|
|
+}
|
|
|
+
|
|
|
+function isWorseProblem(candidate, current) {
|
|
|
+ if (current.severity < 0) return true
|
|
|
+ if (candidate.severity !== current.severity) return candidate.severity > current.severity
|
|
|
+ if (Math.abs(candidate.deviation - current.deviation) > 1e-6) return candidate.deviation > current.deviation
|
|
|
+ return candidate.level < current.level
|
|
|
+}
|
|
|
+
|
|
|
function withDiagnosisUnit(value, unit) {
|
|
|
const text = value == null ? '' : String(value)
|
|
|
const normalizedUnit = unit == null ? '' : String(unit).trim()
|