ai-manus/chat-client/src/views/datasets/components/DocUpload.vue

1026 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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.

<template>
<div class="doc-upload-container">
<!-- 步骤条 -->
<el-steps :active="currentStep" align-center class="upload-steps">
<el-step :title="props.replaceFileId ? '选择替换文件' : '选择文件'" icon="Upload" />
<el-step title="解析模式" icon="Setting" />
<el-step :title="getStep3Title()" :icon="getStep3Icon()" />
</el-steps>
<!-- 步骤内容 -->
<div class="step-content">
<!-- 第一步文件选择 -->
<div v-if="currentStep === 0" class="step-panel file-selection">
<div class="panel-header">
<h3>{{ props.replaceFileId ? '选择要替换的文件' : '选择要上传的文件' }}</h3>
<p class="panel-description">支持多种文档格式,拖拽或点击选择文件</p>
</div>
<el-upload
ref="uploadRef"
multiple
drag
:auto-upload="false"
:on-change="handleFileChange"
:file-list="[]"
:show-file-list="false"
accept=".txt,.md,.markdown,.mdx,.pdf,.html,.htm,.xlsx,.xls,.docx,.csv,.vtt,.properties"
class="upload-area"
:class="{ 'is-dragover': isDragOver }"
@drop="handleDrop"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
>
<div class="upload-content">
<el-icon class="upload-icon" :class="{ 'bounce': isDragOver }">
<Upload />
</el-icon>
<div class="upload-text">
<h4>拖拽文件到此处 或 <span class="link-text">点击选择</span></h4>
<p>支持 TXT、MD、PDF、DOCX、Excel、HTML、CSV 等格式不支持DOC格式</p>
<p>单个文件不超过 100MB最多可选择 10 个文件</p>
</div>
</div>
</el-upload>
<!-- 已选文件列表 -->
<div v-if="uploadFiles.length > 0" class="selected-files">
<h4>已选择文件 ({{ uploadFiles.length }})</h4>
<div class="file-list">
<div v-for="(file, index) in uploadFiles" :key="index" class="file-item">
<el-icon class="file-icon"><Document /></el-icon>
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<el-button
type="danger"
:icon="Delete"
text
size="small"
@click="removeFile(index)"
/>
</div>
</div>
</div>
</div>
<!-- 第二步:解析模式选择 -->
<div v-if="currentStep === 1" class="step-panel mode-selection">
<div class="panel-header">
<h3>选择解析模式</h3>
<p class="panel-description">根据您的需求选择合适的文档解析策略</p>
</div>
<div class="mode-options">
<div
v-for="mode in analysisMode"
:key="mode.value"
class="mode-card"
:class="{ 'selected': uploadForm.analysisStrategyType === mode.value }"
@click="selectMode(mode.value)"
>
<div class="mode-icon">
<el-icon :size="32"><component :is="mode.icon" /></el-icon>
</div>
<h4>{{ mode.title }}</h4>
<p>{{ mode.description }}</p>
<div class="mode-features">
<el-tag v-for="feature in mode.features" :key="feature" size="small" type="info">
{{ feature }}
</el-tag>
</div>
</div>
</div>
</div>
<!-- 第三步:参数设置或进度显示 -->
<div v-if="currentStep === 2" class="step-panel config-panel">
<!-- 自定义模式 - 参数设置 -->
<div v-if="uploadForm.analysisStrategyType === 'custom'" class="custom-config">
<div class="panel-header">
<h3>自定义解析参数</h3>
<p class="panel-description">配置详细的文档解析参数</p>
</div>
<el-form :model="uploadForm.docAnalysisStrategy" label-width="140px" class="config-form">
<div class="form-section">
<h4 class="section-title">基础设置</h4>
<!--
<el-form-item label="策略名称">
<el-input v-model="uploadForm.docAnalysisStrategy.name" placeholder="请输入策略名称(可选)" />
</el-form-item>
-->
<el-form-item label="分段模式">
<el-select v-model="uploadForm.docAnalysisStrategy.segmentationMode" placeholder="选择分段模式">
<el-option label="普通分段" value="custom" />
<el-option label="父子分段" value="hierarchical" />
</el-select>
</el-form-item>
<el-form-item v-if="uploadForm.docAnalysisStrategy.segmentationMode === 'parent_child'" label="父分段召回模式">
<el-select v-model="uploadForm.docAnalysisStrategy.parentSegmentMode" placeholder="选择父分段召回模式">
<el-option label="全文召回" value="full-doc" />
<el-option label="段落召回" value="paragraph" />
</el-select>
</el-form-item>
<el-form-item label="索引技术">
<el-select v-model="uploadForm.docAnalysisStrategy.indexingTechnique" placeholder="选择索引技术">
<el-option label="高质量索引" value="high_quality" />
<el-option label="经济模式" value="economy" />
</el-select>
</el-form-item>
</div>
<!-- 普通分段参数 -->
<div v-if="uploadForm.docAnalysisStrategy.segmentationMode === 'custom'" class="form-section">
<h4 class="section-title">分段参数</h4>
<el-form-item label="段分隔符">
<el-input v-model="uploadForm.docAnalysisStrategy.segmentSeparator" placeholder="###" />
</el-form-item>
<el-form-item label="最大长度">
<el-input-number
v-model="uploadForm.docAnalysisStrategy.maxLength"
:min="100"
:max="2000"
placeholder="500"
/>
</el-form-item>
<el-form-item label="重叠长度">
<el-input-number
v-model="uploadForm.docAnalysisStrategy.overlapLength"
:min="0"
:max="200"
placeholder="50"
/>
</el-form-item>
</div>
<!-- 父子分段参数 -->
<div v-if="uploadForm.docAnalysisStrategy.segmentationMode === 'hierarchical'" class="form-section">
<!-- 父分段设置 (仅在段落召回模式下显示) -->
<div v-if="uploadForm.docAnalysisStrategy.parentSegmentMode === 'paragraph'">
<h4 class="section-title">父分段设置</h4>
<el-form-item label="父段分隔符">
<el-input v-model="uploadForm.docAnalysisStrategy.segmentSeparator" placeholder="###" />
</el-form-item>
<el-form-item label="父段最大长度">
<el-input-number
v-model="uploadForm.docAnalysisStrategy.maxLength"
:min="100"
:max="2000"
placeholder="500"
/>
</el-form-item>
<el-form-item label="父段重叠长度">
<el-input-number
v-model="uploadForm.docAnalysisStrategy.overlapLength"
:min="0"
:max="200"
placeholder="50"
/>
</el-form-item>
</div>
<!-- 子分段设置 (始终显示) -->
<h4 class="section-title">子分段设置</h4>
<el-form-item label="子段分隔符">
<el-input v-model="uploadForm.docAnalysisStrategy.childSegmentSeparator" placeholder="\n\n" />
</el-form-item>
<el-form-item label="子段最大长度">
<el-input-number
v-model="uploadForm.docAnalysisStrategy.childMaxLength"
:min="50"
:max="1000"
placeholder="200"
/>
</el-form-item>
<el-form-item label="子段重叠长度">
<el-input-number
v-model="uploadForm.docAnalysisStrategy.childOverlapLength"
:min="0"
:max="100"
placeholder="20"
/>
</el-form-item>
</div>
<div class="form-section">
<h4 class="section-title">检索设置</h4>
<el-form-item label="搜索方法">
<el-select v-model="uploadForm.docAnalysisStrategy.searchMethod" placeholder="选择搜索方法">
<el-option label="语义搜索" value="semantic_search" />
<el-option label="全文搜索" value="full_text_search" />
<el-option label="混合搜索" value="hybrid_search" />
</el-select>
</el-form-item>
<el-form-item label="Top K">
<el-input-number
v-model="uploadForm.docAnalysisStrategy.topK"
:min="1"
:max="20"
placeholder="3"
/>
</el-form-item>
<el-form-item label="分数阈值">
<div class="score-threshold-config">
<el-switch
v-model="uploadForm.docAnalysisStrategy.scoreThresholdEnabled"
active-text="启用"
inactive-text="关闭"
/>
<el-input-number
v-model="uploadForm.docAnalysisStrategy.scoreThreshold"
:min="0"
:max="1"
:step="0.1"
:precision="1"
:disabled="!uploadForm.docAnalysisStrategy.scoreThresholdEnabled"
placeholder="0.5"
/>
</div>
</el-form-item>
</div>
<div class="form-section">
<h4 class="section-title">重排序设置</h4>
<el-form-item label="启用重排序">
<el-switch
v-model="uploadForm.docAnalysisStrategy.rerankingEnable"
active-text="启用"
inactive-text="关闭"
/>
</el-form-item>
</div>
</el-form>
</div>
<!-- 深度解析模式 - 进度显示 -->
<div v-else-if="uploadForm.analysisStrategyType === 'deep'" class="deep-analysis-progress">
<div class="panel-header">
<h3>数据预处理进度</h3>
<p class="panel-description">正在进行深度分析,请耐心等待</p>
</div>
<div class="progress-container">
<div class="progress-item" v-for="(task, index) in processingTasks" :key="index">
<div class="task-info">
<span class="task-name">{{ task.name }}</span>
<span class="task-status">{{ task.status }}</span>
</div>
<el-progress
:percentage="task.progress"
:status="task.progress === 100 ? 'success' : undefined"
:stroke-width="8"
/>
</div>
</div>
</div>
<!-- 自适应模式 - 确认信息 -->
<div v-else class="adaptive-confirm">
<div class="panel-header">
<h3>{{ props.replaceFileId ? '确认替换' : '确认上传' }}</h3>
<p class="panel-description">使用自适应模式系统将自动选择最佳解析策略</p>
</div>
<div class="confirm-info">
<div class="info-card">
<el-icon class="info-icon"><InfoFilled /></el-icon>
<div class="info-content">
<h4>自适应模式特点</h4>
<ul>
<li>自动识别文档类型和结构</li>
<li>智能选择最佳分段策略</li>
<li>优化索引质量和检索效果</li>
<li>无需手动配置参数</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="step-actions">
<el-button v-if="currentStep > 0" @click="prevStep">上一步</el-button>
<el-button
v-if="currentStep < 2"
type="primary"
@click="nextStep"
:disabled="!canProceedToNext()"
>
下一步
</el-button>
<el-button
v-if="currentStep === 2"
type="primary"
@click="handleUpload"
:loading="uploading"
:disabled="!canUpload()"
>
{{ props.replaceFileId ? '开始替换' : '开始上传' }}
</el-button>
<el-button @click="handleCancel">取消</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, watch } from 'vue'
import { ElMessage, ElNotification, ElLoading } from 'element-plus'
import type { UploadFile as ElUploadFile } from 'element-plus'
import {
Upload,
Setting,
Document,
Delete,
InfoFilled,
Star,
Cpu,
Tools,
Check
} from '@element-plus/icons-vue'
import { uploadDocument, deleteDocument } from '@/api/dataset'
// Props
interface Props {
datasetId: string
visible: boolean
parentId?: number
replaceFileId?: string
}
const props = withDefaults(defineProps<Props>(), {
datasetId: '',
visible: false,
parentId: undefined,
replaceFileId: undefined
})
// Emits
const emit = defineEmits<{
'update:visible': [value: boolean]
'success': []
}>()
// 响应式数据
const currentStep = ref(0)
const uploading = ref(false)
const uploadFiles = ref<ElUploadFile[]>([])
const isDragOver = ref(false)
const uploadRef = ref()
const isProcessingFileLimit = ref(false)
// 解析模式选项
const analysisMode = [
{
value: 'adaption',
title: '自适应模式',
description: '智能识别文档结构,自动选择最佳解析策略',
icon: 'Star',
features: ['智能识别', '自动优化', '快速处理']
},
{
value: 'deep',
title: '深度解析',
description: '深度分析扫描文档及图片内容,提供最高质量的解析结果',
icon: 'Cpu',
features: ['深度分析', '高质量', '扫描件']
},
{
value: 'custom',
title: '自定义模式',
description: '完全自定义解析参数,满足特殊需求',
icon: 'Tools',
features: ['完全自定义', '灵活配置', '专业级']
}
]
// 上传表单数据
const uploadForm = reactive({
analysisStrategyType: 'adaption',
docAnalysisStrategy: {
name: '',
segmentationMode: 'custom',
indexingTechnique: 'high_quality',
docForm: 'text_model',
parentSegmentMode: 'paragraph',
segmentSeparator: '###',
maxLength: 500,
overlapLength: 50,
childSegmentSeparator: '\n\n',
childMaxLength: 200,
childOverlapLength: 20,
searchMethod: 'hybrid_search',
rerankingEnable: false,
rerankingProviderName: 'langgenius/huggingface_tei/huggingface_tei',
rerankingModelName: 'bge-reanker-v2-m3',
topK: 3,
scoreThresholdEnabled: false,
scoreThreshold: 0.5,
describe: ''
}
})
// 深度解析进度任务
const processingTasks = ref([
{ name: '文档解析', progress: 0, status: '等待中' },
{ name: '内容提取', progress: 0, status: '等待中' },
{ name: '结构分析', progress: 0, status: '等待中' },
{ name: '索引构建', progress: 0, status: '等待中' }
])
// 监听分段模式变化自动设置docForm
watch(() => uploadForm.docAnalysisStrategy.segmentationMode, (newMode) => {
if (newMode === 'custom') {
uploadForm.docAnalysisStrategy.docForm = 'text_model'
} else if (newMode === 'hierarchical') {
uploadForm.docAnalysisStrategy.docForm = 'hierarchical_model'
}
})
// 计算属性
const getStep3Title = () => {
switch (uploadForm.analysisStrategyType) {
case 'custom': return '参数设置'
case 'deep': return '处理进度'
default: return props.replaceFileId ? '确认替换' : '确认上传'
}
}
const getStep3Icon = () => {
switch (uploadForm.analysisStrategyType) {
case 'custom': return 'Tools'
case 'deep': return 'Loading'
default: return 'Check'
}
}
// 方法
const handleFileChange = (file: ElUploadFile, fileList: ElUploadFile[]) => {
// 防止无限循环
if (isProcessingFileLimit.value) {
return
}
// 文件大小检查
if (file.size && file.size > 100 * 1024 * 1024) {
ElMessage.error('文件大小不能超过 100MB')
return false
}
// 文件数量检查如果超过10个只保留前10个
if (fileList.length > 10) {
isProcessingFileLimit.value = true
const truncatedList = fileList.slice(0, 10)
const removedCount = fileList.length - 10
ElMessage.warning({
message: `已选择 ${fileList.length} 个文件,超出限制。系统已自动保留前 10 个文件,移除了 ${removedCount} 个文件。`,
duration: 4000,
showClose: true
})
// 更新我们的文件列表
uploadFiles.value = truncatedList
// 清除并重新设置上传组件的文件列表
nextTick(() => {
if (uploadRef.value) {
uploadRef.value.clearFiles()
// 重新添加前10个文件
truncatedList.forEach(uploadFile => {
if (uploadFile.raw) {
uploadRef.value.handleStart(uploadFile.raw)
}
})
}
// 重置标志
setTimeout(() => {
isProcessingFileLimit.value = false
}, 100)
})
} else {
// 更新文件列表
uploadFiles.value = fileList
}
}
const handleDrop = () => {
isDragOver.value = false
}
const handleDragOver = () => {
isDragOver.value = true
}
const handleDragLeave = () => {
isDragOver.value = false
}
const removeFile = (index: number) => {
uploadFiles.value.splice(index, 1)
// 同步更新 el-upload 组件的内部文件列表
if (uploadRef.value) {
uploadRef.value.clearFiles()
// 重新添加剩余文件到 el-upload 组件
uploadFiles.value.forEach(file => {
uploadRef.value.handleStart(file.raw)
})
}
}
const formatFileSize = (size?: number) => {
if (!size) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let index = 0
while (size >= 1024 && index < units.length - 1) {
size /= 1024
index++
}
return `${size.toFixed(1)} ${units[index]}`
}
const selectMode = (mode: string) => {
uploadForm.analysisStrategyType = mode
}
const canProceedToNext = () => {
if (currentStep.value === 0) {
return uploadFiles.value.length > 0
}
if (currentStep.value === 1) {
return uploadForm.analysisStrategyType !== ''
}
return true
}
const canUpload = () => {
// 自定义模式不需要策略名称验证因为UI中已经隐藏了该字段
// 只要有基本的配置参数就可以上传
return true
}
const nextStep = () => {
if (canProceedToNext()) {
currentStep.value++
// 如果选择深度解析,模拟进度
if (currentStep.value === 2 && uploadForm.analysisStrategyType === 'deep') {
simulateDeepAnalysisProgress()
}
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
const simulateDeepAnalysisProgress = () => {
processingTasks.value.forEach((task, index) => {
setTimeout(() => {
task.status = '处理中'
const interval = setInterval(() => {
task.progress += Math.random() * 10
if (task.progress >= 100) {
task.progress = 100
task.status = '已完成'
clearInterval(interval)
}
}, 200)
}, index * 1000)
})
}
const handleUpload = async () => {
if (!uploadFiles.value.length) {
ElMessage.warning('请选择要上传的文件')
return
}
const loading = ElLoading.service({
lock: true,
text: props.replaceFileId ? '正在替换文件...' : '正在上传文件...',
background: 'rgba(0, 0, 0, 0.7)'
})
try {
uploading.value = true
// 如果是文件替换,先删除旧文件
if (props.replaceFileId) {
// 关闭当前loading显示删除进度
loading.close()
const deleteLoading = ElLoading.service({
lock: true,
text: '正在删除旧文件...',
background: 'rgba(0, 0, 0, 0.7)'
})
await deleteDocument(parseInt(props.replaceFileId))
deleteLoading.close()
// 重新显示上传进度X
const uploadLoading = ElLoading.service({
lock: true,
text: '正在上传新文件...',
background: 'rgba(0, 0, 0, 0.7)'
})
// 更新loading引用
loading.close = uploadLoading.close
}
// 构造请求数据符合后端DocUploadReq结构
const requestData: any = {
datasetId: props.datasetId,
analysisStrategyType: uploadForm.analysisStrategyType,
parentId: props.parentId, // 添加parentId参数根目录时为undefined/null
// 注意:删除 replaceFileId因为我们已经手动删除了旧文件
}
// 如果是自定义模式,添加解析策略
if (uploadForm.analysisStrategyType === 'custom') {
requestData.docAnalysisStrategy = {
name: uploadForm.docAnalysisStrategy.name || '自定义策略',
segmentationMode: uploadForm.docAnalysisStrategy.segmentationMode,
indexingTechnique: uploadForm.docAnalysisStrategy.indexingTechnique,
docForm: uploadForm.docAnalysisStrategy.docForm,
parentSegmentMode: uploadForm.docAnalysisStrategy.parentSegmentMode,
segmentSeparator: uploadForm.docAnalysisStrategy.segmentSeparator,
maxLength: uploadForm.docAnalysisStrategy.maxLength,
overlapLength: uploadForm.docAnalysisStrategy.overlapLength,
childSegmentSeparator: uploadForm.docAnalysisStrategy.childSegmentSeparator || '',
childMaxLength: uploadForm.docAnalysisStrategy.childMaxLength || 200,
childOverlapLength: uploadForm.docAnalysisStrategy.childOverlapLength || 20,
searchMethod: uploadForm.docAnalysisStrategy.searchMethod,
rerankingEnable: uploadForm.docAnalysisStrategy.rerankingEnable,
rerankingProviderName: uploadForm.docAnalysisStrategy.rerankingProviderName || '',
rerankingModelName: uploadForm.docAnalysisStrategy.rerankingModelName || '',
topK: uploadForm.docAnalysisStrategy.topK,
scoreThresholdEnabled: uploadForm.docAnalysisStrategy.scoreThresholdEnabled,
scoreThreshold: uploadForm.docAnalysisStrategy.scoreThreshold,
describe: uploadForm.docAnalysisStrategy.describe || ''
}
}
// 遍历上传文件
for (const file of uploadFiles.value) {
const formData = new FormData()
formData.append('file', file.raw!)
formData.append('request', new Blob([JSON.stringify(requestData)], {
type: 'application/json'
}))
await uploadDocument(formData)
}
ElNotification({
title: props.replaceFileId ? '替换成功' : '上传成功',
message: props.replaceFileId
? `成功替换 ${uploadFiles.value.length} 个文件`
: `成功上传 ${uploadFiles.value.length} 个文件`,
type: 'success'
})
emit('success')
handleCancel()
} catch (error) {
console.error(props.replaceFileId ? '替换失败:' : '上传失败:', error)
//ElMessage.error(props.replaceFileId ? '替换失败,请重试' : '上传失败,请重试')
} finally {
uploading.value = false
loading.close()
}
}
const handleCancel = () => {
// 重置状态
currentStep.value = 0
uploadFiles.value = []
uploadForm.analysisStrategyType = 'adaption'
uploadForm.docAnalysisStrategy.name = ''
emit('update:visible', false)
}
</script>
<style scoped lang="scss">
.doc-upload-container {
padding: 24px;
background: #fff;
border-radius: 8px;
.upload-steps {
margin-bottom: 32px;
:deep(.el-step__title) {
font-size: 14px;
font-weight: 500;
}
}
.step-content {
min-height: 400px;
margin-bottom: 24px;
}
.step-panel {
.panel-header {
text-align: center;
margin-bottom: 24px;
h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.panel-description {
margin: 0;
color: #909399;
font-size: 14px;
}
}
}
// 文件选择步骤
.file-selection {
.upload-area {
margin-bottom: 24px;
:deep(.el-upload-dragger) {
width: 100%;
height: 200px;
border: 2px dashed #dcdfe6;
border-radius: 8px;
background: #fafafa;
transition: all 0.3s;
&:hover, &.is-dragover {
border-color: #409eff;
background: #f0f9ff;
}
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
.upload-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
transition: all 0.3s;
&.bounce {
animation: bounce 0.6s infinite alternate;
}
}
.upload-text {
text-align: center;
h4 {
margin: 0 0 8px 0;
font-size: 16px;
color: #303133;
.link-text {
color: #409eff;
cursor: pointer;
}
}
p {
margin: 4px 0;
font-size: 12px;
color: #909399;
}
}
}
}
.selected-files {
h4 {
margin: 0 0 12px 0;
font-size: 14px;
color: #303133;
}
.file-list {
border: 1px solid #ebeef5;
border-radius: 6px;
overflow: hidden;
.file-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
.file-icon {
font-size: 16px;
color: #409eff;
margin-right: 8px;
}
.file-name {
flex: 1;
font-size: 14px;
color: #303133;
margin-right: 12px;
}
.file-size {
font-size: 12px;
color: #909399;
margin-right: 12px;
}
}
}
}
}
// 模式选择步骤
.mode-selection {
.mode-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
.mode-card {
padding: 24px;
border: 2px solid #ebeef5;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.1);
}
&.selected {
border-color: #409eff;
background: #f0f9ff;
}
.mode-icon {
margin-bottom: 16px;
color: #409eff;
}
h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
p {
margin: 0 0 16px 0;
font-size: 14px;
color: #606266;
line-height: 1.5;
}
.mode-features {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: center;
}
}
}
}
// 配置面板
.config-panel {
.custom-config {
.config-form {
max-width: 600px;
margin: 0 auto;
.form-section {
margin-bottom: 24px;
padding: 16px;
background: #fafafa;
border-radius: 6px;
.section-title {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #ebeef5;
padding-bottom: 8px;
}
}
.score-threshold-config {
display: flex;
align-items: center;
gap: 12px;
}
}
}
.deep-analysis-progress {
.progress-container {
max-width: 500px;
margin: 0 auto;
.progress-item {
margin-bottom: 20px;
.task-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.task-name {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.task-status {
font-size: 12px;
color: #909399;
}
}
}
}
}
.adaptive-confirm {
.confirm-info {
max-width: 500px;
margin: 0 auto;
.info-card {
display: flex;
padding: 20px;
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 8px;
.info-icon {
font-size: 24px;
color: #409eff;
margin-right: 16px;
flex-shrink: 0;
}
.info-content {
h4 {
margin: 0 0 12px 0;
font-size: 16px;
color: #303133;
}
ul {
margin: 0;
padding-left: 16px;
li {
margin-bottom: 6px;
font-size: 14px;
color: #606266;
line-height: 1.4;
}
}
}
}
}
}
}
.step-actions {
display: flex;
justify-content: center;
gap: 12px;
padding-top: 24px;
border-top: 1px solid #ebeef5;
}
}
@keyframes bounce {
0% { transform: translateY(0); }
100% { transform: translateY(-10px); }
}
</style>