feat(data-process): 完善后台生成与失败重试

This commit is contained in:
caoxiaozhu
2026-07-28 10:56:05 +08:00
parent 8a6a6574bb
commit 01d2e6c76a
178 changed files with 4472 additions and 595 deletions

View File

@@ -1,23 +1,45 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { PreviewItem, ResultItem } from './types'
import type { BulkResultRegenerationState, PreviewItem, ResultItem } from './types'
const props = defineProps<{
items: ResultItem[]
previewItems: PreviewItem[]
selectedId: string | null
regeneratingResultId: string | null
bulkRegeneration: BulkResultRegenerationState
}>()
const emit = defineEmits<{
'update:selectedId': [value: string]
'update:field': [id: string, field: 'instruction' | 'input' | 'output', value: string]
'restore:item': [id: string]
'regenerate:item': [id: string]
'regenerate:all': []
}>()
const search = ref('')
const invalidOnly = ref(false)
const selectedItem = computed(() => props.items.find((item) => item.id === props.selectedId) ?? props.items[0])
const selectedIndex = computed(() => props.items.findIndex((item) => item.id === selectedItem.value?.id))
const invalidCount = computed(() => (
props.items.filter((item) => item.savedStatus === 'invalid').length
))
const bulkRegenerationActive = computed(() => props.bulkRegeneration.status === 'running')
const bulkRegenerationVisible = computed(() => (
props.bulkRegeneration.status !== 'idle' && props.bulkRegeneration.total > 0
))
const bulkRegenerationPercentage = computed(() => (
props.bulkRegeneration.total > 0
? Math.round((props.bulkRegeneration.completed / props.bulkRegeneration.total) * 100)
: 0
))
const itemRegenerating = (id: string) => (
props.regeneratingResultId === id
|| props.bulkRegeneration.targetIds.includes(id)
)
const selectedItemRegenerating = computed(() => (
selectedItem.value ? itemRegenerating(selectedItem.value.id) : false
))
const selectedSource = computed(() => {
const previewItemId = selectedItem.value?.previewItemId
if (!previewItemId) return null
@@ -67,8 +89,34 @@ function selectRelative(offset: number) {
<template>
<section class="result-step">
<div class="result-workspace">
<aside class="result-list-pane">
<div class="pane-header"><strong>生成结果</strong><span> {{ items.length }} </span></div>
<aside class="result-list-pane" :class="{ 'has-bulk-progress': bulkRegenerationVisible }">
<div class="pane-header result-list-header">
<div class="result-list-title"><strong>生成结果</strong><span> {{ items.length }} </span></div>
<el-button
v-if="invalidCount > 0"
size="small"
plain
type="primary"
:loading="bulkRegenerationActive"
:disabled="Boolean(regeneratingResultId) || bulkRegenerationActive"
@click="emit('regenerate:all')"
>
<i v-if="!bulkRegenerationActive" class="fa fa-refresh" style="margin-right: 4px;" />
{{ bulkRegenerationActive ? '重新生成中' : `全部重新生成(${invalidCount}` }}
</el-button>
</div>
<div v-if="bulkRegenerationVisible" class="bulk-regeneration-progress">
<div>
<span>已处理 {{ bulkRegeneration.completed }} / {{ bulkRegeneration.total }}</span>
<span>成功 {{ bulkRegeneration.succeeded }} · 失败 {{ bulkRegeneration.failed }}</span>
</div>
<el-progress
:percentage="bulkRegenerationPercentage"
:show-text="false"
:stroke-width="5"
:color="bulkRegeneration.failed > 0 ? '#d97706' : '#5b50f2'"
/>
</div>
<div class="result-toolbar">
<el-input v-model="search" clearable size="small" placeholder="搜索结果">
<template #prefix><i class="fa fa-search" /></template>
@@ -89,7 +137,12 @@ function selectRelative(offset: number) {
<strong>{{ item.instruction || '未填写指令' }}</strong>
<small>{{ item.output || '未填写输出' }}</small>
</span>
<i class="fa" :class="item.status === 'invalid' ? 'fa-exclamation-circle is-error' : 'fa-check-circle is-valid'" />
<i v-if="itemRegenerating(item.id)" class="css-spinner" />
<i
v-else
class="fa"
:class="item.status === 'invalid' ? 'fa-exclamation-circle is-error' : 'fa-check-circle is-valid'"
/>
</button>
</div>
</aside>
@@ -106,7 +159,19 @@ function selectRelative(offset: number) {
:type="selectedItem.qualityScore >= 80 ? 'success' : selectedItem.qualityScore >= 60 ? 'warning' : 'danger'"
>质量 {{ selectedItem.qualityScore.toFixed(1) }}</el-tag>
</div>
<el-button link @click="emit('restore:item', selectedItem.id)"><i class="fa fa-undo" /> 恢复生成结果</el-button>
<div class="result-header-actions">
<el-button
v-if="selectedItem.savedStatus === 'invalid'"
size="small"
plain
type="primary"
:loading="selectedItemRegenerating"
:disabled="bulkRegenerationActive || (Boolean(regeneratingResultId) && !selectedItemRegenerating)"
@click="emit('regenerate:item', selectedItem.id)"
>
<i v-if="!selectedItemRegenerating" class="fa fa-refresh" style="margin-right: 4px;" /> 重新生成
</el-button>
</div>
</div>
<article class="source-reference" aria-labelledby="source-reference-title">
@@ -126,6 +191,7 @@ function selectRelative(offset: number) {
<label>Instruction <em>必填</em></label>
<el-input
:model-value="selectedItem.instruction"
:disabled="selectedItemRegenerating"
type="textarea"
:rows="3"
@update:model-value="emit('update:field', selectedItem.id, 'instruction', $event)"
@@ -135,6 +201,7 @@ function selectRelative(offset: number) {
<label>Input <span>选填</span></label>
<el-input
:model-value="selectedItem.input"
:disabled="selectedItemRegenerating"
type="textarea"
:rows="2"
@update:model-value="emit('update:field', selectedItem.id, 'input', $event)"
@@ -144,6 +211,7 @@ function selectRelative(offset: number) {
<label>Output <em>必填</em></label>
<el-input
:model-value="selectedItem.output"
:disabled="selectedItemRegenerating"
type="textarea"
:rows="7"
@update:model-value="emit('update:field', selectedItem.id, 'output', $event)"
@@ -155,15 +223,6 @@ function selectRelative(offset: number) {
<div v-else class="validation-success">
<i class="fa fa-check-circle" /> 字段校验通过
</div>
<div v-if="selectedItem.qualityFlags?.length" class="quality-flags">
<el-tag
v-for="flag in selectedItem.qualityFlags"
:key="flag"
size="small"
type="warning"
effect="plain"
>{{ flag }}</el-tag>
</div>
<div class="editor-pagination">
<el-button :disabled="selectedIndex <= 0" @click="selectRelative(-1)">上一条</el-button>
<span>{{ selectedIndex + 1 }} / {{ items.length }}</span>
@@ -194,6 +253,25 @@ function selectRelative(offset: number) {
border-right: 1px solid #e5e8ee;
}
.result-list-title {
min-width: 0;
span {
margin-left: 8px;
color: #8a93a3;
font-size: 11px;
font-weight: 400;
}
}
.result-list-header {
gap: 10px;
:deep(.el-button) {
flex: none;
}
}
.pane-header {
display: flex;
align-items: center;
@@ -227,12 +305,37 @@ function selectRelative(offset: number) {
border-bottom: 1px solid #edf0f5;
}
.bulk-regeneration-progress {
padding: 9px 12px 10px;
background: #fafaff;
border-bottom: 1px solid #e8e7ff;
> div {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 7px;
color: #667085;
font-size: 10px;
}
}
.result-list {
height: 476px;
padding: 7px;
overflow: auto;
}
.has-bulk-progress .result-list {
height: 420px;
}
.result-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.result-item {
display: grid;
grid-template-columns: 46px minmax(0, 1fr) 18px;
@@ -294,6 +397,20 @@ function selectRelative(offset: number) {
color: #d97706;
}
.css-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(91, 80, 242, 0.2);
border-top-color: #5b50f2;
border-radius: 50%;
animation: css-spin 0.8s linear infinite;
display: inline-block;
}
@keyframes css-spin {
to { transform: rotate(360deg); }
}
.result-editor-pane {
min-width: 0;
}
@@ -389,12 +506,6 @@ function selectRelative(offset: number) {
border-radius: 6px;
}
.quality-flags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.validation-error {
color: #b45309;
background: #fff7e8;