知识库创建
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,556 @@
|
||||
<template>
|
||||
<div class="knowledge-basic-container">
|
||||
<!-- 查看模式 -->
|
||||
<div v-if="type === 'view'" class="view-mode">
|
||||
<div class="info-section">
|
||||
<div class="avatar-section">
|
||||
<img v-if="knowledge?.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
|
||||
<div v-else class="default-avatar">📚</div>
|
||||
</div>
|
||||
<div class="info-details">
|
||||
<h2 class="knowledge-name">{{ knowledge?.title || '未命名' }}</h2>
|
||||
<p class="knowledge-description">{{ knowledge?.description || '暂无描述' }}</p>
|
||||
<div class="meta-info">
|
||||
<div class="meta-item">
|
||||
<span class="label">状态:</span>
|
||||
<el-tag :type="knowledge?.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ knowledge?.status === 1 ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="meta-item" v-if="knowledge?.difyDatasetId">
|
||||
<span class="label">Dify数据集ID:</span>
|
||||
<span class="value">{{ knowledge.difyDatasetId }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">索引方式:</span>
|
||||
<span class="value">{{ getIndexingText(knowledge?.difyIndexingTechnique) }}</span>
|
||||
</div>
|
||||
<div class="meta-item" v-if="knowledge?.difyIndexingTechnique == 'high_quality'">
|
||||
<span class="label">Embedding模型:</span>
|
||||
<span class="value">{{ knowledge?.embeddingModel || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">文档数量:</span>
|
||||
<span class="value">{{ knowledge?.documentCount || 0 }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">创建时间:</span>
|
||||
<span class="value">{{ formatDate(knowledge?.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑模式 -->
|
||||
<div v-else class="edit-mode">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="knowledge-form"
|
||||
>
|
||||
<el-form-item label="知识库标题" prop="title" required>
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
placeholder="请输入知识库标题"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
:disabled="props.type === 'view'"
|
||||
/>
|
||||
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="知识库头像" prop="avatar">
|
||||
<FileUpload
|
||||
:cover-url="formData.avatar"
|
||||
@update:cover-url="handleAvatarUpdate"
|
||||
:as-dialog="false"
|
||||
list-type="cover"
|
||||
accept="image/*"
|
||||
:max-size="2"
|
||||
module="ai-knowledge"
|
||||
tip="点击上传知识库头像"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="知识库描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入知识库描述,介绍知识库的内容、用途等..."
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="索引方式" prop="difyIndexingTechnique" required>
|
||||
<el-radio-group v-model="formData.difyIndexingTechnique">
|
||||
<el-radio value="high_quality">
|
||||
<span class="radio-label">高质量</span>
|
||||
<span class="radio-desc">更精确的检索结果,消耗更多tokens</span>
|
||||
</el-radio>
|
||||
<el-radio
|
||||
value="economy"
|
||||
:disabled="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'"
|
||||
>
|
||||
<span class="radio-label">经济</span>
|
||||
<span class="radio-desc">快速检索,消耗较少tokens</span>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="type === 'edit' && knowledge?.difyIndexingTechnique === 'high_quality'" class="form-tip">
|
||||
高质量模式不能降级为经济模式
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.difyIndexingTechnique == 'high_quality'" label="Embedding模型" prop="embeddingModel">
|
||||
<el-select
|
||||
v-model="formData.embeddingModel"
|
||||
placeholder="请选择Embedding模型(可选)"
|
||||
clearable
|
||||
filterable
|
||||
:loading="modelsLoading"
|
||||
@change="handleModelChange"
|
||||
>
|
||||
<el-option-group
|
||||
v-for="provider in embeddingModels"
|
||||
:key="provider.provider"
|
||||
:label="provider.label || provider.provider"
|
||||
>
|
||||
<el-option
|
||||
v-for="model in provider.models"
|
||||
:key="model.model"
|
||||
:label="getModelLabel(model)"
|
||||
:value="model.model"
|
||||
:data-provider="model.provider"
|
||||
>
|
||||
<span>{{ getModelLabel(model) }}</span>
|
||||
<span v-if="model.contextSize" style="float: right; color: var(--el-text-color-secondary); font-size: 13px">
|
||||
上下文: {{ model.contextSize }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
<div v-if="type === 'edit'" class="form-tip form-tip-success">
|
||||
可以切换Embedding模型以优化检索效果
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Dify数据集ID" prop="difyDatasetId">
|
||||
<el-input
|
||||
v-model="formData.difyDatasetId"
|
||||
placeholder="可选,如需关联现有Dify数据集请填写"
|
||||
:disabled="type === 'edit'"
|
||||
/>
|
||||
<div v-if="type === 'edit'" class="form-tip">
|
||||
Dify数据集ID创建后不可修改
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch
|
||||
v-model="formData.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
{{ type === 'add' ? '创建知识库' : '保存修改' }}
|
||||
</el-button>
|
||||
<el-button @click="handleCancel">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { FileUpload } from '@/components/file';
|
||||
import type { AiKnowledge } from '@/types/ai';
|
||||
import { knowledgeApi } from '@/apis/ai';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
defineOptions({
|
||||
name: 'KnowledgeBasic'
|
||||
});
|
||||
|
||||
interface KnowledgeBasicProps {
|
||||
type: 'view' | 'add' | 'edit';
|
||||
knowledge?: AiKnowledge | null;
|
||||
}
|
||||
|
||||
const props = defineProps<KnowledgeBasicProps>();
|
||||
const emit = defineEmits<{
|
||||
success: [knowledge: AiKnowledge];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
// 表单引用
|
||||
const formRef = ref<FormInstance>();
|
||||
const submitting = ref(false);
|
||||
const modelsLoading = ref(false);
|
||||
const embeddingModels = ref<any[]>([]);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<Partial<AiKnowledge>>({
|
||||
title: '',
|
||||
avatar: '',
|
||||
description: '',
|
||||
difyIndexingTechnique: 'high_quality',
|
||||
embeddingModel: '',
|
||||
embeddingModelProvider: '',
|
||||
difyDatasetId: '',
|
||||
status: 1
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入知识库标题', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '标题长度在2-100个字符之间', trigger: 'blur' }
|
||||
],
|
||||
difyIndexingTechnique: [
|
||||
{ required: true, message: '请选择索引方式', trigger: 'change' }
|
||||
]
|
||||
};
|
||||
|
||||
// 监听知识库数据变化
|
||||
watch(() => props.knowledge, (newVal) => {
|
||||
if (newVal && (props.type === 'view' || props.type === 'edit')) {
|
||||
Object.assign(formData, {
|
||||
id: newVal.id,
|
||||
title: newVal.title,
|
||||
avatar: newVal.avatar,
|
||||
description: newVal.description,
|
||||
difyIndexingTechnique: newVal.difyIndexingTechnique || 'high_quality',
|
||||
embeddingModel: newVal.embeddingModel,
|
||||
embeddingModelProvider: newVal.embeddingModelProvider,
|
||||
difyDatasetId: newVal.difyDatasetId,
|
||||
status: newVal.status ?? 1
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 处理头像更新
|
||||
function handleAvatarUpdate(val: string) {
|
||||
formData.avatar = val;
|
||||
}
|
||||
|
||||
// 加载嵌入模型列表
|
||||
async function loadEmbeddingModels() {
|
||||
try {
|
||||
modelsLoading.value = true;
|
||||
const result = await knowledgeApi.getAvailableEmbeddingModels();
|
||||
if (result.success && result.data) {
|
||||
// 按提供商分组
|
||||
const providers = result.data.providers || [];
|
||||
embeddingModels.value = providers.map((provider: any) => ({
|
||||
provider: provider.provider,
|
||||
label: provider.label?.zh_Hans || provider.label?.en_US || provider.provider,
|
||||
models: (provider.models || []).map((model: any) => ({
|
||||
model: model.model,
|
||||
provider: model.provider || provider.provider, // 添加 provider 到 model 对象
|
||||
label: model.label?.zh_Hans || model.label?.en_US || model.model,
|
||||
contextSize: model.model_properties?.context_size,
|
||||
status: model.status
|
||||
}))
|
||||
}));
|
||||
} else {
|
||||
ElMessage.warning('获取嵌入模型列表失败,使用默认选项');
|
||||
// 设置默认模型列表
|
||||
embeddingModels.value = [{
|
||||
provider: 'openai',
|
||||
label: 'OpenAI',
|
||||
models: [
|
||||
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
|
||||
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
|
||||
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
|
||||
]
|
||||
}];
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载嵌入模型列表失败:', error);
|
||||
// 设置默认模型列表
|
||||
embeddingModels.value = [{
|
||||
provider: 'openai',
|
||||
label: 'OpenAI',
|
||||
models: [
|
||||
{ model: 'text-embedding-ada-002', label: 'text-embedding-ada-002' },
|
||||
{ model: 'text-embedding-3-small', label: 'text-embedding-3-small' },
|
||||
{ model: 'text-embedding-3-large', label: 'text-embedding-3-large' }
|
||||
]
|
||||
}];
|
||||
} finally {
|
||||
modelsLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模型显示标签
|
||||
function getModelLabel(model: any): string {
|
||||
return model.label || model.model;
|
||||
}
|
||||
|
||||
// 处理模型变化
|
||||
function handleModelChange(modelName: string) {
|
||||
if (!modelName) {
|
||||
formData.embeddingModelProvider = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找选中模型的提供商
|
||||
for (const providerGroup of embeddingModels.value) {
|
||||
const foundModel = providerGroup.models.find((m: any) => m.model === modelName);
|
||||
if (foundModel) {
|
||||
formData.embeddingModelProvider = foundModel.provider;
|
||||
console.log('选择模型:', modelName, '提供商:', foundModel.provider);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载模型列表
|
||||
loadEmbeddingModels();
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
const valid = await formRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
if (props.type === 'add') {
|
||||
// 创建知识库
|
||||
const result = await knowledgeApi.createKnowledge(formData as AiKnowledge);
|
||||
if (result.success && result.data) {
|
||||
ElMessage.success('创建成功');
|
||||
emit('success', result.data);
|
||||
} else {
|
||||
ElMessage.error(result.message || '创建失败');
|
||||
}
|
||||
} else if (props.type === 'edit') {
|
||||
// 更新知识库
|
||||
const result = await knowledgeApi.updateKnowledge(formData as AiKnowledge);
|
||||
if (result.success && result.data) {
|
||||
ElMessage.success('保存成功');
|
||||
emit('success', result.data);
|
||||
} else {
|
||||
ElMessage.error(result.message || '保存失败');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('提交失败:', error);
|
||||
ElMessage.error(props.type === 'add' ? '创建失败' : '保存失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
function handleCancel() {
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
// 获取索引方式文本
|
||||
function getIndexingText(technique: string | undefined): string {
|
||||
switch (technique) {
|
||||
case 'high_quality':
|
||||
return '高质量';
|
||||
case 'economy':
|
||||
return '经济';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.knowledge-basic-container {
|
||||
padding: 24px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 14px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
// 查看模式样式
|
||||
.view-mode {
|
||||
.info-section {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #F8F9FA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.default-avatar {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.knowledge-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.knowledge-description {
|
||||
font-size: 14px;
|
||||
color: #667085;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
.label {
|
||||
color: #667085;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #101828;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑模式样式
|
||||
.edit-mode {
|
||||
.knowledge-form {
|
||||
max-width: 600px;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-radio) {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
height: auto;
|
||||
|
||||
.el-radio__label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
}
|
||||
|
||||
.radio-desc {
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #F59E0B;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-tip-success {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #F3F3F5;
|
||||
|
||||
.el-button {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 12px 24px;
|
||||
|
||||
&.el-button--primary {
|
||||
background: #E7000B;
|
||||
border-color: #E7000B;
|
||||
|
||||
&:hover {
|
||||
background: #C90009;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-switch) {
|
||||
.el-switch__label {
|
||||
font-size: 14px;
|
||||
color: #667085;
|
||||
|
||||
&.is-active {
|
||||
color: #E7000B;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="knowledge-card" @click="handleClick">
|
||||
<div class="card-header">
|
||||
<!-- 头像 -->
|
||||
<div class="knowledge-avatar">
|
||||
<img v-if="knowledge.avatar" :src="FILE_DOWNLOAD_URL + knowledge.avatar" alt="知识库头像" />
|
||||
<div v-else class="default-avatar">📚</div>
|
||||
</div>
|
||||
|
||||
<!-- 标题和状态 -->
|
||||
<div class="knowledge-title-section">
|
||||
<h3 class="knowledge-title">{{ knowledge.title }}</h3>
|
||||
<span
|
||||
class="knowledge-status"
|
||||
:class="knowledge.status === 1 ? 'status-active' : 'status-inactive'"
|
||||
>
|
||||
{{ knowledge.status === 1 ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div class="knowledge-description">
|
||||
{{ knowledge.description || '暂无描述' }}
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="knowledge-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">文档数量</span>
|
||||
<span class="stat-value">{{ knowledge.documentCount || 0 }}</span>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">状态</span>
|
||||
<span class="stat-value">
|
||||
{{ getStatusText(knowledge.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="knowledge-actions" @click.stop>
|
||||
<el-button size="small" @click="handleEdit">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AiKnowledge } from '@/types/ai';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
|
||||
defineOptions({
|
||||
name: 'KnowledgeCard'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
knowledge: AiKnowledge;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['click', 'edit', 'delete', 'sync']);
|
||||
|
||||
function handleClick() {
|
||||
emit('click', props.knowledge);
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
emit('edit', props.knowledge);
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
emit('delete', props.knowledge);
|
||||
}
|
||||
|
||||
function getStatusText(status?: number) {
|
||||
switch (status) {
|
||||
case 0: return '禁用';
|
||||
case 1: return '启用';
|
||||
case 2: return '处理中';
|
||||
default: return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.knowledge-card {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #E7000B;
|
||||
box-shadow: 0 4px 12px rgba(231, 0, 11, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.knowledge-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #F3F3F5;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.default-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-title-section {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.knowledge-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.knowledge-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&.status-active {
|
||||
background: #DCFCE7;
|
||||
color: #008236;
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
background: #FEF2F2;
|
||||
color: #DC2626;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-description {
|
||||
font-size: 14px;
|
||||
color: #6B7280;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.knowledge-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #F3F3F5;
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #9CA3AF;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
:deep(.el-button) {
|
||||
flex: 1;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.01em;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
&.el-button--default {
|
||||
background: #F3F3F5;
|
||||
border-color: transparent;
|
||||
color: #0A0A0A;
|
||||
|
||||
&:hover {
|
||||
background: #E5E5E7;
|
||||
}
|
||||
}
|
||||
|
||||
&.el-button--danger {
|
||||
background: #FEF2F2;
|
||||
border-color: transparent;
|
||||
color: #DC2626;
|
||||
|
||||
&:hover {
|
||||
background: #FEE2E2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,639 @@
|
||||
<template>
|
||||
<div class="knowledge-info-panel" v-if="knowledge">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="panel-header">
|
||||
<el-button text @click="$emit('close')">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回列表
|
||||
</el-button>
|
||||
<div class="header-actions">
|
||||
<el-button @click="handleEdit">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleUpload">
|
||||
<el-icon><Upload /></el-icon>
|
||||
上传文档
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识库信息卡片 -->
|
||||
<div class="knowledge-header">
|
||||
<div class="knowledge-avatar">
|
||||
<el-image
|
||||
v-if="knowledge.avatar"
|
||||
:src="knowledge.avatar"
|
||||
fit="cover"
|
||||
/>
|
||||
<el-icon v-else :size="48"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="knowledge-details">
|
||||
<h2 class="knowledge-name">{{ knowledge.title }}</h2>
|
||||
<p class="knowledge-description">{{ knowledge.description || '暂无描述' }}</p>
|
||||
<div class="knowledge-meta">
|
||||
<el-tag :type="knowledge.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ knowledge.status === 1 ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
<span class="meta-item">
|
||||
<el-icon><Clock /></el-icon>
|
||||
{{ formatDate(knowledge.createTime) }}
|
||||
</span>
|
||||
<span class="meta-item" v-if="knowledge.difyDatasetId">
|
||||
<el-icon><Link /></el-icon>
|
||||
Dify: {{ knowledge.difyDatasetId }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<el-icon color="#409EFF"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ knowledge.documentCount || 0 }}</div>
|
||||
<div class="stat-label">文档数量</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<el-icon color="#67C23A"><Files /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<el-icon color="#E6A23C"><Paperclip /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ documents.length }}</div>
|
||||
<div class="stat-label">上传文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文档列表 -->
|
||||
<div class="documents-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">文档列表</h3>
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索文档..."
|
||||
clearable
|
||||
style="width: 300px;"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="filteredDocuments"
|
||||
v-loading="loading"
|
||||
style="width: 100%"
|
||||
:empty-text="loading ? '加载中...' : '暂无文档'"
|
||||
>
|
||||
<el-table-column prop="fileName" label="文件名" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="file-name-cell">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>{{ row.fileName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileType" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.fileType || 'unknown' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileSize" label="大小" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.fileSize) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 2 ? 'success' : row.status === 1 ? 'warning' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="chunkCount" label="分段数" width="100" />
|
||||
<el-table-column prop="createTime" label="上传时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewSegments(row)">
|
||||
查看分段
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="handleDownload(row)">
|
||||
下载
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDeleteDocument(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 上传对话框 -->
|
||||
<el-dialog
|
||||
v-model="uploadDialogVisible"
|
||||
title="上传文档到知识库"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:file-list="fileList"
|
||||
drag
|
||||
multiple
|
||||
accept=".txt,.md,.pdf,.doc,.docx"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 txt/md/pdf/doc/docx 格式文件,单个文件不超过50MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirmUpload" :loading="uploading">
|
||||
确认上传
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 文档分段对话框 -->
|
||||
<DocumentSegmentDialog
|
||||
v-if="selectedDocument && selectedDocument.difyDocumentId && props.knowledge?.difyDatasetId"
|
||||
:model-value="segmentDialogVisible"
|
||||
:dataset-id="props.knowledge.difyDatasetId"
|
||||
:document-id="selectedDocument.difyDocumentId"
|
||||
@update:model-value="segmentDialogVisible = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import type { UploadUserFile, UploadInstance } from 'element-plus';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Upload,
|
||||
Refresh,
|
||||
Edit,
|
||||
Document,
|
||||
Clock,
|
||||
Link,
|
||||
Files,
|
||||
Paperclip,
|
||||
Search,
|
||||
UploadFilled
|
||||
} from '@element-plus/icons-vue';
|
||||
import type { AiKnowledge, AiUploadFile } from '@/types/ai';
|
||||
import { fileUploadApi, knowledgeApi } from '@/apis/ai';
|
||||
import { DocumentSegmentDialog } from './';
|
||||
|
||||
defineOptions({
|
||||
name: 'KnowledgeInfo'
|
||||
});
|
||||
|
||||
interface Props {
|
||||
knowledge: AiKnowledge | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
refresh: [];
|
||||
edit: [knowledge: AiKnowledge];
|
||||
}>();
|
||||
|
||||
// 数据状态
|
||||
const documents = ref<AiUploadFile[]>([]);
|
||||
const loading = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const uploadDialogVisible = ref(false);
|
||||
const segmentDialogVisible = ref(false);
|
||||
const selectedDocument = ref<AiUploadFile | null>(null);
|
||||
const uploading = ref(false);
|
||||
|
||||
// 上传相关
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const fileList = ref<UploadUserFile[]>([]);
|
||||
|
||||
// 计算属性
|
||||
const filteredDocuments = computed(() => {
|
||||
if (!searchQuery.value) return documents.value;
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return documents.value.filter(doc =>
|
||||
doc.fileName?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// 监听知识库变化
|
||||
watch(() => props.knowledge, (newVal) => {
|
||||
if (newVal?.id) {
|
||||
loadDocuments();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 加载文档列表
|
||||
async function loadDocuments() {
|
||||
if (!props.knowledge?.id) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const result = await fileUploadApi.listFilesByKnowledge(props.knowledge.id);
|
||||
if (result.success) {
|
||||
documents.value = (result.dataList || []) as AiUploadFile[];
|
||||
} else {
|
||||
ElMessage.error(result.message || '加载文档列表失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载文档列表失败:', error);
|
||||
ElMessage.error('加载文档列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑知识库
|
||||
function handleEdit() {
|
||||
if (props.knowledge) {
|
||||
emit('edit', props.knowledge);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传
|
||||
function handleUpload() {
|
||||
uploadDialogVisible.value = true;
|
||||
fileList.value = [];
|
||||
}
|
||||
|
||||
// 文件变化
|
||||
function handleFileChange(file: any, fileListParam: any) {
|
||||
fileList.value = fileListParam;
|
||||
}
|
||||
|
||||
// 确认上传
|
||||
async function handleConfirmUpload() {
|
||||
if (fileList.value.length === 0) {
|
||||
ElMessage.warning('请选择要上传的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.knowledge?.id) {
|
||||
ElMessage.error('知识库ID不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uploading.value = true;
|
||||
const files = fileList.value
|
||||
.map(f => f.raw)
|
||||
.filter(f => f !== null && f !== undefined) as File[];
|
||||
|
||||
// 逐个上传文件
|
||||
for (const file of files) {
|
||||
const result = await fileUploadApi.uploadFile(props.knowledge.id, file);
|
||||
if (!result.success) {
|
||||
ElMessage.error(`文件 ${file.name} 上传失败: ${result.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success('文件上传成功');
|
||||
uploadDialogVisible.value = false;
|
||||
fileList.value = [];
|
||||
await loadDocuments();
|
||||
emit('refresh');
|
||||
} catch (error: any) {
|
||||
console.error('上传文件失败:', error);
|
||||
ElMessage.error('上传文件失败');
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查看分段
|
||||
function handleViewSegments(document: AiUploadFile) {
|
||||
selectedDocument.value = document;
|
||||
segmentDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
function handleDownload(document: AiUploadFile) {
|
||||
if (document.sysFileId) {
|
||||
window.open(`/api/file/download/${document.sysFileId}`, '_blank');
|
||||
} else if (document.filePath) {
|
||||
window.open(`/api/file/download/${document.filePath}`, '_blank');
|
||||
} else {
|
||||
ElMessage.warning('文件路径不存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文档
|
||||
async function handleDeleteDocument(document: AiUploadFile) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除文档"${document.fileName}"吗?此操作不可恢复。`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
);
|
||||
|
||||
if (document.id) {
|
||||
const result = await fileUploadApi.deleteFile(document.id);
|
||||
if (result.success) {
|
||||
ElMessage.success('删除成功');
|
||||
await loadDocuments();
|
||||
emit('refresh');
|
||||
} else {
|
||||
ElMessage.error(result.message || '删除失败');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除文档失败:', error);
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatDate(date: string | Date | undefined): string {
|
||||
if (!date) return '-';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '-';
|
||||
return d.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number | undefined): string {
|
||||
if (!bytes || 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 (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getStatusText(status: number | undefined): string {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '处理中';
|
||||
case 2:
|
||||
return '已完成';
|
||||
case 3:
|
||||
return '失败';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.knowledge-info-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #FFFFFF;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
background: #FFFFFF;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
|
||||
&.el-button--primary {
|
||||
background: #E7000B;
|
||||
border-color: #E7000B;
|
||||
|
||||
&:hover {
|
||||
background: #C90009;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #F8F9FA 0%, #FFFFFF 100%);
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
|
||||
.knowledge-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 12px;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
flex-shrink: 0;
|
||||
|
||||
:deep(.el-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: #E7000B;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.knowledge-name {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0 0 8px 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.knowledge-description {
|
||||
font-size: 14px;
|
||||
color: #667085;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.knowledge-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
|
||||
.stat-card {
|
||||
background: #F8F9FA;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.documents-section {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #101828;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.el-icon {
|
||||
color: #E7000B;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.el-table__header th {
|
||||
background: #F8F9FA;
|
||||
color: #667085;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
|
||||
.el-dialog__header {
|
||||
border-bottom: 1px solid #F3F3F5;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.el-upload-dragger {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as KnowledgeCard } from './KnowledgeCard.vue';
|
||||
export { default as KnowledgeInfo } from './KnowledgeInfo.vue';
|
||||
export { default as KnowledgeBasic } from './KnowledgeBasic.vue';
|
||||
export { default as DocumentSegmentDialog } from './DocumentSegmentDialog.vue';
|
||||
@@ -679,6 +679,19 @@ async function sendMessage() {
|
||||
// 调用API
|
||||
isGenerating.value = true;
|
||||
|
||||
// 立即创建一个空的AI消息,用于显示加载动画
|
||||
messages.value.push({
|
||||
id: `temp-ai-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
conversationID: currentConversation.value?.id || '',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString()
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
let aiMessageContent = '';
|
||||
|
||||
@@ -710,6 +723,12 @@ async function sendMessage() {
|
||||
// 保存AI消息的数据库ID(task_id),用于停止生成
|
||||
currentMessageId.value = initData.messageId;
|
||||
console.log('[保存MessageID(TaskID)]', initData.messageId);
|
||||
|
||||
// 更新最后一条AI消息的临时ID为真实的数据库ID
|
||||
const lastMessage = messages.value[messages.value.length - 1];
|
||||
if (lastMessage && lastMessage.role === 'assistant') {
|
||||
lastMessage.id = initData.messageId;
|
||||
}
|
||||
},
|
||||
onMessage: (chunk: string) => {
|
||||
// 确保AI消息已创建(即使内容为空)
|
||||
@@ -900,6 +919,11 @@ async function regenerateMessage(messageId: string) {
|
||||
// 保存AI消息的数据库ID(task_id),用于停止生成
|
||||
currentMessageId.value = initData.messageId;
|
||||
console.log('[保存MessageID(TaskID)-重新生成]', initData.messageId);
|
||||
|
||||
// 如果后端返回了新的messageId,更新消息对象的ID
|
||||
if (initData.messageId !== messageId) {
|
||||
messages.value[messageIndex].id = initData.messageId;
|
||||
}
|
||||
},
|
||||
onMessage: (chunk: string) => {
|
||||
// 累加内容(包括空chunk,因为后端可能分块发送)
|
||||
@@ -907,10 +931,9 @@ async function regenerateMessage(messageId: string) {
|
||||
aiMessageContent += chunk;
|
||||
}
|
||||
|
||||
// 找到对应消息并更新
|
||||
const msgIndex = messages.value.findIndex(m => m.id === messageId);
|
||||
if (msgIndex !== -1) {
|
||||
messages.value[msgIndex].content = aiMessageContent;
|
||||
// 直接使用messageIndex更新消息内容
|
||||
if (messageIndex !== -1) {
|
||||
messages.value[messageIndex].content = aiMessageContent;
|
||||
}
|
||||
|
||||
nextTick(() => scrollToBottom());
|
||||
|
||||
Reference in New Issue
Block a user