Files
Enterprise-Knowledge-Base/frontend/src/views/initialization/InitializationContent.vue
T
2025-09-16 20:30:09 +08:00

2713 lines
106 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="initialization-content">
<!-- 顶部Ollama服务状态 -->
<div class="ollama-summary-card">
<div class="summary-header">
<span class="title"><t-icon name="server" />Ollama 服务状态</span>
<t-tag :theme="ollamaStatus.available ? 'success' : 'danger'" size="small" class="state">
{{ ollamaStatus.available ? `正常 (${ollamaStatus.version||'v?'} )` : (ollamaStatus.error || '未运行') }}
</t-tag>
<t-tooltip content="刷新状态">
<t-icon name="refresh" class="refresh-icon" :class="{ spinning: summaryRefreshing }" @click="refreshOllamaSummary" />
</t-tooltip>
</div>
<div class="summary-body">
<div class="models">
<span class="label">Ollama 服务地址</span>
<div class="model-list">
<t-tag size="small" theme="default" class="model-pill">{{ ollamaStatus.baseUrl || '未配置' }}</t-tag>
</div>
</div>
<div class="models">
<span class="label">已安装模型</span>
<div class="model-list">
<t-tag v-for="m in installedModels" :key="m" size="small" theme="default" class="model-pill">{{ m }}</t-tag>
<span v-if="installedModels.length===0" class="empty">暂无</span>
</div>
</div>
</div>
</div>
<!-- 主配置表单 -->
<t-form ref="form" :data="formData" :rules="rules" @submit.prevent layout="vertical">
<!-- LLM 大语言模型配置区域 -->
<div class="config-section">
<h3><t-icon name="chat" class="section-icon" />LLM 大语言模型配置</h3>
<div class="form-row">
<t-form-item label="模型来源" name="llm.source">
<t-radio-group v-model="formData.llm.source" @change="onModelSourceChange('llm')">
<t-radio value="local">Ollama (本地)</t-radio>
<t-radio value="remote">Remote API (远程)</t-radio>
</t-radio-group>
</t-form-item>
</div>
<div class="form-row">
<t-form-item label="模型名称" name="llm.modelName">
<div class="model-input-with-status">
<t-input v-model="formData.llm.modelName" placeholder="例如: qwen3:0.6b"
@blur="onModelNameChange('llm')"
@input="onModelNameInput('llm')"
@keyup.enter="onModelNameChange('llm')" />
<div class="model-status-icon">
<t-icon
v-if="formData.llm.source === 'local' && formData.llm.modelName && modelStatus.llm.checked"
:name="modelStatus.llm.available ? 'check-circle-filled' : 'close-circle-filled'"
:class="['status-icon', modelStatus.llm.available ? 'installed' : 'not-installed']"
:title="modelStatus.llm.available ? '已安装' : '未安装'"
/>
<t-icon
v-else-if="formData.llm.source === 'local' && formData.llm.modelName && !modelStatus.llm.checked"
name="help-circle"
class="status-icon unknown"
title="未检查"
/>
<t-icon
v-else-if="formData.llm.source === 'local' && formData.llm.modelName && modelStatus.llm.downloading"
name="loading"
class="status-icon downloading spinning"
title="下载中"
/>
</div>
<!-- 下载按钮:未安装时显示 -->
<div v-if="formData.llm.source === 'local' && formData.llm.modelName && modelStatus.llm.checked && !modelStatus.llm.available && !modelStatus.llm.downloading" class="download-action">
<t-tooltip content="下载模型">
<t-button
size="small"
theme="primary"
@click="downloadModel('llm', formData.llm.modelName)"
class="download-btn"
:disabled="modelStatus.llm.downloading"
>
<t-icon name="download" />
</t-button>
</t-tooltip>
</div>
</div>
</t-form-item>
<!-- 下载进度:下载中时显示 -->
<div v-if="formData.llm.source === 'local' && formData.llm.modelName && modelStatus.llm.downloading" class="download-progress">
<div class="progress-info">
<t-icon name="loading" class="loading-icon spinning" />
<span class="progress-text">下载中</span>
</div>
<t-progress :percentage="Number(modelStatus.llm.progress.toFixed(1))" :show-info="false" size="small" class="progress-bar" />
<div class="progress-message">{{ modelStatus.llm.message }}</div>
</div>
</div>
<!-- 远程 API 配置区域 -->
<div v-if="formData.llm.source === 'remote'" class="remote-config">
<div class="form-row">
<t-form-item label="Base URL" name="llm.baseUrl">
<div class="url-input-with-check">
<t-input v-model="formData.llm.baseUrl" placeholder="例如: https://api.openai.com/v1, 去除末尾/chat/completions路径后的URL的前面部分"
@blur="onRemoteConfigChange('llm')"
@input="onRemoteConfigInput('llm')" />
<div v-if="formData.llm.modelName && formData.llm.baseUrl" class="check-action">
<t-tooltip v-if="!modelStatus.llm.available && modelStatus.llm.checked" content="重新检查连接">
<t-icon
name="refresh"
class="refresh-icon"
:class="{ spinning: modelStatus.llm.checking }"
@click="checkRemoteModelStatus('llm')"
/>
</t-tooltip>
<t-icon
v-else-if="modelStatus.llm.checked"
:name="modelStatus.llm.available ? 'check-circle-filled' : 'close-circle-filled'"
:class="['status-icon', modelStatus.llm.available ? 'installed' : 'not-installed']"
:title="modelStatus.llm.available ? '连接正常' : '连接失败'"
/>
<t-icon
v-else
name="loading"
class="status-icon checking spinning"
title="检查连接中"
/>
</div>
</div>
</t-form-item>
</div>
<div class="form-row">
<t-form-item label="API Key (可选)" name="llm.apiKey">
<t-input v-model="formData.llm.apiKey" type="password" placeholder="请输入API Key (可选)"
@blur="onRemoteConfigChange('llm')"
@input="onRemoteConfigInput('llm')" />
</t-form-item>
</div>
<!-- 错误信息显示 -->
<div v-if="modelStatus.llm.checked && !modelStatus.llm.available && modelStatus.llm.message" class="error-message">
<t-icon name="error-circle" />
<span>{{ modelStatus.llm.message }}</span>
</div>
</div>
</div>
<!-- Embedding 嵌入模型配置区域 -->
<div class="config-section">
<h3><t-icon name="layers" class="section-icon" />Embedding 嵌入模型配置</h3>
<!-- 已有文件时的禁用提示 -->
<div v-if="hasFiles" class="embedding-warning">
<t-alert theme="warning" message="知识库中已有文件,无法修改Embedding模型配置" />
</div>
<div class="form-row">
<t-form-item label="模型来源" name="embedding.source">
<t-radio-group v-model="formData.embedding.source" @change="onModelSourceChange('embedding')" :disabled="hasFiles">
<t-radio value="local">Ollama (本地)</t-radio>
<t-radio value="remote">Remote API (远程)</t-radio>
</t-radio-group>
</t-form-item>
</div>
<div class="form-row">
<t-form-item label="模型名称" name="embedding.modelName">
<div class="model-input-with-status">
<t-input v-model="formData.embedding.modelName" placeholder="例如: nomic-embed-text:latest"
@blur="onModelNameChange('embedding')"
@input="onModelNameInput('embedding')"
@keyup.enter="onModelNameChange('embedding')"
:disabled="hasFiles" />
<div class="model-status-icon">
<t-icon
v-if="formData.embedding.source === 'local' && formData.embedding.modelName && modelStatus.embedding.checked"
:name="modelStatus.embedding.available ? 'check-circle-filled' : 'close-circle-filled'"
:class="['status-icon', modelStatus.embedding.available ? 'installed' : 'not-installed']"
:title="modelStatus.embedding.available ? '已安装' : '未安装'"
/>
<t-icon
v-else-if="formData.embedding.source === 'local' && formData.embedding.modelName && !modelStatus.embedding.checked"
name="help-circle"
class="status-icon unknown"
title="未检查"
/>
<t-icon
v-else-if="formData.embedding.source === 'local' && formData.embedding.modelName && modelStatus.embedding.downloading"
name="loading"
class="status-icon downloading spinning"
title="下载中"
/>
</div>
<!-- 下载按钮:未安装时显示 -->
<div v-if="formData.embedding.source === 'local' && formData.embedding.modelName && modelStatus.embedding.checked && !modelStatus.embedding.available && !modelStatus.embedding.downloading" class="download-action">
<t-tooltip content="下载模型">
<t-button
size="small"
theme="primary"
@click="downloadModel('embedding', formData.embedding.modelName)"
class="download-btn"
:disabled="modelStatus.embedding.downloading"
>
<t-icon name="download" />
</t-button>
</t-tooltip>
</div>
</div>
</t-form-item>
</div>
<!-- 向量维度设置 -->
<div class="form-row">
<t-form-item label="维度" name="embedding.dimension">
<div class="dimension-input-with-action">
<t-input v-model="formData.embedding.dimension"
:disabled="hasFiles"
placeholder="请输入向量维度"
style="width: 100px;"
@input="onDimensionInput" />
<t-button
size="small"
variant="outline"
class="detect-dim-btn"
:loading="embeddingDimDetecting"
:disabled="hasFiles"
@click="detectEmbeddingDimension"
>
检测维度
</t-button>
</div>
</t-form-item>
<!-- 下载进度:下载中时显示 -->
<div v-if="formData.embedding.source === 'local' && formData.embedding.modelName && modelStatus.embedding.downloading" class="download-progress">
<div class="progress-info">
<t-icon name="loading" class="loading-icon spinning" />
<span class="progress-text">下载中 {{ modelStatus.embedding.progress.toFixed(1) }}%</span>
</div>
<t-progress :percentage="Number(modelStatus.embedding.progress.toFixed(1))" :show-info="false" size="small" class="progress-bar" />
<div class="progress-message">{{ modelStatus.embedding.message }}</div>
</div>
</div>
<!-- 远程 Embedding API 配置 -->
<div v-if="formData.embedding.source === 'remote'" class="remote-config">
<div class="form-row">
<t-form-item label="Base URL" name="embedding.baseUrl">
<div class="url-input-with-check">
<t-input v-model="formData.embedding.baseUrl" placeholder="例如: https://api.openai.com/v1, 去除末尾/embeddings路径后的URL的前面部分"
:disabled="hasFiles" @blur="onRemoteConfigChange('embedding')"
@input="onRemoteConfigInput('embedding')" />
<div v-if="formData.embedding.modelName && formData.embedding.baseUrl && !hasFiles" class="check-action">
<t-tooltip v-if="!modelStatus.embedding.available && modelStatus.embedding.checked" content="重新检查连接">
<t-icon
name="refresh"
class="refresh-icon"
:class="{ spinning: modelStatus.embedding.checking }"
@click="checkEmbeddingModelStatus()"
/>
</t-tooltip>
<t-icon
v-else-if="modelStatus.embedding.checked"
:name="modelStatus.embedding.available ? 'check-circle-filled' : 'close-circle-filled'"
:class="['status-icon', modelStatus.embedding.available ? 'installed' : 'not-installed']"
:title="modelStatus.embedding.available ? '连接正常' : '连接失败'"
/>
<t-icon
v-else
name="loading"
class="input-icon checking spinning"
title="检查连接中"
/>
</div>
</div>
</t-form-item>
</div>
<div class="form-row">
<t-form-item label="API Key (可选)" name="embedding.apiKey">
<t-input v-model="formData.embedding.apiKey" type="password" placeholder="请输入API Key (可选)"
:disabled="hasFiles" @blur="onRemoteConfigChange('embedding')"
@input="onRemoteConfigInput('embedding')" />
</t-form-item>
</div>
<!-- 错误信息显示 -->
<div v-if="modelStatus.embedding.checked && !modelStatus.embedding.available && modelStatus.embedding.message" class="error-message">
<t-icon name="error-circle" />
<span>{{ modelStatus.embedding.message }}</span>
</div>
</div>
</div>
<!-- Rerank 重排模型配置区域 -->
<div class="config-section">
<h3><t-icon name="swap" class="section-icon" />Rerank 重排模型配置</h3>
<div class="form-row">
<t-form-item name="rerank.enabled">
<div class="switch-container">
<t-switch v-model="formData.rerank.enabled" @change="onRerankChange" />
<span class="switch-label">启用Rerank重排模型</span>
</div>
</t-form-item>
</div>
<!-- Rerank 详细配置 -->
<div v-if="formData.rerank.enabled" class="rerank-config">
<div class="form-row">
<t-form-item label="模型名称" name="rerank.modelName">
<div class="model-input-with-status">
<t-input v-model="formData.rerank.modelName" placeholder="例如: bge-reranker-v2-m3"
@blur="onRerankConfigChange"
@input="onRerankConfigInput" />
<div class="model-status-icon">
<t-icon
v-if="formData.rerank.modelName && modelStatus.rerank.checked"
:name="modelStatus.rerank.available ? 'check-circle-filled' : 'close-circle-filled'"
:class="['status-icon', modelStatus.rerank.available ? 'installed' : 'not-installed']"
:title="modelStatus.rerank.available ? '连接正常' : '连接失败'"
/>
<t-icon
v-else-if="formData.rerank.modelName && !modelStatus.rerank.checked"
name="help-circle"
class="status-icon unknown"
title="未检查"
/>
</div>
</div>
</t-form-item>
</div>
<div class="form-row">
<t-form-item label="Base URL" name="rerank.baseUrl">
<div class="url-input-with-check">
<t-input v-model="formData.rerank.baseUrl" placeholder="例如: http://localhost:11434, 去除末尾/rerank路径后的URL的前面部分"
@blur="onRerankConfigChange"
@input="onRerankConfigInput" />
<div v-if="formData.rerank.modelName && formData.rerank.baseUrl" class="check-action">
<t-tooltip v-if="!modelStatus.rerank.available && modelStatus.rerank.checked" content="重新检查连接">
<t-icon
name="refresh"
class="refresh-icon"
:class="{ spinning: modelStatus.rerank.checking }"
@click="checkRerankModelStatus"
/>
</t-tooltip>
<t-icon
v-else-if="modelStatus.rerank.checked"
:name="modelStatus.rerank.available ? 'check-circle-filled' : 'close-circle-filled'"
:class="['status-icon', modelStatus.rerank.available ? 'installed' : 'not-installed']"
:title="modelStatus.rerank.available ? '连接正常' : '连接失败'"
/>
<t-icon
v-else
name="loading"
class="input-icon checking spinning"
title="检查连接中"
/>
</div>
</div>
</t-form-item>
</div>
<div class="form-row">
<t-form-item label="API Key" name="rerank.apiKey">
<t-input v-model="formData.rerank.apiKey" type="password" placeholder="请输入API Key (可选)"
@blur="onRerankConfigChange"
@input="onRerankConfigInput" />
</t-form-item>
</div>
<!-- 错误信息显示 -->
<div v-if="modelStatus.rerank.checked && !modelStatus.rerank.available && modelStatus.rerank.message" class="error-message">
<t-icon name="error-circle" />
<span>{{ modelStatus.rerank.message }}</span>
</div>
</div>
</div>
<!-- 多模态配置区域 -->
<div class="config-section">
<h3><t-icon name="image" class="section-icon" />多模态配置</h3>
<div class="form-row">
<t-form-item name="multimodal.enabled">
<div class="switch-container">
<t-switch v-model="formData.multimodal.enabled" @change="onMultimodalChange" />
<span class="switch-label">启用多模态图片信息提取</span>
</div>
</t-form-item>
</div>
<!-- 多模态详细配置 -->
<div v-if="formData.multimodal.enabled" class="multimodal-config">
<!-- VLM 视觉语言模型配置 -->
<h4>视觉语言模型配置</h4>
<div class="form-row">
<t-form-item label="模型名称" name="multimodal.vlm.modelName">
<div class="model-input-with-status">
<t-input v-model="formData.multimodal.vlm.modelName" placeholder="例如: qwen2.5vl:3b"
@blur="onModelNameChange('vlm')"
@input="onModelNameInput('vlm')"
@keyup.enter="onModelNameChange('vlm')" />
<div class="model-status-icon">
<t-icon
v-if="formData.multimodal.vlm.interfaceType === 'ollama' && formData.multimodal.vlm.modelName && modelStatus.vlm.checked"
:name="modelStatus.vlm.available ? 'check-circle-filled' : 'close-circle-filled'"
:class="['status-icon', modelStatus.vlm.available ? 'installed' : 'not-installed']"
:title="modelStatus.vlm.available ? '已安装' : '未安装'"
/>
<t-icon
v-else-if="formData.multimodal.vlm.interfaceType === 'ollama' && formData.multimodal.vlm.modelName && !modelStatus.vlm.checked"
name="help-circle"
class="status-icon unknown"
title="未检查"
/>
<t-icon
v-else-if="formData.multimodal.vlm.interfaceType === 'ollama' && formData.multimodal.vlm.modelName && modelStatus.vlm.downloading"
name="loading"
class="status-icon downloading spinning"
title="下载中"
/>
</div>
<!-- 下载按钮:未安装时显示 -->
<div v-if="formData.multimodal.vlm.interfaceType === 'ollama' && formData.multimodal.vlm.modelName && modelStatus.vlm.checked && !modelStatus.vlm.available && !modelStatus.vlm.downloading" class="download-action">
<t-tooltip content="下载模型">
<t-button
size="small"
theme="primary"
@click="downloadModel('vlm', formData.multimodal.vlm.modelName)"
class="download-btn"
:disabled="modelStatus.vlm.downloading"
>
<t-icon name="download" />
</t-button>
</t-tooltip>
</div>
</div>
</t-form-item>
<!-- 下载进度:下载中时显示 -->
<div v-if="formData.multimodal.vlm.interfaceType === 'ollama' && formData.multimodal.vlm.modelName && modelStatus.vlm.downloading" class="download-progress">
<div class="progress-info">
<t-icon name="loading" class="loading-icon spinning" />
<span class="progress-text">下载中 {{ modelStatus.vlm.progress.toFixed(1) }}%</span>
</div>
<t-progress :percentage="Number(modelStatus.vlm.progress.toFixed(1))" :show-info="false" size="small" class="progress-bar" />
<div class="progress-message">{{ modelStatus.vlm.message }}</div>
</div>
</div>
<div class="form-row">
<t-form-item label="接口类型" name="multimodal.vlm.interfaceType">
<t-radio-group v-model="formData.multimodal.vlm.interfaceType" @change="onVlmInterfaceTypeChange">
<t-radio value="ollama">Ollama (本地)</t-radio>
<t-radio value="openai">OpenAI 兼容接口</t-radio>
</t-radio-group>
</t-form-item>
</div>
<div class="form-row" v-if="formData.multimodal.vlm.interfaceType === 'openai'">
<t-form-item label="Base URL" name="multimodal.vlm.baseUrl">
<t-input v-model="formData.multimodal.vlm.baseUrl" placeholder="例如: http://localhost:11434/v1,去除末尾/chat/completions路径后的URL的前面部分"
@blur="onVlmBaseUrlChange"
@input="onVlmBaseUrlInput" />
</t-form-item>
</div>
<div class="form-row" v-if="formData.multimodal.vlm.interfaceType === 'openai'">
<t-form-item label="API Key" name="multimodal.vlm.apiKey">
<t-input v-model="formData.multimodal.vlm.apiKey" type="password" placeholder="请输入API Key (可选)"
@blur="onVlmApiKeyChange" />
</t-form-item>
</div>
<!-- 对象存储服务配置 -->
<h4>对象存储服务配置</h4>
<div class="form-row">
<t-form-item label="存储类型">
<t-radio-group v-model="formData.multimodal.storageType" @change="onStorageTypeChange">
<t-radio value="cos">COS</t-radio>
<t-radio value="minio">MinIO</t-radio>
</t-radio-group>
</t-form-item>
</div>
<!-- MinIO 配置区域 -->
<div v-if="formData.multimodal.storageType === 'minio'">
<div class="form-row">
<t-form-item label="Bucket Name" name="multimodal.minio.bucketName">
<t-input v-model="formData.multimodal.minio.bucketName" placeholder="请输入Bucket名称" />
</t-form-item>
</div>
<div class="form-row">
<t-form-item label="Path Prefix" name="multimodal.minio.pathPrefix">
<t-input v-model="formData.multimodal.minio.pathPrefix" placeholder="例如: images" />
</t-form-item>
</div>
</div>
<!-- COS 配置区域 -->
<div class="form-row">
<t-form-item v-if="formData.multimodal.storageType === 'cos'" label="Secret ID" name="multimodal.cos.secretId">
<t-input v-model="formData.multimodal.cos.secretId" placeholder="请输入COS Secret ID"
@blur="onCosConfigChange" />
</t-form-item>
</div>
<div class="form-row">
<t-form-item v-if="formData.multimodal.storageType === 'cos'" label="Secret Key" name="multimodal.cos.secretKey">
<t-input v-model="formData.multimodal.cos.secretKey" type="password" placeholder="请输入COS Secret Key"
@blur="onCosConfigChange" />
</t-form-item>
</div>
<div class="form-row">
<t-form-item v-if="formData.multimodal.storageType === 'cos'" label="Region" name="multimodal.cos.region">
<t-input v-model="formData.multimodal.cos.region" placeholder="例如: ap-beijing"
@blur="onCosConfigChange" />
</t-form-item>
</div>
<div class="form-row">
<t-form-item v-if="formData.multimodal.storageType === 'cos'" label="Bucket Name" name="multimodal.cos.bucketName">
<t-input v-model="formData.multimodal.cos.bucketName" placeholder="请输入Bucket名称"
@blur="onCosConfigChange" />
</t-form-item>
</div>
<div class="form-row">
<t-form-item v-if="formData.multimodal.storageType === 'cos'" label="App ID" name="multimodal.cos.appId">
<t-input v-model="formData.multimodal.cos.appId" placeholder="请输入App ID"
@blur="onCosConfigChange" />
</t-form-item>
</div>
<div class="form-row">
<t-form-item v-if="formData.multimodal.storageType === 'cos'" label="Path Prefix" name="multimodal.cos.pathPrefix">
<t-input v-model="formData.multimodal.cos.pathPrefix" placeholder="例如: images"
@blur="onCosConfigChange" />
</t-form-item>
</div>
<!-- 多模态功能测试区域 -->
<div v-if="canTestMultimodal" class="multimodal-test">
<h5>功能测试</h5>
<p class="test-desc">上传图片测试VLM模型的图片描述和文字识别功能</p>
<div class="test-area">
<!-- 上传区域 -->
<div class="upload-section">
<div class="upload-buttons">
<t-upload
ref="imageUpload"
v-model="multimodalTest.uploadedFiles"
:show-upload-list="false"
:auto-upload="false"
:accept="'image/*'"
:size-limit="10485760"
@change="onImageChange"
>
<t-button theme="default" variant="outline" size="small">
<t-icon name="upload" />
选择图片
</t-button>
</t-upload>
</div>
</div>
<!-- 图片预览 -->
<div v-if="multimodalTest.selectedFile" class="image-preview">
<img :src="multimodalTest.previewUrl" alt="测试图片" />
<div class="image-meta">
<span class="file-name">{{ multimodalTest.selectedFile.name }}</span>
<span class="file-size">{{ formatFileSize(multimodalTest.selectedFile.size) }}</span>
</div>
</div>
<div class="test-button-wrapper">
<t-button
v-if="multimodalTest.selectedFile"
theme="primary"
size="small"
:loading="multimodalTest.testing"
@click="startMultimodalTest"
>
开始测试
</t-button>
</div>
<!-- 测试结果 -->
<div v-if="multimodalTest.result" class="test-result">
<div v-if="multimodalTest.result.success" class="result-success">
<h6>测试结果</h6>
<div v-if="multimodalTest.result.caption" class="result-item">
<label>图片描述:</label>
<div class="result-text">{{ multimodalTest.result.caption }}</div>
</div>
<div v-if="multimodalTest.result.ocr" class="result-item">
<label>文字识别:</label>
<div class="result-text">{{ multimodalTest.result.ocr }}</div>
</div>
<div v-if="multimodalTest.result.processing_time" class="result-time">
处理时间: {{ multimodalTest.result.processing_time }}ms
</div>
</div>
<div v-else class="result-error">
<h6>测试失败</h6>
<div class="error-msg">
<t-icon name="error-circle" />
{{ multimodalTest.result.message || '多模态处理失败' }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 文档分割配置区域 -->
<div class="config-section">
<h3><t-icon name="cut" class="section-icon" />文档分割配置</h3>
<!-- 预设配置选择 -->
<div class="form-row preset-row">
<t-form-item label="分割策略">
<t-radio-group v-model="selectedPreset" @change="onPresetChange" class="preset-radio-group">
<t-radio value="balanced" class="preset-radio">
<div class="preset-content">
<div class="preset-title">均衡模式</div>
<div class="preset-desc">块大小: 1000 / 重叠: 200</div>
</div>
</t-radio>
<t-radio value="precision" class="preset-radio">
<div class="preset-content">
<div class="preset-title">精准模式</div>
<div class="preset-desc">块大小: 512 / 重叠: 100</div>
</div>
</t-radio>
<t-radio value="context" class="preset-radio">
<div class="preset-content">
<div class="preset-title">上下文模式</div>
<div class="preset-desc">块大小: 2048 / 重叠: 400</div>
</div>
</t-radio>
<t-radio value="custom" class="preset-radio">
<div class="preset-content">
<div class="preset-title">自定义</div>
<div class="preset-desc">手动配置参数</div>
</div>
</t-radio>
</t-radio-group>
</t-form-item>
</div>
<!-- 参数配置网格 -->
<div class="parameters-grid" :class="{ 'disabled-grid': selectedPreset !== 'custom' }">
<div class="parameter-group">
<div class="parameter-label">分块大小</div>
<div class="parameter-control">
<t-slider
v-model="formData.documentSplitting.chunkSize"
:min="100"
:max="4000"
:step="1"
:disabled="selectedPreset !== 'custom'"
:marks="{ 100: '100', 1000: '1000', 2000: '2000', 4000: '4000' }"
class="parameter-slider"
/>
<div class="parameter-value">{{ formData.documentSplitting.chunkSize }} 字符</div>
</div>
<div class="parameter-desc">控制每个文档分块的大小,影响检索精度</div>
</div>
<div class="parameter-group">
<div class="parameter-label">分块重叠</div>
<div class="parameter-control">
<t-slider
v-model="formData.documentSplitting.chunkOverlap"
:min="0"
:max="1000"
:step="1"
:disabled="selectedPreset !== 'custom'"
:marks="{ 0: '0', 200: '200', 500: '500', 1000: '1000' }"
class="parameter-slider"
/>
<div class="parameter-value">{{ formData.documentSplitting.chunkOverlap }} 字符</div>
</div>
<div class="parameter-desc">分块间重叠的字符数,保持上下文连贯性</div>
</div>
<div class="parameter-group">
<div class="parameter-label">分隔符设置</div>
<div class="parameter-control">
<t-select
v-model="formData.documentSplitting.separators"
multiple
:disabled="selectedPreset !== 'custom'"
placeholder="选择或自定义分隔符"
class="parameter-select"
clearable
creatable
:options="separatorOptions"
/>
</div>
</div>
</div>
</div>
<!-- 提交按钮区域 -->
<div class="submit-section">
<t-button theme="primary" type="button" size="large"
:loading="submitting" :disabled="!canSubmit || isSubmitDebounced"
@click="handleSubmit">
{{ isUpdateMode ? '更新配置信息' : '完成配置' }}
</t-button>
<!-- 提交状态提示 -->
<div v-if="!canSubmit && hasOllamaModels" class="submit-tips">
<t-icon name="info-circle" class="tip-icon" />
<span>请等待所有Ollama模型下载完成后再进行配置更新</span>
</div>
<!-- 远程API配置提示 -->
<div v-if="!canSubmit && !hasOllamaModels" class="submit-tips">
<t-icon name="info-circle" class="tip-icon" />
<span>请完善模型配置信息</span>
</div>
</div>
</t-form>
</div>
</template>
<script setup lang="ts">
/**
* 导入必要的 Vue 组合式 API 和外部依赖
*/
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next';
import {
initializeSystem,
checkOllamaStatus,
checkOllamaModels,
downloadOllamaModel,
getDownloadProgress,
getCurrentConfig,
checkRemoteModel,
type DownloadTask,
checkRerankModel,
testMultimodalFunction,
listOllamaModels,
testEmbeddingModel
} from '@/api/initialization';
import { useAuthStore } from '@/stores/auth';
const router = useRouter();
const authStore = useAuthStore();
type TFormRef = {
validate: (fields?: string[] | undefined) => Promise<true | any>;
clearValidate?: (fields?: string | string[]) => void;
} | null;
const form = ref<TFormRef>(null);
const submitting = ref(false);
const hasFiles = ref(false);
const isUpdateMode = ref(false); // 是否为更新模式
// 防抖机制:防止按钮快速重复点击
const submitDebounceTimer = ref<ReturnType<typeof setTimeout> | null>(null);
const isSubmitDebounced = ref(false);
// Ollama服务状态
const ollamaStatus = reactive({
checked: false,
available: false,
version: '',
error: '',
baseUrl: ''
});
// 顶部摘要:已安装模型
const installedModels = ref<string[]>([]);
const summaryRefreshing = ref<boolean>(false);
// 下载任务管理
const downloadTasks = reactive<Record<string, DownloadTask>>({});
const progressTimers = reactive<Record<string, any>>({});
// 模型状态管理
const modelStatus = reactive({
llm: {
checked: false,
available: false,
downloading: false,
checking: false,
taskId: '',
progress: 0,
message: ''
},
embedding: {
checked: false,
available: false,
downloading: false,
checking: false,
taskId: '',
progress: 0,
message: ''
},
vlm: {
checked: false,
available: false,
downloading: false,
checking: false,
taskId: '',
progress: 0,
message: ''
},
rerank: {
checked: false,
available: false,
downloading: false,
checking: false,
taskId: '',
progress: 0,
message: ''
}
});
// 表单数据
const formData = reactive({
llm: {
source: 'local',
modelName: '',
baseUrl: '',
apiKey: ''
},
embedding: {
source: 'local',
modelName: '',
baseUrl: '',
apiKey: '',
dimension: 0 // 默认嵌入维度值
},
rerank: {
enabled: false,
modelName: '',
baseUrl: '',
apiKey: ''
},
multimodal: {
enabled: false,
storageType: 'minio' as 'minio' | 'cos',
vlm: {
modelName: '',
baseUrl: '',
apiKey: '',
interfaceType: 'ollama'
},
cos: {
secretId: '',
secretKey: '',
region: '',
bucketName: '',
appId: '',
pathPrefix: ''
},
minio: {
bucketName: '',
useSSL: false,
pathPrefix: ''
}
},
documentSplitting: {
chunkSize: 1000,
chunkOverlap: 200,
separators: ['\n\n', '\n', '。', '!', '?', ';', ';']
}
});
// 输入防抖定时器
const inputDebounceTimers = reactive<Record<string, any>>({});
// Embedding 维度检测状态
const embeddingDimDetecting = ref(false);
// 预设配置选择
const selectedPreset = ref('balanced');
// 分隔符选项
const separatorOptions = [
{ label: '段落分隔符 (\\n\\n)', value: '\n\n' },
{ label: '换行符 (\\n)', value: '\n' },
{ label: '句号 (。)', value: '。' },
{ label: '感叹号 (!)', value: '!' },
{ label: '问号 (?)', value: '?' },
{ label: '分号 (;)', value: ';' },
{ label: '中文分号 (;)', value: ';' },
{ label: '逗号 (,)', value: ',' },
{ label: '中文逗号 (,)', value: ',' }
];
// 多模态测试状态
const multimodalTest = reactive({
uploadedFiles: [],
selectedFile: null as File | null,
previewUrl: '',
testing: false,
result: null as any
});
// 计算属性
const isVlmOllama = computed(() => {
return formData.multimodal.vlm.interfaceType === 'ollama';
});
const hasOllamaModels = computed(() => {
// 检查是否真正需要Ollama模型
const needsLlmOllama = formData.llm.source === 'local' && formData.llm.modelName;
const needsEmbeddingOllama = formData.embedding.source === 'local' && formData.embedding.modelName;
const needsVlmOllama = formData.multimodal.enabled &&
formData.multimodal.vlm.interfaceType === 'ollama' &&
formData.multimodal.vlm.modelName;
return needsLlmOllama || needsEmbeddingOllama || needsVlmOllama;
});
const canTestMultimodal = computed(() => {
// 检查VLM模型配置是否完整
const vlmConfigComplete = formData.multimodal.vlm.modelName &&
(formData.multimodal.vlm.interfaceType === 'ollama' ||
(formData.multimodal.vlm.interfaceType === 'openai' && formData.multimodal.vlm.baseUrl));
// 检查存储配置是否完整
let storageConfigComplete = false;
if (formData.multimodal.storageType === 'cos') {
storageConfigComplete = !!(formData.multimodal.cos.secretId &&
formData.multimodal.cos.secretKey &&
formData.multimodal.cos.region &&
formData.multimodal.cos.bucketName &&
formData.multimodal.cos.appId);
} else if (formData.multimodal.storageType === 'minio') {
storageConfigComplete = !!formData.multimodal.minio.bucketName;
}
return vlmConfigComplete && storageConfigComplete;
});
const canSubmit = computed(() => {
// 基本必填项检查
const basicRequired = formData.llm.modelName &&
formData.embedding.modelName;
if (!basicRequired) return false;
// LLM模型检查
let llmOk = true;
if (formData.llm.source === 'local') {
// Ollama模型需要检查可用性
llmOk = modelStatus.llm.available && !modelStatus.llm.downloading;
} else {
// 远程API只需要有BaseURL即可(如果需要的话,在表单验证中处理)
llmOk = true;
}
// Embedding模型检查
let embeddingOk = true;
if (formData.embedding.source === 'local') {
// Ollama模型需要检查可用性
embeddingOk = modelStatus.embedding.available && !modelStatus.embedding.downloading;
} else {
// 远程API只需要有BaseURL即可
embeddingOk = true;
}
// Rerank模型检查(如果启用)
let rerankOk = true;
if (formData.rerank.enabled && formData.rerank.modelName) {
if (formData.rerank.baseUrl) {
// 远程API,不需要特殊检查
rerankOk = true;
} else {
// 本地Ollama模型需要检查可用性
rerankOk = modelStatus.rerank.available && !modelStatus.rerank.downloading;
}
}
// VLM模型检查(如果启用多模态)
let vlmOk = true;
if (formData.multimodal.enabled && formData.multimodal.vlm.modelName) {
if (formData.multimodal.vlm.interfaceType === 'ollama') {
// Ollama模型需要检查可用性
vlmOk = modelStatus.vlm.available && !modelStatus.vlm.downloading;
} else {
// OpenAI兼容API,不需要特殊检查
vlmOk = true;
}
}
return llmOk && embeddingOk && rerankOk && vlmOk;
});
const imageUpload = ref(null);
// 清理完成,开始函数定义
// 验证Embedding维度
const validateEmbeddingDimension = (val: any) => {
val = Number(val);
if (!val || isNaN(val)) {
return false;
}
// 验证是否为整数且在合理范围内
return Number.isInteger(val) ;
};
// 表单验证规则
const rules = {
'llm.modelName': [{ required: true, message: '请输入LLM模型名称', type: 'error' }],
'llm.baseUrl': [
{ required: (t: any) => formData.llm.source === 'remote', message: '请输入BaseURL', type: 'error' }
],
'embedding.modelName': [{ required: true, message: '请输入Embedding模型名称', type: 'error' }],
'embedding.baseUrl': [
{ required: (t: any) => formData.embedding.source === 'remote', message: '请输入BaseURL', type: 'error' }
],
'embedding.dimension': [
{ required: true, message: '请输入Embedding维度', type: 'error' },
{ validator: validateEmbeddingDimension, message: '维度必须为有效整数值,常见取值为768, 1024, 1536, 3584等', type: 'error' }
]
};
// 检查Ollama服务状态
const checkOllama = async () => {
try {
const result = await checkOllamaStatus();
ollamaStatus.checked = true;
ollamaStatus.available = result.available;
ollamaStatus.version = result.version || '';
ollamaStatus.error = result.error || '';
ollamaStatus.baseUrl = result.baseUrl || '';
if (ollamaStatus.available) {
// 如果Ollama可用,检查已配置的模型
await checkAllOllamaModels();
}
} catch (error) {
console.error('检查Ollama状态失败:', error);
ollamaStatus.checked = true;
ollamaStatus.available = false;
ollamaStatus.error = '检查失败';
}
};
// 刷新顶部摘要(状态 + 已安装模型)
const refreshOllamaSummary = async () => {
if (summaryRefreshing.value) return;
summaryRefreshing.value = true;
try {
await checkOllama();
const models = await listOllamaModels();
installedModels.value = models;
} catch (e) {
installedModels.value = [];
} finally {
// 略延时以保证旋转动画可见
setTimeout(() => { summaryRefreshing.value = false; }, 300);
}
};
// 检查所有Ollama模型状态
const checkAllOllamaModels = async () => {
const modelsToCheck = [];
if (formData.llm.source === 'local' && formData.llm.modelName) {
modelsToCheck.push(formData.llm.modelName);
}
if (formData.embedding.source === 'local' && formData.embedding.modelName) {
modelsToCheck.push(formData.embedding.modelName);
}
if (modelsToCheck.length === 0) return;
try {
const result = await checkOllamaModels(modelsToCheck);
// 更新模型状态
if (formData.llm.source === 'local' && formData.llm.modelName) {
modelStatus.llm.checked = true;
modelStatus.llm.available = result.models[formData.llm.modelName] || false;
}
if (formData.embedding.source === 'local' && formData.embedding.modelName) {
modelStatus.embedding.checked = true;
modelStatus.embedding.available = result.models[formData.embedding.modelName] || false;
}
} catch (error) {
console.error('检查模型状态失败:', error);
}
};
// 下载模型
const downloadModel = async (type: 'llm' | 'embedding' | 'vlm', modelName: string) => {
// 防止重复点击
if (modelStatus[type].downloading) {
return;
}
try {
// 立即更新状态,防止重复点击
modelStatus[type].downloading = true;
modelStatus[type].progress = 0;
modelStatus[type].message = '正在启动下载...';
// 启动下载任务
const result = await downloadOllamaModel(modelName);
// 更新任务ID和初始进度
modelStatus[type].taskId = result.taskId;
modelStatus[type].progress = result.progress || 0;
modelStatus[type].message = '下载已开始';
// 如果已经完成,直接更新状态
if (result.status === 'completed') {
modelStatus[type].available = true;
modelStatus[type].downloading = false;
modelStatus[type].progress = 100;
modelStatus[type].message = '下载完成';
MessagePlugin.success(`模型 ${modelName} 下载成功`);
return;
}
// 开始轮询进度
startProgressPolling(type, result.taskId, modelName);
} catch (error) {
console.error(`启动模型 ${modelName} 下载失败:`, error);
MessagePlugin.error(`启动模型 ${modelName} 下载失败`);
modelStatus[type].downloading = false;
modelStatus[type].message = '下载启动失败';
}
};
// 开始轮询下载进度
const startProgressPolling = (type: 'llm' | 'embedding' | 'vlm', taskId: string, modelName: string) => {
// 清除之前的定时器
if (progressTimers[taskId]) {
clearInterval(progressTimers[taskId]);
}
// 每2秒查询一次进度
progressTimers[taskId] = setInterval(async () => {
try {
const task = await getDownloadProgress(taskId);
// 更新模型状态
modelStatus[type].progress = task.progress;
modelStatus[type].message = task.message;
// 检查是否完成
if (task.status === 'completed') {
modelStatus[type].available = true;
modelStatus[type].downloading = false;
modelStatus[type].progress = 100;
modelStatus[type].message = '下载完成';
// 清除定时器
clearInterval(progressTimers[taskId]);
delete progressTimers[taskId];
MessagePlugin.success(`模型 ${modelName} 下载成功`);
} else if (task.status === 'failed') {
modelStatus[type].downloading = false;
modelStatus[type].message = task.message || '下载失败';
// 清除定时器
clearInterval(progressTimers[taskId]);
delete progressTimers[taskId];
MessagePlugin.error(`模型 ${modelName} 下载失败: ${task.message}`);
}
} catch (error) {
console.error('查询下载进度失败:', error);
// 如果查询失败,停止轮询
clearInterval(progressTimers[taskId]);
delete progressTimers[taskId];
modelStatus[type].downloading = false;
modelStatus[type].message = '查询进度失败';
}
}, 2000);
};
// 配置回填
const loadCurrentConfig = async () => {
try {
const config = await getCurrentConfig();
// 设置hasFiles状态
hasFiles.value = config.hasFiles || false;
// 检查是否已有配置(判断是否为更新模式)
const hasExistingConfig = config.llm?.modelName || config.embedding?.modelName || config.rerank?.modelName;
isUpdateMode.value = !!hasExistingConfig;
// 回填表单数据
if (config.llm) {
Object.assign(formData.llm, config.llm);
console.log('LLM config loaded:', formData.llm);
}
if (config.embedding) {
Object.assign(formData.embedding, config.embedding);
console.log('Embedding config loaded:', formData.embedding);
}
if (config.rerank) {
Object.assign(formData.rerank, config.rerank);
console.log('Rerank config loaded:', formData.rerank);
}
if (config.multimodal) {
Object.assign(formData.multimodal, config.multimodal);
// 强制同步 storageType 到单选框
await nextTick();
formData.multimodal.storageType = (config.multimodal.storageType || 'minio') as 'minio' | 'cos';
console.log('Multimodal config loaded:', formData.multimodal);
}
if (config.documentSplitting) {
Object.assign(formData.documentSplitting, config.documentSplitting);
// 根据回填的配置设置预设模式
const { chunkSize, chunkOverlap } = config.documentSplitting;
if (chunkSize === 1000 && chunkOverlap === 200) {
selectedPreset.value = 'balanced';
} else if (chunkSize === 512 && chunkOverlap === 100) {
selectedPreset.value = 'precision';
} else if (chunkSize === 2048 && chunkOverlap === 400) {
selectedPreset.value = 'context';
} else {
selectedPreset.value = 'custom';
}
}
// 在配置加载完成后,检查模型状态
await checkModelsAfterLoading(config);
} catch (error) {
console.warn('Failed to load current configuration:', error);
// 如果加载失败,使用默认配置
isUpdateMode.value = false;
}
};
// 加载配置后检查模型状态
const checkModelsAfterLoading = async (config: any) => {
// 延迟一点执行,确保DOM已经更新
setTimeout(async () => {
// 检查所有已配置模型的状态
await checkAllConfiguredModels();
// 检查Rerank模型状态
if (formData.rerank.enabled && formData.rerank.modelName && formData.rerank.baseUrl) {
await checkRerankModelStatus();
}
}, 300);
};
// 事件处理
const onModelSourceChange = async (type: 'llm' | 'embedding') => {
// 重置模型状态
modelStatus[type].checked = false;
modelStatus[type].available = false;
modelStatus[type].downloading = false;
// 如果切换到本地,检查Ollama状态
if (formData[type].source === 'local' && !ollamaStatus.checked) {
await checkOllama();
}
};
const onModelNameChange = async (type: 'llm' | 'embedding' | 'vlm') => {
if (type === 'vlm') {
// 总是重置VLM模型状态
modelStatus.vlm.checked = false;
modelStatus.vlm.available = false;
modelStatus.vlm.downloading = false;
if (formData.multimodal.enabled && isVlmOllama.value && formData.multimodal.vlm.modelName) {
if (ollamaStatus.available) {
await checkAllOllamaModels();
// 触发表单验证
setTimeout(() => {
form.value?.validate(['multimodal.vlm.modelName']);
}, 100);
}
}
} else {
// 总是重置模型状态
modelStatus[type].checked = false;
modelStatus[type].available = false;
modelStatus[type].downloading = false;
if (formData[type].source === 'local' && formData[type].modelName) {
if (ollamaStatus.available) {
await checkAllOllamaModels();
// 触发表单验证
setTimeout(() => {
form.value?.validate([`${type}.modelName`]);
}, 100);
}
}
}
};
const onModelNameInput = (type: 'llm' | 'embedding' | 'vlm') => {
// 清除之前的定时器
if (inputDebounceTimers[type]) {
clearTimeout(inputDebounceTimers[type]);
}
// 重置模型状态
if (type === 'vlm') {
modelStatus.vlm.checked = false;
modelStatus.vlm.available = false;
modelStatus.vlm.message = '';
} else {
modelStatus[type].checked = false;
modelStatus[type].available = false;
modelStatus[type].message = '';
}
// 设置防抖延迟
inputDebounceTimers[type] = setTimeout(async () => {
const modelName = type === 'vlm' ? formData.multimodal.vlm.modelName : formData[type].modelName;
// 只有输入了模型名称才进行校验
if (modelName && modelName.trim()) {
// 触发表单验证
form.value?.validate([type === 'vlm' ? 'multimodal.vlm.modelName' : `${type}.modelName`]);
// 如果是远程API,自动检查模型状态
if (type === 'llm' && formData.llm.source === 'remote' && formData.llm.baseUrl) {
await checkRemoteModelStatus('llm');
} else if (type === 'embedding' && formData.embedding.source === 'remote' && formData.embedding.baseUrl) {
await checkEmbeddingModelStatus();
} else if (type === 'vlm' && !isVlmOllama.value) {
// VLM远程API校验可以在这里添加
}
// 如果是本地模型且Ollama可用,检查模型状态
if ((type === 'llm' || type === 'embedding') && formData[type].source === 'local' && ollamaStatus.available) {
await checkAllOllamaModels();
} else if (type === 'vlm' && isVlmOllama.value && ollamaStatus.available) {
await checkAllOllamaModels();
}
}
}, 500); // 500ms防抖延迟
};
// 远程配置改变时的处理
const onRemoteConfigChange = async (type: 'llm' | 'embedding') => {
// 重置模型状态
modelStatus[type].checked = false;
modelStatus[type].available = false;
modelStatus[type].message = '';
// 如果配置完整,检查模型
if (formData[type].modelName && formData[type].baseUrl) {
if (type === 'llm') {
await checkRemoteModelStatus(type);
} else if (type === 'embedding') {
await checkEmbeddingModelStatus();
}
}
};
// 远程配置输入时的处理
const onRemoteConfigInput = async (type: 'llm' | 'embedding') => {
// 清除之前的定时器
const timerKey = `${type}_remote`;
if (inputDebounceTimers[timerKey]) {
clearTimeout(inputDebounceTimers[timerKey]);
}
// 重置模型状态
modelStatus[type].checked = false;
modelStatus[type].available = false;
modelStatus[type].message = '';
// 设置防抖延迟
inputDebounceTimers[timerKey] = setTimeout(async () => {
// 只有在有模型名称和Base URL时才进行校验
if (formData[type].modelName && formData[type].modelName.trim() &&
formData[type].baseUrl && formData[type].baseUrl.trim()) {
// 触发表单验证
form.value?.validate([`${type}.modelName`, `${type}.baseUrl`]);
// 自动检查远程API模型状态
if (type === 'llm') {
await checkRemoteModelStatus(type);
} else if (type === 'embedding') {
await checkEmbeddingModelStatus();
}
}
}, 500); // 500ms防抖延迟
};
// 检查远程模型
const checkRemoteModelStatus = async (type: 'llm') => {
if (!formData[type].modelName || !formData[type].baseUrl) {
return;
}
try {
modelStatus[type].checking = true;
modelStatus[type].checked = false;
modelStatus[type].available = false;
modelStatus[type].message = '';
const result = await checkRemoteModel({
modelName: formData[type].modelName,
baseUrl: formData[type].baseUrl,
apiKey: formData[type].apiKey
});
modelStatus[type].checked = true;
modelStatus[type].available = result.available;
modelStatus[type].message = result.message || '';
// 触发表单验证
setTimeout(() => {
form.value?.validate([`${type}.modelName`]);
}, 100);
} catch (error) {
console.error(`检查远程${type}模型失败:`, error);
modelStatus[type].checked = true;
modelStatus[type].available = false;
const err = error as any;
modelStatus[type].message = (err && err.message) || '网络连接失败';
} finally {
modelStatus[type].checking = false;
}
};
// 检查所有已配置模型的状态
const checkAllConfiguredModels = async () => {
// 检查LLM模型
if (formData.llm.source === 'local' && formData.llm.modelName && ollamaStatus.available) {
await checkAllOllamaModels();
} else if (formData.llm.source === 'remote' && formData.llm.modelName && formData.llm.baseUrl) {
await checkRemoteModelStatus('llm');
}
// 检查Embedding模型(如果没有文件)
if (!hasFiles.value) {
if (formData.embedding.source === 'local' && formData.embedding.modelName && ollamaStatus.available) {
await checkAllOllamaModels();
} else if (formData.embedding.source === 'remote' && formData.embedding.modelName && formData.embedding.baseUrl) {
await checkEmbeddingModelStatus();
}
}
};
const onDimensionInput = (event: any) => {
formData.embedding.dimension = Number(event.target.value);
};
// 检查Embedding模型状态
const checkEmbeddingModelStatus = async () => {
if (!formData.embedding.modelName) {
return;
}
try {
modelStatus.embedding.checking = true;
modelStatus.embedding.checked = false;
modelStatus.embedding.available = false;
modelStatus.embedding.message = '';
const result = await testEmbeddingModel({
source: formData.embedding.source as 'local' | 'remote',
modelName: formData.embedding.modelName,
baseUrl: formData.embedding.source === 'remote' ? formData.embedding.baseUrl : undefined,
apiKey: formData.embedding.apiKey || undefined,
dimension: formData.embedding.dimension || undefined,
});
modelStatus.embedding.checked = true;
modelStatus.embedding.available = result.available || false;
modelStatus.embedding.message = result.message || '';
// 如果检测到维度信息,自动更新
if (result.available && result.dimension && result.dimension > 0) {
formData.embedding.dimension = result.dimension;
}
// 触发表单验证
setTimeout(() => {
form.value?.validate(['embedding.modelName']);
}, 100);
} catch (error) {
console.error('检查Embedding模型失败:', error);
modelStatus.embedding.checked = true;
modelStatus.embedding.available = false;
const err = error as any;
modelStatus.embedding.message = (err && err.message) || '检查失败';
} finally {
modelStatus.embedding.checking = false;
}
};
// 检测并自动填写 Embedding 维度
const detectEmbeddingDimension = async () => {
if (hasFiles.value) return;
// 校验必填:模型来源 + 模型名称;若远程还需 BaseURL
const source = (formData.embedding.source || '').trim();
const modelName = (formData.embedding.modelName || '').trim();
const baseUrl = (formData.embedding.baseUrl || '').trim();
if (!source || !modelName || (source === 'remote' && !baseUrl)) {
// 触发对应字段校验提示
const fields: string[] = ['embedding.source', 'embedding.modelName'];
if (source === 'remote') fields.push('embedding.baseUrl');
try { await form.value?.validate(fields); } catch {}
MessagePlugin.warning('请先完整填写Embedding配置');
return;
}
embeddingDimDetecting.value = true;
try {
const res = await testEmbeddingModel({
source: source as 'local' | 'remote',
modelName,
baseUrl: source === 'remote' ? baseUrl : undefined,
apiKey: formData.embedding.apiKey || undefined,
dimension: formData.embedding.dimension || undefined,
});
const available = !!res.available;
const message = res.message || '';
const dim = Number(res.dimension || 0);
if (available && dim > 0) {
formData.embedding.dimension = dim;
MessagePlugin.success(`检测成功,维度已自动填写为 ${dim}`);
} else {
MessagePlugin.error(message || '检测失败');
}
} catch (e: any) {
const msg = e?.message || '检测失败,请检查配置';
MessagePlugin.error(msg);
} finally {
embeddingDimDetecting.value = false;
}
};
// 第一个handleSubmit函数已删除,使用下面完整的版本
// 停止所有进度轮询
const stopAllProgressPolling = () => {
Object.keys(progressTimers).forEach(taskId => {
clearInterval(progressTimers[taskId]);
delete progressTimers[taskId];
});
};
// 组件卸载时清理定时器
onUnmounted(() => {
stopAllProgressPolling();
// 清理输入防抖定时器
Object.keys(inputDebounceTimers).forEach(key => {
if (inputDebounceTimers[key]) {
clearTimeout(inputDebounceTimers[key]);
delete inputDebounceTimers[key];
}
});
// 清理提交防抖定时器
if (submitDebounceTimer.value) {
clearTimeout(submitDebounceTimer.value);
submitDebounceTimer.value = null;
}
});
// 监听表单变化
watch(() => formData.llm.source, () => onModelSourceChange('llm'));
watch(() => formData.embedding.source, () => onModelSourceChange('embedding'));
// 添加缺失的函数
const onRerankChange = () => {
console.log('Rerank enabled:', formData.rerank.enabled);
};
const onMultimodalChange = async () => {
if (formData.multimodal.enabled) {
// 设置默认的VLM接口类型
if (!formData.multimodal.vlm.interfaceType) {
formData.multimodal.vlm.interfaceType = 'ollama';
}
// 如果是Ollama接口,设置默认的Base URL
if (formData.multimodal.vlm.interfaceType === 'ollama' && !formData.multimodal.vlm.baseUrl) {
formData.multimodal.vlm.baseUrl = 'http://localhost:11434/v1';
}
}
};
const onPresetChange = () => {
if (selectedPreset.value === 'balanced') {
formData.documentSplitting.chunkSize = 1000;
formData.documentSplitting.chunkOverlap = 200;
formData.documentSplitting.separators = ['\n\n', '\n', '。', '!', '?', ';', ';'];
} else if (selectedPreset.value === 'precision') {
formData.documentSplitting.chunkSize = 512;
formData.documentSplitting.chunkOverlap = 100;
formData.documentSplitting.separators = ['\n\n', '\n', '。', '!', '?', ';', ';'];
} else if (selectedPreset.value === 'context') {
formData.documentSplitting.chunkSize = 2048;
formData.documentSplitting.chunkOverlap = 400;
formData.documentSplitting.separators = ['\n\n', '\n', '。', '!', '?', ';', ';'];
}
};
const onRerankConfigChange = async () => {
// 重置模型状态
modelStatus.rerank.checked = false;
modelStatus.rerank.available = false;
modelStatus.rerank.message = '';
// 如果配置完整,检查模型
if (formData.rerank.modelName && formData.rerank.baseUrl) {
await checkRerankModelStatus();
}
};
const onRerankConfigInput = async () => {
// 清除之前的定时器
const timerKey = 'rerank_remote';
if (inputDebounceTimers[timerKey]) {
clearTimeout(inputDebounceTimers[timerKey]);
}
// 重置模型状态
modelStatus.rerank.checked = false;
modelStatus.rerank.available = false;
modelStatus.rerank.message = '';
// 设置防抖延迟
inputDebounceTimers[timerKey] = setTimeout(async () => {
// 只有在有模型名称和Base URL时才进行校验
if (formData.rerank.modelName && formData.rerank.modelName.trim() &&
formData.rerank.baseUrl && formData.rerank.baseUrl.trim()) {
// 触发表单验证
form.value?.validate(['rerank.modelName', 'rerank.baseUrl']);
// 自动检查Rerank模型状态
await checkRerankModelStatus();
}
}, 500); // 500ms防抖延迟
};
const checkRerankModelStatus = async () => {
if (!formData.rerank.modelName || !formData.rerank.baseUrl) {
return;
}
try {
modelStatus.rerank.checking = true;
modelStatus.rerank.checked = false;
modelStatus.rerank.available = false;
modelStatus.rerank.message = '';
const result = await checkRerankModel({
modelName: formData.rerank.modelName,
baseUrl: formData.rerank.baseUrl,
apiKey: formData.rerank.apiKey
});
modelStatus.rerank.checked = true;
modelStatus.rerank.available = result.available;
modelStatus.rerank.message = result.message || '';
// 触发表单验证
setTimeout(() => {
form.value?.validate(['rerank.modelName']);
}, 100);
} catch (error) {
console.error('检查Rerank模型失败:', error);
modelStatus.rerank.checked = true;
modelStatus.rerank.available = false;
const err = error as any;
modelStatus.rerank.message = (err && err.message) || '网络连接失败';
} finally {
modelStatus.rerank.checking = false;
}
};
// 添加一些基本的多模态函数(简化版)
const onVlmInterfaceTypeChange = () => {
console.log('VLM interface type changed:', formData.multimodal.vlm.interfaceType);
};
const onVlmBaseUrlChange = async () => {
console.log('VLM base URL changed:', formData.multimodal.vlm.baseUrl);
};
const onVlmBaseUrlInput = () => {
console.log('VLM base URL input');
};
const onVlmApiKeyChange = () => {
console.log('VLM API key changed');
};
const onCosConfigChange = () => {
console.log('COS config changed');
};
const onStorageTypeChange = () => {
console.log('Storage type changed:', formData.multimodal.storageType);
};
const onImageChange = (files: any) => {
if (files && files.length > 0) {
// 检查多模态配置是否完整
const missingConfigs: string[] = [];
// 根据存储类型检查必填项
if (formData.multimodal.storageType === 'cos') {
if (!formData.multimodal.cos.secretId) missingConfigs.push('COS Secret ID');
if (!formData.multimodal.cos.secretKey) missingConfigs.push('COS Secret Key');
if (!formData.multimodal.cos.region) missingConfigs.push('COS Region');
if (!formData.multimodal.cos.bucketName) missingConfigs.push('COS Bucket Name');
if (!formData.multimodal.cos.appId) missingConfigs.push('COS App ID');
} else if (formData.multimodal.storageType === 'minio') {
if (!formData.multimodal.minio.bucketName) missingConfigs.push('MinIO Bucket Name');
}
// 检查VLM配置
if (!formData.multimodal.vlm.modelName) {
missingConfigs.push('VLM 模型名称');
}
// 如果是OpenAI兼容接口,还需要检查Base URL
if (formData.multimodal.vlm.interfaceType === 'openai' && !formData.multimodal.vlm.baseUrl) {
missingConfigs.push('VLM Base URL');
}
if (missingConfigs.length > 0) {
const missingList = missingConfigs.join('、');
MessagePlugin.error(`多模态配置不完整,请先完成多模态配置后再上传图片`);
return;
}
const file = files[0].raw || files[0];
multimodalTest.selectedFile = file;
// 创建预览URL
if (multimodalTest.previewUrl) {
URL.revokeObjectURL(multimodalTest.previewUrl);
}
multimodalTest.previewUrl = URL.createObjectURL(file);
// 清除之前的测试结果
multimodalTest.result = null;
MessagePlugin.success('图片上传成功,可以开始测试');
}
};
const startMultimodalTest = async () => {
if (!multimodalTest.selectedFile) {
MessagePlugin.warning('请先选择一张图片');
return;
}
multimodalTest.testing = true;
multimodalTest.result = null;
try {
// 准备API参数
const apiParams: {
image: File;
vlm_model: string;
vlm_base_url: string;
vlm_api_key: string;
vlm_interface_type: string;
storage_type?: 'cos' | 'minio';
cos_secret_id?: string;
cos_secret_key?: string;
cos_region?: string;
cos_bucket_name?: string;
cos_app_id?: string;
cos_path_prefix?: string;
minio_bucket_name?: string;
minio_use_ssl?: boolean;
minio_path_prefix?: string;
chunk_size: number;
chunk_overlap: number;
separators: string[];
} = {
image: multimodalTest.selectedFile,
vlm_model: formData.multimodal.vlm.modelName,
vlm_base_url: '', // 将在下方设置
vlm_api_key: '', // 将在下方设置
vlm_interface_type: formData.multimodal.vlm.interfaceType,
chunk_size: formData.documentSplitting.chunkSize,
chunk_overlap: formData.documentSplitting.chunkOverlap,
separators: formData.documentSplitting.separators
};
// 根据存储类型附带不同的参数
if (formData.multimodal.storageType === 'cos') {
apiParams.storage_type = 'cos';
apiParams.cos_secret_id = formData.multimodal.cos.secretId;
apiParams.cos_secret_key = formData.multimodal.cos.secretKey;
apiParams.cos_region = formData.multimodal.cos.region;
apiParams.cos_bucket_name = formData.multimodal.cos.bucketName;
apiParams.cos_app_id = formData.multimodal.cos.appId;
apiParams.cos_path_prefix = formData.multimodal.cos.pathPrefix || '';
} else if (formData.multimodal.storageType === 'minio') {
apiParams.storage_type = 'minio';
apiParams.minio_bucket_name = formData.multimodal.minio.bucketName;
apiParams.minio_use_ssl = formData.multimodal.minio.useSSL;
apiParams.minio_path_prefix = formData.multimodal.minio.pathPrefix;
}
// 如果是OpenAI兼容接口,设置baseUrl和apiKey
if (formData.multimodal.vlm.interfaceType === 'openai') {
apiParams.vlm_base_url = formData.multimodal.vlm.baseUrl;
apiParams.vlm_api_key = formData.multimodal.vlm.apiKey || '';
} else {
// Ollama接口使用默认的本地URL
apiParams.vlm_base_url = 'http://localhost:11434/v1';
apiParams.vlm_api_key = '';
}
const result = await testMultimodalFunction(apiParams);
multimodalTest.result = result;
if (result.success) {
MessagePlugin.success('多模态测试成功');
} else {
MessagePlugin.error(`多模态测试失败: ${result.message}`);
}
} catch (error) {
console.error('多模态测试失败:', error);
multimodalTest.result = {
success: false,
message: (error as any)?.message || '测试过程中发生错误'
};
MessagePlugin.error('多模态测试失败');
} finally {
multimodalTest.testing = false;
}
};
// 处理按钮提交
const handleSubmit = async () => {
// 防止重复提交和防抖
if (submitting.value || isSubmitDebounced.value) {
return;
}
// 启动防抖机制
isSubmitDebounced.value = true;
if (submitDebounceTimer.value) {
clearTimeout(submitDebounceTimer.value);
}
submitDebounceTimer.value = setTimeout(() => {
isSubmitDebounced.value = false;
}, 3000); // 3秒防抖
try {
// 表单验证
const isValid = await form.value?.validate();
if (!isValid) {
MessagePlugin.error('请检查表单填写是否正确');
return;
}
submitting.value = true;
// 确保embedding.dimension是数字类型
if (formData.embedding.dimension) {
formData.embedding.dimension = Number(formData.embedding.dimension);
}
// 直接使用formData,API期望原始结构
const result = await initializeSystem(formData);
if (result.success) {
MessagePlugin.success(isUpdateMode.value ? '配置更新成功' : '系统初始化完成');
// 如果是初始化模式,跳转到主页面
if (!isUpdateMode.value) {
setTimeout(() => {
router.push('/platform/knowledgeBase');
}, 1500);
}
} else {
MessagePlugin.error(result.message || '操作失败');
}
} catch (error) {
console.error('提交失败:', error);
MessagePlugin.error('操作失败,请检查网络连接');
} finally {
submitting.value = false;
// 清理防抖定时器
if (submitDebounceTimer.value) {
clearTimeout(submitDebounceTimer.value);
submitDebounceTimer.value = null;
}
isSubmitDebounced.value = false;
}
};
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 组件挂载时检查Ollama状态
onMounted(async () => {
// 加载当前配置
await loadCurrentConfig();
// 总是检查Ollama状态,因为这是独立于具体配置的
await refreshOllamaSummary();
// 检查已配置模型状态
await checkAllConfiguredModels();
});
</script>
<style lang="less" scoped>
.initialization-content {
padding: 0;
.ollama-summary-card {
margin-bottom: 24px;
background: #ffffff;
border: 1px solid #e9edf5;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
padding: 16px 20px;
.summary-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
.title {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 600;
color: #1f2937;
font-size: 16px;
}
.state{ margin-left: 8px; }
.refresh-icon{
margin-left: 8px;
cursor: pointer;
color: #6b7280;
transition: transform .2s ease, color .2s ease;
}
.refresh-icon:hover{ color: #0ea5e9; }
.refresh-icon.spinning{ animation: spin 1s linear infinite; }
}
.summary-body {
.models {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #6b7280;
font-size: 14px;
margin-top: 2px;
min-width: 100px;
}
.model-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.model-pill {
border-radius: 8px;
}
.empty {
color: #9ca3af;
font-size: 14px;
}
}
}
}
.config-section {
margin-bottom: 32px;
background: #fff;
border: 1px solid #eef4f0;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(7, 192, 95, 0.04);
padding: 24px;
h3 {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 20px;
font-size: 18px;
font-weight: 600;
color: #0f172a;
.section-icon {
color: #07c05f;
font-size: 20px;
}
}
}
.form-row {
margin-bottom: 20px;
}
.model-input-with-status {
display: flex;
gap: 8px;
align-items: center;
}
.model-input-with-status :deep(.t-input) {
flex: 1;
min-width: 300px;
}
.model-status-icon {
flex-shrink: 0;
width: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.model-status-icon .status-icon {
font-size: 18px;
cursor: help;
}
.model-status-icon .status-icon.installed {
color: #00a870;
}
.model-status-icon .status-icon.not-installed {
color: #e34d59;
}
.model-status-icon .status-icon.unknown {
color: #d54941;
}
.model-status-icon .status-icon.downloading {
color: #0052d9;
}
.download-action {
flex-shrink: 0;
}
.download-action .download-btn {
height: 28px;
width: 28px;
min-width: 28px;
padding: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #07c05f, #00a651);
border: none;
color: white;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(7, 192, 95, 0.2);
&:hover:not(:disabled) {
background: linear-gradient(135deg, #00a651, #008f47);
box-shadow: 0 4px 12px rgba(7, 192, 95, 0.3);
}
&:disabled {
background: #e5e7eb;
color: #9ca3af;
cursor: not-allowed;
box-shadow: none;
}
.t-icon {
font-size: 14px;
}
}
.download-progress {
margin-top: 12px;
padding: 12px 16px;
background: linear-gradient(135deg, #f0fdf4, #f8fafc);
border-radius: 8px;
border: 1px solid #dcfce7;
box-shadow: 0 1px 4px rgba(7, 192, 95, 0.08);
.progress-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
.loading-icon {
color: #07c05f;
font-size: 14px;
animation: spin 1s linear infinite;
}
.progress-text {
font-size: 14px;
color: #166534;
font-weight: 600;
}
}
.progress-bar {
margin-bottom: 8px;
:deep(.t-progress__bar) {
background: linear-gradient(90deg, #07c05f, #00a651);
border-radius: 4px;
height: 6px;
box-shadow: 0 1px 2px rgba(7, 192, 95, 0.2);
}
:deep(.t-progress__track) {
background-color: #e5e7eb;
border-radius: 4px;
height: 6px;
}
}
.progress-message {
font-size: 12px;
color: #6b7280;
text-align: center;
line-height: 1.2;
font-style: italic;
}
}
.remote-config {
margin-top: 20px;
padding: 20px;
background: #f9fcff;
border-radius: 12px;
border: 1px solid #edf2f7;
}
.url-input-with-check {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.url-input-with-check :deep(.t-input) {
flex: 1;
min-width: 0;
width: 100%;
}
.check-action {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
min-width: 24px;
}
.check-action .status-icon,
.check-action .input-icon {
font-size: 18px;
cursor: help;
color: #666;
}
.check-action .status-icon.installed,
.check-action .input-icon.installed {
color: #00a870;
}
.check-action .status-icon.not-installed,
.check-action .input-icon.not-installed {
color: #e34d59;
}
.check-action .status-icon.checking,
.check-action .input-icon.checking {
color: #0052d9;
}
.error-message {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 12px 16px;
background-color: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 8px;
color: #d4380d;
font-size: 14px;
.t-icon {
color: #d4380d;
font-size: 16px;
flex-shrink: 0;
}
span {
line-height: 1.4;
}
}
.dimension-input-with-action {
display: flex;
align-items: center;
gap: 12px;
}
.dimension-input-with-action :deep(.t-input) {
flex: 0 0 auto;
}
.detect-dim-btn {
flex: 0 0 auto;
}
.embedding-warning {
margin-bottom: 20px;
:deep(.t-alert) {
background-color: #fffbe6;
border-color: #ffe58f;
border-radius: 8px;
.t-alert__message {
color: #d48806;
font-weight: 500;
}
.t-alert__icon {
color: #faad14;
}
}
}
.rerank-config, .multimodal-config {
margin-top: 20px;
padding: 20px;
background: #f9fcff;
border-radius: 12px;
border: 1px solid #edf2f7;
h4 {
color: #333;
font-size: 16px;
font-weight: 500;
margin: 20px 0 15px 0;
padding-left: 12px;
border-left: 3px solid #07c05f;
}
}
.switch-container {
display: flex;
align-items: center;
justify-content: flex-start;
margin-left: 0;
.switch-label {
margin-left: 10px;
font-size: 14px;
color: #333;
font-weight: 500;
}
}
.preset-row {
margin-bottom: 30px;
.preset-radio-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
:deep(.t-radio) {
border: 2px solid #e8f5e8;
border-radius: 8px;
padding: 15px;
transition: all 0.3s ease;
cursor: pointer;
background: white;
&:hover {
border-color: #07c05f;
background-color: #f8fdf8;
box-shadow: 0 4px 12px rgba(7, 192, 95, 0.1);
}
&.t-is-checked {
border-color: #07c05f;
background: linear-gradient(135deg, #07c05f, #00a651);
color: white;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(7, 192, 95, 0.2);
.preset-title {
color: white !important;
}
.preset-desc {
color: rgba(255, 255, 255, 0.9) !important;
}
}
.preset-content {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.preset-title {
font-weight: 600;
font-size: 14px;
color: #333;
margin-bottom: 4px;
}
.preset-desc {
font-size: 12px;
color: #666;
line-height: 1.4;
}
}
}
}
.parameters-grid {
display: flex;
flex-direction: column;
gap: 24px;
margin-top: 20px;
padding: 20px 16px;
background: #fff;
border-radius: 12px;
border: 1px solid #edf2f7;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.04);
&.disabled-grid {
opacity: 0.6;
pointer-events: none;
background-color: #f8f9fa;
border-color: #ddd;
}
.parameter-group {
.parameter-label {
font-weight: 600;
color: #333;
margin-bottom: 12px;
font-size: 14px;
}
.parameter-control {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
.parameter-slider {
flex: 1;
:deep(.t-slider__rail) {
background-color: #e8f5e8;
}
:deep(.t-slider__track) {
background-color: #07c05f;
}
:deep(.t-slider__handle) {
background-color: #07c05f;
border-color: #07c05f;
}
}
.parameter-value {
font-size: 14px;
color: #666;
font-weight: 500;
min-width: 80px;
text-align: right;
white-space: nowrap;
}
.parameter-select {
flex: 1;
}
}
.parameter-desc {
font-size: 12px;
color: #999;
line-height: 1.4;
margin-top: 8px;
}
}
}
.multimodal-test {
margin-top: 20px;
padding: 20px;
background: #f8fff9;
border-radius: 10px;
border: 1px solid #e8f5e8;
h5 {
font-size: 16px;
color: #07c05f;
margin-bottom: 8px;
font-weight: 600;
}
.test-desc {
margin-bottom: 16px;
font-size: 13px;
color: #6b7280;
line-height: 1.4;
}
.test-area {
display: flex;
flex-direction: column;
gap: 16px;
}
.upload-section {
display: flex;
flex-direction: column;
gap: 12px;
.upload-buttons {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
}
.image-preview {
text-align: center;
padding: 20px;
background: white;
border-radius: 10px;
border: 1px solid #e8f5e8;
box-shadow: 0 2px 8px rgba(7, 192, 95, 0.08);
img {
max-width: 100%;
max-height: 200px;
object-fit: contain;
border-radius: 8px;
margin-bottom: 12px;
border: 1px solid #f0f0f0;
}
.image-meta {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #f8fff9;
border-radius: 6px;
border: 1px solid #e8f5e8;
.file-name {
font-weight: 500;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 70%;
color: #333;
font-size: 13px;
}
.file-size {
color: #6b7280;
font-size: 12px;
font-weight: 500;
}
}
}
.test-button-wrapper {
text-align: center;
margin-top: 16px;
padding: 12px 20px;
background: #f8fff9;
border-radius: 8px;
border: 1px solid #e8f5e8;
.t-button {
min-width: 100px;
height: 32px;
font-weight: 500;
font-size: 13px;
}
}
.test-result {
background: white;
border-radius: 10px;
padding: 20px;
border: 1px solid #e8f5e8;
box-shadow: 0 2px 8px rgba(7, 192, 95, 0.08);
h6 {
font-size: 15px;
margin-bottom: 16px;
color: #07c05f;
font-weight: 600;
padding-bottom: 8px;
border-bottom: 2px solid #f0fdf4;
}
.result-item {
margin-bottom: 16px;
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
font-size: 14px;
}
.result-text {
background: #f8fff9;
padding: 12px 16px;
border-radius: 8px;
color: #333;
white-space: pre-wrap;
max-height: 120px;
overflow-y: auto;
border: 1px solid #e8f5e8;
font-size: 13px;
line-height: 1.5;
}
}
.result-time {
text-align: right;
color: #6b7280;
font-size: 12px;
margin-top: 12px;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.result-error {
color: #e34d59;
.error-msg {
display: flex;
align-items: center;
gap: 8px;
background: #fff2f0;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #ffccc7;
font-size: 13px;
line-height: 1.4;
}
}
}
}
.submit-section {
text-align: center;
padding: 32px 0 0;
// border-top: 1px solid #f0f0f0;
// margin-top: 32px;
:deep(.t-button--theme-primary) {
background: linear-gradient(135deg, #07c05f, #00a651);
border: none;
border-radius: 8px;
font-weight: 600;
padding: 12px 32px;
font-size: 16px;
transition: all 0.2s ease;
min-height: 44px;
min-width: 160px;
&:hover:not(.t-is-disabled) {
background: linear-gradient(135deg, #00a651, #009645);
box-shadow: 0 8px 25px rgba(7, 192, 95, 0.3);
}
&.t-is-disabled {
background: #ccc;
color: #999;
box-shadow: none;
}
&.t-is-loading {
background: linear-gradient(135deg, #07c05f, #00a651);
color: white;
box-shadow: 0 4px 15px rgba(7, 192, 95, 0.2);
}
}
.submit-tips {
margin-top: 16px;
display: flex;
align-items: center;
justify-content: center;
color: #fa8c16;
font-size: 14px;
.tip-icon {
margin-right: 6px;
}
}
}
// 动画定义
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// 表单样式优化
:deep(.t-form-item__label) {
font-weight: 500;
color: #333;
font-size: 14px;
}
:deep(.t-input), :deep(.t-radio-group), :deep(.t-input-number) {
width: 100%;
.t-input__inner {
border-radius: 6px;
transition: all 0.3s ease;
&:focus {
border-color: #07c05f;
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.2);
}
}
}
:deep(.t-input) {
border-radius: 8px;
&.t-is-focused {
border-color: #07c05f;
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
}
.t-input__inner {
border-radius: 8px;
transition: all 0.2s ease;
&:focus {
border-color: #07c05f;
box-shadow: none;
outline: none;
}
&:hover {
border-color: #07c05f;
}
}
}
}
</style>