1486 lines
40 KiB
Vue
1486 lines
40 KiB
Vue
<template>
|
||
<div class="rag-evaluation-container">
|
||
<!-- 顶部工具栏 -->
|
||
<div class="toolbar">
|
||
<div class="toolbar-left">
|
||
<!-- 基准选择 -->
|
||
<div class="benchmark-selector">
|
||
<label class="selector-label">评估基准</label>
|
||
<a-select
|
||
v-model:value="selectedBenchmarkId"
|
||
placeholder="请选择评估基准"
|
||
style="width: 240px"
|
||
@change="onBenchmarkChanged"
|
||
:loading="benchmarksLoading"
|
||
>
|
||
<a-select-option
|
||
v-for="benchmark in availableBenchmarks"
|
||
:key="benchmark.benchmark_id"
|
||
:value="benchmark.benchmark_id"
|
||
>
|
||
{{ benchmark.name }} ({{ benchmark.question_count }} 个问题)
|
||
</a-select-option>
|
||
</a-select>
|
||
<a-button
|
||
type="text"
|
||
size="middle"
|
||
:loading="benchmarksLoading"
|
||
@click="() => loadBenchmarks(true)"
|
||
:icon="h(ReloadOutlined)"
|
||
class="refresh-benchmarks-btn"
|
||
title="刷新评估基准列表"
|
||
/>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<!-- 检索配置按钮 -->
|
||
<a-button
|
||
size="middle"
|
||
@click="openSearchConfigModal"
|
||
:icon="h(SettingOutlined)"
|
||
/>
|
||
<!-- 开始评估按钮 -->
|
||
<a-button
|
||
type="primary"
|
||
:loading="startingEvaluation"
|
||
@click="startEvaluation"
|
||
:disabled="!selectedBenchmark"
|
||
size="middle"
|
||
>
|
||
开始评估
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 评估结果区域 -->
|
||
<div class="evaluation-results">
|
||
<!-- 模型配置(仅在选中基准且需要时显示) -->
|
||
<div v-if="selectedBenchmark && (selectedBenchmark.has_gold_chunks || selectedBenchmark.has_gold_answers)" class="model-config-section">
|
||
<a-row :gutter="24">
|
||
<a-col :span="12">
|
||
<a-form-item
|
||
:label="selectedBenchmark.has_gold_answers ? '答案生成模型' : '答案生成模型(当前基准无需)'"
|
||
>
|
||
<ModelSelectorComponent
|
||
v-model:model_spec="configForm.answer_llm"
|
||
size="small"
|
||
:disabled="!selectedBenchmark || !selectedBenchmark.has_gold_answers"
|
||
@select-model="(value) => configForm.answer_llm = value"
|
||
style="width: 100%"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
|
||
<a-col :span="12">
|
||
<a-form-item
|
||
:label="selectedBenchmark.has_gold_answers ? '答案评判模型' : '答案评判模型(当前基准无需)'"
|
||
>
|
||
<ModelSelectorComponent
|
||
v-model:model_spec="configForm.judge_llm"
|
||
size="small"
|
||
:disabled="!selectedBenchmark || !selectedBenchmark.has_gold_answers"
|
||
@select-model="(value) => configForm.judge_llm = value"
|
||
style="width: 100%"
|
||
/>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
|
||
<template v-if="!selectedBenchmark">
|
||
<div class="empty-state">
|
||
<a-empty description="请在顶部选择评估基准或前往基准管理">
|
||
<a-space>
|
||
<a-button @click="$emit('switch-to-benchmarks')">
|
||
前往基准管理
|
||
</a-button>
|
||
</a-space>
|
||
</a-empty>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
|
||
<!-- 历史评估记录 -->
|
||
<div class="history-section">
|
||
<div class="section-header">
|
||
<h4 class="section-title">历史评估记录</h4>
|
||
<a-button
|
||
type="text"
|
||
size="small"
|
||
:loading="refreshingHistory"
|
||
@click="refreshHistory"
|
||
:icon="h('ReloadOutlined')"
|
||
class="refresh-btn"
|
||
>
|
||
刷新
|
||
</a-button>
|
||
</div>
|
||
<a-table
|
||
class="history-table"
|
||
:columns="historyColumns"
|
||
:data-source="evaluationHistory"
|
||
:pagination="{ pageSize: 10 }"
|
||
size="small"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'status'">
|
||
<a-tag :color="getStatusColor(record.status)">
|
||
{{ getStatusText(record.status) }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-else-if="column.key === 'overall_score'">
|
||
<span v-if="record.overall_score !== null">
|
||
<a-tag :color="getScoreTagColor(record.overall_score)">
|
||
{{ (record.overall_score * 100).toFixed(0) }}%
|
||
</a-tag>
|
||
</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'actions'">
|
||
<a-space>
|
||
<a-button
|
||
v-if="record.status === 'completed'"
|
||
type="link"
|
||
size="small"
|
||
@click="viewResults(record.task_id)"
|
||
>
|
||
查看结果
|
||
</a-button>
|
||
<a-popconfirm
|
||
title="确定要删除这条评估记录吗?"
|
||
description="删除后将无法恢复"
|
||
@confirm="deleteEvaluationRecord(record.task_id)"
|
||
ok-text="确定"
|
||
cancel-text="取消"
|
||
>
|
||
<a-button
|
||
type="link"
|
||
size="small"
|
||
danger
|
||
:loading="record.deleting"
|
||
>
|
||
删除
|
||
</a-button>
|
||
</a-popconfirm>
|
||
</a-space>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 评估结果详情模态框 -->
|
||
<a-modal
|
||
v-model:open="resultModalVisible"
|
||
:title="`评估结果 - ${selectedResult?.task_id?.slice(0, 8) || ''}`"
|
||
width="1200px"
|
||
:footer="null"
|
||
>
|
||
<div v-if="resultsLoading" class="loading-container">
|
||
<a-spin size="large" />
|
||
<p style="margin-top: 16px; color: var(--gray-600)">正在加载评估结果...</p>
|
||
</div>
|
||
|
||
<div v-else-if="selectedResult && detailedResults.length > 0">
|
||
<!-- 基本信息 -->
|
||
<a-descriptions title="基本信息" :column="3" size="small" bordered style="margin-bottom: 20px">
|
||
<a-descriptions-item label="任务ID">{{ selectedResult.task_id }}</a-descriptions-item>
|
||
<a-descriptions-item label="状态">
|
||
<a-tag :color="getStatusColor(selectedResult.status)">
|
||
{{ getStatusText(selectedResult.status) }}
|
||
</a-tag>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="总体评分">
|
||
<span v-if="selectedResult.overall_score !== null">
|
||
<a-tag :color="getScoreTagColor(selectedResult.overall_score)">
|
||
{{ (selectedResult.overall_score * 100).toFixed(1) }}%
|
||
</a-tag>
|
||
</span>
|
||
<span v-else>-</span>
|
||
</a-descriptions-item>
|
||
<a-descriptions-item label="总问题数">{{ selectedResult.total_questions }}</a-descriptions-item>
|
||
<a-descriptions-item label="完成数">{{ selectedResult.completed_questions }}</a-descriptions-item>
|
||
<a-descriptions-item label="总耗时">
|
||
<span v-if="evaluationStats.totalDuration">
|
||
{{ formatDuration(evaluationStats.totalDuration) }}
|
||
</span>
|
||
<span v-else>-</span>
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
|
||
<!-- 检索配置和整体评估报告 -->
|
||
<a-row :gutter="16" style="margin-bottom: 20px">
|
||
<!-- 检索配置 -->
|
||
<a-col :span="12" v-if="selectedResult.retrieval_config">
|
||
<a-card size="small" title="检索配置">
|
||
<div class="json-viewer-container">
|
||
<pre class="json-viewer">{{ JSON.stringify(selectedResult.retrieval_config, null, 2) }}</pre>
|
||
</div>
|
||
</a-card>
|
||
</a-col>
|
||
|
||
<!-- 整体评估报告 -->
|
||
<a-col :span="selectedResult.retrieval_config ? 12 : 24">
|
||
<a-card size="small" title="整体评估报告">
|
||
<!-- 检索指标 -->
|
||
<div style="margin-bottom: 20px;">
|
||
<h5 style="margin-bottom: 12px; font-size: 14px; font-weight: 500;">检索指标</h5>
|
||
<div v-if="Object.keys(evaluationStats.retrievalMetrics || {}).length > 0">
|
||
<div v-for="(value, key) in evaluationStats.retrievalMetrics" :key="key" class="report-metric">
|
||
<span class="metric-label">{{ getMetricTitle(key) }}:</span>
|
||
<span class="metric-value" :style="{ color: getScoreColor(value) }">
|
||
{{ formatMetricValue(value) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<span v-else class="no-metrics">-</span>
|
||
</div>
|
||
|
||
<!-- 答案准确性 -->
|
||
<div>
|
||
<h5 style="margin-bottom: 12px; font-size: 14px; font-weight: 500;">答案准确性</h5>
|
||
<div class="accuracy-stats">
|
||
<div class="accuracy-item">
|
||
<span class="accuracy-label">正确答案数:</span>
|
||
<span class="accuracy-value">{{ evaluationStats.correctAnswers || 0 }} / {{ evaluationStats.totalQuestions || 0 }}</span>
|
||
</div>
|
||
<div class="accuracy-item">
|
||
<span class="accuracy-label">准确率:</span>
|
||
<span class="accuracy-value" :style="{ color: getScoreColor(evaluationStats.answerAccuracy) }">
|
||
{{ (evaluationStats.answerAccuracy * 100).toFixed(1) }}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- 详细结果表格 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||
<div>
|
||
<h4 style="margin: 0;">详细评估结果</h4>
|
||
<span style="font-size: 12px; color: var(--gray-600); margin-left: 8px;">
|
||
{{ showErrorsOnly ? `仅显示错误结果(共 ${paginationTotal} 条)` : `显示全部结果(共 ${paginationTotal} 条)` }}
|
||
</span>
|
||
</div>
|
||
<a-button
|
||
type="default"
|
||
size="small"
|
||
@click="toggleErrorOnly"
|
||
:class="{ 'error-only-active': showErrorsOnly }"
|
||
>
|
||
{{ showErrorsOnly ? '显示全部' : '仅查看错误' }}
|
||
</a-button>
|
||
</div>
|
||
<a-table
|
||
:columns="resultColumns"
|
||
:data-source="detailedResults"
|
||
:pagination="{
|
||
current: currentPage,
|
||
pageSize: pageSize,
|
||
total: paginationTotal,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||
onChange: handlePageChange,
|
||
onShowSizeChange: handlePageSizeChange
|
||
}"
|
||
:scroll="{ x: 1000 }"
|
||
size="small"
|
||
:loading="resultsLoading"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'query'">
|
||
<a-tooltip :title="record.query">
|
||
<div class="query-text">{{ record.query }}</div>
|
||
</a-tooltip>
|
||
</template>
|
||
<template v-else-if="column.key === 'generated_answer'">
|
||
<a-tooltip :title="record.generated_answer">
|
||
<div class="answer-text">{{ record.generated_answer || '-' }}</div>
|
||
</a-tooltip>
|
||
</template>
|
||
<template v-else-if="column.key === 'retrieval_score'">
|
||
<div v-if="record.metrics && Object.keys(record.metrics).some(k => k.startsWith('recall') || k.startsWith('precision') || k === 'map' || k === 'ndcg')" class="retrieval-metrics">
|
||
<div v-for="(val, key) in record.metrics" :key="key" class="metric-item">
|
||
<span v-if="key.startsWith('recall') || key.startsWith('precision') || key === 'map' || key === 'ndcg'"
|
||
class="metric-content"
|
||
:class="`metric-${getMetricType(key)}`">
|
||
<span class="metric-name">{{ getMetricShortName(key) }}</span>
|
||
<span class="metric-value">{{ formatMetricValue(val) }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<span v-else class="no-metrics">-</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'answer_score'">
|
||
<div v-if="record.metrics && record.metrics.score !== undefined">
|
||
<a-tag :color="record.metrics.score > 0.5 ? 'green' : 'red'">
|
||
{{ record.metrics.score === 1.0 ? '正确' : '错误' }}
|
||
</a-tag>
|
||
<div v-if="record.metrics.reasoning" class="answer-reasoning">
|
||
<a-tooltip :title="record.metrics.reasoning">
|
||
{{ record.metrics.reasoning }}
|
||
</a-tooltip>
|
||
</div>
|
||
</div>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
|
||
<div v-else-if="selectedResult" class="empty-results">
|
||
<a-empty description="暂无详细结果数据">
|
||
<a-button @click="viewDetails(selectedResult)">查看基本信息</a-button>
|
||
</a-empty>
|
||
</div>
|
||
</a-modal>
|
||
|
||
<!-- 检索配置弹窗 -->
|
||
<SearchConfigModal
|
||
v-model="searchConfigModalVisible"
|
||
:database-id="databaseId"
|
||
@save="handleSearchConfigSave"
|
||
/>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, computed, h } from 'vue';
|
||
import { message } from 'ant-design-vue';
|
||
import { evaluationApi } from '@/apis/knowledge_api';
|
||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
|
||
import SearchConfigModal from './SearchConfigModal.vue';
|
||
import { SettingOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||
import { useTaskerStore } from '@/stores/tasker';
|
||
|
||
const props = defineProps({
|
||
databaseId: {
|
||
type: String,
|
||
required: true
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['switch-to-benchmarks']);
|
||
|
||
// 使用任务中心 store
|
||
const taskerStore = useTaskerStore();
|
||
|
||
// 状态
|
||
const selectedBenchmarkId = ref(null);
|
||
const selectedBenchmark = ref(null);
|
||
const benchmarksLoading = ref(false);
|
||
const availableBenchmarks = ref([]);
|
||
const startingEvaluation = ref(false);
|
||
const evaluationHistory = ref([]);
|
||
const resultModalVisible = ref(false);
|
||
const selectedResult = ref(null);
|
||
const detailedResults = ref([]);
|
||
const evaluationStats = ref({});
|
||
const resultsLoading = ref(false);
|
||
const searchConfigModalVisible = ref(false);
|
||
const refreshingHistory = ref(false);
|
||
const showErrorsOnly = ref(false);
|
||
const currentPage = ref(1);
|
||
const pageSize = ref(20);
|
||
const paginationTotal = ref(0);
|
||
const paginationTotalPages = ref(0);
|
||
|
||
|
||
// 评估配置表单(使用知识库默认配置)
|
||
const configForm = reactive({
|
||
answer_llm: '', // 答案生成模型
|
||
judge_llm: '', // 评判模型
|
||
});
|
||
|
||
// 表格列定义
|
||
const resultColumns = computed(() => {
|
||
const columns = [
|
||
{
|
||
title: '问题',
|
||
dataIndex: 'query',
|
||
key: 'query',
|
||
width: 100
|
||
},
|
||
{
|
||
title: '生成答案',
|
||
key: 'generated_answer',
|
||
width: 180
|
||
},
|
||
{
|
||
title: '答案评判',
|
||
key: 'answer_score',
|
||
width: 260
|
||
}
|
||
];
|
||
|
||
// 检查是否有检索指标数据
|
||
const hasRetrievalMetrics = detailedResults.value.some(item => {
|
||
if (!item.metrics) return false;
|
||
return Object.keys(item.metrics).some(key =>
|
||
key.startsWith('recall') ||
|
||
key.startsWith('precision') ||
|
||
key === 'map' ||
|
||
key === 'ndcg'
|
||
);
|
||
});
|
||
|
||
// 如果有检索指标数据,添加检索指标列
|
||
if (hasRetrievalMetrics) {
|
||
columns.splice(2, 0, {
|
||
title: '检索指标',
|
||
key: 'retrieval_score',
|
||
width: 100
|
||
});
|
||
}
|
||
|
||
return columns;
|
||
});
|
||
|
||
const historyColumns = [
|
||
{
|
||
title: '开始时间',
|
||
dataIndex: 'started_at',
|
||
key: 'started_at',
|
||
width: 180,
|
||
customRender: ({ record }) => formatTime(record.started_at)
|
||
},
|
||
{
|
||
title: '评估基准',
|
||
key: 'benchmark_name',
|
||
width: 200,
|
||
customRender: ({ record }) => {
|
||
// 根据 benchmark_id 查找基准名称
|
||
const benchmark = availableBenchmarks.value.find(b => b.benchmark_id === record.benchmark_id);
|
||
return benchmark ? benchmark.name : record.benchmark_id?.slice(0, 8) || '-';
|
||
}
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
width: 100
|
||
},
|
||
{
|
||
title: 'Recall@10',
|
||
key: 'recall_10',
|
||
width: 100,
|
||
customRender: ({ record }) => {
|
||
// 使用后端返回的 metrics.recall@10 数据
|
||
if (record.metrics && record.metrics['recall@10'] !== undefined && record.metrics['recall@10'] !== null) {
|
||
const recallValue = record.metrics['recall@10'];
|
||
const displayValue = formatMetricValue(recallValue);
|
||
return h('a-tag', {
|
||
color: getScoreTagColor(recallValue)
|
||
}, displayValue);
|
||
}
|
||
|
||
// 如果是运行中的任务,显示计算中
|
||
if (record.status === 'running') {
|
||
return h('a-tag', {
|
||
color: 'processing'
|
||
}, '计算中');
|
||
}
|
||
|
||
// 已完成但没有 recall@10 数据
|
||
if (record.status === 'completed') {
|
||
return h('a-tag', {
|
||
color: 'default'
|
||
}, '无数据');
|
||
}
|
||
|
||
// 其他情况显示横线
|
||
return h('span', '-');
|
||
}
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 150
|
||
}
|
||
];
|
||
|
||
// 计算属性:当前选中的基准对象
|
||
const currentBenchmark = computed(() => {
|
||
if (!selectedBenchmarkId.value || !availableBenchmarks.value) {
|
||
return null;
|
||
}
|
||
return availableBenchmarks.value.find(b => b.benchmark_id === selectedBenchmarkId.value);
|
||
});
|
||
|
||
// 切换错误显示模式
|
||
const toggleErrorOnly = async () => {
|
||
resultsLoading.value = true;
|
||
showErrorsOnly.value = !showErrorsOnly.value;
|
||
currentPage.value = 1; // 切换模式时重置到第一页
|
||
|
||
// 立即加载新的分页数据
|
||
await loadResultsWithPagination();
|
||
};
|
||
|
||
// 处理分页变化
|
||
const handlePageChange = (page, size) => {
|
||
currentPage.value = page;
|
||
if (size !== pageSize.value) {
|
||
pageSize.value = size;
|
||
}
|
||
loadResultsWithPagination();
|
||
};
|
||
|
||
// 处理页面大小变化
|
||
const handlePageSizeChange = (current, size) => {
|
||
currentPage.value = 1;
|
||
pageSize.value = size;
|
||
loadResultsWithPagination();
|
||
};
|
||
|
||
// 加载分页结果
|
||
const loadResultsWithPagination = async () => {
|
||
if (!selectedResult.value) return;
|
||
|
||
try {
|
||
resultsLoading.value = true;
|
||
const response = await evaluationApi.getEvaluationResultsByDb(
|
||
props.databaseId,
|
||
selectedResult.value.task_id,
|
||
{
|
||
page: currentPage.value,
|
||
pageSize: pageSize.value,
|
||
errorOnly: showErrorsOnly.value
|
||
}
|
||
);
|
||
|
||
if (response.message === 'success' && response.data) {
|
||
const resultData = response.data;
|
||
|
||
// 更新详细结果
|
||
detailedResults.value = resultData.interim_results || [];
|
||
|
||
// 更新分页信息
|
||
if (resultData.pagination) {
|
||
paginationTotal.value = resultData.pagination.total;
|
||
paginationTotalPages.value = resultData.pagination.total_pages;
|
||
} else {
|
||
// 兼容旧格式数据
|
||
paginationTotal.value = detailedResults.value.length;
|
||
paginationTotalPages.value = 1;
|
||
}
|
||
|
||
// 更新统计信息
|
||
// 如果是过滤模式,需要基于过滤后的总数计算统计
|
||
if (showErrorsOnly.value) {
|
||
// 在过滤模式下,只计算当前页的统计(避免重复计算)
|
||
evaluationStats.value = {
|
||
...evaluationStats.value,
|
||
totalQuestions: paginationTotal.value,
|
||
// 可以在这里添加其他基于过滤后数据的统计
|
||
};
|
||
} else if (currentPage.value === 1) {
|
||
// 非过滤模式且是第一页时,才计算完整统计
|
||
evaluationStats.value = calculateEvaluationStats(detailedResults.value);
|
||
}
|
||
|
||
// 更新其他基本信息(保持原有的信息不变)
|
||
if (resultData.started_at && resultData.completed_at) {
|
||
const startTime = new Date(resultData.started_at);
|
||
const endTime = new Date(resultData.completed_at);
|
||
evaluationStats.value.totalDuration = (endTime - startTime) / 1000;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载评估结果失败:', error);
|
||
message.error('加载评估结果失败');
|
||
} finally {
|
||
resultsLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 打开检索配置弹窗
|
||
const openSearchConfigModal = () => {
|
||
searchConfigModalVisible.value = true;
|
||
};
|
||
|
||
// 处理检索配置保存
|
||
const handleSearchConfigSave = (config) => {
|
||
console.log('RAG评估中的检索配置已更新:', config);
|
||
// 可以在这里添加配置更新后的处理逻辑
|
||
};
|
||
|
||
// 加载基准列表
|
||
const loadBenchmarks = async (showSuccessMessage = false) => {
|
||
if (!props.databaseId) return;
|
||
|
||
benchmarksLoading.value = true;
|
||
try {
|
||
const response = await evaluationApi.getBenchmarks(props.databaseId);
|
||
|
||
if (response && response.message === 'success' && Array.isArray(response.data)) {
|
||
availableBenchmarks.value = response.data;
|
||
|
||
// 如果没有选中的基准,且有可用基准,默认选中第一个
|
||
if (!selectedBenchmarkId.value && response.data.length > 0) {
|
||
selectedBenchmarkId.value = response.data[0].benchmark_id;
|
||
selectedBenchmark.value = response.data[0];
|
||
} else if (selectedBenchmarkId.value) {
|
||
// 如果之前有选中的基准,重新验证其有效性
|
||
const exists = response.data.some(b => b.benchmark_id === selectedBenchmarkId.value);
|
||
if (!exists) {
|
||
selectedBenchmarkId.value = null;
|
||
selectedBenchmark.value = null;
|
||
} else {
|
||
// 更新选中的基准对象
|
||
selectedBenchmark.value = response.data.find(b => b.benchmark_id === selectedBenchmarkId.value);
|
||
}
|
||
}
|
||
|
||
// 如果是手动刷新,显示成功提示
|
||
if (showSuccessMessage) {
|
||
message.success(`已刷新,找到 ${response.data.length} 个评估基准`);
|
||
}
|
||
} else {
|
||
console.error('响应格式不符合预期:', response);
|
||
message.error('基准数据格式错误');
|
||
}
|
||
} catch (error) {
|
||
console.error('加载评估基准失败:', error);
|
||
message.error('加载评估基准失败');
|
||
} finally {
|
||
benchmarksLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 下拉框选择变化
|
||
const onBenchmarkChanged = (benchmarkId) => {
|
||
const benchmark = availableBenchmarks.value.find(b => b.benchmark_id === benchmarkId);
|
||
selectedBenchmark.value = benchmark || null;
|
||
};
|
||
|
||
// 刷新历史评估记录
|
||
const refreshHistory = async () => {
|
||
refreshingHistory.value = true;
|
||
try {
|
||
await loadEvaluationHistory();
|
||
message.success('历史记录已刷新');
|
||
} catch (error) {
|
||
console.error('刷新历史记录失败:', error);
|
||
message.error('刷新历史记录失败');
|
||
} finally {
|
||
refreshingHistory.value = false;
|
||
}
|
||
};
|
||
|
||
|
||
// 开始评估
|
||
const startEvaluation = async () => {
|
||
if (!selectedBenchmark.value) {
|
||
message.error('请先选择评估基准');
|
||
return;
|
||
}
|
||
|
||
startingEvaluation.value = true;
|
||
|
||
// 只传递模型配置,检索配置由服务器从知识库读取
|
||
const params = {
|
||
benchmark_id: selectedBenchmark.value.benchmark_id,
|
||
model_config: {
|
||
answer_llm: configForm.answer_llm, // 传递答案生成模型
|
||
judge_llm: configForm.judge_llm // 传递评判模型
|
||
}
|
||
};
|
||
|
||
try {
|
||
const response = await evaluationApi.runEvaluation(props.databaseId, params);
|
||
|
||
if (response.message === 'success') {
|
||
message.success('评估任务已开始');
|
||
loadEvaluationHistory();
|
||
// 刷新任务中心的任务列表
|
||
taskerStore.loadTasks();
|
||
} else {
|
||
message.error(response.message || '启动评估失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('启动评估失败:', error);
|
||
message.error('启动评估失败');
|
||
} finally {
|
||
startingEvaluation.value = false;
|
||
}
|
||
};
|
||
|
||
|
||
// 加载评估历史
|
||
const loadEvaluationHistory = async () => {
|
||
try {
|
||
const response = await evaluationApi.getEvaluationHistory(props.databaseId);
|
||
if (response.message === 'success') {
|
||
evaluationHistory.value = response.data || [];
|
||
}
|
||
} catch (error) {
|
||
console.error('加载评估历史失败:', error);
|
||
message.error('加载评估历史失败');
|
||
}
|
||
};
|
||
|
||
|
||
// 计算评估统计信息
|
||
const calculateEvaluationStats = (results) => {
|
||
if (!results || results.length === 0) {
|
||
return {};
|
||
}
|
||
|
||
const stats = {
|
||
totalQuestions: results.length,
|
||
retrievalMetrics: {},
|
||
answerAccuracy: 0,
|
||
correctAnswers: 0,
|
||
averageResponseTime: 0,
|
||
totalResponseTime: 0
|
||
};
|
||
|
||
const metricSums = {};
|
||
const metricCounts = {};
|
||
|
||
results.forEach(item => {
|
||
// 答案准确率
|
||
if (item.metrics && item.metrics.score !== undefined) {
|
||
if (item.metrics.score > 0.5) {
|
||
stats.correctAnswers++;
|
||
}
|
||
}
|
||
|
||
// 检索指标统计
|
||
if (item.metrics) {
|
||
Object.keys(item.metrics).forEach(key => {
|
||
if (key.startsWith('recall') || key.startsWith('precision') || key === 'map' || key === 'ndcg') {
|
||
if (!metricSums[key]) {
|
||
metricSums[key] = 0;
|
||
metricCounts[key] = 0;
|
||
}
|
||
metricSums[key] += item.metrics[key];
|
||
metricCounts[key]++;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 计算平均值
|
||
Object.keys(metricSums).forEach(key => {
|
||
stats.retrievalMetrics[key] = metricSums[key] / metricCounts[key];
|
||
});
|
||
|
||
// 计算答案准确率
|
||
stats.answerAccuracy = stats.totalQuestions > 0 ? (stats.correctAnswers / stats.totalQuestions) : 0;
|
||
|
||
return stats;
|
||
};
|
||
|
||
// 查看结果
|
||
const viewResults = async (taskId) => {
|
||
try {
|
||
resultsLoading.value = true;
|
||
|
||
// 重置分页状态
|
||
currentPage.value = 1;
|
||
showErrorsOnly.value = false;
|
||
|
||
// 先获取基本信息(不分页)
|
||
const response = await evaluationApi.getEvaluationResultsByDb(props.databaseId, taskId);
|
||
|
||
if (response.message === 'success' && response.data) {
|
||
const resultData = response.data;
|
||
|
||
// 从历史记录中找到对应的任务信息,如果没有则使用API返回的数据
|
||
selectedResult.value = evaluationHistory.value.find(r => r.task_id === taskId) || {
|
||
task_id: resultData.task_id,
|
||
status: resultData.status,
|
||
started_at: resultData.started_at,
|
||
completed_at: resultData.completed_at,
|
||
total_questions: resultData.total_questions || 0,
|
||
completed_questions: resultData.completed_questions || 0,
|
||
overall_score: resultData.overall_score,
|
||
retrieval_config: resultData.retrieval_config
|
||
};
|
||
|
||
// 如果是从历史记录获取的,确保也有 retrieval_config
|
||
if (selectedResult.value && !selectedResult.value.retrieval_config) {
|
||
selectedResult.value.retrieval_config = resultData.retrieval_config;
|
||
}
|
||
|
||
// 打开模态框
|
||
resultModalVisible.value = true;
|
||
|
||
// 加载分页数据
|
||
await loadResultsWithPagination();
|
||
} else {
|
||
message.error('获取评估结果失败:数据格式错误');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取评估结果失败:', error);
|
||
message.error('获取评估结果失败');
|
||
} finally {
|
||
resultsLoading.value = false;
|
||
}
|
||
};
|
||
|
||
|
||
// 删除评估记录
|
||
const deleteEvaluationRecord = async (taskId) => {
|
||
try {
|
||
// 找到对应的记录并设置loading状态
|
||
const record = evaluationHistory.value.find(r => r.task_id === taskId);
|
||
if (record) {
|
||
record.deleting = true;
|
||
}
|
||
|
||
const response = await evaluationApi.deleteEvaluationResultByDb(props.databaseId, taskId);
|
||
if (response.message === 'success') {
|
||
message.success('删除成功');
|
||
// 重新加载评估历史
|
||
await loadEvaluationHistory();
|
||
}
|
||
} catch (error) {
|
||
console.error('删除评估记录失败:', error);
|
||
message.error('删除评估记录失败');
|
||
} finally {
|
||
// 清除loading状态
|
||
const record = evaluationHistory.value.find(r => r.task_id === taskId);
|
||
if (record) {
|
||
record.deleting = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 工具函数
|
||
const truncateText = (text, maxLength) => {
|
||
if (!text) return '';
|
||
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
|
||
};
|
||
|
||
const formatTime = (timeStr) => {
|
||
if (!timeStr) return '-';
|
||
const date = new Date(timeStr);
|
||
return date.toLocaleString('zh-CN');
|
||
};
|
||
|
||
const getScoreColor = (score) => {
|
||
if (score >= 0.8) return 'var(--color-success-500)';
|
||
if (score >= 0.6) return 'var(--color-warning-500)';
|
||
return 'var(--color-error-500)';
|
||
};
|
||
|
||
const getScoreTagColor = (score) => {
|
||
if (score >= 0.8) return 'success';
|
||
if (score >= 0.6) return 'warning';
|
||
return 'error';
|
||
};
|
||
|
||
const getStatusColor = (status) => {
|
||
const colors = {
|
||
running: 'blue',
|
||
completed: 'green',
|
||
failed: 'red',
|
||
paused: 'orange'
|
||
};
|
||
return colors[status] || 'default';
|
||
};
|
||
|
||
const getStatusText = (status) => {
|
||
const texts = {
|
||
running: '运行中',
|
||
completed: '已完成',
|
||
failed: '失败',
|
||
paused: '已暂停'
|
||
};
|
||
return texts[status] || status;
|
||
};
|
||
|
||
const getMetricTitle = (key) => {
|
||
const titles = {
|
||
precision: '精确率',
|
||
recall: '召回率',
|
||
map: '平均精度',
|
||
ndcg: 'NDCG',
|
||
bleu: 'BLEU分数',
|
||
rouge: 'ROUGE分数',
|
||
answer_correctness: '答案准确性',
|
||
score: '评分',
|
||
reasoning: '理由',
|
||
overall_score: '综合评分'
|
||
};
|
||
// 处理 recall@k
|
||
if (key.startsWith('recall@')) return `召回率 (${key.split('@')[1]})`;
|
||
if (key.startsWith('precision@')) return `精确率 (${key.split('@')[1]})`;
|
||
|
||
return titles[key] || key;
|
||
};
|
||
|
||
// 获取指标类型
|
||
const getMetricType = (key) => {
|
||
if (key.startsWith('recall')) return 'recall';
|
||
if (key.startsWith('precision')) return 'precision';
|
||
if (key === 'map') return 'map';
|
||
if (key === 'ndcg') return 'ndcg';
|
||
return 'default';
|
||
};
|
||
|
||
// 获取指标短名称
|
||
const getMetricShortName = (key) => {
|
||
if (key.startsWith('recall@')) return `R@${key.split('@')[1]}`;
|
||
if (key.startsWith('precision@')) return `P@${key.split('@')[1]}`;
|
||
if (key === 'precision') return 'Precision';
|
||
if (key === 'recall') return 'Recall';
|
||
if (key === 'map') return 'MAP';
|
||
if (key === 'ndcg') return 'NDCG';
|
||
return key;
|
||
};
|
||
|
||
// 格式化指标值
|
||
const formatMetricValue = (val) => {
|
||
if (typeof val !== 'number') return '-';
|
||
// 检索指标(recall, precision, f1 等)范围是 0.0-1.0,统一转换为百分比
|
||
if (val <= 1) return (val * 100).toFixed(1) + '%';
|
||
return val.toFixed(3);
|
||
};
|
||
|
||
// 格式化持续时间
|
||
const formatDuration = (seconds) => {
|
||
if (seconds < 60) {
|
||
return `${Math.round(seconds)}秒`;
|
||
} else if (seconds < 3600) {
|
||
const minutes = Math.floor(seconds / 60);
|
||
const remainingSeconds = Math.round(seconds % 60);
|
||
return `${minutes}分${remainingSeconds}秒`;
|
||
} else {
|
||
const hours = Math.floor(seconds / 3600);
|
||
const minutes = Math.floor((seconds % 3600) / 60);
|
||
return `${hours}小时${minutes}分`;
|
||
}
|
||
};
|
||
|
||
|
||
// 组件挂载时加载数据
|
||
onMounted(() => {
|
||
loadBenchmarks();
|
||
loadEvaluationHistory();
|
||
});
|
||
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.rag-evaluation-container {
|
||
height: 100%;
|
||
background: var(--gray-0);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
// 顶部工具栏
|
||
.toolbar {
|
||
padding: 12px 16px;
|
||
background: var(--gray-0);
|
||
border-bottom: 1px solid var(--gray-200);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
|
||
.toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.benchmark-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.selector-label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--gray-700);
|
||
margin: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.refresh-benchmarks-btn {
|
||
color: var(--gray-600);
|
||
}
|
||
}
|
||
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
}
|
||
|
||
// 评估内容区域
|
||
.evaluation-content {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
padding: 10px 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
|
||
// 评估结果区域
|
||
.evaluation-results {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
overflow: auto;
|
||
padding: 16px;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background-color: rgba(0, 0, 0, 0.2);
|
||
border-radius: 3px;
|
||
|
||
&:hover {
|
||
background-color: rgba(0, 0, 0, 0.3);
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--gray-0);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--gray-200);
|
||
}
|
||
|
||
.progress-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
|
||
.progress-stats {
|
||
flex: 1;
|
||
margin-right: 24px;
|
||
min-width: 300px;
|
||
|
||
.ant-statistic {
|
||
margin-bottom: 12px;
|
||
|
||
.ant-statistic-title {
|
||
font-size: 13px;
|
||
color: var(--gray-600);
|
||
}
|
||
|
||
.ant-statistic-content {
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.progress-actions {
|
||
flex-shrink: 0;
|
||
padding-top: 24px;
|
||
}
|
||
}
|
||
|
||
.query-text {
|
||
font-size: 12px;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 4;
|
||
line-height: 1.5;
|
||
word-wrap: break-word;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.answer-text {
|
||
font-size: 12px;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 4;
|
||
line-height: 1.5;
|
||
word-wrap: break-word;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.log-time {
|
||
color: var(--gray-500);
|
||
margin-left: 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
|
||
// 优化表格样式
|
||
:deep(.ant-table) {
|
||
.ant-table-tbody > tr > td {
|
||
padding: 12px 12px;
|
||
vertical-align: top;
|
||
}
|
||
|
||
.ant-table-thead > tr > th {
|
||
padding: 8px 12px;
|
||
font-weight: 500;
|
||
background-color: var(--gray-50);
|
||
}
|
||
}
|
||
|
||
// 优化卡片间距
|
||
:deep(.ant-card) {
|
||
.ant-card-head {
|
||
padding: 8px 16px;
|
||
min-height: 40px;
|
||
|
||
.ant-card-head-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
padding: 4px 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 优化时间线样式
|
||
:deep(.ant-timeline) {
|
||
.ant-timeline-item-content {
|
||
margin-left: 20px;
|
||
padding-bottom: 12px;
|
||
}
|
||
}
|
||
|
||
// 优化描述列表样式
|
||
:deep(.ant-descriptions) {
|
||
.ant-descriptions-item-label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--gray-600);
|
||
}
|
||
|
||
.ant-descriptions-item-content {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
// 优化表单项间距
|
||
:deep(.ant-form) {
|
||
.ant-form-item {
|
||
margin-bottom: 16px;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-form-inline) {
|
||
.ant-form-item {
|
||
margin-right: 24px;
|
||
margin-bottom: 16px;
|
||
|
||
&:last-child {
|
||
margin-right: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 优化模型配置表单项
|
||
.model-config-section {
|
||
padding: 6px 8px;
|
||
background: var(--gray-10);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--gray-150);
|
||
|
||
.ant-form-item {
|
||
margin-bottom: 0;
|
||
|
||
.ant-form-item-label {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.ant-form-item-extra {
|
||
font-size: 12px;
|
||
color: var(--gray-500);
|
||
margin-top: 4px;
|
||
}
|
||
}
|
||
|
||
// 为模型配置部分内的列添加特定样式
|
||
.ant-col {
|
||
&:not(:last-child) .ant-form-item {
|
||
padding-right: 12px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 优化统计数字样式
|
||
:deep(.ant-row) {
|
||
.ant-col {
|
||
.ant-statistic {
|
||
padding: 12px;
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
border-color: var(--gray-300);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检索指标样式
|
||
.retrieval-metrics {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
min-height: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.metric-item {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.metric-content {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
|
||
&.metric-recall {
|
||
background-color: var(--color-info-50);
|
||
color: var(--color-info-900);
|
||
}
|
||
|
||
&.metric-precision {
|
||
background-color: var(--color-success-50);
|
||
color: var(--color-success-900);
|
||
}
|
||
|
||
|
||
&.metric-map,
|
||
&.metric-ndcg {
|
||
background-color: var(--color-accent-50);
|
||
color: var(--color-accent-900);
|
||
}
|
||
}
|
||
|
||
.metric-name {
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.4px;
|
||
}
|
||
|
||
.metric-value {
|
||
font-weight: 700;
|
||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||
}
|
||
|
||
.no-metrics {
|
||
color: var(--gray-400);
|
||
font-style: italic;
|
||
}
|
||
|
||
|
||
// 答案推理样式
|
||
.answer-reasoning {
|
||
font-size: 12px;
|
||
color: var(--gray-600);
|
||
margin-top: 8px;
|
||
line-height: 1.4;
|
||
cursor: pointer;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 3;
|
||
word-wrap: break-word;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
|
||
&:hover {
|
||
color: var(--gray-800);
|
||
}
|
||
}
|
||
|
||
// 加载和空状态样式
|
||
.loading-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40px 0;
|
||
}
|
||
|
||
.empty-results {
|
||
padding: 40px 0;
|
||
text-align: center;
|
||
}
|
||
|
||
// 评估报告样式
|
||
.evaluation-report {
|
||
margin-bottom: 20px;
|
||
|
||
.report-metric {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 14px;
|
||
padding-right: 18px;
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||
}
|
||
}
|
||
|
||
.accuracy-stats {
|
||
.accuracy-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid var(--gray-100);
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.accuracy-label {
|
||
font-size: 14px;
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.accuracy-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-card) {
|
||
.ant-card-head {
|
||
border-bottom: 1px solid var(--gray-200);
|
||
|
||
.ant-card-head-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 历史评估记录区域
|
||
.history-section {
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
|
||
.section-title {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--gray-700);
|
||
}
|
||
|
||
.refresh-btn {
|
||
color: var(--gray-600);
|
||
border: none;
|
||
box-shadow: none;
|
||
padding: 4px 8px;
|
||
height: auto;
|
||
font-size: 13px;
|
||
|
||
&:hover {
|
||
color: var(--color-primary-600);
|
||
background-color: var(--color-primary-50);
|
||
}
|
||
|
||
&:active {
|
||
color: var(--color-primary-700);
|
||
background-color: var(--color-primary-100);
|
||
}
|
||
|
||
.anticon {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.ant-table) {
|
||
border: 1px solid var(--gray-100);
|
||
}
|
||
}
|
||
|
||
// JSON 查看器样式
|
||
.json-viewer-container {
|
||
max-height: 400px;
|
||
overflow: auto;
|
||
|
||
.json-viewer {
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Menlo', monospace;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: var(--gray-800);
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
background: var(--gray-50);
|
||
border: 1px solid var(--gray-200);
|
||
border-radius: 6px;
|
||
padding: 12px;
|
||
}
|
||
}
|
||
|
||
// 仅查看错误按钮样式
|
||
.error-only-active {
|
||
background-color: var(--color-error-500) !important;
|
||
border-color: var(--color-error-500) !important;
|
||
color: white !important;
|
||
|
||
&:hover {
|
||
background-color: var(--color-error-600) !important;
|
||
border-color: var(--color-error-600) !important;
|
||
}
|
||
|
||
&:focus {
|
||
background-color: var(--color-error-500) !important;
|
||
border-color: var(--color-error-500) !important;
|
||
}
|
||
}
|
||
|
||
</style>
|