mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-03 19:06:10 +00:00
新增 CardBase 组件,重构知识文档卡片、知识库卡片和分段卡片以使用新组件,优化按钮组件并更新相关页面以提升用户体验。
This commit is contained in:
44
frontend/src/components/common/CardBase.vue
Normal file
44
frontend/src/components/common/CardBase.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<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 mb-3">
|
||||||
|
<div class="flex-1">
|
||||||
|
<h5 :class="[titleClass || 'text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate']">
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</h5>
|
||||||
|
<div v-if="$slots.subtitle" class="flex items-center mb-2">
|
||||||
|
<slot name="subtitle"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="$slots['header-actions']" class="flex space-x-2">
|
||||||
|
<slot name="header-actions"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.content" class="text-sm text-gray-600 mb-3 space-y-2">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<slot name="footer-left">
|
||||||
|
<span v-if="$slots.timestamp" class="text-xs text-gray-500">
|
||||||
|
<slot name="timestamp"></slot>
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
<div v-if="$slots['footer-actions']" class="flex space-x-2">
|
||||||
|
<slot name="footer-actions"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
titleClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
4
frontend/src/components/common/index.ts
Normal file
4
frontend/src/components/common/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import CardBase from "./CardBase.vue";
|
||||||
|
import PromotionBanner from "./PromotionBanner.vue";
|
||||||
|
|
||||||
|
export { CardBase, PromotionBanner };
|
||||||
@@ -1,31 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
<CardBase>
|
||||||
<div class="p-4">
|
<template #title>{{ doc.name }}</template>
|
||||||
<div class="flex justify-between items-start">
|
<template #subtitle>
|
||||||
<div>
|
<KnowledgeStatusBadge :status="doc.status" type="status" class="mr-2" />
|
||||||
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ doc.name }}</h5>
|
<KnowledgeStatusBadge :enabled="doc.enable" type="enabled" />
|
||||||
<div class="flex items-center mb-2">
|
</template>
|
||||||
<KnowledgeStatusBadge :status="doc.status" type="status" class="mr-2" />
|
<template #header-actions>
|
||||||
<KnowledgeStatusBadge :enabled="doc.enable" type="enabled" />
|
<slot name="toggle-switch"></slot>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<template #footer-left>
|
||||||
<div class="flex space-x-2">
|
<span class="text-xs text-gray-500">
|
||||||
<slot name="toggle-switch"></slot>
|
上传时间: {{ formatDateString(doc.createTime) }}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
<div class="flex justify-between items-center">
|
<template #footer-actions>
|
||||||
<span class="text-xs text-gray-500">
|
<slot name="actions"></slot>
|
||||||
上传时间: {{ formatDateString(doc.createTime) }}
|
</template>
|
||||||
</span>
|
</CardBase>
|
||||||
<div class="flex space-x-2">
|
|
||||||
<slot name="actions"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CardBase from '@/components/common/CardBase.vue';
|
||||||
import { KnowledgeStatusBadge } from '@/components/common/knowledge';
|
import { KnowledgeStatusBadge } from '@/components/common/knowledge';
|
||||||
import type { LibraryDoc } from "@/types/KnowledgeTypes";
|
import type { LibraryDoc } from "@/types/KnowledgeTypes";
|
||||||
import { formatDateString } from '@/utils/dateUtil';
|
import { formatDateString } from '@/utils/dateUtil';
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
<CardBase>
|
||||||
<div class="p-4">
|
<template #title>{{ library.name }}</template>
|
||||||
<div class="flex justify-between items-start">
|
<template #header-actions>
|
||||||
<h5 class="text-xl font-semibold tracking-tight text-gray-900 mb-1 truncate">{{ library.name }}</h5>
|
<slot name="actions-top"></slot>
|
||||||
<div class="flex space-x-2">
|
</template>
|
||||||
<slot name="actions-top"></slot>
|
<template #content>
|
||||||
</div>
|
<p class="text-sm text-gray-600 line-clamp-2">
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-600 mb-3 line-clamp-2">
|
|
||||||
{{ library.description || '暂无描述' }}
|
{{ library.description || '暂无描述' }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex justify-between items-center">
|
</template>
|
||||||
<span class="text-xs text-gray-500">
|
<template #footer-left>
|
||||||
创建时间: {{ formatDateString(library.createTime) }}
|
<span class="text-xs text-gray-500">
|
||||||
</span>
|
创建时间: {{ formatDateString(library.createTime) }}
|
||||||
<slot name="actions-bottom"></slot>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<template #footer-actions>
|
||||||
</div>
|
<slot name="actions-bottom"></slot>
|
||||||
|
</template>
|
||||||
|
</CardBase>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CardBase from '@/components/common/CardBase.vue';
|
||||||
import type { Library } from "@/types/KnowledgeTypes";
|
import type { Library } from "@/types/KnowledgeTypes";
|
||||||
import { formatDateString } from '@/utils/dateUtil';
|
import { formatDateString } from '@/utils/dateUtil';
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm p-4">
|
<CardBase>
|
||||||
<div class="flex justify-between items-start mb-2">
|
<template #title>分段 #{{ index + 1 }}</template>
|
||||||
<h5 class="text-lg font-semibold text-gray-900">分段 #{{ index + 1 }}</h5>
|
<template #header-actions>
|
||||||
<div class="text-xs text-gray-500">
|
<div class="text-xs text-gray-500">
|
||||||
ID: {{ segment.id }}
|
ID: {{ segment.id }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="text-sm text-gray-500 mb-2">
|
<template #subtitle>
|
||||||
<div class="flex flex-wrap gap-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">
|
<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 || '无' }}
|
Embedding ID: {{ segment.embeddingId || '无' }}
|
||||||
@@ -15,16 +15,19 @@
|
|||||||
Token 使用量: {{ segment.tokenUsage || 0 }}
|
Token 使用量: {{ segment.tokenUsage || 0 }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="border-t border-gray-200 pt-3 mt-3">
|
<template #content>
|
||||||
<h6 class="text-sm font-medium text-gray-900 mb-2">内容:</h6>
|
<div class="border-t border-gray-200 pt-3">
|
||||||
<pre
|
<h6 class="text-sm font-medium text-gray-900 mb-2">内容:</h6>
|
||||||
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>
|
<pre
|
||||||
</div>
|
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>
|
||||||
|
</CardBase>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CardBase from '@/components/common/CardBase.vue';
|
||||||
import type { LibraryDocSegment } from "@/types/KnowledgeTypes";
|
import type { LibraryDocSegment } from "@/types/KnowledgeTypes";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
122
frontend/src/components/ui/Button.vue
Normal file
122
frontend/src/components/ui/Button.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<button :class="[
|
||||||
|
'flex items-center justify-center gap-x-1 whitespace-nowrap font-medium rounded-lg focus:ring-4 focus:outline-none',
|
||||||
|
sizeClasses,
|
||||||
|
colorClasses,
|
||||||
|
(disabled || (isLoading && !abortable)) ? 'opacity-50 cursor-not-allowed' : '',
|
||||||
|
fullWidth ? 'w-full' : '',
|
||||||
|
className
|
||||||
|
]" :disabled="disabled || (isLoading && !abortable)" @click="handleClick" :type="type">
|
||||||
|
<div v-if="isLoading && !abortable" class="animate-spin mr-1" :class="iconSizeClasses">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<StopIcon v-else-if="isLoading && abortable" :class="iconSizeClasses" />
|
||||||
|
<slot v-else name="icon"></slot>
|
||||||
|
<span v-if="$slots.default">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { StopIcon } from "@/components/icons";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
export type ButtonVariant =
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "success"
|
||||||
|
| "danger"
|
||||||
|
| "warning"
|
||||||
|
| "info";
|
||||||
|
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
||||||
|
export type ButtonType = "button" | "submit" | "reset";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 按钮变体类型 */
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
/** 按钮尺寸 */
|
||||||
|
size?: ButtonSize;
|
||||||
|
/** 是否禁用 */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** 自定义CSS类名 */
|
||||||
|
className?: string;
|
||||||
|
/** 是否为移动端尺寸 */
|
||||||
|
isMobile?: boolean;
|
||||||
|
/** 是否处于加载状态 */
|
||||||
|
isLoading?: boolean;
|
||||||
|
/** 是否可中止 */
|
||||||
|
abortable?: boolean;
|
||||||
|
/** 按钮类型 */
|
||||||
|
type?: ButtonType;
|
||||||
|
/** 是否占满宽度 */
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [event: MouseEvent];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 按钮颜色样式映射 */
|
||||||
|
const colorClasses = computed(() => {
|
||||||
|
const variants: Record<ButtonVariant, string> = {
|
||||||
|
primary: "text-white bg-blue-700 hover:bg-blue-800 focus:ring-blue-300",
|
||||||
|
secondary:
|
||||||
|
"text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-gray-100",
|
||||||
|
success: "text-white bg-green-700 hover:bg-green-800 focus:ring-green-300",
|
||||||
|
danger: "text-white bg-red-700 hover:bg-red-800 focus:ring-red-300",
|
||||||
|
warning:
|
||||||
|
"text-gray-900 bg-yellow-400 hover:bg-yellow-500 focus:ring-yellow-300",
|
||||||
|
info: "text-white bg-cyan-700 hover:bg-cyan-800 focus:ring-cyan-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
return variants[props.variant || "primary"];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 按钮尺寸样式映射 */
|
||||||
|
const sizeClasses = computed(() => {
|
||||||
|
// 移动端尺寸
|
||||||
|
if (props.isMobile) {
|
||||||
|
const sizes: Record<ButtonSize, string> = {
|
||||||
|
xs: "text-xs px-2 py-1",
|
||||||
|
sm: "text-xs px-3 py-1.5",
|
||||||
|
md: "text-sm px-3 py-2",
|
||||||
|
lg: "text-sm px-4 py-2.5",
|
||||||
|
};
|
||||||
|
return sizes[props.size || "sm"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// PC端尺寸
|
||||||
|
const sizes: Record<ButtonSize, string> = {
|
||||||
|
xs: "text-xs px-3 py-1.5",
|
||||||
|
sm: "text-sm px-3 py-2",
|
||||||
|
md: "text-sm px-4 py-2.5",
|
||||||
|
lg: "text-base px-5 py-3",
|
||||||
|
};
|
||||||
|
|
||||||
|
return sizes[props.size || "md"];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 图标尺寸样式映射 */
|
||||||
|
const iconSizeClasses = computed(() => {
|
||||||
|
const sizes: Record<ButtonSize, string> = {
|
||||||
|
xs: "w-3.5 h-3.5",
|
||||||
|
sm: "w-4 h-4",
|
||||||
|
md: "w-4.5 h-4.5",
|
||||||
|
lg: "w-5 h-5",
|
||||||
|
};
|
||||||
|
return sizes[props.size || "md"];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 处理点击事件 */
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
if (!props.disabled && !(props.isLoading && !props.abortable)) {
|
||||||
|
emit("click", event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
7
frontend/src/components/ui/index.ts
Normal file
7
frontend/src/components/ui/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Alert from "./Alert.vue";
|
||||||
|
import Avatar from "./Avatar.vue";
|
||||||
|
import Button from "./Button.vue";
|
||||||
|
import InputButton from "./InputButton.vue";
|
||||||
|
import SortIcon from "./SortIcon.vue";
|
||||||
|
|
||||||
|
export { Alert, Avatar, Button, InputButton, SortIcon };
|
||||||
@@ -12,6 +12,7 @@ export const useKnowledgeQuery = () => {
|
|||||||
const libraries = ref<Library[]>([]);
|
const libraries = ref<Library[]>([]);
|
||||||
const docs = ref<LibraryDoc[]>([]);
|
const docs = ref<LibraryDoc[]>([]);
|
||||||
const segments = ref<LibraryDocSegment[]>([]);
|
const segments = ref<LibraryDocSegment[]>([]);
|
||||||
|
const doc = ref<LibraryDoc | null>(null);
|
||||||
|
|
||||||
const fetchLibraries = async () => {
|
const fetchLibraries = async () => {
|
||||||
const { data } = await client.GET("/knowledge/libraries", {});
|
const { data } = await client.GET("/knowledge/libraries", {});
|
||||||
@@ -29,11 +30,22 @@ export const useKnowledgeQuery = () => {
|
|||||||
docs.value = data || [];
|
docs.value = data || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchDocById = async (docId: number) => {
|
||||||
|
const { data } = await client.GET("/knowledge/docs", {
|
||||||
|
params: {
|
||||||
|
query: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (data && Array.isArray(data)) {
|
||||||
|
doc.value = data.find((item) => item.id === docId) || null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchDocSegments = async (params: SegmentQueryParams) => {
|
const fetchDocSegments = async (params: SegmentQueryParams) => {
|
||||||
const { data } = await client.GET("/knowledge/segments", {
|
const { data } = await client.GET("/knowledge/segments", {
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
libraryDocId: params.libraryDocId,
|
libraryDocId: params.libraryDocId || params.docId || 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -45,6 +57,8 @@ export const useKnowledgeQuery = () => {
|
|||||||
fetchLibraries,
|
fetchLibraries,
|
||||||
docs,
|
docs,
|
||||||
fetchLibraryDocs,
|
fetchLibraryDocs,
|
||||||
|
doc,
|
||||||
|
fetchDocById,
|
||||||
segments,
|
segments,
|
||||||
fetchDocSegments,
|
fetchDocSegments,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ export interface DocQueryParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SegmentQueryParams {
|
export interface SegmentQueryParams {
|
||||||
libraryDocId: number;
|
libraryDocId?: number;
|
||||||
}
|
docId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export enum DocStatus {
|
export enum DocStatus {
|
||||||
SUCCESS = "SUCCESS",
|
SUCCESS = "SUCCESS",
|
||||||
|
|||||||
@@ -1,56 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||||
<Breadcrumbs :names="['知识库管理', '文档管理']" :routes="[Routes.KNOWLEDGEVIEW.fullPath()]" />
|
<Breadcrumbs :names="['知识库管理', '文档管理']" :routes="[Routes.KNOWLEDGEVIEW.fullPath()]" />
|
||||||
<div class="mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
<div class="mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">{{ currentLibrary?.name || '知识库' }} - 文档管理</h1>
|
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">{{ currentLibrary?.name || '知识库' }} - 文档管理</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文档列表 -->
|
<!-- 文档列表 -->
|
||||||
<div v-if="docs.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<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">
|
<KnowledgeDocCard v-for="doc in docs" :key="doc.id" :doc="doc">
|
||||||
<template #toggle-switch>
|
<template #toggle-switch>
|
||||||
<label class="inline-flex items-center mb-5"
|
<label class="inline-flex items-center mb-5"
|
||||||
:class="!doc.enable ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'">
|
:class="!doc.enable ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'">
|
||||||
<input type="checkbox" class="sr-only peer" :checked="doc.enable" @change="handleToggleDocStatus(doc)">
|
<input type="checkbox" class="sr-only peer" :checked="doc.enable" @change="handleToggleDocStatus(doc)">
|
||||||
<div
|
<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">
|
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>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<button @click="navigateToDocSegments(doc)" :class="
|
<Button variant="primary" size="xs" @click="navigateToDocSegments(doc)"
|
||||||
['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',
|
:disabled="doc.status !== DocStatus.SUCCESS">
|
||||||
doc.status !== DocStatus.SUCCESS ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer']"
|
查看内容
|
||||||
:disabled="doc.status !== DocStatus.SUCCESS">
|
</Button>
|
||||||
查看内容
|
<Button variant="danger" size="xs" @click="handleDeleteDoc(doc)">
|
||||||
</button>
|
删除
|
||||||
<button @click="handleDeleteDoc(doc)"
|
</Button>
|
||||||
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">
|
</template>
|
||||||
删除
|
</KnowledgeDocCard>
|
||||||
</button>
|
</div>
|
||||||
</template>
|
|
||||||
</KnowledgeDocCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else 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 class="text-gray-500 text-lg mb-4">暂无文档</div>
|
||||||
<div>
|
<div>
|
||||||
<input ref="fileInputRef" class="hidden" id="doc_file_input" type="file" @change="handleFileChange">
|
<input ref="fileInputRef" class="hidden" id="doc_file_input" type="file" @change="handleFileChange">
|
||||||
<TableButton variant="primary" size="md" @click="triggerFileInput">
|
<Button variant="primary" @click="triggerFileInput">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<PlusIcon class="w-4 h-4" />
|
<PlusIcon class="w-4 h-4" />
|
||||||
</template>
|
</template>
|
||||||
上传文档
|
上传文档
|
||||||
</TableButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 删除确认对话框 -->
|
<!-- 删除确认对话框 -->
|
||||||
<ConfirmationDialog :id="'doc-delete-modal'" :title="`确定删除文档 '${selectedDoc?.name || ''}' 吗?`"
|
<ConfirmationDialog :id="'doc-delete-modal'" :title="`确定删除文档 '${selectedDoc?.name || ''}' 吗?`"
|
||||||
content="删除后将无法恢复,且其中的所有分段内容也将被删除。" :closeModal="() => {
|
content="删除后将无法恢复,且其中的所有分段内容也将被删除。" :closeModal="() => {
|
||||||
docDeleteModal?.hide();
|
docDeleteModal?.hide();
|
||||||
}" :onSubmit="handleDocDeleteSubmit" />
|
}" :onSubmit="handleDocDeleteSubmit" />
|
||||||
|
|
||||||
@@ -61,11 +58,11 @@ import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
|||||||
import { onMounted, ref, watchEffect } from "vue";
|
import { onMounted, ref, watchEffect } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
import { KnowledgeDocCard, KnowledgeStatusBadge } from "@/components/common/knowledge";
|
import { KnowledgeDocCard } from "@/components/common/knowledge";
|
||||||
import { PlusIcon } from "@/components/icons";
|
import { PlusIcon } from "@/components/icons";
|
||||||
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
||||||
import TableButton from "@/components/tables/TableButton.vue";
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
||||||
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
||||||
|
|||||||
@@ -27,10 +27,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #actions-bottom>
|
<template #actions-bottom>
|
||||||
<button @click="navigateToLibraryDocs(library)"
|
<Button variant="primary" size="xs" @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>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</KnowledgeLibraryCard>
|
</KnowledgeLibraryCard>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,10 +38,9 @@
|
|||||||
<div v-else 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 class="text-gray-500 text-lg mb-4">暂无知识库</div>
|
||||||
<div>
|
<div>
|
||||||
<button @click="handleCreateLibraryClick"
|
<Button variant="primary" @click="handleCreateLibraryClick">
|
||||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 focus:outline-none">
|
|
||||||
创建知识库
|
创建知识库
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,6 +67,7 @@ import { KnowledgeLibraryCard } from "@/components/common/knowledge";
|
|||||||
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
||||||
import LibraryFormDialog from "@/components/modals/LibraryFormDialog.vue";
|
import LibraryFormDialog from "@/components/modals/LibraryFormDialog.vue";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
||||||
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
import { useKnowledgeUpsert } from "@/composables/knowledge/useKnowledgeUpsert";
|
||||||
|
|||||||
@@ -1,76 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||||
<Breadcrumbs :names="['知识库管理', '文档管理', '内容管理']"
|
<Breadcrumbs :names="['知识库管理', '文档管理', '文档分段']" :routes="[
|
||||||
:routes="[Routes.KNOWLEDGEVIEW.fullPath(), Routes.KNOWLEDGEDOCVIEW.withParams({ libraryId: libraryId })]" />
|
Routes.KNOWLEDGEVIEW.fullPath(),
|
||||||
<div class="mb-4">
|
Routes.KNOWLEDGEDOCVIEW.withParams({ libraryId })
|
||||||
<h1 class="text-2xl font-semibold text-gray-900 mb-2">{{ currentDoc?.name || '文档' }} - 分段内容</h1>
|
]" />
|
||||||
<p class="text-sm text-gray-500">
|
<div class="mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
共 {{ segments.length }} 个分段
|
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">{{ currentDoc?.name || '文档' }} - 分段内容</h1>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<div class="mb-4">
|
||||||
<div v-if="segments.length === 0" class="flex flex-col items-center justify-center py-10">
|
<Button variant="secondary" size="sm" @click="navigateBack">
|
||||||
<div class="text-gray-500 text-lg">暂无分段内容</div>
|
<template #icon>
|
||||||
|
<svg class="w-4 h-4" 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="M10 19l-7-7m0 0l7-7m-7 7h18">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
返回文档列表
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分段列表 -->
|
<!-- 分段列表 -->
|
||||||
<div v-else class="space-y-4">
|
<div v-if="segments.length > 0" class="space-y-4">
|
||||||
<SegmentCard v-for="(segment, index) in segments" :key="segment.id" :segment="segment" :index="index" />
|
<SegmentCard v-for="(segment, index) in segments" :key="segment.id" :segment="segment" :index="index" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="flex flex-col items-center justify-center py-10">
|
||||||
|
<div class="text-gray-500 text-lg mb-4">暂无分段内容</div>
|
||||||
|
<Button variant="secondary" @click="navigateBack">
|
||||||
|
返回文档列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watchEffect } from "vue";
|
import { onMounted, ref, watchEffect } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
import { SegmentCard } from "@/components/common/knowledge";
|
import { SegmentCard } from "@/components/common/knowledge";
|
||||||
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
import { useKnowledgeQuery } from "@/composables/knowledge/useKnowledgeQuery";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
|
|
||||||
import type { Library, LibraryDoc } from "@/types/KnowledgeTypes";
|
import type { LibraryDoc } from "@/types/KnowledgeTypes";
|
||||||
|
|
||||||
// 路由参数
|
// 路由参数
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const libraryId = ref<number>(
|
const router = useRouter();
|
||||||
Number.parseInt(route.params.libraryId as string, 10),
|
const libraryId = Number.parseInt(route.params.libraryId as string, 10);
|
||||||
);
|
const docId = Number.parseInt(route.params.docId as string, 10);
|
||||||
const docId = ref<number>(Number.parseInt(route.params.docId as string, 10));
|
|
||||||
|
|
||||||
// 获取知识库信息
|
// 获取文档信息和分段列表
|
||||||
const { libraries, fetchLibraries } = useKnowledgeQuery();
|
const { docs, segments, fetchLibraryDocs, fetchDocSegments } = useKnowledgeQuery();
|
||||||
const currentLibrary = ref<Library | undefined>();
|
|
||||||
|
|
||||||
// 获取文档信息
|
|
||||||
const { docs, fetchLibraryDocs } = useKnowledgeQuery();
|
|
||||||
const currentDoc = ref<LibraryDoc | undefined>();
|
const currentDoc = ref<LibraryDoc | undefined>();
|
||||||
|
|
||||||
// 获取分段列表
|
// 导航回文档列表
|
||||||
const { segments, fetchDocSegments } = useKnowledgeQuery();
|
const navigateBack = () => {
|
||||||
|
router.push(Routes.KNOWLEDGEDOCVIEW.withParams({ libraryId }));
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 获取知识库列表、文档列表和分段列表
|
await fetchLibraryDocs({ libraryId });
|
||||||
await fetchLibraries();
|
await fetchDocSegments({ libraryDocId: docId });
|
||||||
await fetchLibraryDocs({ libraryId: libraryId.value });
|
|
||||||
await fetchDocSegments({ libraryDocId: docId.value });
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听知识库列表变化,找到当前知识库
|
|
||||||
watchEffect(() => {
|
|
||||||
if (libraries.value && libraries.value.length > 0) {
|
|
||||||
currentLibrary.value = libraries.value.find(
|
|
||||||
(lib) => lib.id === libraryId.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听文档列表变化,找到当前文档
|
// 监听文档列表变化,找到当前文档
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (docs.value && docs.value.length > 0) {
|
if (docs.value && docs.value.length > 0) {
|
||||||
currentDoc.value = docs.value.find((doc) => doc.id === docId.value);
|
currentDoc.value = docs.value.find((doc) => doc.id === docId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user