901 lines
26 KiB
Vue
901 lines
26 KiB
Vue
<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 { useRouter } from 'vue-router';
|
||
import { loadModelProviderEnumOptions, type EnumOption } from '@/api/modelEnums';
|
||
import {
|
||
getRagStoreModelConfig,
|
||
queryModelConfigs,
|
||
rebuildRagStoreIndex,
|
||
saveRagStoreModelConfig,
|
||
type ModelConfig,
|
||
} from '@/api/modelProvider';
|
||
|
||
import {
|
||
deleteRagStore,
|
||
getRagStoreById,
|
||
getRagStoreDocumentOverview,
|
||
getRagStoreOverview,
|
||
queryRagStores,
|
||
saveRagStore,
|
||
type RagStoreDocumentOverview,
|
||
type RagStoreOverview,
|
||
type RagStore,
|
||
} from '@/api/ragStores';
|
||
import RagDocumentBatchUploadDialog from '@/components/rag/RagDocumentBatchUploadDialog.vue';
|
||
|
||
type StoreStatus = '启用' | '停用';
|
||
|
||
const router = useRouter();
|
||
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 pageOverview = ref<RagStoreOverview | null>(null);
|
||
const activeStoreDocumentOverview = ref<RagStoreDocumentOverview | null>(null);
|
||
const embeddingModels = ref<ModelConfig[]>([]);
|
||
const ragConfigLoading = ref(false);
|
||
const ragConfigSaving = ref(false);
|
||
const ragConfig = reactive({
|
||
id: '',
|
||
embeddingModelId: '',
|
||
embeddingDimension: 1024,
|
||
chunkStrategy: null as number | null,
|
||
chunkSize: null as number | null,
|
||
chunkOverlap: null as number | null,
|
||
delimiter: '',
|
||
remark: '',
|
||
});
|
||
const chunkStrategyOptions = ref<EnumOption[]>([]);
|
||
|
||
const queryForm = reactive({
|
||
storeName: '',
|
||
});
|
||
|
||
const createDialogVisible = ref(false);
|
||
const editDialogVisible = ref(false);
|
||
const uploadDialogVisible = 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 = pageOverview.value?.totalStores ?? storeRows.value.length;
|
||
const totalDocuments = pageOverview.value?.totalDocuments ?? '-';
|
||
const totalChunks = pageOverview.value?.totalChunks ?? '-';
|
||
const retrievableStores =
|
||
pageOverview.value?.retrievableStores
|
||
?? storeRows.value.filter((row) => row.status === '启用').length;
|
||
|
||
return [
|
||
{ label: '知识库总数', value: totalStores, hint: '当前已登记知识库' },
|
||
{ label: '文档总数', value: totalDocuments, hint: '当前知识库已登记文档总量' },
|
||
{ label: '切片总数', value: totalChunks, hint: '当前未接入切片表,待后续能力补充' },
|
||
{ label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态统计' },
|
||
];
|
||
});
|
||
|
||
async function loadOverview() {
|
||
try {
|
||
const response = await getRagStoreOverview();
|
||
pageOverview.value = response.data ?? null;
|
||
} catch {
|
||
pageOverview.value = null;
|
||
}
|
||
}
|
||
|
||
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;
|
||
activeStoreDocumentOverview.value = null;
|
||
ragConfig.id = '';
|
||
ragConfig.embeddingModelId = '';
|
||
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;
|
||
activeStoreDocumentOverview.value = null;
|
||
return;
|
||
}
|
||
await selectStore(targetId);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
async function selectStore(storeId: string) {
|
||
activeStoreId.value = storeId;
|
||
detailLoading.value = true;
|
||
try {
|
||
const [storeResponse, documentOverviewResponse] = await Promise.all([
|
||
getRagStoreById(storeId),
|
||
getRagStoreDocumentOverview(storeId),
|
||
]);
|
||
activeStore.value = storeResponse.data ?? null;
|
||
activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null;
|
||
await loadRagModelConfig(storeId);
|
||
} 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 openBatchUploadDialog() {
|
||
if (!activeStore.value?.id) {
|
||
ElMessage.warning('请选择知识库');
|
||
return;
|
||
}
|
||
uploadDialogVisible.value = true;
|
||
}
|
||
|
||
function viewActiveStoreDocuments() {
|
||
if (!activeStore.value?.id) {
|
||
return;
|
||
}
|
||
router.push({
|
||
name: 'rag-documents',
|
||
query: { storeId: String(activeStore.value.id) },
|
||
});
|
||
}
|
||
|
||
async function refreshAfterUpload() {
|
||
await Promise.all([
|
||
loadOverview(),
|
||
activeStoreId.value ? selectStore(activeStoreId.value) : Promise.resolve(),
|
||
]);
|
||
}
|
||
|
||
async function loadRagModelConfig(storeId: string) {
|
||
ragConfigLoading.value = true;
|
||
try {
|
||
const response = await getRagStoreModelConfig(storeId);
|
||
const data = response.data;
|
||
ragConfig.id = data?.id ?? '';
|
||
ragConfig.embeddingModelId = data?.embeddingModelId ?? '';
|
||
ragConfig.embeddingDimension = data?.embeddingDimension ?? 1024;
|
||
ragConfig.chunkStrategy = data?.chunkStrategy ?? null;
|
||
ragConfig.chunkSize = data?.chunkSize ?? null;
|
||
ragConfig.chunkOverlap = data?.chunkOverlap ?? null;
|
||
ragConfig.delimiter = data?.delimiter ?? '';
|
||
ragConfig.remark = data?.remark ?? '';
|
||
} finally {
|
||
ragConfigLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function saveStoreModelConfig() {
|
||
if (!activeStoreId.value) {
|
||
return;
|
||
}
|
||
if (!ragConfig.embeddingModelId) {
|
||
ElMessage.warning('请选择 Embedding 模型');
|
||
return;
|
||
}
|
||
ragConfigSaving.value = true;
|
||
try {
|
||
await saveRagStoreModelConfig({
|
||
id: ragConfig.id || undefined,
|
||
storeId: activeStoreId.value,
|
||
embeddingModelId: ragConfig.embeddingModelId,
|
||
embeddingDimension: ragConfig.embeddingDimension,
|
||
chunkStrategy: ragConfig.chunkStrategy ?? undefined,
|
||
chunkSize: ragConfig.chunkSize ?? undefined,
|
||
chunkOverlap: ragConfig.chunkOverlap ?? undefined,
|
||
delimiter: ragConfig.delimiter || undefined,
|
||
remark: ragConfig.remark || undefined,
|
||
});
|
||
ElMessage.success('知识库模型配置已保存');
|
||
await loadRagModelConfig(activeStoreId.value);
|
||
} finally {
|
||
ragConfigSaving.value = false;
|
||
}
|
||
}
|
||
|
||
async function triggerRebuildIndex() {
|
||
if (!activeStoreId.value) {
|
||
return;
|
||
}
|
||
await rebuildRagStoreIndex(activeStoreId.value);
|
||
ElMessage.success('已触发重建索引');
|
||
}
|
||
|
||
function syncEmbeddingDimensionFromModel() {
|
||
const model = embeddingModels.value.find((item) => item.id === ragConfig.embeddingModelId);
|
||
if (!model) {
|
||
return;
|
||
}
|
||
if (model.embeddingDimension) {
|
||
ragConfig.embeddingDimension = model.embeddingDimension;
|
||
}
|
||
}
|
||
|
||
function getStatusTagType(status?: string | null) {
|
||
return status === '启用' ? 'success' : 'info';
|
||
}
|
||
|
||
onMounted(() => {
|
||
Promise.all([
|
||
loadOverview(),
|
||
loadStores(),
|
||
queryModelConfigs().then((response) => {
|
||
embeddingModels.value = (response.data ?? []).filter((item) => item.modelType === 'EMBEDDING');
|
||
}),
|
||
loadModelProviderEnumOptions().then((enums) => {
|
||
chunkStrategyOptions.value = enums.chunk_strategy ?? [];
|
||
}),
|
||
]);
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<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
|
||
data-test="store-batch-upload"
|
||
type="primary"
|
||
:icon="UploadFilled"
|
||
@click="openBatchUploadDialog"
|
||
>
|
||
批量导入文件
|
||
</el-button>
|
||
<el-button :icon="FolderAdd" @click="triggerRebuildIndex">重建索引</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>
|
||
<el-button
|
||
data-test="view-store-documents"
|
||
link
|
||
type="primary"
|
||
@click="viewActiveStoreDocuments"
|
||
>
|
||
查看文档
|
||
</el-button>
|
||
</div>
|
||
<el-descriptions :column="2" border>
|
||
<el-descriptions-item label="文档总数">
|
||
{{ activeStoreDocumentOverview?.documentCount ?? 0 }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="启用文档数">
|
||
{{ activeStoreDocumentOverview?.enabledDocumentCount ?? 0 }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="已解析文档数">
|
||
{{ activeStoreDocumentOverview?.parsedDocumentCount ?? 0 }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="已索引文档数">
|
||
{{ activeStoreDocumentOverview?.indexedDocumentCount ?? 0 }}
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="最近上传时间" :span="2">
|
||
{{ activeStoreDocumentOverview?.lastUploadTime || '-' }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</article>
|
||
|
||
<article class="detail-card">
|
||
<div class="detail-card__header">
|
||
<h4>检索配置</h4>
|
||
<el-button type="primary" link :loading="ragConfigSaving" @click="saveStoreModelConfig">
|
||
保存配置
|
||
</el-button>
|
||
</div>
|
||
<div v-loading="ragConfigLoading" class="rag-config-form">
|
||
<el-form :model="ragConfig" label-width="112px">
|
||
<el-form-item label="Embedding 模型" required>
|
||
<el-select
|
||
v-model="ragConfig.embeddingModelId"
|
||
placeholder="请选择模型"
|
||
@change="syncEmbeddingDimensionFromModel"
|
||
>
|
||
<el-option
|
||
v-for="item in embeddingModels"
|
||
:key="item.id"
|
||
:label="`${item.modelName}(${item.modelCode})`"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="向量维度">
|
||
<el-input-number v-model="ragConfig.embeddingDimension" :min="1" controls-position="right" />
|
||
</el-form-item>
|
||
<el-form-item label="切片策略">
|
||
<el-select v-model="ragConfig.chunkStrategy" clearable placeholder="可选">
|
||
<el-option
|
||
v-for="item in chunkStrategyOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="Number(item.value)"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="切片大小">
|
||
<el-input-number v-model="ragConfig.chunkSize" :min="1" controls-position="right" />
|
||
</el-form-item>
|
||
<el-form-item label="切片重叠">
|
||
<el-input-number v-model="ragConfig.chunkOverlap" :min="0" controls-position="right" />
|
||
</el-form-item>
|
||
<el-form-item label="分隔符">
|
||
<el-input v-model="ragConfig.delimiter" placeholder="可选,如 \\n\\n" />
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="ragConfig.remark" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</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>
|
||
|
||
<RagDocumentBatchUploadDialog
|
||
v-model="uploadDialogVisible"
|
||
:stores="activeStore ? [activeStore] : storeRows"
|
||
:locked-store-id="activeStoreId"
|
||
@uploaded="refreshAfterUpload"
|
||
/>
|
||
</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;
|
||
}
|
||
|
||
.rag-config-form :deep(.el-select),
|
||
.rag-config-form :deep(.el-input-number) {
|
||
width: 100%;
|
||
}
|
||
|
||
@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>
|