新增知识库相关组件,包括文档卡片、知识库卡片、状态徽章和分段卡片,优化日期格式化工具函数,更新文档管理和知识库管理页面以使用新组件。

This commit is contained in:
Chuck1sn
2025-06-28 08:12:59 +08:00
parent 6ec07686a9
commit 56d6a992f8
9 changed files with 220 additions and 129 deletions

View File

@@ -0,0 +1,36 @@
<template>
<div class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div class="p-4">
<div class="flex justify-between items-start">
<div>
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ doc.name }}</h5>
<div class="flex items-center mb-2">
<KnowledgeStatusBadge :status="doc.status" type="status" class="mr-2" />
<KnowledgeStatusBadge :enabled="doc.enable" type="enabled" />
</div>
</div>
<div class="flex space-x-2">
<slot name="toggle-switch"></slot>
</div>
</div>
<div class="flex justify-between items-center">
<span class="text-xs text-gray-500">
上传时间: {{ formatDateString(doc.createTime) }}
</span>
<div class="flex space-x-2">
<slot name="actions"></slot>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { KnowledgeStatusBadge } from '@/components/common/knowledge';
import type { LibraryDoc } from "@/types/KnowledgeTypes";
import { formatDateString } from '@/utils/dateUtil';
const props = defineProps<{
doc: LibraryDoc;
}>();
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div class="p-4">
<div class="flex justify-between items-start">
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ library.name }}</h5>
<div class="flex space-x-2">
<slot name="actions-top"></slot>
</div>
</div>
<p class="text-sm text-gray-600 mb-3 line-clamp-2">
{{ library.description || '暂无描述' }}
</p>
<div class="flex justify-between items-center">
<span class="text-xs text-gray-500">
创建时间: {{ formatDateString(library.createTime) }}
</span>
<slot name="actions-bottom"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Library } from "@/types/KnowledgeTypes";
import { formatDateString } from '@/utils/dateUtil';
const props = defineProps<{
library: Library;
}>();
</script>

View File

@@ -0,0 +1,37 @@
<template>
<span :class="`inline-flex items-center px-2 py-1 text-xs font-medium rounded-full ${
getStatusClass()
}`">
{{ getStatusText() }}
</span>
</template>
<script setup lang="ts">
import { DocStatus } from "@/types/KnowledgeTypes";
const props = defineProps<{
status?: string;
enabled?: boolean;
type: 'status' | 'enabled';
}>();
const getStatusClass = () => {
if (props.type === 'status') {
return props.status === DocStatus.SUCCESS
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800';
}
return props.enabled
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800';
};
const getStatusText = () => {
if (props.type === 'status') {
return props.status === DocStatus.SUCCESS ? '解析完成' : '解析中';
}
return props.enabled ? '已启用' : '已禁用';
};
</script>

View File

@@ -0,0 +1,34 @@
<template>
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
<div class="flex justify-between items-start mb-2">
<h5 class="text-lg font-semibold text-gray-900">分段 #{{ index + 1 }}</h5>
<div class="text-xs text-gray-500">
ID: {{ segment.id }}
</div>
</div>
<div class="text-sm text-gray-500 mb-2">
<div class="flex flex-wrap gap-2">
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
Embedding ID: {{ segment.embeddingId || '无' }}
</span>
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
Token 使用量: {{ segment.tokenUsage || 0 }}
</span>
</div>
</div>
<div class="border-t border-gray-200 pt-3 mt-3">
<h6 class="text-sm font-medium text-gray-900 mb-2">内容:</h6>
<pre
class="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded-lg max-h-60 overflow-y-auto">{{ segment.content }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import type { LibraryDocSegment } from "@/types/KnowledgeTypes";
const props = defineProps<{
segment: LibraryDocSegment;
index: number;
}>();
</script>

View File

@@ -0,0 +1,11 @@
import KnowledgeDocCard from "./KnowledgeDocCard.vue";
import KnowledgeLibraryCard from "./KnowledgeLibraryCard.vue";
import KnowledgeStatusBadge from "./KnowledgeStatusBadge.vue";
import SegmentCard from "./SegmentCard.vue";
export {
KnowledgeStatusBadge,
KnowledgeDocCard,
KnowledgeLibraryCard,
SegmentCard,
};

View File

@@ -13,4 +13,9 @@ const formatDate = (date?: Date) => {
return dayjs(date).format("YYYY-MM-DDTHH:mm:ss.SSSZ");
};
export { dayjs, formatDate };
const formatDateString = (dateString?: string, format = "YYYY-MM-DD HH:mm") => {
if (!dateString) return "未知";
return dayjs(dateString).format(format);
};
export { dayjs, formatDate, formatDateString };

View File

@@ -5,63 +5,36 @@
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">{{ currentLibrary?.name || '知识库' }} - 文档管理</h1>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div v-for="doc in docs" :key="doc.id"
class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div class="p-4">
<div class="flex justify-between items-start">
<div>
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ doc.name }}</h5>
<div class="flex items-center mb-2">
<span :class="`inline-flex items-center px-2 py-1 text-xs font-medium rounded-full ${
doc.status === DocStatus.SUCCESS ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
} mr-2`">
{{ doc.status === DocStatus.SUCCESS ? '解析完成' : '解析中' }}
</span>
<span :class="`inline-flex items-center px-2 py-1 text-xs font-medium rounded-full ${
doc.enable ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'
}`">
{{ doc.enable ? '已启用' : '已禁用' }}
</span>
</div>
<!-- 文档列表 -->
<div v-if="docs.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<KnowledgeDocCard v-for="doc in docs" :key="doc.id" :doc="doc">
<template #toggle-switch>
<label class="inline-flex items-center mb-5"
:class="!doc.enable ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'">
<input type="checkbox" class="sr-only peer" :checked="doc.enable" @change="handleToggleDocStatus(doc)">
<div
class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600">
</div>
<div class="flex space-x-2">
<label class="inline-flex items-center mb-5"
:class="!doc.enable ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'">
<input type="checkbox" class="sr-only peer" :checked="doc.enable" @change="handleToggleDocStatus(doc)">
<div
class="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600">
</div>
</label>
</div>
</div>
<!-- <div class="text-sm text-gray-600 mb-3">
<div class="truncate">{{ doc.path || '无' }}</div>
</div> -->
<div class="flex justify-between items-center">
<span class="text-xs text-gray-500">
上传时间: {{ formatDate(doc.createTime) }}
</span>
<div class="flex space-x-2">
<button @click="navigateToDocSegments(doc)" :class="
['text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-content',
doc.status !== DocStatus.SUCCESS ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer']"
:disabled="doc.status !== DocStatus.SUCCESS">
查看内容
</button>
<button @click="handleDeleteDoc(doc)"
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-1.5">
删除
</button>
</div>
</div>
</div>
</div>
</label>
</template>
<template #actions>
<button @click="navigateToDocSegments(doc)" :class="
['text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-content',
doc.status !== DocStatus.SUCCESS ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer']"
:disabled="doc.status !== DocStatus.SUCCESS">
查看内容
</button>
<button @click="handleDeleteDoc(doc)"
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-1.5">
删除
</button>
</template>
</KnowledgeDocCard>
</div>
<div class="flex flex-col items-center justify-center py-10">
<div v-if="docs.length === 0" class="text-gray-500 text-lg mb-4">暂无文档</div>
<!-- 空状态 -->
<div v-else class="flex flex-col items-center justify-center py-10">
<div class="text-gray-500 text-lg mb-4">暂无文档</div>
<div>
<input ref="fileInputRef" class="hidden" id="doc_file_input" type="file" @change="handleFileChange">
<TableButton variant="primary" size="md" @click="triggerFileInput">
@@ -84,11 +57,11 @@
</template>
<script setup lang="ts">
import dayjs from "dayjs";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, ref, watchEffect } from "vue";
import { useRoute, useRouter } from "vue-router";
import { KnowledgeDocCard, KnowledgeStatusBadge } from "@/components/common/knowledge";
import { PlusIcon } from "@/components/icons";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
@@ -98,6 +71,7 @@ import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants";
import { formatDateString } from '@/utils/dateUtil';
import type { Library, LibraryDoc } from "@/types/KnowledgeTypes";
import { DocStatus } from "@/types/KnowledgeTypes";
@@ -129,12 +103,6 @@ const selectedDoc = ref<LibraryDoc | undefined>();
// 提示store
const alertStore = useAlertStore();
// 格式化日期
const formatDate = (dateString?: string) => {
if (!dateString) return "未知";
return dayjs(dateString).format("YYYY-MM-DD HH:mm");
};
// 触发文件选择
const triggerFileInput = () => {
fileInputRef.value?.click();

View File

@@ -5,48 +5,38 @@
<h1 class="text-2xl font-semibold text-gray-900">知识库管理</h1>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div v-for="library in libraries" :key="library.id"
class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div class="p-4">
<div class="flex justify-between items-start">
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ library.name }}</h5>
<div class="flex space-x-2">
<button @click="handleEditLibrary(library)" class="text-gray-500 hover:text-blue-700 focus:outline-none">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z">
</path>
</svg>
</button>
<button @click="handleDeleteLibrary(library)" class="text-gray-500 hover:text-red-700 focus:outline-none">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
</path>
</svg>
</button>
</div>
</div>
<p class="text-sm text-gray-600 mb-3 line-clamp-2">
{{ library.description || '暂无描述' }}
</p>
<div class="flex justify-between items-center">
<span class="text-xs text-gray-500">
创建时间: {{ formatDate(library.createTime) }}
</span>
<button @click="navigateToLibraryDocs(library)"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5">
查看知识库
</button>
</div>
</div>
</div>
<!-- 知识库列表 -->
<div v-if="libraries.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<KnowledgeLibraryCard v-for="library in libraries" :key="library.id" :library="library">
<template #actions-top>
<button @click="handleEditLibrary(library)" class="text-gray-500 hover:text-blue-700 focus:outline-none">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z">
</path>
</svg>
</button>
<button @click="handleDeleteLibrary(library)" class="text-gray-500 hover:text-red-700 focus:outline-none">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
</path>
</svg>
</button>
</template>
<template #actions-bottom>
<button @click="navigateToLibraryDocs(library)"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5">
查看知识库
</button>
</template>
</KnowledgeLibraryCard>
</div>
<div v-if="libraries.length === 0" class="flex flex-col items-center justify-center py-10">
<!-- 空状态 -->
<div v-else class="flex flex-col items-center justify-center py-10">
<div class="text-gray-500 text-lg mb-4">暂无知识库</div>
<div>
<button @click="handleCreateLibraryClick"
@@ -75,6 +65,7 @@ import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { KnowledgeLibraryCard } from "@/components/common/knowledge";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
import LibraryFormDialog from "@/components/modals/LibraryFormDialog.vue";

View File

@@ -9,36 +9,14 @@
</p>
</div>
<!-- 分段列表 -->
<!-- 空状态 -->
<div v-if="segments.length === 0" class="flex flex-col items-center justify-center py-10">
<div class="text-gray-500 text-lg">暂无分段内容</div>
</div>
<!-- 分段列表 -->
<div v-else class="space-y-4">
<div v-for="(segment, index) in segments" :key="segment.id"
class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
<div class="flex justify-between items-start mb-2">
<h5 class="text-lg font-semibold text-gray-900">分段 #{{ index + 1 }}</h5>
<div class="text-xs text-gray-500">
ID: {{ segment.id }}
</div>
</div>
<div class="text-sm text-gray-500 mb-2">
<div class="flex flex-wrap gap-2">
<span class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
Embedding ID: {{ segment.embeddingId || '无' }}
</span>
<span
class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
Token 使用量: {{ segment.tokenUsage || 0 }}
</span>
</div>
</div>
<div class="border-t border-gray-200 pt-3 mt-3">
<h6 class="text-sm font-medium text-gray-900 mb-2">内容:</h6>
<pre
class="text-sm text-gray-700 whitespace-pre-wrap bg-gray-50 p-3 rounded-lg max-h-60 overflow-y-auto">{{ segment.content }}</pre>
</div>
</div>
<SegmentCard v-for="(segment, index) in segments" :key="segment.id" :segment="segment" :index="index" />
</div>
</div>
</template>
@@ -47,6 +25,7 @@
import { onMounted, ref, watchEffect } from "vue";
import { useRoute } from "vue-router";
import { SegmentCard } from "@/components/common/knowledge";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
import { Routes } from "@/router/constants";