feat: 新增风险规则生成引擎与知识图谱可视化
后端新增风险规则自动生成和模板执行服务,支持从规则资产 批量生成并持久化风险规则文件;知识库入库日志增强图谱 查询和本地 RAG 回退,前端审计页面增加风险规则模型和流 程图组件,知识入库面板拆分为图谱可视化子组件,报销创 建页面增加引导式流程模型,更新知识库索引数据。
This commit is contained in:
195
web/src/components/logs/KnowledgeIngestRunInfo.vue
Normal file
195
web/src/components/logs/KnowledgeIngestRunInfo.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<section class="ingest-run-info">
|
||||
<div class="info-title">
|
||||
<div>
|
||||
<span>基本信息</span>
|
||||
<h4>{{ model.folder || '未指定知识目录' }}</h4>
|
||||
</div>
|
||||
<strong :class="model.statusTone">{{ model.statusLabel }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div v-for="item in infoItems" :key="item.label" class="info-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
run: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const infoItems = computed(() => {
|
||||
const routeJson = props.run?.route_json || {}
|
||||
const currentDocument = props.model.documents?.find(
|
||||
(item) => item.documentId === props.model.currentDocumentId
|
||||
)
|
||||
return [
|
||||
{ label: 'Trace ID', value: props.run?.run_id || '-' },
|
||||
{ label: '任务类型', value: resolveJobType(routeJson.job_type) },
|
||||
{ label: '触发来源', value: resolveSource(props.run?.source) },
|
||||
{ label: '当前阶段', value: props.model.phaseLabel || '-' },
|
||||
{ label: '开始时间', value: formatDateTime(props.run?.started_at) },
|
||||
{ label: '结束时间', value: formatDateTime(props.run?.finished_at) },
|
||||
{ label: '执行耗时', value: formatElapsed(props.run?.started_at, props.run?.finished_at) },
|
||||
{ label: '当前文件', value: currentDocument?.name || '-' }
|
||||
]
|
||||
})
|
||||
|
||||
function resolveJobType(value) {
|
||||
const jobType = String(value || '').trim()
|
||||
if (jobType === 'knowledge_index_sync') return 'LightRAG 知识归纳'
|
||||
if (jobType === 'llm_wiki_sync') return 'LLM Wiki 知识归纳'
|
||||
return jobType || '-'
|
||||
}
|
||||
|
||||
function resolveSource(value) {
|
||||
const source = String(value || '').trim()
|
||||
if (source === 'manual') return '手动触发'
|
||||
if (source === 'scheduled') return '定时任务'
|
||||
if (source === 'system') return '系统任务'
|
||||
return source || '-'
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return String(value)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
|
||||
function formatElapsed(startedAt, finishedAt) {
|
||||
const started = new Date(startedAt || '')
|
||||
const finished = finishedAt ? new Date(finishedAt) : new Date()
|
||||
if (Number.isNaN(started.getTime()) || Number.isNaN(finished.getTime())) return '-'
|
||||
const seconds = Math.max(0, Math.round((finished.getTime() - started.getTime()) / 1000))
|
||||
if (seconds < 60) return `${seconds}s`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const restSeconds = seconds % 60
|
||||
if (minutes < 60) return `${minutes}m ${restSeconds}s`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
return `${hours}h ${minutes % 60}m`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ingest-run-info {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid #dbe6ef;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-title span {
|
||||
color: #0f766e;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.info-title h4 {
|
||||
margin: 4px 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-title > strong {
|
||||
align-self: flex-start;
|
||||
min-height: 26px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-title > strong.success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.info-title > strong.warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.info-title > strong.danger {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.info-title > strong.muted {
|
||||
background: #eef2f7;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-item strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.info-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.info-title {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user