提取 GenerationOptionsPanel 组件统一管理生成模型、温度、最大长度、JSON 模式与质量过滤配置,StructuredProcessOptions 与 UnstructuredProcessOptions 继承 GenerationControlOptions,CreateView、TaskSetupStep 及各子步骤同步接入,详情/列表小幅调整,回归脚本扩充生成选项断言。
586 lines
14 KiB
Vue
586 lines
14 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, ref, watch } from 'vue'
|
||
import { sourceLines } from './previewModel'
|
||
import type { PreviewItem, ProcessType } from './types'
|
||
|
||
const props = defineProps<{
|
||
sourceText: string
|
||
items: PreviewItem[]
|
||
selectedId: string | null
|
||
processType: ProcessType
|
||
fileName: string
|
||
files: { id: string; name: string; count: number; modifiedCount: number }[]
|
||
selectedFileId: string | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:selectedId': [value: string]
|
||
'update:selectedFileId': [value: string]
|
||
'update:item-content': [id: string, value: string]
|
||
'remove:item': [id: string]
|
||
}>()
|
||
|
||
const sourceViewerRef = ref<HTMLElement | null>(null)
|
||
const search = ref('')
|
||
const currentPage = ref(1)
|
||
const PREVIEW_PAGE_SIZE = 6
|
||
const editingItemId = ref<string | null>(null)
|
||
const editorDraft = ref('')
|
||
const lines = computed(() => sourceLines(props.sourceText))
|
||
const selectedItem = computed(() => props.items.find((item) => item.id === props.selectedId) ?? props.items[0])
|
||
const editingItem = computed(() => props.items.find((item) => item.id === editingItemId.value))
|
||
|
||
const filteredItems = computed(() => props.items.filter((item, index) => {
|
||
const matchesSearch = !search.value.trim()
|
||
|| item.editedContent.toLowerCase().includes(search.value.trim().toLowerCase())
|
||
|| String(index + 1).includes(search.value.trim())
|
||
return matchesSearch
|
||
}))
|
||
|
||
const pagedItems = computed(() => {
|
||
const start = (currentPage.value - 1) * PREVIEW_PAGE_SIZE
|
||
return filteredItems.value.slice(start, start + PREVIEW_PAGE_SIZE)
|
||
})
|
||
|
||
const selectedIndex = computed(() => props.items.findIndex((item) => item.id === selectedItem.value?.id))
|
||
|
||
function isLineHighlighted(lineStart: number, lineEnd: number) {
|
||
const item = selectedItem.value
|
||
if (!item || item.sourceStart == null || item.sourceEnd == null) return false
|
||
return lineEnd >= item.sourceStart && lineStart <= item.sourceEnd
|
||
}
|
||
|
||
function selectItem(id: string) {
|
||
emit('update:selectedId', id)
|
||
}
|
||
|
||
function openEditor(item: PreviewItem) {
|
||
selectItem(item.id)
|
||
editingItemId.value = item.id
|
||
editorDraft.value = item.editedContent
|
||
}
|
||
|
||
function closeEditor() {
|
||
editingItemId.value = null
|
||
editorDraft.value = ''
|
||
}
|
||
|
||
function saveEditor() {
|
||
if (!editingItem.value) return
|
||
emit('update:item-content', editingItem.value.id, editorDraft.value)
|
||
closeEditor()
|
||
}
|
||
|
||
function removeItem(item: PreviewItem) {
|
||
selectItem(item.id)
|
||
emit('remove:item', item.id)
|
||
}
|
||
|
||
function handlePageChange() {
|
||
closeEditor()
|
||
}
|
||
|
||
watch(search, () => {
|
||
currentPage.value = 1
|
||
closeEditor()
|
||
})
|
||
|
||
watch(() => props.selectedFileId, closeEditor)
|
||
|
||
watch(selectedItem, async (item) => {
|
||
if (!item) return
|
||
const visibleIndex = filteredItems.value.findIndex((entry) => entry.id === item.id)
|
||
if (visibleIndex >= 0) {
|
||
currentPage.value = Math.floor(visibleIndex / PREVIEW_PAGE_SIZE) + 1
|
||
}
|
||
|
||
if (item.sourceStart == null) return
|
||
await nextTick()
|
||
const target = sourceViewerRef.value?.querySelector<HTMLElement>(`[data-source-start="${item.sourceStart}"]`)
|
||
?? sourceViewerRef.value?.querySelector<HTMLElement>('.source-line.is-highlighted')
|
||
target?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||
}, { immediate: true })
|
||
|
||
function itemNumber(item: PreviewItem) {
|
||
return props.items.findIndex((entry) => entry.id === item.id) + 1
|
||
}
|
||
|
||
function lineRange(item: PreviewItem) {
|
||
if (item.sourceStartLine == null || item.sourceEndLine == null) return '手动新增,无源文件定位'
|
||
return item.sourceStartLine === item.sourceEndLine
|
||
? `来源:第 ${item.sourceStartLine} 行`
|
||
: `来源:第 ${item.sourceStartLine}–${item.sourceEndLine} 行`
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="preview-step">
|
||
<div class="preview-file-switcher">
|
||
<div class="file-switcher-control">
|
||
<span>当前文件</span>
|
||
<el-select
|
||
:model-value="selectedFileId"
|
||
filterable
|
||
placeholder="选择文件"
|
||
aria-label="选择当前预览文件"
|
||
@update:model-value="emit('update:selectedFileId', $event)"
|
||
>
|
||
<el-option
|
||
v-for="file in files"
|
||
:key="file.id"
|
||
:label="file.name"
|
||
:value="file.id"
|
||
>
|
||
<div class="file-option">
|
||
<strong :title="file.name">{{ file.name }}</strong>
|
||
<span>{{ file.count.toLocaleString() }} {{ processType === 'unstructured' ? '个切片' : '条记录' }}</span>
|
||
<em v-if="file.modifiedCount">{{ file.modifiedCount }} 处已修改</em>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</div>
|
||
<span class="file-switcher-summary">
|
||
{{ files.length }} 个文件 · 当前文件 {{ items.length.toLocaleString() }} {{ processType === 'unstructured' ? '个切片' : '条记录' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="preview-workspace">
|
||
<div class="source-pane">
|
||
<div class="pane-header">
|
||
<div>
|
||
<strong>源文件 · {{ fileName }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div ref="sourceViewerRef" class="source-viewer" tabindex="0" aria-label="源文件内容">
|
||
<div
|
||
v-for="line in lines"
|
||
:key="line.number"
|
||
class="source-line"
|
||
:class="{ 'is-highlighted': isLineHighlighted(line.start, line.end) }"
|
||
:data-source-start="line.start"
|
||
>
|
||
<span class="line-number">{{ line.number }}</span>
|
||
<span class="line-content">{{ line.content || ' ' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="preview-pane">
|
||
<div class="pane-header">
|
||
<strong>{{ processType === 'unstructured' ? '切片内容' : '记录内容' }}</strong>
|
||
<span>共 {{ items.length.toLocaleString() }} 条</span>
|
||
</div>
|
||
|
||
<template v-if="!editingItem">
|
||
<div class="preview-toolbar">
|
||
<el-input v-model="search" clearable placeholder="搜索编号或内容" size="small">
|
||
<template #prefix><i class="fa fa-search" /></template>
|
||
</el-input>
|
||
</div>
|
||
|
||
<div class="preview-list" aria-label="预览条目列表">
|
||
<div
|
||
v-for="item in pagedItems"
|
||
:key="item.id"
|
||
class="preview-item"
|
||
:class="{ 'is-active': item.id === selectedItem?.id }"
|
||
role="button"
|
||
tabindex="0"
|
||
@click="selectItem(item.id)"
|
||
@keydown.enter="selectItem(item.id)"
|
||
@keydown.space.prevent="selectItem(item.id)"
|
||
>
|
||
<span class="item-name">{{ processType === 'unstructured' ? '切片' : '记录' }} #{{ String(itemNumber(item)).padStart(3, '0') }}</span>
|
||
<span class="item-source">{{ lineRange(item) }}</span>
|
||
<span class="item-actions">
|
||
<el-button link aria-label="编辑切片" title="编辑" @click.stop="openEditor(item)">
|
||
<i class="fa fa-pencil" />
|
||
</el-button>
|
||
<el-button link type="danger" aria-label="删除切片" title="删除" @click.stop="removeItem(item)">
|
||
<i class="fa fa-trash-o" />
|
||
</el-button>
|
||
</span>
|
||
</div>
|
||
<div v-if="!filteredItems.length" class="empty-result">没有符合条件的内容</div>
|
||
</div>
|
||
|
||
<el-pagination
|
||
v-if="filteredItems.length > PREVIEW_PAGE_SIZE"
|
||
v-model:current-page="currentPage"
|
||
:page-size="PREVIEW_PAGE_SIZE"
|
||
:total="filteredItems.length"
|
||
:pager-count="5"
|
||
small
|
||
background
|
||
layout="prev, pager, next"
|
||
class="preview-pagination"
|
||
@current-change="handlePageChange"
|
||
/>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<div class="preview-editor">
|
||
<div class="editor-heading">
|
||
<div>
|
||
<strong>{{ processType === 'unstructured' ? '切片' : '记录' }} #{{ String(itemNumber(editingItem)).padStart(3, '0') }} 正文</strong>
|
||
<small>{{ lineRange(editingItem) }}</small>
|
||
</div>
|
||
</div>
|
||
<el-input
|
||
v-model="editorDraft"
|
||
type="textarea"
|
||
:rows="7"
|
||
resize="none"
|
||
/>
|
||
<div class="editor-actions">
|
||
<div>
|
||
<el-button @click="closeEditor">取消</el-button>
|
||
<el-button type="primary" @click="saveEditor">保存修改</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.preview-step {
|
||
min-width: 0;
|
||
}
|
||
|
||
.preview-file-switcher {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
min-height: 58px;
|
||
padding: 10px 14px;
|
||
margin-bottom: 12px;
|
||
background: #fff;
|
||
border: 1px solid #e2e5ec;
|
||
border-radius: 9px;
|
||
}
|
||
|
||
.file-switcher-control {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
gap: 10px;
|
||
|
||
> span {
|
||
flex: none;
|
||
color: #667085;
|
||
font-size: 12px;
|
||
}
|
||
|
||
:deep(.el-select) {
|
||
width: min(360px, 42vw);
|
||
}
|
||
}
|
||
|
||
.file-switcher-summary {
|
||
color: #7d8798;
|
||
font-size: 12px;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.file-option {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||
align-items: center;
|
||
gap: 12px;
|
||
max-width: 440px;
|
||
|
||
strong,
|
||
span,
|
||
em {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
strong {
|
||
color: #344054;
|
||
font-size: 13px;
|
||
font-style: normal;
|
||
}
|
||
|
||
span {
|
||
color: #98a2b3;
|
||
font-size: 11px;
|
||
}
|
||
|
||
em {
|
||
color: #5549dc;
|
||
font-size: 11px;
|
||
font-style: normal;
|
||
}
|
||
}
|
||
|
||
.preview-workspace {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 58fr) minmax(380px, 42fr);
|
||
height: clamp(560px, calc(100vh - 370px), 720px);
|
||
overflow: hidden;
|
||
border: 1px solid #e2e5ec;
|
||
border-radius: 9px;
|
||
}
|
||
|
||
.source-pane,
|
||
.preview-pane {
|
||
display: flex;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
}
|
||
|
||
.source-pane {
|
||
border-right: 1px solid #e5e8ee;
|
||
}
|
||
|
||
.pane-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 52px;
|
||
padding: 0 15px;
|
||
color: #313949;
|
||
background: #fff;
|
||
border-bottom: 1px solid #e8ebf0;
|
||
font-size: 13px;
|
||
|
||
> div {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
> span {
|
||
color: #8a93a3;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.source-viewer {
|
||
flex: 1;
|
||
height: 538px;
|
||
padding: 12px 0 24px;
|
||
overflow: auto;
|
||
outline: none;
|
||
background: #fff;
|
||
scroll-behavior: smooth;
|
||
}
|
||
|
||
.source-line {
|
||
display: grid;
|
||
grid-template-columns: 48px minmax(0, 1fr);
|
||
min-height: 29px;
|
||
color: #424b5d;
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
border-left: 3px solid transparent;
|
||
transition: background-color 0.18s ease, border-color 0.18s ease;
|
||
|
||
&.is-highlighted {
|
||
background: #eeedff;
|
||
border-left-color: #5b50f2;
|
||
}
|
||
}
|
||
|
||
.line-number {
|
||
padding-right: 11px;
|
||
color: #a0a7b4;
|
||
text-align: right;
|
||
user-select: none;
|
||
}
|
||
|
||
.line-content {
|
||
min-width: 0;
|
||
padding: 2px 14px 2px 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.preview-toolbar {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid #edf0f5;
|
||
}
|
||
|
||
.preview-list {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
padding: 8px;
|
||
overflow: auto;
|
||
border-bottom: 1px solid #e8ebf0;
|
||
}
|
||
|
||
.preview-pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
min-height: 38px;
|
||
padding: 6px 12px;
|
||
border-bottom: 1px solid #e8ebf0;
|
||
}
|
||
|
||
.preview-item {
|
||
display: grid;
|
||
grid-template-columns: 94px minmax(130px, 1fr) auto;
|
||
align-items: center;
|
||
width: 100%;
|
||
min-height: 40px;
|
||
padding: 0 10px;
|
||
color: #6b7382;
|
||
text-align: left;
|
||
background: #fff;
|
||
border: 1px solid transparent;
|
||
border-bottom-color: #edf0f5;
|
||
cursor: pointer;
|
||
|
||
&:hover {
|
||
background: #fafaff;
|
||
}
|
||
|
||
&.is-active {
|
||
color: #3f36c8;
|
||
background: #f6f5ff;
|
||
border-color: #5b50f2;
|
||
border-radius: 6px;
|
||
}
|
||
}
|
||
|
||
.item-name {
|
||
color: #344054;
|
||
font-size: 12px;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.item-source,
|
||
.item-actions {
|
||
overflow: hidden;
|
||
font-size: 11px;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.item-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
|
||
:deep(.el-button) {
|
||
width: 28px;
|
||
min-height: 28px;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
.empty-result {
|
||
padding: 34px 16px;
|
||
color: #98a2b3;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.preview-editor {
|
||
display: flex;
|
||
min-height: 0;
|
||
flex: 1 1 auto;
|
||
flex-direction: column;
|
||
padding: 13px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.editor-heading {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 10px;
|
||
|
||
strong,
|
||
small {
|
||
display: block;
|
||
}
|
||
|
||
strong {
|
||
color: #344054;
|
||
font-size: 12px;
|
||
}
|
||
|
||
small {
|
||
margin-top: 4px;
|
||
color: #98a2b3;
|
||
font-size: 10px;
|
||
}
|
||
}
|
||
|
||
.preview-editor :deep(.el-textarea__inner) {
|
||
min-height: 260px !important;
|
||
color: #3f4756;
|
||
font-size: 12px;
|
||
line-height: 1.75;
|
||
}
|
||
|
||
.editor-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
margin-top: auto;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
@media (max-width: 1100px) {
|
||
.preview-workspace {
|
||
grid-template-columns: minmax(0, 52fr) minmax(360px, 48fr);
|
||
}
|
||
|
||
.preview-item {
|
||
grid-template-columns: 88px minmax(0, 1fr) auto;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.preview-file-switcher {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-switcher-control {
|
||
width: 100%;
|
||
|
||
:deep(.el-select) {
|
||
flex: 1;
|
||
width: auto;
|
||
}
|
||
}
|
||
|
||
.file-switcher-summary {
|
||
text-align: left;
|
||
white-space: normal;
|
||
}
|
||
|
||
.preview-workspace {
|
||
grid-template-columns: minmax(0, 1fr);
|
||
height: auto;
|
||
}
|
||
|
||
.source-pane {
|
||
border-right: 0;
|
||
border-bottom: 1px solid #e5e8ee;
|
||
}
|
||
|
||
.source-viewer {
|
||
height: 320px;
|
||
flex: none;
|
||
}
|
||
}
|
||
</style>
|