feat(operations): add auditable source provider maintenance
This commit is contained in:
@@ -30,23 +30,31 @@ function SourcePolicyRow({ vin, source, diagnostic, editable, onSaved }: {
|
||||
}) {
|
||||
const [enabled, setEnabled] = useState(source.enabled);
|
||||
const [priority, setPriority] = useState(source.priority);
|
||||
const [remark, setRemark] = useState('');
|
||||
const [providerName, setProviderName] = useState(source.providerOverride || '');
|
||||
const [providerEvidence, setProviderEvidence] = useState('');
|
||||
const [remark, setRemark] = useState(source.policyRemark || '');
|
||||
useEffect(() => {
|
||||
setEnabled(source.enabled);
|
||||
setPriority(source.priority);
|
||||
setRemark('');
|
||||
}, [source.enabled, source.priority, source.sourceRef]);
|
||||
setProviderName(source.providerOverride || '');
|
||||
setProviderEvidence('');
|
||||
setRemark(source.policyRemark || '');
|
||||
}, [source.enabled, source.priority, source.providerOverride, source.policyRemark, source.sourceRef]);
|
||||
const save = useMutation({
|
||||
mutationFn: () => api.updateVehicleSourcePolicy(vin, {
|
||||
version: diagnostic.policy.version,
|
||||
sourceRef: source.sourceRef ?? '',
|
||||
providerName,
|
||||
providerEvidence,
|
||||
enabled,
|
||||
priority,
|
||||
remark
|
||||
}),
|
||||
onSuccess: onSaved
|
||||
});
|
||||
const changed = enabled !== source.enabled || priority !== source.priority || remark.trim() !== '';
|
||||
const providerChanged = providerName.trim() !== (source.providerOverride || '');
|
||||
const policyChanged = enabled !== source.enabled || priority !== source.priority || remark.trim() !== (source.policyRemark || '');
|
||||
const changed = policyChanged || providerChanged;
|
||||
return <tr className={source.recommended ? 'is-recommended' : ''}>
|
||||
<td><strong>{source.sourceLabel}</strong><span>{source.terminalLabel || source.sourceKind || '未维护终端'}</span></td>
|
||||
<td><b>{source.protocol}</b><span>{source.selectedWithinProtocol ? '协议内已选' : '协议内候选'}</span></td>
|
||||
@@ -58,8 +66,10 @@ function SourcePolicyRow({ vin, source, diagnostic, editable, onSaved }: {
|
||||
<td className="v2-source-policy-cell">
|
||||
<label><input type="checkbox" checked={enabled} disabled={!editable || save.isPending} onChange={(event) => setEnabled(event.target.checked)} />启用</label>
|
||||
<input aria-label={`${source.sourceLabel} 优先级`} type="number" min="1" max="1000" value={priority} disabled={!editable || save.isPending} onChange={(event) => setPriority(Number(event.target.value))} />
|
||||
<input aria-label={`${source.sourceLabel} 策略备注`} value={remark} maxLength={200} disabled={!editable || save.isPending} onChange={(event) => setRemark(event.target.value)} placeholder="调整原因" />
|
||||
<button type="button" disabled={!editable || !changed || save.isPending || !source.sourceRef || priority < 1 || priority > 1000} onClick={() => save.mutate()}>{save.isPending ? '保存中' : '保存策略'}</button>
|
||||
<input className="v2-source-provider-input" aria-label={`${source.sourceLabel} 提供方`} value={providerName} maxLength={128} disabled={!editable || save.isPending} onChange={(event) => setProviderName(event.target.value)} placeholder="提供方,如 G7s" />
|
||||
<input className="v2-source-provider-evidence-input" aria-label={`${source.sourceLabel} 提供方核验依据`} value={providerEvidence} maxLength={255} disabled={!editable || save.isPending || !providerChanged} onChange={(event) => setProviderEvidence(event.target.value)} placeholder={providerChanged ? '权威终端清单、厂商确认记录等(必填)' : '修改提供方后填写核验依据'} />
|
||||
<input className="v2-source-policy-remark-input" aria-label={`${source.sourceLabel} 策略备注`} value={remark} maxLength={200} disabled={!editable || save.isPending} onChange={(event) => setRemark(event.target.value)} placeholder="启停或优先级调整原因(可选)" />
|
||||
<button type="button" disabled={!editable || !changed || save.isPending || !source.sourceRef || priority < 1 || priority > 1000 || (providerChanged && !providerEvidence.trim())} onClick={() => save.mutate()}>{save.isPending ? '保存中' : '保存策略'}</button>
|
||||
{save.isError ? <em role="alert">{save.error instanceof Error ? save.error.message : '保存失败'}</em> : null}
|
||||
</td>
|
||||
</tr>;
|
||||
@@ -127,10 +137,17 @@ function SourceDiagnosticWorkspace() {
|
||||
<div className="v2-source-recommendation"><strong>推荐说明</strong><p>{data.recommendationReason}</p><span>{data.refreshHint}</span></div>
|
||||
{!editable ? <p className="v2-source-readonly">当前账号可查看诊断证据;只有管理员可以调整启停和优先级。</p> : null}
|
||||
<div className="v2-source-table-wrap"><table className="v2-source-table"><thead><tr><th>来源 / 终端</th><th>协议</th><th>在线 / 质量</th><th>首次 / 最近上报</th><th>上报周期</th><th>位置 / 里程</th><th>选举结论</th><th>运维策略</th></tr></thead><tbody>
|
||||
{data.evidence.locationSources.map((source) => <SourcePolicyRow key={source.sourceRef || `${source.protocol}-${source.sourceLabel}-${source.terminalLabel}`} vin={data.evidence.vin} source={source} diagnostic={data} editable={editable && Boolean(source.sourceRef)} onSaved={(next) => queryClient.setQueryData(['ops-source-diagnostic', data.evidence.vin], next)} />)}
|
||||
{data.evidence.locationSources.map((source) => <SourcePolicyRow key={source.sourceRef || `${source.protocol}-${source.sourceLabel}-${source.terminalLabel}`} vin={data.evidence.vin} source={source} diagnostic={data} editable={editable && Boolean(source.sourceRef)} onSaved={(next) => {
|
||||
queryClient.setQueryData(['ops-source-diagnostic', data.evidence.vin], next);
|
||||
void Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['access-summary'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['access-vehicles'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['ops-source-readiness-v2'] })
|
||||
]);
|
||||
}} />)}
|
||||
</tbody></table></div>
|
||||
<section className="v2-source-audit"><header><strong>最近策略审计</strong><span>仅记录实际变更,原始 source_key 不对前端暴露</span></header>
|
||||
{data.policy.audit.length ? <ol>{data.policy.audit.map((item) => <li key={`${item.version}-${item.sourceRef}`}><b>v{item.version}</b><span>{item.summary}</span><em>{item.actor} · {fmt(item.changedAt)}</em></li>)}</ol> : <p>该车辆尚无人工来源策略变更。</p>}
|
||||
{data.policy.audit.length ? <ol>{data.policy.audit.map((item) => <li key={`${item.changeType}-${item.version}-${item.sourceRef}`}><b>v{item.version}</b><span>{item.summary}</span><em>{item.changeType === 'provider' ? '提供方' : '策略'} · {item.actor} · {fmt(item.changedAt)}</em></li>)}</ol> : <p>该车辆尚无人工来源策略或提供方变更。</p>}
|
||||
</section>
|
||||
</> : null}
|
||||
</section>;
|
||||
|
||||
Reference in New Issue
Block a user