feat(profitability): 第⑤步报价填写时实时提示报价口径不足

- 新增 revenueWarning 实时计算:随岗位单价/加成率/管理费/合同总额变化即时判断
- 报价信息不足时在报价模式区下方显示醒目橙色 alert 横幅,明确指出该填哪个字段
- 与运行前 handleRun 校验形成双重保障(填写时提示+运行时拦截)
This commit is contained in:
freedakgmail
2026-06-14 10:20:53 +08:00
parent 0501e6e8c2
commit 3716564b58
+48
View File
@@ -763,6 +763,31 @@ export function NewAssessment(): JSX.Element {
const answeredCount = Object.values(answers).filter((v) => typeof v === 'number').length;
const totalIndicators = indicators.length;
/* --------- 实时报价口径校验:判断当前填写是否足以产生收入(用于第⑤步即时提示) --------- */
const revenueWarning: string | null = (() => {
const toNum = (s: string): number | undefined => {
const t = s.replace(/,/g, '').trim();
return t !== '' && Number.isFinite(Number(t)) ? Number(t) : undefined;
};
const anyUnitPrice = positions.some(
(p) => p.name.trim() !== '' && (toNum(p.unitPrice) ?? 0) > 0,
);
if (pricingModel === 'per_head' || pricingModel === 'volume') {
if (!anyUnitPrice && toNum(mgmtFeePerHead) === undefined) {
return '尚未填写对客月单价或管理费(元/人/月)。当前报价信息不足,盈利分析收入将为 0。请为岗位填写「对客月单价」,或填写「管理费」。';
}
} else if (pricingModel === 'cost_plus') {
if (toNum(markupRate) === undefined && !anyUnitPrice) {
return '尚未填写成本加成率或人月单价。业务/服务外包(成本加成)需填写「成本加成率」(如 0.15)或为岗位填写「人月单价」,否则盈利分析收入将为 0。';
}
} else if (pricingModel === 'fixed_total') {
if ((toNum(contractTotal) ?? 0) <= 0) {
return '尚未填写合同总额。固定总价模式请填写「合同总额(含税,元)」,否则盈利分析收入将为 0。';
}
}
return null;
})();
/* ----------------------------- 渲染 ----------------------------- */
return (
<div style={{ fontFamily: FONT_FAMILY, maxWidth: 1200, margin: '0 auto' }}>
@@ -1031,6 +1056,29 @@ export function NewAssessment(): JSX.Element {
)}
</div>
{revenueWarning !== null && (
<div
role="alert"
style={{
display: 'flex',
alignItems: 'flex-start',
gap: `${space(2)}px`,
padding: `${space(2)}px ${space(3)}px`,
marginBottom: `${space(3)}px`,
borderRadius: `${RADIUS.md}px`,
border: `1px solid ${colorVar('color.risk.high')}`,
backgroundColor: 'rgba(245,158,11,0.10)',
color: colorVar('color.text.primary'),
...typographyStyle('caption'),
}}
>
<span style={{ color: colorVar('color.risk.high'), flexShrink: 0, marginTop: 1 }}>
<Icon name="alert" size={16} />
</span>
<span><strong></strong>{revenueWarning}</span>
</div>
)}
<div style={{ ...typographyStyle('caption'), fontWeight: 700, color: colorVar('color.text.secondary'), marginBottom: `${space(2)}px` }}>
</div>