feat(小宝预警): 调整风险列表布局和导航徽标
This commit is contained in:
@@ -15,7 +15,9 @@ export interface XiaobaoVersionRef {
|
||||
id: string;
|
||||
name: string;
|
||||
status?: string;
|
||||
productId?: string;
|
||||
productName?: string;
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
expectedReleaseDate?: string | null;
|
||||
members?: Array<{ id?: string; name: string; role?: string }>;
|
||||
@@ -50,7 +52,9 @@ export interface XiaobaoRiskSignals {
|
||||
export interface XiaobaoVersionRisk {
|
||||
versionId: string;
|
||||
versionName: string;
|
||||
productId?: string;
|
||||
productName?: string;
|
||||
projectId?: string;
|
||||
projectName?: string;
|
||||
riskScore: number;
|
||||
riskLevel: XiaobaoRiskLevel;
|
||||
@@ -224,7 +228,9 @@ export function calcXiaobaoVersionRisk(input: CalcXiaobaoVersionRiskInput): Xiao
|
||||
return {
|
||||
versionId: input.version.id,
|
||||
versionName: input.version.name,
|
||||
productId: input.version.productId,
|
||||
productName: input.version.productName,
|
||||
projectId: input.version.projectId,
|
||||
projectName: input.version.projectName,
|
||||
riskScore,
|
||||
riskLevel,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { VersionWithContext } from './derive';
|
||||
import { filterXiaobaoWarningVersions, formatRemainingWork, sanitizeRiskInsight } from './xiaobao-warning-view';
|
||||
import type { XiaobaoVersionRisk } from './xiaobao-risk';
|
||||
import {
|
||||
filterXiaobaoRiskWarnings,
|
||||
filterXiaobaoWarningVersions,
|
||||
formatRemainingWork,
|
||||
getXiaobaoWarningRiskCount,
|
||||
sanitizeRiskInsight,
|
||||
} from './xiaobao-warning-view';
|
||||
|
||||
function version(patch: Partial<VersionWithContext> = {}): VersionWithContext {
|
||||
return {
|
||||
@@ -20,6 +27,49 @@ function version(patch: Partial<VersionWithContext> = {}): VersionWithContext {
|
||||
};
|
||||
}
|
||||
|
||||
function risk(patch: Partial<XiaobaoVersionRisk> = {}): XiaobaoVersionRisk {
|
||||
return {
|
||||
versionId: 'ver-1',
|
||||
versionName: 'V1.0',
|
||||
productId: 'prod-1',
|
||||
productName: 'Product A',
|
||||
projectId: 'proj-1',
|
||||
projectName: 'Project A',
|
||||
riskScore: 68,
|
||||
riskLevel: 'attention',
|
||||
expectedReleaseDate: '2026-07-05',
|
||||
confidence: 80,
|
||||
confidenceLevel: 'high',
|
||||
delayDays: 0,
|
||||
remainingWorkHours: 8,
|
||||
reasons: [],
|
||||
silentRisks: [],
|
||||
signals: {
|
||||
unfinishedCount: 1,
|
||||
openBugCount: 0,
|
||||
criticalBugCount: 0,
|
||||
failedTestCount: 0,
|
||||
blockedCount: 0,
|
||||
silentRiskCount: 0,
|
||||
},
|
||||
currentSnapshot: {
|
||||
versionId: 'ver-1',
|
||||
date: '2026-06-30',
|
||||
riskScore: 68,
|
||||
riskLevel: 'attention',
|
||||
openBugCount: 0,
|
||||
criticalBugCount: 0,
|
||||
failedTestCount: 0,
|
||||
blockedCount: 0,
|
||||
silentRiskCount: 0,
|
||||
confidence: 80,
|
||||
createdAt: '2026-06-30T10:00:00.000Z',
|
||||
},
|
||||
trend: { direction: 'flat', delta: 0, summary: 'Risk is stable.', pattern: 'stable' },
|
||||
...patch,
|
||||
} as XiaobaoVersionRisk;
|
||||
}
|
||||
|
||||
test('filterXiaobaoWarningVersions lets managers see every unfinished version', () => {
|
||||
const result = filterXiaobaoWarningVersions(
|
||||
[
|
||||
@@ -46,6 +96,38 @@ test('filterXiaobaoWarningVersions limits non-managers to versions where they ar
|
||||
assert.deepEqual(result.map((item) => item.id), ['ver-1']);
|
||||
});
|
||||
|
||||
test('filterXiaobaoRiskWarnings hides on_track risks and badge count follows visible risks', () => {
|
||||
const risks = [
|
||||
risk({ versionId: 'ver-1', riskLevel: 'on_track', riskScore: 10 }),
|
||||
risk({ versionId: 'ver-2', riskLevel: 'attention', riskScore: 38 }),
|
||||
risk({ versionId: 'ver-3', riskLevel: 'blocked', riskScore: 100 }),
|
||||
];
|
||||
|
||||
assert.deepEqual(filterXiaobaoRiskWarnings(risks).map((item) => item.versionId), ['ver-2', 'ver-3']);
|
||||
assert.equal(getXiaobaoWarningRiskCount(risks), 2);
|
||||
});
|
||||
|
||||
test('filterXiaobaoRiskWarnings filters by product project and risk tier', () => {
|
||||
const risks = [
|
||||
risk({ versionId: 'ver-1', productId: 'prod-1', projectId: 'proj-1', riskLevel: 'attention', riskScore: 38 }),
|
||||
risk({ versionId: 'ver-2', productId: 'prod-1', projectId: 'proj-2', riskLevel: 'likely_delayed', riskScore: 88 }),
|
||||
risk({ versionId: 'ver-3', productId: 'prod-2', projectId: 'proj-3', riskLevel: 'blocked', riskScore: 100 }),
|
||||
];
|
||||
|
||||
assert.deepEqual(
|
||||
filterXiaobaoRiskWarnings(risks, { productId: 'prod-1' }).map((item) => item.versionId),
|
||||
['ver-1', 'ver-2'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterXiaobaoRiskWarnings(risks, { productId: 'prod-1', projectId: 'proj-2' }).map((item) => item.versionId),
|
||||
['ver-2'],
|
||||
);
|
||||
assert.deepEqual(
|
||||
filterXiaobaoRiskWarnings(risks, { riskFilter: 'high' }).map((item) => item.versionId),
|
||||
['ver-2', 'ver-3'],
|
||||
);
|
||||
});
|
||||
|
||||
test('formatRemainingWork keeps hours and adds work-day conversion', () => {
|
||||
assert.equal(formatRemainingWork(0), '0h / 0天');
|
||||
assert.equal(formatRemainingWork(8), '8h / 1天');
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { VersionWithContext } from './derive';
|
||||
import type { XiaobaoRiskInsight } from './xiaobao-risk-cache';
|
||||
import type { XiaobaoVersionRisk } from './xiaobao-risk';
|
||||
import { WORK_HOURS } from './work-hours';
|
||||
|
||||
const UNFINISHED_VERSION_STATUSES = new Set(['planned', 'developing', 'paused']);
|
||||
const HIGH_RISK_LEVELS = new Set<XiaobaoVersionRisk['riskLevel']>(['at_risk', 'likely_delayed', 'blocked']);
|
||||
const PAGE_REFRESH_ADVICE_PATTERNS = [
|
||||
/(刷新|重新加载|重载).*(页面|浏览器|小宝|预警)/i,
|
||||
/(页面|浏览器|小宝|预警).*(刷新|重新加载|重载)/i,
|
||||
@@ -16,6 +18,14 @@ export interface XiaobaoWarningVersionFilter {
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
export type XiaobaoWarningRiskFilter = 'all' | 'attention' | 'high';
|
||||
|
||||
export interface XiaobaoWarningRiskListFilter {
|
||||
riskFilter?: XiaobaoWarningRiskFilter;
|
||||
productId?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export function filterXiaobaoWarningVersions(
|
||||
versions: VersionWithContext[],
|
||||
filter: XiaobaoWarningVersionFilter,
|
||||
@@ -28,6 +38,24 @@ export function filterXiaobaoWarningVersions(
|
||||
});
|
||||
}
|
||||
|
||||
export function filterXiaobaoRiskWarnings(
|
||||
risks: XiaobaoVersionRisk[],
|
||||
filter: XiaobaoWarningRiskListFilter = {},
|
||||
): XiaobaoVersionRisk[] {
|
||||
return risks.filter((risk) => {
|
||||
if (risk.riskLevel === 'on_track') return false;
|
||||
if (filter.productId && risk.productId !== filter.productId) return false;
|
||||
if (filter.projectId && risk.projectId !== filter.projectId) return false;
|
||||
if (filter.riskFilter === 'attention') return true;
|
||||
if (filter.riskFilter === 'high') return HIGH_RISK_LEVELS.has(risk.riskLevel);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function getXiaobaoWarningRiskCount(risks: XiaobaoVersionRisk[]): number {
|
||||
return filterXiaobaoRiskWarnings(risks).length;
|
||||
}
|
||||
|
||||
export function formatRemainingWork(hours: number): string {
|
||||
const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 0;
|
||||
const days = safeHours / WORK_HOURS.hoursPerDay;
|
||||
|
||||
Reference in New Issue
Block a user