162abdc97c
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports) - Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier - Add root scripts: format, format:check, lint, lint:fix - Reformat 257 files across admin-web, ops-web, desktop-client, packages - Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters - Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex - Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
4.5 KiB
Vue
181 lines
4.5 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>
|