feat:新增知识库管理页面并联调知识库接口
This commit is contained in:
@@ -1,8 +1,668 @@
|
||||
<script setup lang="ts">
|
||||
import { CirclePlus, Delete, Edit, FolderAdd, Refresh, Search, UploadFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
deleteRagStore,
|
||||
getRagStoreById,
|
||||
queryRagStores,
|
||||
saveRagStore,
|
||||
type RagStore,
|
||||
} from '@/api/ragStores';
|
||||
|
||||
type StoreStatus = '启用' | '停用';
|
||||
|
||||
const loading = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const storeRows = ref<RagStore[]>([]);
|
||||
const activeStoreId = ref<string | null>(null);
|
||||
const activeStore = ref<RagStore | null>(null);
|
||||
|
||||
const queryForm = reactive({
|
||||
storeName: '',
|
||||
});
|
||||
|
||||
const createDialogVisible = ref(false);
|
||||
const editDialogVisible = ref(false);
|
||||
|
||||
const createForm = reactive({
|
||||
storeCode: '',
|
||||
storeName: '',
|
||||
description: '',
|
||||
status: '启用' as StoreStatus,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const editForm = reactive({
|
||||
id: '',
|
||||
storeCode: '',
|
||||
storeName: '',
|
||||
description: '',
|
||||
status: '启用' as StoreStatus,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const overviewCards = computed(() => {
|
||||
const totalStores = storeRows.value.length;
|
||||
const retrievableStores = storeRows.value.filter((row) => row.status === '启用').length;
|
||||
|
||||
return [
|
||||
{ label: '知识库总数', value: totalStores, hint: '当前已登记知识库' },
|
||||
{ label: '文档总数', value: '-', hint: '待文档统计接口补充' },
|
||||
{ label: '切片总数', value: '-', hint: '待切片统计接口补充' },
|
||||
{ label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态暂代统计' },
|
||||
];
|
||||
});
|
||||
|
||||
async function loadStores(preferredStoreId?: string | null) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await queryRagStores({
|
||||
storeName: queryForm.storeName.trim() || undefined,
|
||||
});
|
||||
storeRows.value = response.data ?? [];
|
||||
|
||||
if (storeRows.value.length === 0) {
|
||||
activeStoreId.value = null;
|
||||
activeStore.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const firstStore = storeRows.value[0];
|
||||
const targetId =
|
||||
preferredStoreId && storeRows.value.some((row) => String(row.id) === preferredStoreId)
|
||||
? preferredStoreId
|
||||
: firstStore
|
||||
? String(firstStore.id)
|
||||
: null;
|
||||
if (!targetId) {
|
||||
activeStoreId.value = null;
|
||||
activeStore.value = null;
|
||||
return;
|
||||
}
|
||||
await selectStore(targetId);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectStore(storeId: string) {
|
||||
activeStoreId.value = storeId;
|
||||
detailLoading.value = true;
|
||||
try {
|
||||
const response = await getRagStoreById(storeId);
|
||||
activeStore.value = response.data ?? null;
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadStores(activeStoreId.value);
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryForm.storeName = '';
|
||||
loadStores();
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
createForm.storeCode = '';
|
||||
createForm.storeName = '';
|
||||
createForm.description = '';
|
||||
createForm.status = '启用';
|
||||
createForm.remark = '';
|
||||
createDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditDialog() {
|
||||
if (!activeStore.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
editForm.id = String(activeStore.value.id ?? '');
|
||||
editForm.storeCode = activeStore.value.storeCode;
|
||||
editForm.storeName = activeStore.value.storeName;
|
||||
editForm.description = activeStore.value.description ?? '';
|
||||
editForm.status = (activeStore.value.status as StoreStatus) || '启用';
|
||||
editForm.remark = activeStore.value.remark ?? '';
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitCreateStore() {
|
||||
if (!createForm.storeCode || !createForm.storeName) {
|
||||
ElMessage.warning('请填写知识库编码和知识库名称');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveRagStore({
|
||||
storeCode: createForm.storeCode,
|
||||
storeName: createForm.storeName,
|
||||
description: createForm.description,
|
||||
status: createForm.status,
|
||||
remark: createForm.remark,
|
||||
});
|
||||
createDialogVisible.value = false;
|
||||
ElMessage.success('知识库已创建');
|
||||
await loadStores();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitEditStore() {
|
||||
if (!editForm.id || !editForm.storeCode || !editForm.storeName) {
|
||||
ElMessage.warning('请填写知识库编码和知识库名称');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveRagStore({
|
||||
id: editForm.id,
|
||||
storeCode: editForm.storeCode,
|
||||
storeName: editForm.storeName,
|
||||
description: editForm.description,
|
||||
status: editForm.status,
|
||||
remark: editForm.remark,
|
||||
});
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success('知识库信息已更新');
|
||||
await loadStores(editForm.id);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeStore() {
|
||||
if (!activeStore.value?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ElMessageBox.confirm(`确认删除知识库「${activeStore.value.storeName}」?`, '删除确认', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
});
|
||||
|
||||
await deleteRagStore(String(activeStore.value.id));
|
||||
ElMessage.success('知识库已删除');
|
||||
await loadStores();
|
||||
}
|
||||
|
||||
function showFutureMessage(actionName: string) {
|
||||
ElMessage.info(`${actionName} 会在下一批接口里补齐`);
|
||||
}
|
||||
|
||||
function getStatusTagType(status?: string | null) {
|
||||
return status === '启用' ? 'success' : 'info';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStores();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<div class="page-panel__header">
|
||||
<h2>知识库</h2>
|
||||
<span>RAG</span>
|
||||
<section class="page-panel rag-store-page">
|
||||
<div class="page-panel__header rag-store-page__header">
|
||||
<div>
|
||||
<h2>知识库</h2>
|
||||
<p>统一管理知识库及其文档、索引状态</p>
|
||||
</div>
|
||||
<span>RAG Stores</span>
|
||||
</div>
|
||||
|
||||
<div class="overview-grid">
|
||||
<article v-for="card in overviewCards" :key="card.label" class="overview-card">
|
||||
<span class="overview-card__label">{{ card.label }}</span>
|
||||
<strong class="overview-card__value">{{ card.value }}</strong>
|
||||
<small class="overview-card__hint">{{ card.hint }}</small>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="rag-store-page__content">
|
||||
<section class="store-list-panel">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3>知识库列表</h3>
|
||||
<p>按知识库名称检索并切换当前查看对象</p>
|
||||
</div>
|
||||
<el-button data-test="create-store" type="primary" :icon="CirclePlus" @click="openCreateDialog">
|
||||
新建知识库
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="store-search-bar">
|
||||
<el-input
|
||||
v-model="queryForm.storeName"
|
||||
data-test="store-name-input"
|
||||
clearable
|
||||
placeholder="请输入知识库名称"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-button data-test="store-search" type="primary" :icon="Search" @click="handleSearch">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="store-list">
|
||||
<button
|
||||
v-for="store in storeRows"
|
||||
:key="store.id"
|
||||
:data-test="`store-card-${String(store.storeCode).toLowerCase()}`"
|
||||
class="store-card"
|
||||
:class="{ 'store-card--active': String(store.id) === activeStoreId }"
|
||||
type="button"
|
||||
@click="selectStore(String(store.id))"
|
||||
>
|
||||
<div class="store-card__title-row">
|
||||
<strong>{{ store.storeName }}</strong>
|
||||
<el-tag size="small" :type="getStatusTagType(store.status)">{{ store.status || '未设置' }}</el-tag>
|
||||
</div>
|
||||
<p>编码:{{ store.storeCode }}</p>
|
||||
<div class="store-card__metrics">
|
||||
<span>描述:{{ store.description || '暂无描述' }}</span>
|
||||
</div>
|
||||
<div class="store-card__metrics">
|
||||
<span>更新时间:{{ store.updateTime || '-' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<el-empty v-if="!loading && storeRows.length === 0" description="未找到匹配的知识库" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="store-detail-panel">
|
||||
<div v-loading="detailLoading" class="store-detail-panel__body">
|
||||
<template v-if="activeStore">
|
||||
<div class="section-heading section-heading--detail">
|
||||
<div>
|
||||
<h3>{{ activeStore.storeName }}</h3>
|
||||
<p>编码:{{ activeStore.storeCode }}</p>
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<el-button :icon="Edit" @click="openEditDialog">编辑</el-button>
|
||||
<el-button type="primary" :icon="UploadFilled" @click="showFutureMessage('批量导入文件')">
|
||||
批量导入文件
|
||||
</el-button>
|
||||
<el-button :icon="FolderAdd" @click="showFutureMessage('重建索引')">重建索引</el-button>
|
||||
<el-button type="danger" :icon="Delete" @click="removeStore">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<article class="detail-card">
|
||||
<div class="detail-card__header">
|
||||
<h4>基本信息</h4>
|
||||
<el-tag :type="getStatusTagType(activeStore.status)">{{ activeStore.status || '未设置' }}</el-tag>
|
||||
</div>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="知识库名称">
|
||||
{{ activeStore.storeName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="知识库编码">
|
||||
{{ activeStore.storeCode }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">
|
||||
{{ activeStore.description || '暂无描述' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ activeStore.remark || '暂无备注' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ activeStore.createTime || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ activeStore.updateTime || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</article>
|
||||
|
||||
<article class="detail-card detail-card--placeholder">
|
||||
<div class="detail-card__header">
|
||||
<h4>文档概览</h4>
|
||||
<span>下一批接口补充</span>
|
||||
</div>
|
||||
<el-empty description="文档数量、切片数量、最近上传时间待后端聚合接口补充" />
|
||||
</article>
|
||||
|
||||
<article class="detail-card detail-card--placeholder">
|
||||
<div class="detail-card__header">
|
||||
<h4>检索配置</h4>
|
||||
<span>下一批接口补充</span>
|
||||
</div>
|
||||
<el-empty description="检索模式、Embedding 模型、Chunk 参数待后端补充" />
|
||||
</article>
|
||||
|
||||
<article class="detail-card detail-card--placeholder">
|
||||
<div class="detail-card__header">
|
||||
<h4>最近任务</h4>
|
||||
<span>下一批接口补充</span>
|
||||
</div>
|
||||
<el-empty description="导入任务、索引任务与状态流转待后端补充" />
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-else description="请选择左侧一个知识库查看详情" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="createDialogVisible" title="新建知识库" width="560px">
|
||||
<el-form :model="createForm" label-width="96px">
|
||||
<el-form-item label="知识库编码" required>
|
||||
<el-input v-model="createForm.storeCode" data-test="create-store-code" placeholder="如 PROD_DOC" />
|
||||
</el-form-item>
|
||||
<el-form-item label="知识库名称" required>
|
||||
<el-input
|
||||
v-model="createForm.storeName"
|
||||
data-test="create-store-name"
|
||||
placeholder="请输入知识库名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="createForm.status">
|
||||
<el-radio-button label="启用" />
|
||||
<el-radio-button label="停用" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="createForm.description" type="textarea" :rows="3" placeholder="请输入知识库描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="createForm.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button data-test="create-store-submit" type="primary" :loading="submitting" @click="submitCreateStore">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="editDialogVisible" title="编辑知识库" width="560px">
|
||||
<el-form :model="editForm" label-width="96px">
|
||||
<el-form-item label="知识库编码" required>
|
||||
<el-input v-model="editForm.storeCode" />
|
||||
</el-form-item>
|
||||
<el-form-item label="知识库名称" required>
|
||||
<el-input v-model="editForm.storeName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="editForm.status">
|
||||
<el-radio-button label="启用" />
|
||||
<el-radio-button label="停用" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="editForm.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitEditStore">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-store-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rag-store-page__header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.rag-store-page__header p {
|
||||
margin: 6px 0 0;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
padding: 20px 22px 0;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
border: 1px solid #e6ebf3;
|
||||
border-radius: 12px;
|
||||
padding: 18px 18px 16px;
|
||||
background: linear-gradient(180deg, #ffffff, #f9fbff);
|
||||
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.overview-card__label {
|
||||
display: block;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.overview-card__value {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #172033;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.overview-card__hint {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rag-store-page__content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.45fr);
|
||||
gap: 18px;
|
||||
padding: 18px 22px 22px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.store-list-panel,
|
||||
.store-detail-panel {
|
||||
border: 1px solid #e6ebf3;
|
||||
border-radius: 12px;
|
||||
background: #fcfdff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.store-detail-panel__body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px 18px 14px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
background: linear-gradient(180deg, #ffffff, #fbfcff);
|
||||
}
|
||||
|
||||
.section-heading h3 {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.section-heading p {
|
||||
margin: 6px 0 0;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.store-search-bar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 10px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.store-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
max-height: 780px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.store-card {
|
||||
border: 1px solid #e3e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.store-card:hover {
|
||||
border-color: #bfdbfe;
|
||||
box-shadow: 0 8px 22px rgba(22, 119, 255, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.store-card--active {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 10px 24px rgba(22, 119, 255, 0.12);
|
||||
background: #f7fbff;
|
||||
}
|
||||
|
||||
.store-card__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.store-card strong {
|
||||
color: #172033;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.store-card p {
|
||||
margin: 10px 0 0;
|
||||
color: #475467;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.store-card__metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.section-heading--detail {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
border: 1px solid #e7edf5;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.detail-card__header h4 {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.detail-card__header span {
|
||||
color: #667085;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-card--placeholder :deep(.el-empty) {
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rag-store-page__content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.rag-store-page__content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.section-heading,
|
||||
.section-heading--detail {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.store-search-bar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
132
frontend/src/pages/__tests__/RagStoresPage.spec.ts
Normal file
132
frontend/src/pages/__tests__/RagStoresPage.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import RagStoresPage from '../RagStoresPage.vue';
|
||||
import { getRagStoreById, queryRagStores, saveRagStore } from '@/api/ragStores';
|
||||
|
||||
vi.mock('@/api/ragStores', () => ({
|
||||
queryRagStores: vi.fn((query?: { storeName?: string }) => {
|
||||
const rows = [
|
||||
{
|
||||
id: '1',
|
||||
storeCode: 'PROD_DOC',
|
||||
storeName: '产品制度库',
|
||||
description: '产品制度、业务规范、流程材料',
|
||||
status: '启用',
|
||||
createTime: '2026-05-03 10:20:00',
|
||||
updateTime: '2026-05-21 16:40:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
storeCode: 'FAQ',
|
||||
storeName: 'FAQ知识库',
|
||||
description: '常见问题知识沉淀',
|
||||
status: '停用',
|
||||
createTime: '2026-05-06 09:10:00',
|
||||
updateTime: '2026-05-21 11:12:00',
|
||||
},
|
||||
];
|
||||
|
||||
const keyword = query?.storeName?.trim();
|
||||
const data = keyword ? rows.filter((row) => row.storeName.includes(keyword)) : rows;
|
||||
return Promise.resolve({ resultcode: '0', message: null, data });
|
||||
}),
|
||||
getRagStoreById: vi.fn((id: string) =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data:
|
||||
id === '2'
|
||||
? {
|
||||
id: '2',
|
||||
storeCode: 'FAQ',
|
||||
storeName: 'FAQ知识库',
|
||||
description: '常见问题知识沉淀',
|
||||
status: '停用',
|
||||
remark: 'FAQ 场景知识',
|
||||
createTime: '2026-05-06 09:10:00',
|
||||
updateTime: '2026-05-21 11:12:00',
|
||||
}
|
||||
: {
|
||||
id: '1',
|
||||
storeCode: 'PROD_DOC',
|
||||
storeName: '产品制度库',
|
||||
description: '产品制度、业务规范、流程材料',
|
||||
status: '启用',
|
||||
remark: '核心制度库',
|
||||
createTime: '2026-05-03 10:20:00',
|
||||
updateTime: '2026-05-21 16:40:00',
|
||||
},
|
||||
}),
|
||||
),
|
||||
saveRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
deleteRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
}));
|
||||
|
||||
describe('RagStoresPage', () => {
|
||||
it('renders overview cards and loads default store detail from backend data', async () => {
|
||||
const wrapper = mount(RagStoresPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('知识库总数');
|
||||
expect(wrapper.text()).toContain('产品制度库');
|
||||
expect(wrapper.text()).toContain('核心制度库');
|
||||
expect(queryRagStores).toHaveBeenCalled();
|
||||
expect(getRagStoreById).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('filters stores by name and updates detail when a store is selected', async () => {
|
||||
const wrapper = mount(RagStoresPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="store-name-input"]').setValue('FAQ');
|
||||
await wrapper.get('[data-test="store-search"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(queryRagStores).toHaveBeenLastCalledWith({
|
||||
storeName: 'FAQ',
|
||||
});
|
||||
expect(wrapper.text()).toContain('FAQ知识库');
|
||||
expect(wrapper.text()).not.toContain('核心制度库');
|
||||
|
||||
await wrapper.get('[data-test="store-card-faq"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(getRagStoreById).toHaveBeenLastCalledWith('2');
|
||||
expect(wrapper.text()).toContain('FAQ 场景知识');
|
||||
});
|
||||
|
||||
it('submits create form through backend api', async () => {
|
||||
const wrapper = mount(RagStoresPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="create-store"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.get('[data-test="create-store-code"]').setValue('NEW_STORE');
|
||||
await wrapper.get('[data-test="create-store-name"]').setValue('新建知识库');
|
||||
await wrapper.get('[data-test="create-store-submit"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(saveRagStore).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
storeCode: 'NEW_STORE',
|
||||
storeName: '新建知识库',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user