feat: 实现业务视图页面
登录、模型调优、评测、推理、对比、模型管理、数据集、数据处理、工具、系统(硬件/日志/训练日志)等全部业务页面视图。
This commit is contained in:
215
frontend/src/views/data-process/create/GenerationStep.vue
Normal file
215
frontend/src/views/data-process/create/GenerationStep.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import type { GenerationState, ProcessType } from './types'
|
||||
|
||||
defineProps<{
|
||||
taskName: string
|
||||
processType: ProcessType
|
||||
fileName: string
|
||||
previewCount: number
|
||||
modifiedCount: number
|
||||
generation: GenerationState
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
stop: []
|
||||
retry: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="generation-step">
|
||||
<div class="generation-layout">
|
||||
<div class="summary-panel">
|
||||
<div class="panel-heading">
|
||||
<strong>任务摘要</strong>
|
||||
<span>已完成预览确认</span>
|
||||
</div>
|
||||
<dl>
|
||||
<div><dt>任务名称</dt><dd>{{ taskName }}</dd></div>
|
||||
<div><dt>数据类型</dt><dd>{{ processType === 'unstructured' ? '非结构化数据' : '结构化数据' }}</dd></div>
|
||||
<div><dt>源文件</dt><dd>{{ fileName }}</dd></div>
|
||||
<div><dt>预览条目</dt><dd>{{ previewCount.toLocaleString() }} 条</dd></div>
|
||||
<div><dt>已修改</dt><dd>{{ modifiedCount.toLocaleString() }} 条</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="run-panel" :class="`is-${generation.status}`">
|
||||
<div class="run-icon">
|
||||
<i v-if="generation.status === 'success'" class="fa fa-check" />
|
||||
<i v-else-if="generation.status === 'failed'" class="fa fa-exclamation" />
|
||||
<i v-else-if="generation.status === 'running'" class="fa fa-cog fa-spin" />
|
||||
<i v-else class="fa fa-play" />
|
||||
</div>
|
||||
<h3>
|
||||
{{ generation.status === 'idle' ? '准备开始处理'
|
||||
: generation.status === 'running' ? '正在生成数据'
|
||||
: generation.status === 'success' ? '数据生成完成'
|
||||
: '生成已停止' }}
|
||||
</h3>
|
||||
<p>{{ generation.message }}</p>
|
||||
<el-progress
|
||||
v-if="generation.status !== 'idle'"
|
||||
:percentage="generation.progress"
|
||||
:stroke-width="10"
|
||||
:status="generation.status === 'success' ? 'success' : undefined"
|
||||
/>
|
||||
<div class="run-meta">
|
||||
<span>解析源数据</span>
|
||||
<span>应用预览修改</span>
|
||||
<span>生成标准结果</span>
|
||||
</div>
|
||||
<el-button v-if="generation.status === 'running'" plain type="warning" @click="emit('stop')">
|
||||
停止生成
|
||||
</el-button>
|
||||
<el-button v-if="generation.status === 'failed'" plain type="primary" @click="emit('retry')">
|
||||
重新生成
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.generation-step {
|
||||
max-width: 1040px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
.generation-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.78fr) minmax(420px, 1.22fr);
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.summary-panel,
|
||||
.run-panel {
|
||||
border: 1px solid #e2e5ec;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 17px;
|
||||
background: #fbfcfe;
|
||||
border-bottom: 1px solid #e8ebf0;
|
||||
|
||||
strong {
|
||||
color: #344054;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #2ca66a;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: 8px 17px;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 13px 0;
|
||||
border-bottom: 1px solid #eef0f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: #8a93a3;
|
||||
}
|
||||
|
||||
dd {
|
||||
max-width: 65%;
|
||||
overflow: hidden;
|
||||
color: #344054;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.run-panel {
|
||||
display: flex;
|
||||
min-height: 360px;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 34px 48px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
|
||||
h3 {
|
||||
margin: 18px 0 8px;
|
||||
color: #2e3646;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
min-height: 22px;
|
||||
margin: 0 0 24px;
|
||||
color: #7b8495;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-progress) {
|
||||
width: 100%;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.run-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
color: #5b50f2;
|
||||
font-size: 22px;
|
||||
background: #f0efff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.run-panel.is-success .run-icon {
|
||||
color: #2ca66a;
|
||||
background: #eaf8f1;
|
||||
}
|
||||
|
||||
.run-panel.is-failed .run-icon {
|
||||
color: #d97706;
|
||||
background: #fff7e8;
|
||||
}
|
||||
|
||||
.run-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
color: #98a2b3;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.generation-layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
585
frontend/src/views/data-process/create/PreviewCompareStep.vue
Normal file
585
frontend/src/views/data-process/create/PreviewCompareStep.vue
Normal file
@@ -0,0 +1,585 @@
|
||||
<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: #fbfcfe;
|
||||
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>
|
||||
315
frontend/src/views/data-process/create/ResultEditorStep.vue
Normal file
315
frontend/src/views/data-process/create/ResultEditorStep.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { ResultItem } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
items: ResultItem[]
|
||||
selectedId: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:selectedId': [value: string]
|
||||
'update:field': [id: string, field: 'instruction' | 'input' | 'output', value: string]
|
||||
'restore:item': [id: string]
|
||||
}>()
|
||||
|
||||
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 filteredItems = computed(() => props.items.filter((item, index) => {
|
||||
const keyword = search.value.trim().toLowerCase()
|
||||
const matchesSearch = !keyword
|
||||
|| item.instruction.toLowerCase().includes(keyword)
|
||||
|| item.output.toLowerCase().includes(keyword)
|
||||
|| String(index + 1).includes(keyword)
|
||||
return matchesSearch && (!invalidOnly.value || item.status === 'invalid')
|
||||
}))
|
||||
|
||||
function selectRelative(offset: number) {
|
||||
if (!props.items.length) return
|
||||
const nextIndex = Math.min(Math.max(selectedIndex.value + offset, 0), props.items.length - 1)
|
||||
emit('update:selectedId', props.items[nextIndex].id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
<div class="result-toolbar">
|
||||
<el-input v-model="search" clearable size="small" placeholder="搜索结果">
|
||||
<template #prefix><i class="fa fa-search" /></template>
|
||||
</el-input>
|
||||
<el-checkbox v-model="invalidOnly">仅看错误</el-checkbox>
|
||||
</div>
|
||||
<div class="result-list">
|
||||
<button
|
||||
v-for="item in filteredItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="result-item"
|
||||
:class="{ 'is-active': item.id === selectedItem?.id }"
|
||||
@click="emit('update:selectedId', item.id)"
|
||||
>
|
||||
<span class="result-index">#{{ String(items.findIndex((entry) => entry.id === item.id) + 1).padStart(3, '0') }}</span>
|
||||
<span class="result-copy">
|
||||
<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'" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div v-if="selectedItem" class="result-editor-pane">
|
||||
<div class="pane-header">
|
||||
<div>
|
||||
<strong>结果 #{{ String(selectedIndex + 1).padStart(3, '0') }}</strong>
|
||||
<span v-if="selectedItem.status === 'modified'" class="modified-label">已修改</span>
|
||||
</div>
|
||||
<el-button link @click="emit('restore:item', selectedItem.id)"><i class="fa fa-undo" /> 恢复生成结果</el-button>
|
||||
</div>
|
||||
|
||||
<div class="field-editor">
|
||||
<label>Instruction <em>必填</em></label>
|
||||
<el-input
|
||||
:model-value="selectedItem.instruction"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
@update:model-value="emit('update:field', selectedItem.id, 'instruction', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="field-editor">
|
||||
<label>Input <span>选填</span></label>
|
||||
<el-input
|
||||
:model-value="selectedItem.input"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
@update:model-value="emit('update:field', selectedItem.id, 'input', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="field-editor">
|
||||
<label>Output <em>必填</em></label>
|
||||
<el-input
|
||||
:model-value="selectedItem.output"
|
||||
type="textarea"
|
||||
:rows="7"
|
||||
@update:model-value="emit('update:field', selectedItem.id, 'output', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="selectedItem.error" class="validation-error">
|
||||
<i class="fa fa-exclamation-circle" /> {{ selectedItem.error }}
|
||||
</div>
|
||||
<div v-else class="validation-success">
|
||||
<i class="fa fa-check-circle" /> 字段校验通过
|
||||
</div>
|
||||
<div class="editor-pagination">
|
||||
<el-button :disabled="selectedIndex <= 0" @click="selectRelative(-1)">上一条</el-button>
|
||||
<span>{{ selectedIndex + 1 }} / {{ items.length }}</span>
|
||||
<el-button :disabled="selectedIndex >= items.length - 1" @click="selectRelative(1)">下一条</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result-step {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
.result-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 34fr) minmax(480px, 66fr);
|
||||
min-height: clamp(420px, calc(100vh - 500px), 590px);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e5ec;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.result-list-pane {
|
||||
min-width: 0;
|
||||
border-right: 1px solid #e5e8ee;
|
||||
}
|
||||
|
||||
.pane-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 52px;
|
||||
padding: 0 15px;
|
||||
color: #344054;
|
||||
background: #fbfcfe;
|
||||
border-bottom: 1px solid #e8ebf0;
|
||||
font-size: 13px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
> span,
|
||||
.modified-label {
|
||||
color: #8a93a3;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
height: 476px;
|
||||
padding: 7px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: grid;
|
||||
grid-template-columns: 46px minmax(0, 1fr) 18px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 62px;
|
||||
padding: 9px;
|
||||
text-align: left;
|
||||
background: #fff;
|
||||
border: 1px solid transparent;
|
||||
border-bottom-color: #edf0f5;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
background: #f7f6ff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: #5b50f2;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-index {
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.result-copy {
|
||||
min-width: 0;
|
||||
|
||||
strong,
|
||||
small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
small {
|
||||
margin-top: 5px;
|
||||
color: #98a2b3;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.is-valid {
|
||||
color: #2ca66a;
|
||||
}
|
||||
|
||||
.is-error {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.result-editor-pane {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-editor {
|
||||
padding: 13px 18px 0;
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 7px;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
em {
|
||||
color: #e05252;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #98a2b3;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.validation-error,
|
||||
.validation-success {
|
||||
margin: 12px 18px 0;
|
||||
padding: 9px 11px;
|
||||
font-size: 11px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.validation-error {
|
||||
color: #b45309;
|
||||
background: #fff7e8;
|
||||
}
|
||||
|
||||
.validation-success {
|
||||
color: #25895c;
|
||||
background: #edf9f3;
|
||||
}
|
||||
|
||||
.editor-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 12px 18px;
|
||||
|
||||
span {
|
||||
color: #8a93a3;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.result-workspace {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.result-list-pane {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #e5e8ee;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
height: 260px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
469
frontend/src/views/data-process/create/TaskSetupStep.vue
Normal file
469
frontend/src/views/data-process/create/TaskSetupStep.vue
Normal file
@@ -0,0 +1,469 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { FormInstance, FormRules, UploadFile } from 'element-plus'
|
||||
import type { ProcessType } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
description: string
|
||||
processType: ProcessType
|
||||
uploadedFiles: { uid: string | number; name: string; size: number; count: number }[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:name': [value: string]
|
||||
'update:description': [value: string]
|
||||
'update:processType': [value: ProcessType]
|
||||
'file-change': [file: UploadFile]
|
||||
'remove-file': [uid: string | number]
|
||||
'use-sample': []
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const formModel = computed(() => ({
|
||||
name: props.name,
|
||||
processType: props.processType,
|
||||
}))
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入任务名称', trigger: 'blur' },
|
||||
{ max: 50, message: '任务名称不能超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
processType: [{ required: true, message: '请选择数据处理类型', trigger: 'change' }],
|
||||
}
|
||||
|
||||
const uploadAccept = computed(() => props.processType === 'unstructured'
|
||||
? '.txt,.md,.pdf,.docx,.doc,.json,.jsonl'
|
||||
: '.json,.jsonl,.csv,.xlsx,.xls')
|
||||
|
||||
const FILE_PAGE_SIZE = 10
|
||||
const currentFilePage = ref(1)
|
||||
const pagedUploadedFiles = computed(() => {
|
||||
const start = (currentFilePage.value - 1) * FILE_PAGE_SIZE
|
||||
return props.uploadedFiles.slice(start, start + FILE_PAGE_SIZE)
|
||||
})
|
||||
|
||||
watch(() => props.uploadedFiles.length, (newLength, oldLength) => {
|
||||
const totalPages = Math.max(1, Math.ceil(newLength / FILE_PAGE_SIZE))
|
||||
if (newLength > oldLength) {
|
||||
currentFilePage.value = totalPages
|
||||
return
|
||||
}
|
||||
currentFilePage.value = Math.min(currentFilePage.value, totalPages)
|
||||
})
|
||||
|
||||
function formatSize(size: number) {
|
||||
if (!size) return '0 KB'
|
||||
if (size >= 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
return `${(size / 1024).toFixed(1)} KB`
|
||||
}
|
||||
|
||||
async function validate() {
|
||||
if (!formRef.value) return false
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="task-setup-step">
|
||||
<el-form ref="formRef" :model="formModel" :rules="rules" label-position="top">
|
||||
<div class="form-section">
|
||||
<h3>基本信息</h3>
|
||||
<div class="basic-grid">
|
||||
<el-form-item label="任务名称" prop="name" required>
|
||||
<el-input
|
||||
:model-value="name"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
placeholder="例如:金融问答清洗任务"
|
||||
@update:model-value="emit('update:name', $event)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务描述(选填)">
|
||||
<el-input
|
||||
:model-value="description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="简要说明本次数据处理目标"
|
||||
@update:model-value="emit('update:description', $event)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>处理类型</h3>
|
||||
<p>类型只影响后续预览方式,不会改变四步流程</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item prop="processType" class="type-form-item">
|
||||
<div class="type-options">
|
||||
<button
|
||||
type="button"
|
||||
class="type-option"
|
||||
:class="{ 'is-active': processType === 'structured' }"
|
||||
@click="emit('update:processType', 'structured')"
|
||||
>
|
||||
<span class="type-icon"><i class="fa fa-table" /></span>
|
||||
<span>
|
||||
<strong>结构化数据</strong>
|
||||
<small>适用于 CSV、Excel、JSONL 等固定字段数据</small>
|
||||
</span>
|
||||
<i class="fa fa-check-circle selection-mark" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="type-option"
|
||||
:class="{ 'is-active': processType === 'unstructured' }"
|
||||
@click="emit('update:processType', 'unstructured')"
|
||||
>
|
||||
<span class="type-icon"><i class="fa fa-file-text-o" /></span>
|
||||
<span>
|
||||
<strong>非结构化数据</strong>
|
||||
<small>适用于文档、文本、问答等需要切分的数据</small>
|
||||
</span>
|
||||
<i class="fa fa-check-circle selection-mark" />
|
||||
</button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-section upload-section">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>源数据上传</h3>
|
||||
<p>系统将在下一步生成可对照编辑的预览内容,支持上传多个文件</p>
|
||||
</div>
|
||||
<el-button v-if="uploadedFiles.length === 0" link type="primary" @click="emit('use-sample')">
|
||||
使用示例数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-upload
|
||||
v-if="uploadedFiles.length === 0"
|
||||
drag
|
||||
multiple
|
||||
:accept="uploadAccept"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="(file: UploadFile) => emit('file-change', file)"
|
||||
>
|
||||
<i class="fa fa-cloud-upload upload-icon" />
|
||||
<div class="el-upload__text">拖拽文件到此处,或<em>点击选择文件</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
{{ processType === 'unstructured'
|
||||
? '支持 TXT、Markdown、PDF、Word、JSON、JSONL,单文件不超过 200MB'
|
||||
: '支持 JSON、JSONL、CSV、Excel,单文件不超过 200MB' }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<section v-else class="uploaded-file-list" aria-label="已上传文件列表">
|
||||
<div class="uploaded-file-list-header">
|
||||
<span>已添加 {{ uploadedFiles.length }} 个文件</span>
|
||||
<div class="continue-upload">
|
||||
<el-upload
|
||||
multiple
|
||||
:accept="uploadAccept"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="(file: UploadFile) => emit('file-change', file)"
|
||||
>
|
||||
<el-button size="small" type="primary">继续上传</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uploaded-file-items">
|
||||
<div v-for="file in pagedUploadedFiles" :key="file.uid" class="uploaded-file">
|
||||
<span class="file-icon"><i class="fa fa-file-text-o" /></span>
|
||||
<div class="file-main">
|
||||
<strong :title="file.name">{{ file.name }}</strong>
|
||||
<span>{{ formatSize(file.size) }}<template v-if="file.count"> · {{ file.count.toLocaleString() }} 条</template></span>
|
||||
</div>
|
||||
<span class="file-status"><i class="fa fa-check-circle" /> 校验通过</span>
|
||||
<el-button link type="danger" @click="emit('remove-file', file.uid)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-pagination
|
||||
v-if="uploadedFiles.length > FILE_PAGE_SIZE"
|
||||
v-model:current-page="currentFilePage"
|
||||
:page-size="FILE_PAGE_SIZE"
|
||||
:total="uploadedFiles.length"
|
||||
:pager-count="5"
|
||||
small
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
class="uploaded-file-pagination"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-setup-step {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.form-section {
|
||||
padding: 0 0 26px;
|
||||
margin-bottom: 26px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px;
|
||||
color: #2f3747;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
}
|
||||
}
|
||||
|
||||
.basic-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #8a93a3;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.type-form-item {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.type-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.type-option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 96px;
|
||||
padding: 18px;
|
||||
color: #4b5563;
|
||||
text-align: left;
|
||||
background: #fff;
|
||||
border: 1px solid #dfe3ea;
|
||||
border-radius: 9px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, background-color 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #a8a3ff;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #fafaff;
|
||||
border-color: #5b50f2;
|
||||
box-shadow: 0 0 0 1px rgba(91, 80, 242, 0.08);
|
||||
}
|
||||
|
||||
strong,
|
||||
small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
strong {
|
||||
margin-bottom: 6px;
|
||||
color: #262d3d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #7b8495;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.type-icon,
|
||||
.file-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #5b50f2;
|
||||
font-size: 18px;
|
||||
background: #f0efff;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.selection-mark {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
color: #5b50f2;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.type-option.is-active .selection-mark {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.upload-section :deep(.el-upload) {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.upload-section :deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
min-height: 154px;
|
||||
padding: 32px 20px;
|
||||
background: #fbfcfe;
|
||||
border-color: #dfe3ea;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
margin-bottom: 12px;
|
||||
color: #5b50f2;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.uploaded-file-list {
|
||||
margin-top: 20px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #dfe3ea;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.uploaded-file-list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
color: #5f6878;
|
||||
font-size: 12px;
|
||||
background: #fbfcfe;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.continue-upload {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.continue-upload :deep(.el-upload) {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.uploaded-file-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 14px;
|
||||
border-top: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.uploaded-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 48px;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.file-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
|
||||
strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #273142;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #8a93a3;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-status {
|
||||
color: #2ca66a;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.uploaded-file :deep(.el-button) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.type-options {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.uploaded-file {
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.file-status {
|
||||
flex: 0 1 auto;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.uploaded-file-pagination {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
89
frontend/src/views/data-process/create/previewModel.ts
Normal file
89
frontend/src/views/data-process/create/previewModel.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { PreviewItem, ProcessType, ResultItem, SourceLine } from './types'
|
||||
|
||||
export const DEFAULT_SOURCE_TEXT = [
|
||||
'问:如何看待当前的通货膨胀风险?',
|
||||
'答:当前通胀水平总体可控,但仍需关注能源价格与供给扰动。',
|
||||
'问:美联储下一次议息会议何时召开?',
|
||||
'答:会议时间以美联储官方日历为准,市场会重点关注利率路径指引。',
|
||||
'问:人民币汇率未来走势如何?',
|
||||
'答:人民币汇率取决于中美利差、经济基本面与政策预期。',
|
||||
'问:银行理财产品收益率为何持续走低?',
|
||||
'答:主要与市场利率下行、资产端收益下降以及风险偏好变化有关。',
|
||||
'问:什么是复利?',
|
||||
'答:复利是指在计算利息时,将上一期利息加入本金,再计算下一期利息。',
|
||||
'问:如何评估股票的投资价值?',
|
||||
'答:评估股票投资价值可以从以下几个方面进行:',
|
||||
'1. 公司基本面:分析公司的财务状况、盈利能力、成长性等。',
|
||||
'2. 行业前景:考察公司所处行业的发展趋势和竞争格局。',
|
||||
'3. 估值水平:通过市盈率、市净率等指标判断估值是否合理。',
|
||||
'4. 财务健康:关注公司的负债情况、现金流状况等。',
|
||||
'5. 管理团队:评估管理层的能力和过往业绩。',
|
||||
'此外,还需要关注宏观经济环境、政策变化等因素对股票市场的影响。',
|
||||
'问:债券和股票的主要区别是什么?',
|
||||
'答:债券收益相对稳定但上行有限,股票波动更大且承担更高风险。',
|
||||
'问:什么是市盈率?',
|
||||
'答:市盈率是股票价格与每股收益的比值,常用于衡量估值水平。',
|
||||
'问:如何进行资产配置?',
|
||||
'答:应根据投资目标、风险承受能力和市场环境合理分配资产。',
|
||||
].join('\n')
|
||||
|
||||
export function sourceLines(sourceText: string): SourceLine[] {
|
||||
const rawLines = sourceText.split('\n')
|
||||
let cursor = 0
|
||||
|
||||
return rawLines.map((content, index) => {
|
||||
const start = cursor
|
||||
const end = start + content.length
|
||||
cursor = end + (index < rawLines.length - 1 ? 1 : 0)
|
||||
return { number: index + 1, content, start, end }
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPreviewItems(sourceText: string, processType: ProcessType, sourceFileId = 'default-source'): PreviewItem[] {
|
||||
const meaningfulLines = sourceLines(sourceText).filter((line) => line.content.trim())
|
||||
const groupSize = processType === 'structured' ? 1 : 3
|
||||
const items: PreviewItem[] = []
|
||||
|
||||
for (let index = 0; index < meaningfulLines.length; index += groupSize) {
|
||||
const group = meaningfulLines.slice(index, index + groupSize)
|
||||
if (!group.length) continue
|
||||
|
||||
const sourceStart = group[0].start
|
||||
const sourceEnd = group[group.length - 1].end
|
||||
const content = sourceText.slice(sourceStart, sourceEnd)
|
||||
|
||||
items.push({
|
||||
id: `preview-${sourceFileId}-${items.length + 1}`,
|
||||
sourceFileId,
|
||||
originalContent: content,
|
||||
editedContent: content,
|
||||
sourceStart,
|
||||
sourceEnd,
|
||||
sourceStartLine: group[0].number,
|
||||
sourceEndLine: group[group.length - 1].number,
|
||||
tokenCount: Math.max(1, Math.ceil(content.length / 2)),
|
||||
status: 'original',
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export function createResults(items: PreviewItem[]): ResultItem[] {
|
||||
return items.slice(0, 12).map((item, index) => {
|
||||
const [firstLine = '', ...rest] = item.editedContent.split('\n')
|
||||
const output = rest.join('\n').trim() || item.editedContent.trim()
|
||||
const instruction = firstLine.replace(/^问[::]\s*/, '').trim() || `数据条目 ${index + 1}`
|
||||
|
||||
return {
|
||||
id: `result-${index + 1}`,
|
||||
instruction,
|
||||
input: '',
|
||||
output,
|
||||
originalInstruction: instruction,
|
||||
originalInput: '',
|
||||
originalOutput: output,
|
||||
status: 'valid',
|
||||
}
|
||||
})
|
||||
}
|
||||
41
frontend/src/views/data-process/create/types.ts
Normal file
41
frontend/src/views/data-process/create/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export type ProcessType = 'structured' | 'unstructured'
|
||||
|
||||
export type StepId = 'create' | 'preview' | 'generate' | 'results'
|
||||
|
||||
export interface SourceLine {
|
||||
number: number
|
||||
content: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface PreviewItem {
|
||||
id: string
|
||||
sourceFileId: string
|
||||
originalContent: string
|
||||
editedContent: string
|
||||
sourceStart: number | null
|
||||
sourceEnd: number | null
|
||||
sourceStartLine: number | null
|
||||
sourceEndLine: number | null
|
||||
tokenCount: number
|
||||
status: 'original' | 'modified' | 'manual' | 'invalid'
|
||||
}
|
||||
|
||||
export interface GenerationState {
|
||||
status: 'idle' | 'running' | 'success' | 'failed'
|
||||
progress: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ResultItem {
|
||||
id: string
|
||||
instruction: string
|
||||
input: string
|
||||
output: string
|
||||
originalInstruction: string
|
||||
originalInput: string
|
||||
originalOutput: string
|
||||
status: 'valid' | 'modified' | 'invalid'
|
||||
error?: string
|
||||
}
|
||||
Reference in New Issue
Block a user