update
This commit is contained in:
@@ -1,22 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ArrowLeftOutlined, CheckCircleOutlined, ClockCircleOutlined, SafetyCertificateOutlined } from '@ant-design/icons-vue'
|
||||
import { ArrowLeftOutlined, CheckCircleOutlined, SafetyCertificateOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api, apiMessage } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { RunDetail, RunEvent, RunFeedback, ScenarioField } from '@/types'
|
||||
import type { RunDetail, RunDimension, RunEvent, RunFeedback, ScenarioField, ScriptFeedback } from '@/types'
|
||||
|
||||
interface DetailResponse{run:RunDetail;events:RunEvent[];feedback:RunFeedback[];fields:ScenarioField[]}
|
||||
interface DetailResponse{run:RunDetail;events:RunEvent[];feedback:RunFeedback[];fields:ScenarioField[];dimensions:RunDimension[];script_feedback:ScriptFeedback[]}
|
||||
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 dimensionWeights=computed(()=>(detail.value?.dimensions||[]).filter(item=>item.weight>0).sort((a,b)=>b.weight-a.weight))
|
||||
const feedbackTypeText=computed(()=>({like:'有帮助',report:'不准确',unreasonable:'不合理'} as Record<string,string>))
|
||||
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:'结束执行',final_result:'保存最终结果'}[v]||v}
|
||||
function actionText(v:string){return{start:'开始执行',enter:'进入节点',answer:'提交回答',next:'进入下一步',back:'返回上一步',script_used:'使用话术',script_feedback:'话术反馈',reset:'重置执行',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)}
|
||||
function eventExtra(event:RunEvent){if(event.action==='answer'&&event.payload?.script_key){return `话术 ${event.payload.script_key} · 选项 ${(event.payload.option_keys||[]).join('、')||'-'}`}if(event.action==='answer'&&event.payload?.dimension_value_key){return `勾选维度值 ${event.payload.dimension_value_key}=${event.payload.selected}`}if(event.action==='script_feedback'){return `话术 ${event.payload.script_key} · ${feedbackTypeText.value[event.payload.feedback_type]||event.payload.feedback_type}`}return ''}
|
||||
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}}
|
||||
async function submitFeedback(){submitting.value=true;try{await api.post(`/runs/${route.params.id}/feedback`,feedback);message.success('反馈已提交');await load()}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
|
||||
onMounted(load)
|
||||
@@ -36,10 +39,10 @@ onMounted(load)
|
||||
<main class="surface timeline-panel">
|
||||
<div class="section-title"><span>执行轨迹</span><small>按实际发生顺序记录</small></div>
|
||||
<a-timeline class="run-timeline">
|
||||
<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'">
|
||||
<a-timeline-item v-for="event in detail.events" :key="event.id" :color="event.action==='finish'||event.node_type==='escalate'?'red':['answer','next','back'].includes(event.action)?'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-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>
|
||||
<p v-if="eventExtra(event)" class="event-extra">{{ eventExtra(event) }}</p>
|
||||
<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,8 +50,10 @@ 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>{{ dimensionWeights.length }} 个有效值</small></div><div v-if="dimensionWeights.length" class="answer-list"><div v-for="item in dimensionWeights" :key="`${item.dimension_id}-${item.value_key}`"><span>{{ item.value_key }}</span><b>{{ item.weight }}</b></div></div><div v-else class="aside-empty">暂无有效维度</div></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>
|
||||
<section v-if="detail.script_feedback.length" class="surface answer-panel"><div class="section-title"><span>话术反馈</span><small>{{ detail.script_feedback.length }} 条</small></div><div class="answer-list"><div v-for="item in detail.script_feedback" :key="item.id"><span>话术 #{{ item.script_id }}</span><b>{{ feedbackTypeText[item.feedback_type]||item.feedback_type }}</b></div></div></section>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
@@ -64,7 +69,5 @@ onMounted(load)
|
||||
</template>
|
||||
|
||||
<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}
|
||||
.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-extra{margin-top:6px;padding:7px 10px;color:#7c5a20;background:#fbf2e3;border-left:2px solid #c08026;font-size:12px}.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;font-size:12px;text-align:right}.answer-list{display:grid;gap:8px}.answer-list>div{display:flex;justify-content:space-between;gap:12px;padding:9px 11px;background:#f4f7f5;border-left:2px solid #a6cbbd}.answer-list span{min-width:0;color:var(--muted);font-size:12px;overflow-wrap:anywhere}.answer-list b{font-size:12px;text-align:right;overflow-wrap:anywhere}.aside-empty{padding:20px 0;color:#98a39e;font-size:12px;text-align:center}.feedback-band{padding:22px 24px}.feedback-list{display:grid;gap:10px}.feedback-list article{padding:12px 14px;background:#f6f8f7;border-left:3px solid var(--green)}.feedback-list article>div{display:flex;justify-content:space-between}.feedback-list p{margin:8px 0 4px;color:#4c5a54}.feedback-list time{color:#96a19c;font-size:11px}.feedback-form{display:grid;gap:10px;max-width:560px}.feedback-form>div{display:flex;align-items:center;gap:14px}.feedback-form .ant-btn{width:130px;justify-self:end}.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}@media(max-width:900px){.detail-layout{grid-template-columns:1fr}.detail-heading{align-items:flex-start;flex-direction:column}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user