181 lines
4.6 KiB
Vue
181 lines
4.6 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
|||
|
|
import { computed, watch } from "vue";
|
|||
|
|
|
|||
|
|
import MarkdownPreview from "@/components/MarkdownPreview.vue";
|
|||
|
|
import { knowledgeApi } from "@/lib/api";
|
|||
|
|
import { formatDateTime } from "@/lib/display";
|
|||
|
|
|
|||
|
|
const props = defineProps<{
|
|||
|
|
open: boolean;
|
|||
|
|
itemId: number | null;
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
close: [];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
const queryClient = useQueryClient();
|
|||
|
|
const enabled = computed(() => props.open && Boolean(props.itemId));
|
|||
|
|
|
|||
|
|
const detailQuery = useQuery({
|
|||
|
|
queryKey: computed(() => ["knowledge", "detail", props.itemId]),
|
|||
|
|
enabled,
|
|||
|
|
queryFn: () => knowledgeApi.detail(props.itemId as number),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const detail = computed(() => detailQuery.data.value);
|
|||
|
|
const hasMarkdown = computed(() => Boolean(detail.value?.markdown_content?.trim()));
|
|||
|
|
const isProcessing = computed(() =>
|
|||
|
|
detail.value?.status === "pending" || detail.value?.status === "processing",
|
|||
|
|
);
|
|||
|
|
const isFailed = computed(() => detail.value?.status === "failed");
|
|||
|
|
const statusLabel = computed(() => formatKnowledgeStatus(detail.value?.status));
|
|||
|
|
const statusColor = computed(() => {
|
|||
|
|
if (detail.value?.status === "completed") return "success";
|
|||
|
|
if (detail.value?.status === "failed") return "error";
|
|||
|
|
if (isProcessing.value) return "processing";
|
|||
|
|
return "default";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
[() => props.open, () => props.itemId, () => detail.value?.status],
|
|||
|
|
([open, itemId, status], _prev, onCleanup) => {
|
|||
|
|
if (!open || !itemId || (status !== "pending" && status !== "processing")) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const timer = window.setInterval(() => {
|
|||
|
|
void Promise.all([
|
|||
|
|
detailQuery.refetch(),
|
|||
|
|
queryClient.invalidateQueries({ queryKey: ["knowledge", "items"] }),
|
|||
|
|
]);
|
|||
|
|
}, 2500);
|
|||
|
|
|
|||
|
|
onCleanup(() => {
|
|||
|
|
window.clearInterval(timer);
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
{ immediate: true },
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
function handleClose(): void {
|
|||
|
|
emit("close");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatKnowledgeStatus(status?: string): string {
|
|||
|
|
switch (status) {
|
|||
|
|
case "completed":
|
|||
|
|
return "学习完成";
|
|||
|
|
case "processing":
|
|||
|
|
return "处理中";
|
|||
|
|
case "failed":
|
|||
|
|
return "解析失败";
|
|||
|
|
case "pending":
|
|||
|
|
return "待处理";
|
|||
|
|
case "deleted":
|
|||
|
|
return "已删除";
|
|||
|
|
default:
|
|||
|
|
return status || "未知状态";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<a-drawer
|
|||
|
|
:open="open"
|
|||
|
|
width="760"
|
|||
|
|
title="知识详情"
|
|||
|
|
class="knowledge-detail-drawer"
|
|||
|
|
@close="handleClose"
|
|||
|
|
>
|
|||
|
|
<div v-if="detailQuery.isPending.value" class="knowledge-detail-drawer__loading">
|
|||
|
|
<a-skeleton active :paragraph="{ rows: 10 }" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<template v-else-if="detail">
|
|||
|
|
<section class="knowledge-detail-drawer__hero">
|
|||
|
|
<div>
|
|||
|
|
<h2>{{ detail.name }}</h2>
|
|||
|
|
<div class="knowledge-detail-drawer__meta">
|
|||
|
|
<span>{{ detail.group_name }}</span>
|
|||
|
|
<span>{{ formatDateTime(detail.created_at) }}</span>
|
|||
|
|
<span v-if="detail.source_uri">{{ detail.source_uri }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<a-tag :color="statusColor">{{ statusLabel }}</a-tag>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<a-alert
|
|||
|
|
v-if="isProcessing"
|
|||
|
|
type="info"
|
|||
|
|
show-icon
|
|||
|
|
message="处理中"
|
|||
|
|
description="内容正在解析并整理为 Markdown,抽屉会自动刷新。"
|
|||
|
|
class="knowledge-detail-drawer__alert"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<a-alert
|
|||
|
|
v-else-if="isFailed"
|
|||
|
|
type="error"
|
|||
|
|
show-icon
|
|||
|
|
message="解析失败"
|
|||
|
|
:description="detail.error_message || '当前内容解析失败,请稍后重试。'"
|
|||
|
|
class="knowledge-detail-drawer__alert"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div v-if="hasMarkdown" class="knowledge-detail-drawer__content">
|
|||
|
|
<MarkdownPreview :content="detail.markdown_content || ''" />
|
|||
|
|
</div>
|
|||
|
|
<a-empty v-else description="无可预览文本" />
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<a-result
|
|||
|
|
v-else
|
|||
|
|
status="404"
|
|||
|
|
title="未找到知识内容"
|
|||
|
|
sub-title="该内容可能已删除,或当前租户无权访问。"
|
|||
|
|
/>
|
|||
|
|
</a-drawer>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.knowledge-detail-drawer__loading {
|
|||
|
|
padding-top: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.knowledge-detail-drawer__hero {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 16px;
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.knowledge-detail-drawer__hero h2 {
|
|||
|
|
margin: 0 0 8px;
|
|||
|
|
color: #111827;
|
|||
|
|
font-size: 22px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.knowledge-detail-drawer__meta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 8px 16px;
|
|||
|
|
color: #6b7280;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.knowledge-detail-drawer__alert {
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.knowledge-detail-drawer__content {
|
|||
|
|
padding: 20px 24px;
|
|||
|
|
border: 1px solid #f0f0f0;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
background: #fff;
|
|||
|
|
}
|
|||
|
|
</style>
|