feat(文档管理): 添加文档状态更新和向量生成功能

refactor(话术管理): 重构话术列表和编辑界面,调整API路径

fix(请求拦截器): 移除调试日志并优化错误处理

style(分页组件): 格式化代码并添加初始化标记

feat(知识库): 实现文档状态切换和向量生成功能
This commit is contained in:
2026-04-11 16:11:00 +08:00
parent c80f67d2ab
commit 4c91dd6fd5
8 changed files with 1199 additions and 563 deletions

View File

@@ -3,7 +3,7 @@ import request from '/@/utils/request';
//获取话术列表
export function getscriptList(data: object) {
return request({
url: '/customer-server/speechcraft/list',
url: '/customer-server/scripted/speech/list',
method: 'get',
params: data,
});
@@ -12,7 +12,7 @@ export function getscriptList(data: object) {
//增加话术
export function addScript(data: object) {
return request({
url: '/customer-server/speechcraft/add',
url: '/customer-server/scripted/speech/add',
method: 'post',
data: data,
});
@@ -21,7 +21,7 @@ export function addScript(data: object) {
//删除话术列表
export function deleteScript(data: object) {
return request({
url: '/customer-server/speechcraft/delete',
url: '/customer-server/scripted/speech/delete',
method: 'post',
data: data,
});
@@ -30,8 +30,17 @@ export function deleteScript(data: object) {
//更新话术列表
export function updateScript(data: object) {
return request({
url: '/customer-server/speechcraft/update',
url: '/customer-server/scripted/speech/update',
method: 'post',
data: data,
});
}
//获取话术详情
export function getScriptDetail(data: object) {
return request({
url: '/customer-server/scripted/speech/getOne',
method: 'get',
params: data,
});
}

View File

@@ -29,6 +29,7 @@ export interface UpdateDocumentParams {
fileSize?: number;
format?: string;
title?: string;
status?: number;
}
// 文档分段查询参数
@@ -184,3 +185,12 @@ export function getDocumentProcess(id: string) {
params: { id },
});
}
// 生成向量
export function generateVector(id: string, datasetId: string) {
return request({
url: '/rag/document/vectorization',
method: 'post',
data: { id, datasetId },
});
}

View File

@@ -1,100 +1,113 @@
<template>
<div :class="{'hidden':hidden}" class="pagination-container">
<el-pagination
:background="background"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<div :class="{ hidden: hidden }" class="pagination-container">
<el-pagination
:background="background"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script lang="ts">
import { toRefs, defineComponent,computed } from 'vue';
import { toRefs, defineComponent, computed, ref, onMounted } from 'vue';
const props = {
total: {
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
// 移动端页码按钮的数量端默认值5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
total: {
required: true,
type: Number,
},
page: {
type: Number,
default: 1,
},
limit: {
type: Number,
default: 20,
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50];
},
},
// 移动端页码按钮的数量端默认值5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7,
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper',
},
background: {
type: Boolean,
default: true,
},
hidden: {
type: Boolean,
default: false,
},
};
export default defineComponent({
name: 'pagination',
props: props,
setup(props,{emit}){
const { page,limit,pageSizes } = toRefs(props);
const currentPage = computed({
get() {
return page.value;
},
set(val) {
emit('update:page', val)
}
});
const pageSize = computed({
get() {
return limit.value
},
set(val) {
emit('update:limit', val)
}
});
const handleSizeChange = (val:number) => {
emit('pagination', { page: currentPage.value, limit: val })
};
const handleCurrentChange=(val:number) => {
emit('pagination', { page: val, limit: pageSizes.value })
}
return {
currentPage,
pageSize,
handleSizeChange,
handleCurrentChange
}
}
name: 'pagination',
props: props,
setup(props, { emit }) {
const { page, limit } = toRefs(props);
const isInitialized = ref(false);
const currentPage = computed({
get() {
return page.value;
},
set(val) {
emit('update:page', val);
},
});
const pageSize = computed({
get() {
return limit.value;
},
set(val) {
emit('update:limit', val);
},
});
const handleSizeChange = (val: number) => {
if (isInitialized.value) {
emit('pagination', { page: currentPage.value, limit: val });
}
};
const handleCurrentChange = (val: number) => {
if (isInitialized.value) {
emit('pagination', { page: val, limit: pageSize.value });
}
};
// 组件挂载后标记为已初始化
onMounted(() => {
isInitialized.value = true;
});
return {
currentPage,
pageSize,
handleSizeChange,
handleCurrentChange,
};
},
});
</script>
<style scoped lang="scss">
.pagination-container {
padding: 32px 16px;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
display: none;
}
</style>

View File

@@ -108,8 +108,6 @@ const requestInterceptor = (config: InternalAxiosRequestConfig) => {
// 没有变化,只传递 id
config.data = { id: idField };
}
console.log('[最小化传参] 原始字段数:', Object.keys(currentData).length, '-> 传递字段数:', Object.keys(config.data).length);
}
}
@@ -155,7 +153,6 @@ const responseInterceptor = (response: AxiosResponse) => {
if (code === 402 && !requestUrl.includes('/assets/asset/sku/')) {
// 获取当前路由路径
const currentPath = window.location.hash.replace('#', '') || window.location.pathname;
console.log('[request.ts] 检测到403错误当前路径:', currentPath);
handleModuleNotEnabled(currentPath);
// 直接返回,不再显示错误消息
return Promise.reject(new Error('模块未开通'));
@@ -173,8 +170,6 @@ const responseInterceptor = (response: AxiosResponse) => {
// 响应错误拦截器
const responseErrorHandler = (error: any) => {
console.error('API请求错误:', error);
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
showErrorMessage('请求超时,请检查网络连接');
return Promise.reject(new Error('请求超时'));
@@ -206,13 +201,11 @@ const responseErrorHandler = (error: any) => {
const lastSubscribeTime = sessionStorage.getItem('lastSubscribeTime');
const now = Date.now();
if (lastSubscribeTime && now - parseInt(lastSubscribeTime) < 5000) {
console.log('[responseErrorHandler] 刚完成开通跳过402处理');
showErrorMessage(responseMessage || '服务开通中,请稍后刷新页面');
return Promise.reject(new Error('模块开通中'));
}
const currentPath = window.location.hash.replace('#', '') || window.location.pathname;
console.log('[responseErrorHandler] 检测到HTTP 402错误当前路径:', currentPath);
handleModuleNotEnabled(currentPath);
return Promise.reject(new Error('模块未开通'));
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,18 @@
<el-form ref="formRef" :model="formData" :rules="rules" size="default" label-width="90px">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="标签" prop="tag">
<el-input v-model="formData.tag" placeholder="请输入标签" clearable />
<el-form-item label="数据集">
<el-input v-model="formData.datasetName" placeholder="数据集" disabled />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="场景类型">
<el-input v-model="formData.sceneType" placeholder="场景类型" disabled />
</el-form-item>
</el-col>
<!-- 富文本编辑器 -->
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="话术" prop="content">
<Editor v-model="formData.content" height="400px" :key="editorKey" placeholder="请输入产品详情" :disableExceptEmotion="true" />
<el-form-item label="话术内容" prop="questionContent">
<Editor v-model="formData.questionContent" height="400px" :key="editorKey" placeholder="请输入话术内容" :disableExceptEmotion="true" />
</el-form-item>
</el-col>
</el-row>
@@ -29,18 +33,19 @@
</template>
<script lang="ts" setup>
import { ref, reactive, toRefs, nextTick } from 'vue';
import { ref, reactive, toRefs, nextTick, onMounted } from 'vue';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import Editor from '/@/components/editor/index.vue';
import { addScript, updateScript } from '/@/api/customerService/script';
import { addScript, updateScript, getScriptDetail } from '/@/api/customerService/script';
import { listknowledges } from '/@/api/knowledge/dataset';
// 定义类型接口
interface DialogRow {
id?: number | string;
tag: string;
creator: string;
content: string;
modifier: string;
id?: number;
datasetId: string | number;
datasetName?: string;
sceneType: number;
questionContent: string;
}
// 定义事件
@@ -48,30 +53,30 @@ const emit = defineEmits<{
(e: 'refresh'): void;
}>();
// 定义数据集选项类型
interface DatasetOption {
label: string;
value: string;
}
// 响应式数据
const state = reactive({
loading: false,
isShowDialog: false,
datasets: [] as DatasetOption[], // 数据集列表
editorKey: 0, // 用于强制重新渲染编辑器
formData: {
id: 0,
tag: '',
content: '',
creator: '',
modifier: '',
datasetId: 0,
datasetName: '',
sceneType: 0,
questionContent: '',
} as DialogRow,
});
// 表单验证规则
const rules: FormRules = {
tag: [
{ required: true, message: '标签名称不能为空', trigger: 'blur' },
{ max: 64, message: '标签长度最多 64 个字符', trigger: 'blur' },
],
content: [
{ required: true, message: '产品详情不能为空', trigger: 'blur' },
{ max: 8126, message: '产品名称长度最多 8126 个字符', trigger: 'blur' },
],
questionContent: [{ required: true, message: '话术内容不能为空', trigger: 'blur' }],
};
// 模板引用
@@ -80,28 +85,74 @@ const formRef = ref<FormInstance>();
// 解构状态数据
const { loading, isShowDialog, formData, editorKey } = toRefs(state);
/**
* 加载数据集列表
*/
const loadDatasets = async () => {
try {
const response = await listknowledges({ pageNum: 1, pageSize: 100 });
if (response.data && response.data.list) {
state.datasets = response.data.list.map((item: any) => ({
label: item.name,
value: item.id,
}));
}
} catch (error) {
ElMessage.error('加载数据集列表失败');
}
};
// 生命周期
onMounted(() => {
loadDatasets();
});
/**
* 打开对话框
* @param row - 可选的编辑数据
*/
const openDialog = (row?: DialogRow) => {
const openDialog = async (row?: DialogRow) => {
resetForm();
if (row) {
// 深拷贝数据,避免引用问题
state.formData = { ...row };
// 重新加载数据集列表,确保数据是最新的
await loadDatasets();
if (row && row.id) {
try {
// 加载话术详情
const response = await getScriptDetail({ id: row.id });
if (response.data) {
// 确保datasetId是字符串类型与datasets选项的value类型一致
const detailData = {
...response.data,
datasetId: String(response.data.datasetId),
datasetName: '',
};
// 查找对应的数据集名称
const dataset = state.datasets.find((d) => d.value === detailData.datasetId);
if (dataset) {
detailData.datasetName = dataset.label;
}
state.formData = detailData;
}
} catch (error) {
ElMessage.error('获取话术详情失败');
return;
}
} else {
// 新增模式,确保清空数据
state.formData = {
id: 0,
tag: '',
content: '',
creator: '',
modifier: '',
datasetId: 0,
datasetName: '',
sceneType: 0,
questionContent: '',
};
}
// 更新编辑器 key 强制重新渲染
// 强制重新渲染编辑器
state.editorKey++;
state.isShowDialog = true;
@@ -139,21 +190,22 @@ const onSubmit = async () => {
const valid = await formRef.value.validate();
if (!valid) return;
// 额外验证话术内容
if (!state.formData.content || state.formData.content === '<p><br></p>' || state.formData.content.trim() === '<p><br></p>') {
ElMessage.warning('话术内容不能为空');
return;
}
// 确保数据类型正确
const submitData = {
...state.formData,
datasetId: String(state.formData.datasetId),
sceneType: Number(state.formData.sceneType),
};
state.loading = true;
if (state.formData.id === 0) {
// 新增模式
await addScript(state.formData);
await addScript(submitData);
ElMessage.success('添加成功');
} else {
// 编辑模式
await updateScript(state.formData);
await updateScript(submitData);
ElMessage.success('修改成功');
}
@@ -161,7 +213,6 @@ const onSubmit = async () => {
state.isShowDialog = false;
emit('refresh');
} catch (error) {
console.error('提交失败:', error);
// 错误已由请求拦截器统一处理
} finally {
state.loading = false;
@@ -174,10 +225,10 @@ const onSubmit = async () => {
const resetForm = () => {
state.formData = {
id: 0,
tag: '',
content: '',
creator: '',
modifier: '',
datasetId: 0,
datasetName: '',
sceneType: 0,
questionContent: '',
};
// 重置表单验证状态

View File

@@ -20,7 +20,7 @@
</el-icon>
重置
</el-button>
<el-button size="default" type="success" @click="handleAdd">
<el-button size="default" type="success" @click="handleAdd">
<el-icon><FolderAdd /></el-icon>
新增话术
</el-button>
@@ -31,9 +31,10 @@
<!-- 数据表格 -->
<el-table :data="tableData.data" v-loading="tableData.loading" style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="tag" label="标签" show-overflow-tooltip min-width="120" />
<el-table-column prop="creator" label="创建人" show-overflow-tooltip min-width="100" />
<el-table-column prop="updater" label="修改人" show-overflow-tooltip min-width="100" />
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="datasetId" label="数据集ID" width="120" align="center" />
<el-table-column prop="sceneType" label="场景类型" width="120" align="center" />
<el-table-column prop="questionContent" label="问题内容" show-overflow-tooltip min-width="200" />
<el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip min-width="140">
<template #default="{ row }">
{{ formatTime(row.createdAt) }}
@@ -81,13 +82,12 @@ import { getscriptList, deleteScript } from '/@/api/customerService/script';
// ==================== 类型定义 ====================
interface ScriptItem {
id: string;
tag: string;
creator: string;
modifier: string;
createdAt: string; // 保持原字段不变
updatedAt: string; // 保持原字段不变
content?: string;
id: number;
datasetId: number;
sceneType: number;
questionContent: string;
createdAt: string;
updatedAt: string;
}
interface TableParams {
@@ -176,7 +176,6 @@ const formatTime = (time: string | number | Date): string => {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch (error) {
console.error('时间格式化错误:', error);
return String(time);
}
};
@@ -196,8 +195,6 @@ const loadTableData = async () => {
tableData.data = list;
tableData.total = total;
} catch (error) {
console.error('加载数据失败:', error);
// 错误已由请求拦截器统一处理
tableData.data = [];
tableData.total = 0;
} finally {
@@ -215,8 +212,8 @@ const handleAdd = () => {
/**
* 编辑话术
*/
const handleEdit = (row: ScriptItem) => {
editRoleRef.value?.openDialog(row as any);
const handleEdit = async (row: ScriptItem) => {
await editRoleRef.value?.openDialog(row as any);
};
/**
@@ -224,7 +221,7 @@ const handleEdit = (row: ScriptItem) => {
*/
const handleDelete = async (row: ScriptItem) => {
try {
await ElMessageBox.confirm(`确定要删除话术「${row.tag}」吗?此操作不可恢复。`, '提示', {
await ElMessageBox.confirm(`确定要删除话术「${row.questionContent}」吗?此操作不可恢复。`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
@@ -237,19 +234,11 @@ const handleDelete = async (row: ScriptItem) => {
await loadTableData();
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error);
// 错误已由请求拦截器统一处理
}
}
};
/**
* 操作成功回调
*/
const handleSuccess = () => {
loadTableData();
};
// ==================== 生命周期 ====================
onMounted(() => {
loadTableData();

View File

@@ -127,32 +127,37 @@
</div>
<div class="file-table" v-loading="fileLoading">
<el-table :data="fileList" style="width: 100%" row-key="id" border>
<el-table-column prop="Title" label="名称" min-width="200">
<el-table-column prop="title" label="名称" min-width="200">
<template #default="scope">
<span class="file-link" @click="onViewDocumentDetail(scope.row)" style="cursor: pointer; color: #409eff">{{
scope.row.Title
scope.row.title
}}</span>
</template>
</el-table-column>
<el-table-column prop="chunkCount" label="分块数" width="80" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
{{ scope.row.status === 1 ? '正常' : '禁用' }}
</el-tag>
<el-switch
v-model="scope.row.statusEnabled"
inline-prompt
active-text=""
inactive-text=""
@change="onFileStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column prop="vectorStatus" label="向量化" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.vectorStatus === 'done' ? 'success' : 'warning'" size="small">
{{ scope.row.vectorStatus || '未处理' }}
<el-tag :type="scope.row.vectorStatus === 2 ? 'success' : 'warning'" size="small">
{{ scope.row.vectorStatus === 2 ? '已完成' : '未完成' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="上传日期" width="180" />
<el-table-column label="动作" width="120" align="center">
<el-table-column label="动作" width="180" align="center">
<template #default="scope">
<el-button text size="small" @click="onPreviewFile(scope.row)">预览</el-button>
<el-button text size="small" type="primary" @click="onGenerateVector(scope.row)">生成向量</el-button>
<el-button text size="small" type="danger" @click="onDeleteFile(scope.row)">删除</el-button>
</template>
</el-table-column>
@@ -286,7 +291,7 @@ import { ElMessage, ElMessageBox } from 'element-plus';
import type { FormInstance, FormRules, UploadFile } from 'element-plus';
import DocumentDetailDialog from './component/documentDetailDialog.vue';
import { listknowledges, createknowledge, updateknowledge, deleteknowledge } from '/@/api/knowledge/dataset';
import { listDocuments, uploadFile, createDocument, deleteDocument } from '/@/api/knowledge/document';
import { listDocuments, uploadFile, createDocument, deleteDocument, updateDocument, generateVector, getDocument } from '/@/api/knowledge/document';
// 数据集相关
const knowledgeLoading = ref(false);
@@ -497,7 +502,10 @@ const getFileList = async () => {
pageNum: 1,
pageSize: 100,
});
fileList.value = response.data?.list || [];
fileList.value = (response.data?.list || []).map((item: any) => ({
...item,
statusEnabled: item.status === 1,
}));
} catch (_error) {
ElMessage.error('获取文件列表失败');
} finally {
@@ -578,14 +586,47 @@ const onConfirmUpload = async () => {
};
// 文件状态变化
const _onFileStatusChange = (row: any) => {
ElMessage.success(row.enabled ? '已启用' : '已禁用');
const onFileStatusChange = async (row: any) => {
const newStatus = row.statusEnabled ? 1 : 0;
try {
// 调用后端API来更新文件状态
await updateDocument({
id: row.id,
status: newStatus,
});
ElMessage.success(row.statusEnabled ? '已启用' : '已禁用');
} catch (error) {
// 失败时恢复原状态
row.statusEnabled = !row.statusEnabled;
ElMessage.error('状态更新失败');
}
};
// 生成向量
const onGenerateVector = async (row: any) => {
try {
// 调用后端API来生成向量传递id和datasetId
await generateVector(row.id, currentknowledge.value.id);
ElMessage.success('生成向量任务已提交');
// 模拟更新状态
setTimeout(() => {
getFileList();
}, 1000);
} catch (error) {
ElMessage.error('生成向量失败');
}
};
// 查看文档详情
const onViewDocumentDetail = (row: any) => {
currentDocument.value = row;
showDocumentDetailDialog.value = true;
const onViewDocumentDetail = async (row: any) => {
try {
// 调用getDocument接口获取最新的文件详情
const response = await getDocument(row.id);
currentDocument.value = response.data;
showDocumentDetailDialog.value = true;
} catch (error) {
ElMessage.error('获取文件详情失败');
}
};
// 预览文件