feat: simplify SOP to immediate-effect configuration
This commit is contained in:
@@ -11,9 +11,10 @@ interface DetailResponse{run:RunDetail;events:RunEvent[];feedback:RunFeedback[];
|
||||
const route=useRoute();const router=useRouter();const auth=useAuthStore();const loading=ref(true);const detail=ref<DetailResponse|null>(null);const submitting=ref(false);const feedback=reactive({score:5,comment:''})
|
||||
const fieldMap=computed(()=>new Map((detail.value?.fields||[]).map(field=>[field.field_key,field.field_name])))
|
||||
const answers=computed(()=>Object.entries(detail.value?.run.answers||{}))
|
||||
const finalResult=computed(()=>detail.value?.run.final_result||null)
|
||||
const canFeedback=computed(()=>auth.can('runs.feedback')&&detail.value?.run.status==='completed'&&!detail.value.feedback.some(item=>item.user_id===auth.user?.user_id))
|
||||
function resultText(v:string){return{finish:'正常结束',escalate:'转人工 / 转诊',manual:'人工结束'}[v]||v||'进行中'}
|
||||
function actionText(v:string){return{start:'开始执行',enter:'进入节点',answer:'提交回答',finish:'人工结束'}[v]||v}
|
||||
function actionText(v:string){return{start:'开始执行',enter:'进入节点',answer:'提交回答',finish:'结束执行',final_result:'保存最终结果'}[v]||v}
|
||||
function valueText(value:any){if(value===true)return'是';if(value===false)return'否';if(Array.isArray(value))return value.join('、');if(value===null||value===undefined||value==='')return'-';return String(value)}
|
||||
function answerEntries(event:RunEvent){const values=event.payload?.answers||{};return Object.entries(values)}
|
||||
async function load(){loading.value=true;try{detail.value=await api.get<DetailResponse>(`/runs/${route.params.id}/detail`)}catch(error){message.error(apiMessage(error));router.replace('/runs')}finally{loading.value=false}}
|
||||
@@ -27,7 +28,7 @@ onMounted(load)
|
||||
<a-spin :spinning="loading">
|
||||
<template v-if="detail">
|
||||
<header class="detail-heading">
|
||||
<div><div class="run-code">RUN-{{ String(detail.run.id).padStart(5,'0') }}</div><h1>{{ detail.run.sop_name }}</h1><p>{{ detail.run.scenario_name }} · V{{ detail.run.version }} · {{ detail.run.operator_name }}</p></div>
|
||||
<div><div class="run-code">RUN-{{ String(detail.run.id).padStart(5,'0') }}</div><h1>{{ detail.run.sop_name }}</h1><p>{{ detail.run.scenario_name }} · {{ detail.run.operator_name }}</p></div>
|
||||
<div class="result-block" :class="detail.run.result==='escalate'?'warning':'success'"><SafetyCertificateOutlined v-if="detail.run.result==='escalate'"/><CheckCircleOutlined v-else/><span><small>{{ detail.run.status==='completed'?'执行结果':'当前状态' }}</small><b>{{ resultText(detail.run.result) }}</b></span></div>
|
||||
</header>
|
||||
|
||||
@@ -38,7 +39,8 @@ onMounted(load)
|
||||
<a-timeline-item v-for="event in detail.events" :key="event.id" :color="event.action==='finish'||event.node_type==='escalate'?'red':event.action==='answer'?'blue':'green'">
|
||||
<div class="event-head"><b>{{ event.node_title||event.node_key }}</b><a-tag>{{ actionText(event.action) }}</a-tag><time>{{ new Date(event.created_at).toLocaleString('zh-CN') }}</time></div>
|
||||
<p v-if="event.action==='enter'&&event.node_content">{{ event.node_content }}</p>
|
||||
<div v-if="event.action==='enter'&&event.knowledge" class="event-knowledge"><div><b>{{ event.knowledge.title }}</b><small>V{{ event.knowledge.version }}</small></div><p>{{ event.knowledge.standard_copy }}</p><p v-if="event.knowledge.risk_note" class="risk-note">{{ event.knowledge.risk_note }}</p></div>
|
||||
<div v-if="event.action==='enter'&&event.knowledge" class="event-knowledge"><div><b>{{ event.knowledge.title }}</b><small>历史知识</small></div><p>{{ event.knowledge.standard_copy }}</p><p v-if="event.knowledge.risk_note" class="risk-note">{{ event.knowledge.risk_note }}</p></div>
|
||||
<div v-for="group in event.outputs||[]" :key="group.key" class="event-knowledge"><div><b>{{ group.name }}</b><small>{{ group.type }}</small></div><div v-for="(items,relation) in group.relations" :key="relation"><p><b>{{ relation }}</b></p><p v-for="item in items" :key="item.key">{{ item.content.template || item.content.standard_copy || item.name }}</p></div></div>
|
||||
<div v-if="answerEntries(event).length" class="event-answers"><span v-for="[key,value] in answerEntries(event)" :key="key"><small>{{ fieldMap.get(key)||key }}</small><b>{{ valueText(value) }}</b></span></div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
@@ -47,6 +49,7 @@ onMounted(load)
|
||||
<aside class="detail-aside">
|
||||
<section class="surface summary-panel"><div class="section-title"><span>执行信息</span></div><dl><div><dt>状态</dt><dd><a-tag :color="detail.run.status==='completed'?'green':'orange'">{{ detail.run.status==='completed'?'已完成':'进行中' }}</a-tag></dd></div><div><dt>开始时间</dt><dd>{{ new Date(detail.run.started_at).toLocaleString('zh-CN') }}</dd></div><div><dt>完成时间</dt><dd>{{ detail.run.completed_at?new Date(detail.run.completed_at).toLocaleString('zh-CN'):'-' }}</dd></div><div><dt>当前节点</dt><dd>{{ detail.run.current_node_key }}</dd></div></dl></section>
|
||||
<section class="surface answer-panel"><div class="section-title"><span>已采集信息</span><small>{{ answers.length }} 项</small></div><div v-if="answers.length" class="answer-list"><div v-for="[key,value] in answers" :key="key"><span>{{ fieldMap.get(key)||key }}</span><b>{{ valueText(value) }}</b></div></div><div v-else class="aside-empty">尚未采集字段</div></section>
|
||||
<section class="surface answer-panel"><div class="section-title"><span>最终结果</span></div><pre v-if="finalResult" class="final-result">{{ JSON.stringify(finalResult,null,2) }}</pre><div v-else class="aside-empty">本次执行未收集最终结果</div></section>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
@@ -64,4 +67,5 @@ onMounted(load)
|
||||
<style scoped>
|
||||
.run-detail-page{max-width:1320px}.back-link{height:auto;margin:0 0 14px;padding:0}.detail-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:20px}.run-code{color:var(--green);font-family:monospace;font-size:12px;font-weight:750}.detail-heading h1{margin:7px 0 5px;font-family:"Noto Serif SC",serif;font-size:28px}.detail-heading p{margin:0;color:var(--muted)}.result-block{min-width:210px;display:flex;align-items:center;gap:12px;padding:15px 17px;border-left:3px solid}.result-block>span:first-child{font-size:25px}.result-block>span:last-child{display:flex;flex-direction:column}.result-block small{font-size:10px}.result-block b{margin-top:3px}.result-block.success{color:#17664f;background:#eaf4f0;border-color:#25866a}.result-block.warning{color:#8d5b16;background:#fbf2e3;border-color:#c08026}.detail-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:16px}.timeline-panel{min-height:560px;padding:22px 24px}.section-title{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:22px}.section-title span{font-weight:700}.section-title small{color:var(--muted);font-size:11px}.run-timeline{padding-top:5px}.event-head{display:flex;align-items:center;gap:9px;min-height:27px}.event-head time{margin-left:auto;color:#8b9691;font-size:11px}.run-timeline p{margin:8px 0 0;color:#56635d;line-height:1.65}.event-answers{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:10px}.event-answers span{display:flex;justify-content:space-between;gap:10px;padding:8px 10px;background:#f4f7f5;border-left:2px solid #a6cbbd}.event-answers small{color:var(--muted)}.event-answers b{font-size:12px}.detail-aside{display:flex;flex-direction:column;gap:16px}.summary-panel,.answer-panel{padding:20px}.summary-panel dl{margin:0}.summary-panel dl>div{display:flex;justify-content:space-between;gap:15px;padding:10px 0;border-bottom:1px solid #edf0ee}.summary-panel dl>div:last-child{border-bottom:0}.summary-panel dt{color:var(--muted);font-size:12px}.summary-panel dd{margin:0;text-align:right;font-size:12px}.answer-list>div{display:flex;flex-direction:column;padding:10px 0;border-bottom:1px solid #edf0ee}.answer-list>div:last-child{border-bottom:0}.answer-list span{color:var(--muted);font-size:11px}.answer-list b{margin-top:4px;font-size:13px}.aside-empty{padding:24px 0;color:#929d98;text-align:center;font-size:12px}.feedback-band{margin-top:16px;padding:22px 24px}.feedback-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.feedback-list article{padding:14px 0;border-top:1px solid var(--line)}.feedback-list article>div{display:flex;align-items:center;justify-content:space-between}.feedback-list :deep(.ant-rate){font-size:15px}.feedback-list p{margin:9px 0;color:#54615b}.feedback-list time{color:#929d98;font-size:11px}.feedback-form{display:grid;grid-template-columns:220px minmax(0,1fr) auto;align-items:center;gap:14px;padding-top:16px;border-top:1px solid var(--line)}.feedback-form>div{display:flex;flex-direction:column;gap:6px}.feedback-form>div>span{font-size:12px;font-weight:650}.feedback-form :deep(.ant-rate){font-size:20px}@media(max-width:900px){.detail-layout{grid-template-columns:1fr}.detail-aside{display:grid;grid-template-columns:1fr 1fr}.feedback-form{grid-template-columns:1fr}.feedback-list{grid-template-columns:1fr}}@media(max-width:620px){.detail-heading{align-items:flex-start;flex-direction:column}.result-block{width:100%}.detail-aside{grid-template-columns:1fr}.timeline-panel{padding:18px}.event-head{align-items:flex-start;flex-wrap:wrap}.event-head time{width:100%;margin:0}.event-answers{grid-template-columns:1fr}}
|
||||
.event-knowledge{margin-top:10px;padding:12px 14px;background:#f2f7f5;border-left:3px solid var(--green)}.event-knowledge>div{display:flex;justify-content:space-between;gap:12px}.event-knowledge small{color:var(--muted)}.event-knowledge .risk-note{padding-top:8px;border-top:1px solid #dce9e4;color:#8d5b16}
|
||||
.final-result{max-height:280px;overflow:auto;margin:0;padding:12px;color:#33413b;background:#f4f7f5;font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user