feat(approval): add safe risk disposition workflow
This commit is contained in:
@@ -58,6 +58,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="risk-disposition-panel" aria-label="风险处置">
|
||||
<div class="risk-disposition-head">
|
||||
<div>
|
||||
<span>风险处置</span>
|
||||
<strong>{{ dispositionStateLabel }}</strong>
|
||||
</div>
|
||||
<div class="risk-disposition-badges">
|
||||
<em :class="currentDisposition.adjudication">
|
||||
{{ formatAdjudication(currentDisposition.adjudication) }}
|
||||
</em>
|
||||
<em :class="currentDisposition.lifecycleStatus">
|
||||
{{ formatLifecycleStatus(currentDisposition.lifecycleStatus) }}
|
||||
</em>
|
||||
<em>v{{ currentDisposition.version }}</em>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="currentDisposition.resolution" class="risk-disposition-resolution">
|
||||
{{ currentDisposition.resolution }}
|
||||
</p>
|
||||
<template v-if="dispositionActions.length">
|
||||
<textarea
|
||||
v-model="dispositionNote"
|
||||
:disabled="dispositionBusy"
|
||||
maxlength="1000"
|
||||
placeholder="填写复核依据、补充要求或解决说明(误报、补材料、豁免和解决时必填)"
|
||||
></textarea>
|
||||
<div class="risk-disposition-actions">
|
||||
<button
|
||||
v-for="action in dispositionActions"
|
||||
:key="action.action"
|
||||
type="button"
|
||||
:class="action.tone"
|
||||
:disabled="dispositionBusy"
|
||||
@click="submitDispositionAction(action.action)"
|
||||
>
|
||||
<i :class="dispositionBusyAction === action.action ? 'mdi mdi-loading mdi-spin' : action.icon"></i>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="risk-disposition-complete">{{ dispositionCompleteMessage }}</p>
|
||||
<p v-if="dispositionError" class="risk-disposition-error">{{ dispositionError }}</p>
|
||||
</section>
|
||||
|
||||
<div class="risk-evidence-grid">
|
||||
<section class="risk-evidence-section">
|
||||
<span class="risk-evidence-section-title">贡献分</span>
|
||||
@@ -141,7 +185,11 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { fetchClaimRiskObservations } from '../../services/riskObservations.js'
|
||||
import {
|
||||
createRiskDispositionRequestId,
|
||||
executeRiskDispositionAction,
|
||||
fetchClaimRiskObservations
|
||||
} from '../../services/riskObservations.js'
|
||||
|
||||
const props = defineProps({
|
||||
claimId: { type: String, default: '' }
|
||||
@@ -151,6 +199,10 @@ const observations = ref([])
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const activeObservationKey = ref('')
|
||||
const dispositionNote = ref('')
|
||||
const dispositionBusyAction = ref('')
|
||||
const dispositionError = ref('')
|
||||
const dispositionRequest = ref({ key: '', requestId: '' })
|
||||
const detailRegionId = 'risk-observation-active-detail'
|
||||
let loadSequence = 0
|
||||
|
||||
@@ -175,6 +227,92 @@ const activeObservationPosition = computed(() => {
|
||||
)
|
||||
return activeIndex >= 0 ? `${activeIndex + 1} / ${observations.value.length}` : '未选择'
|
||||
})
|
||||
const currentDisposition = computed(() => {
|
||||
if (mainObservation.value?.disposition) {
|
||||
return mainObservation.value.disposition
|
||||
}
|
||||
const feedbackStatus = String(mainObservation.value?.feedbackStatus || '').trim()
|
||||
const status = String(mainObservation.value?.status || '').trim()
|
||||
return {
|
||||
adjudication: ['confirmed', 'false_positive'].includes(feedbackStatus)
|
||||
? feedbackStatus
|
||||
: 'unreviewed',
|
||||
lifecycleStatus: status === 'resolved' ? 'resolved' : 'open',
|
||||
version: 0,
|
||||
resolution: ''
|
||||
}
|
||||
})
|
||||
const dispositionBusy = computed(() => Boolean(dispositionBusyAction.value))
|
||||
const dispositionStateLabel = computed(() => {
|
||||
if (currentDisposition.value.adjudication === 'false_positive') {
|
||||
return '该观察已判定为误报,不再阻断审批'
|
||||
}
|
||||
if (currentDisposition.value.lifecycleStatus === 'resolved') {
|
||||
return '风险已完成处置,不再阻断审批'
|
||||
}
|
||||
if (['high', 'critical'].includes(String(mainObservation.value?.riskLevel || ''))) {
|
||||
return '高风险未关闭,将阻断审批通过'
|
||||
}
|
||||
return '请结合证据完成人工裁决'
|
||||
})
|
||||
const dispositionActions = computed(() => {
|
||||
const adjudication = currentDisposition.value.adjudication
|
||||
const lifecycle = currentDisposition.value.lifecycleStatus
|
||||
if (adjudication === 'false_positive' || lifecycle === 'resolved') {
|
||||
return []
|
||||
}
|
||||
if (adjudication !== 'confirmed') {
|
||||
const actions = [
|
||||
{ action: 'confirm', label: '确认风险', icon: 'mdi mdi-shield-check', tone: 'danger' },
|
||||
{ action: 'false_positive', label: '标记误报', icon: 'mdi mdi-shield-off-outline', tone: 'safe' }
|
||||
]
|
||||
if (lifecycle !== 'supplement_requested') {
|
||||
actions.push({
|
||||
action: 'request_supplement',
|
||||
label: '请求补材料',
|
||||
icon: 'mdi mdi-file-plus-outline',
|
||||
tone: 'warning'
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
return [
|
||||
{
|
||||
action: 'request_supplement',
|
||||
label: '请求补材料',
|
||||
icon: 'mdi mdi-file-plus-outline',
|
||||
tone: 'warning'
|
||||
},
|
||||
{
|
||||
action: 'start_remediation',
|
||||
label: '启动整改',
|
||||
icon: 'mdi mdi-tools',
|
||||
tone: 'warning'
|
||||
},
|
||||
{
|
||||
action: 'request_waiver',
|
||||
label: '申请豁免',
|
||||
icon: 'mdi mdi-file-sign',
|
||||
tone: 'neutral'
|
||||
},
|
||||
{
|
||||
action: 'resolve',
|
||||
label: '确认已解决',
|
||||
icon: 'mdi mdi-check-decagram-outline',
|
||||
tone: 'safe'
|
||||
}
|
||||
].filter((item) => (
|
||||
item.action === 'resolve'
|
||||
|| (item.action === 'request_supplement' && lifecycle !== 'supplement_requested')
|
||||
|| (item.action === 'start_remediation' && lifecycle !== 'remediation_in_progress')
|
||||
|| (item.action === 'request_waiver' && lifecycle !== 'waiver_requested')
|
||||
))
|
||||
})
|
||||
const dispositionCompleteMessage = computed(() => (
|
||||
currentDisposition.value.adjudication === 'false_positive'
|
||||
? '该风险已标记为误报;如需更正,请由管理员通过审计流程处理。'
|
||||
: '该风险已解决,处置事件会保留在追加式审计链中。'
|
||||
))
|
||||
const scoreItems = computed(() => {
|
||||
const scores = mainObservation.value?.contributionScores || {}
|
||||
return Object.entries(scores).map(([key, value]) => {
|
||||
@@ -287,6 +425,50 @@ function selectObservation(item, index = -1) {
|
||||
const key = observationIdentity(item, index)
|
||||
if (key) {
|
||||
activeObservationKey.value = key
|
||||
dispositionNote.value = ''
|
||||
dispositionError.value = ''
|
||||
dispositionRequest.value = { key: '', requestId: '' }
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDispositionAction(action) {
|
||||
const observationId = String(mainObservation.value?.id || '').trim()
|
||||
if (!observationId || dispositionBusy.value) {
|
||||
return
|
||||
}
|
||||
const note = dispositionNote.value.trim()
|
||||
if (['false_positive', 'request_supplement', 'request_waiver', 'resolve'].includes(action) && !note) {
|
||||
dispositionError.value = '该处置动作需要填写复核依据或处理说明。'
|
||||
return
|
||||
}
|
||||
const requestKey = [observationId, action, currentDisposition.value.version, note].join(':')
|
||||
if (dispositionRequest.value.key !== requestKey) {
|
||||
dispositionRequest.value = {
|
||||
key: requestKey,
|
||||
requestId: createRiskDispositionRequestId(action, observationId)
|
||||
}
|
||||
}
|
||||
dispositionBusyAction.value = action
|
||||
dispositionError.value = ''
|
||||
try {
|
||||
await executeRiskDispositionAction(observationId, {
|
||||
action,
|
||||
expectedVersion: currentDisposition.value.version,
|
||||
requestId: dispositionRequest.value.requestId,
|
||||
comment: note,
|
||||
resolution: action === 'resolve' ? note : ''
|
||||
})
|
||||
dispositionNote.value = ''
|
||||
dispositionRequest.value = { key: '', requestId: '' }
|
||||
await loadObservations()
|
||||
} catch (error) {
|
||||
dispositionError.value = error?.message || '风险处置失败,请刷新状态后重试。'
|
||||
if (error?.code === 'RISK_DISPOSITION_VERSION_CONFLICT') {
|
||||
dispositionRequest.value = { key: '', requestId: '' }
|
||||
await loadObservations()
|
||||
}
|
||||
} finally {
|
||||
dispositionBusyAction.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +558,26 @@ function formatFeedbackStatus(value) {
|
||||
}
|
||||
return labels[String(value || '').trim()] || '未复核'
|
||||
}
|
||||
|
||||
function formatAdjudication(value) {
|
||||
const labels = {
|
||||
unreviewed: '待裁决',
|
||||
confirmed: '风险成立',
|
||||
false_positive: '误报'
|
||||
}
|
||||
return labels[String(value || '').trim()] || '待裁决'
|
||||
}
|
||||
|
||||
function formatLifecycleStatus(value) {
|
||||
const labels = {
|
||||
open: '待处置',
|
||||
supplement_requested: '待补材料',
|
||||
remediation_in_progress: '整改中',
|
||||
waiver_requested: '豁免申请中',
|
||||
resolved: '已解决'
|
||||
}
|
||||
return labels[String(value || '').trim()] || '待处置'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/risk-observation-evidence-card.css"></style>
|
||||
|
||||
Reference in New Issue
Block a user