吸收数字员工审批跨端冲突可视化

This commit is contained in:
baiyanyun
2026-06-11 21:53:08 +08:00
parent 84d34236e8
commit e57c94c19d
15 changed files with 625 additions and 208 deletions

View File

@@ -302,6 +302,24 @@ function approvalActionLabel(value?: string | null): string {
return normalized || '暂无动作';
}
function approvalConflictReasonLabel(value?: string | null): string {
const normalized = compactText(value).toLowerCase();
if (normalized === 'terminal_status_mismatch') return '终态不一致';
if (normalized === 'local_newer_status_mismatch') return '本地状态较新';
if (normalized === 'workflow_step_mismatch') return '审批步骤不一致';
return normalized || '跨端状态不一致';
}
function approvalStatusShortLabel(value?: string | null): string {
const normalized = compactText(value).toLowerCase();
if (normalized === 'approved') return '已同意';
if (normalized === 'rejected') return '已驳回';
if (normalized === 'cancelled') return '已取消';
if (normalized === 'pending') return '待确认';
if (normalized === 'dismissed') return '已忽略';
return normalized || '未知';
}
function dutyCardClass(planItem: DigitalPlanItem, selected: boolean): string {
const classes = ['de-template-card-row', 'de-duty-card', 'de-duty-card--compact'];
if (!planItem.implemented) classes.push('de-duty-card--disabled');
@@ -1464,7 +1482,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
await fetchProjection();
if (!mountedRef.current) return;
setActionNotice(result.ok
? `审批更新已拉取:应用 ${result.applied ?? 0} 条,忽略 ${result.ignored ?? 0} 条。`
? `审批更新已拉取:应用 ${result.applied ?? 0} 条,忽略 ${result.ignored ?? 0},冲突 ${result.conflicted ?? 0}`
: `审批更新拉取失败:${result.error || '未知错误'}`);
} catch (error) {
if (mountedRef.current) {
@@ -3363,6 +3381,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
const timeoutBusy = actionBusy === `${actionBusyPrefix}mark_timeout`;
const markRiskBusy = actionBusy === `${actionBusyPrefix}mark_risk`;
const clearRiskBusy = actionBusy === `${actionBusyPrefix}clear_risk`;
const approvalConflict = decision.conflict?.active ? decision.conflict : null;
return (
<div key={decision.id} className="de-task-manager-decision-card">
<div className="de-log-entry-meta">
@@ -3371,6 +3390,9 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
</div>
<p>{decision.scenario}</p>
<div className="de-approval-summary-grid">
{approvalConflict && (
<span className="de-badge de-badge-danger"></span>
)}
<span className={`de-badge ${badgeClass(approvalRiskTone(decision.risk_level))}`}>
{approvalRiskLabel(decision.risk_level)}
</span>
@@ -3386,6 +3408,15 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
<span>{decision.current_participant ? `处理人 ${decision.current_participant}` : '未指定处理人'}</span>
<span>{decision.next_step_label ? `下一步 ${decision.next_step_label}` : '无后续步骤'}</span>
</div>
{approvalConflict && (
<div className="de-decision-chain-note de-decision-chain-note--conflict">
<span className="de-badge de-badge-danger"></span>
<span>
{approvalConflictReasonLabel(approvalConflict.reason)} {approvalStatusShortLabel(approvalConflict.local_status ?? decision.status)} {approvalStatusShortLabel(approvalConflict.remote_status)}
{approvalConflict.detected_at ? ` 检测 ${formatBeijingDateTime(approvalConflict.detected_at) || approvalConflict.detected_at}` : ''}
</span>
</div>
)}
<div className="de-approval-action-fields">
<input
value={draft.comment}
@@ -3451,8 +3482,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
</button>
</div>
<div className="de-decision-chain-note">
<span className="de-badge de-badge-success"></span>
<span>{isDecisionOpen(decision.status) ? '处理结果会写入 qimingclaw runtime event。' : '该事项已在本地状态中处理。'}</span>
<span className={`de-badge ${approvalConflict ? 'de-badge-danger' : 'de-badge-success'}`}>{approvalConflict ? '冲突保护' : '本地记录'}</span>
<span>{approvalConflict ? '直接同意/驳回已暂停,请先同步或在管理端工作台排查。' : isDecisionOpen(decision.status) ? '处理结果会写入 qimingclaw runtime event。' : '该事项已在本地状态中处理。'}</span>
</div>
<div className="de-workbench-actions">
{decision.actions.map((action) => (
@@ -3460,7 +3491,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
key={action.id}
type="button"
className={action.tone === 'primary' ? 'de-workbench-action-accent' : ''}
disabled={actionBusy === `${decision.id}:${action.id}` || !isDecisionOpen(decision.status)}
disabled={actionBusy === `${decision.id}:${action.id}` || !isDecisionOpen(decision.status) || Boolean(approvalConflict)}
onClick={() => { void resolveDecision(decision, action.id); }}
>
{actionBusy === `${decision.id}:${action.id}` ? '处理中...' : action.label}