Files
iqudo-top1/web/src/views/ExecuteView.vue
2026-08-06 21:52:03 +08:00

66 lines
8.7 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { CheckCircleOutlined, PlayCircleOutlined, SafetyCertificateOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { api, apiMessage } from '@/api/client'
import type { ScenarioField, SOPNode, SOPRun } from '@/types'
interface PublishedSOP { id:number; name:string; description:string; scenario_id:number; scenario_name:string; version:number }
interface RunView { run:SOPRun; node:SOPNode; fields:ScenarioField[] }
const loading=ref(true);const sops=ref<PublishedSOP[]>([]);const active=ref<RunView|null>(null);const answers=reactive<Record<string,any>>({});const submitting=ref(false)
const currentConfig=computed(()=>active.value?.node.config||{})
const currentFields=computed(()=>{if(!active.value)return[];const node=active.value.node;if(node.type==='question'||node.type==='choice')return active.value.fields.filter(f=>f.field_key===currentConfig.value.field_key);if(node.type==='form')return active.value.fields.filter(f=>(currentConfig.value.field_keys||[]).includes(f.field_key));return[]})
async function load(){loading.value=true;try{sops.value=(await api.get<{items:PublishedSOP[]}>('/published-sops')).items}catch(error){message.error(apiMessage(error))}finally{loading.value=false}}
async function start(sopID:number){try{active.value=await api.post<RunView>('/runs',{sop_id:sopID});Object.keys(answers).forEach(k=>delete answers[k])}catch(error){message.error(apiMessage(error))}}
async function next(){if(!active.value)return;submitting.value=true;try{active.value=await api.post<RunView>(`/runs/${active.value.run.id}/answer`,{answers:{...answers}});Object.keys(answers).forEach(k=>delete answers[k])}catch(error){message.error(apiMessage(error))}finally{submitting.value=false}}
function inputFor(field:ScenarioField){return field.field_type}
function reset(){active.value=null;load()}
onMounted(load)
</script>
<template>
<div class="page-shell execution-shell">
<div class="page-heading"><div><h1>æ§è¡Œè¯<EFBFBD>术</h1><p>鿩已å<EFBFBD>布 SOP,系统会根æ<EFBFBD>®å®¢æˆ·åžç­ç»åºä¸ä¸æ­¥ã</p></div></div>
<a-spin :spinning="loading">
<div v-if="!active" class="sop-catalog">
<button v-for="item in sops" :key="item.id" class="sop-entry surface" @click="start(item.id)"><span class="entry-seq">{{ String(item.id).padStart(2,'0') }}</span><div><small>{{ item.scenario_name }} · V{{ item.version }}</small><h2>{{ item.name }}</h2><p>{{ item.description || '按标准步骤执行这套è¯<C3A8>术æµ<C3A6>ç¨ã€' }}</p></div><span class="play"><PlayCircleOutlined /></span></button>
<a-empty v-if="!sops.length" description="还没有已å<C2B2>布的 SOP" />
</div>
<div v-else class="run-workspace">
<aside class="run-context">
<span class="run-label">RUN-{{ String(active.run.id).padStart(5,'0') }}</span>
<h2>æ§è¡Œè¿è¡Œä¸­</h2><p>客户åžç­ä¼šèªåЍä¿<EFBFBD>存在å½å<EFBFBD>æ§è¡Œè®°å½ä¸­ã</p>
<div class="context-meta"><span>å½å<EFBFBD>èŠç¹</span><b>{{ active.node.title }}</b></div><div class="context-meta"><span>å·²ééå­æ®µ</span><b>{{ Object.keys(active.run.answers || {}).length }}</b></div>
<div class="privacy-note"><SafetyCertificateOutlined /><span>æ<EFBFBD>æŸä¿¡æ<EFBFBD>¯è¯·é<EFBFBD>µå¾ªä¼<EFBFBD>ä¸šæ°æ<EFBFBD>®è§èŒƒ</span></div>
</aside>
<main class="conversation surface">
<div class="conversation-progress"><span :class="{done:active.run.status==='completed'}"></span><b>{{ active.run.status==='completed'?'æµ<C3A6>ç¨å·²å®Œæˆ<C3A6>':'正在执行' }}</b></div>
<div class="node-content"><small>{{ active.node.type.toUpperCase() }}</small><h1>{{ active.node.title }}</h1><blockquote v-if="active.node.content">{{ active.node.content }}</blockquote></div>
<div v-if="active.run.status==='completed'" class="completed-state"><CheckCircleOutlined /><h3>{{ active.node.type==='escalate'?'已转交处ç<E2809E>†':'本次执行已完æˆ<C3A6>' }}</h3><p>{{ active.node.content }}</p><a-button type="primary" @click="reset">æ§è¡Œå<EFBFBD>¦ä¸å¥ SOP</a-button></div>
<template v-else>
<a-form layout="vertical" class="answer-form">
<a-form-item v-for="field in currentFields" :key="field.id" :label="field.field_name" :required="field.required">
<a-input v-if="inputFor(field)==='text'" v-model:value="answers[field.field_key]" />
<a-textarea v-else-if="inputFor(field)==='textarea'" v-model:value="answers[field.field_key]" :rows="3" />
<a-input-number v-else-if="inputFor(field)==='number'" v-model:value="answers[field.field_key]" style="width:100%" />
<a-radio-group v-else-if="inputFor(field)==='boolean'" v-model:value="answers[field.field_key]"><a-radio :value="true">是</a-radio><a-radio :value="false">å<EFBFBD>¦</a-radio></a-radio-group>
<a-select v-else-if="inputFor(field)==='select'" v-model:value="answers[field.field_key]" :options="(field.options||[]).map(value=>({value,label:value}))" />
<a-select v-else-if="inputFor(field)==='multiselect'" v-model:value="answers[field.field_key]" mode="multiple" :options="(field.options||[]).map(value=>({value,label:value}))" />
<a-date-picker v-else-if="inputFor(field)==='date'" v-model:value="answers[field.field_key]" style="width:100%" />
<a-input v-else v-model:value="answers[field.field_key]" />
</a-form-item>
<a-form-item v-if="active.node.type==='choice'&&currentFields.length===0"><a-radio-group v-model:value="answers[currentConfig.field_key]" class="choice-group"><a-radio-button v-for="option in currentConfig.options||[]" :key="option" :value="option">{{ option }}</a-radio-button></a-radio-group></a-form-item>
</a-form>
<div class="run-actions"><span>{{ currentFields.length ? '填写å<E284A2>Žç»§ç»­ä¸ä¸€æ­¥' : '确认当å‰<C3A5>è¯<C3A8>术已完æˆ<C3A6>' }}</span><a-button type="primary" size="large" :loading="submitting" @click="next">ç»§ç»­ä¸ä¸æ­¥</a-button></div>
</template>
</main>
</div>
</a-spin>
</div>
</template>
<style scoped>
.execution-shell{max-width:1220px}.sop-catalog{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.sop-entry{min-height:150px;display:grid;grid-template-columns:40px 1fr 42px;align-items:start;gap:14px;padding:22px;text-align:left;cursor:pointer}.sop-entry:hover{border-color:#9ac8b8;box-shadow:0 8px 24px rgba(32,40,37,.07)}.entry-seq{color:#8c9893;font-family:monospace;font-size:12px}.sop-entry small{color:var(--green);font-weight:650}.sop-entry h2{margin:8px 0 7px;font-family:"Noto Serif SC",serif;font-size:20px}.sop-entry p{margin:0;color:var(--muted);line-height:1.6}.play{width:38px;height:38px;display:grid;place-items:center;color:white;background:#202825;border-radius:4px;font-size:18px}.run-workspace{display:grid;grid-template-columns:270px minmax(0,1fr);gap:18px}.run-context{align-self:start;padding:24px;color:white;background:#202825;border-radius:6px;position:sticky;top:86px}.run-label{color:#72c9ab;font-size:11px;font-weight:700}.run-context h2{margin:12px 0 8px;font-family:"Noto Serif SC",serif}.run-context>p{margin:0 0 28px;color:#aebdb7;line-height:1.6}.context-meta{display:flex;align-items:center;justify-content:space-between;padding:13px 0;border-top:1px solid #3a4742}.context-meta span{color:#9eada7;font-size:12px}.privacy-note{display:flex;gap:8px;margin-top:28px;color:#889b93;font-size:11px}.conversation{min-height:590px;padding:30px 34px}.conversation-progress{display:flex;align-items:center;gap:8px;color:#66736d;font-size:12px}.conversation-progress span{width:8px;height:8px;border-radius:50%;background:#d08736;box-shadow:0 0 0 4px #faefe1}.conversation-progress span.done{background:var(--green);box-shadow:0 0 0 4px #e4f1ec}.node-content{padding:44px 0 26px}.node-content small{color:var(--green);font-size:10px;font-weight:800}.node-content h1{margin:8px 0 22px;font-family:"Noto Serif SC",serif;font-size:28px}.node-content blockquote{margin:0;padding:18px 20px;color:#29342f;background:#f2f6f4;border-left:3px solid var(--green);font-size:17px;line-height:1.8}.answer-form{max-width:680px}.choice-group{display:flex;flex-wrap:wrap}.run-actions{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:24px -34px -30px;padding:18px 34px;border-top:1px solid var(--line);color:var(--muted);font-size:12px}.completed-state{padding:40px 0;text-align:center}.completed-state>span{color:var(--green);font-size:50px}.completed-state h3{margin:14px 0 8px;font-family:"Noto Serif SC",serif;font-size:24px}.completed-state p{margin:0 0 24px;color:var(--muted)}
@media(max-width:800px){.sop-catalog{grid-template-columns:1fr}.run-workspace{grid-template-columns:1fr}.run-context{position:static}.conversation{padding:22px}.run-actions{margin:24px -22px -22px;padding:16px 22px}.run-actions span{display:none}}
</style>