Files
prop-ai-hr/frontend/src/views/train/rubrics.vue
T

316 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="rubric-page">
<div class="page-heading">
<div>
<h1>Rubric 评分规则</h1>
<p>维护评分维度、权重、版本和生效状态</p>
</div>
<el-button type="danger" :icon="Plus" @click="createVersion">新建版本</el-button>
</div>
<div class="rubric-layout">
<section class="version-card">
<h2>版本列表</h2>
<button
v-for="item in rubrics"
:key="item.code"
class="version-item"
:class="{ active: item.code === selectedCode }"
@click="selectRubric(item)"
>
<strong>{{ item.name }} {{ item.version }}</strong>
<span>{{ item.enabled ? '当前生效' : '历史版本' }} · {{ item.dimensions.length }} 维度</span>
</button>
<el-empty v-if="!loading && rubrics.length === 0" description="暂无评分规则" />
</section>
<section v-if="form" class="editor-card">
<div class="editor-heading">
<div>
<h2>{{ form.name }} {{ form.version }}</h2>
<p>{{ selectedRubric?.scenarioName || form.scenarioCode }} · {{ form.description || '维护评分维度与权重' }}</p>
</div>
<el-tag :type="form.enabled ? 'success' : 'info'">{{ form.enabled ? '已启用' : '已停用' }}</el-tag>
</div>
<h3>评分维度与权重</h3>
<div v-for="dimension in form.dimensions" :key="dimension.code" class="dimension-row">
<div class="dimension-label">
<strong>{{ dimension.name }}</strong>
<span>{{ dimension.description || '请补充该维度的评分说明' }}</span>
</div>
<div class="dimension-control">
<span>{{ Math.round(dimension.weight * 100) }}% 权重</span>
<el-slider v-model="dimension.weight" :min="0" :max="1" :step="0.05" @change="normalizeWeights" />
</div>
</div>
<div class="band-card">
<strong>评分档位</strong>
<span>90–100 优秀&nbsp; · &nbsp;75–89 良好&nbsp; · &nbsp;60–74 待提升&nbsp; · &nbsp;&lt;60 不合格</span>
</div>
<div class="editor-actions">
<el-button @click="copyVersion">复制版本</el-button>
<el-button @click="selectedRubric && toggleEnabled(selectedRubric)">{{ form.enabled ? '停用版本' : '启用版本' }}</el-button>
<el-button type="danger" :loading="saving" @click="saveVersion">保存并发布</el-button>
</div>
</section>
<el-empty v-else-if="!loading" description="请选择一个评分规则" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
import {
listPracticeRubrics,
savePracticeRubric,
updatePracticeRubricEnabled,
type PracticeRubricAdmin,
type PracticeRubricSaveRequest
} from '@/api/aihr/practice';
const loading = ref(false);
const saving = ref(false);
const rubrics = ref<PracticeRubricAdmin[]>([]);
const selectedCode = ref('');
const form = ref<PracticeRubricSaveRequest>();
const selectedRubric = computed(() => rubrics.value.find((item) => item.code === selectedCode.value));
const cloneForm = (item: PracticeRubricAdmin): PracticeRubricSaveRequest => ({
code: item.code,
scenarioCode: item.scenarioCode,
name: item.name,
version: item.version,
enabled: item.enabled,
description: item.description,
dimensions: item.dimensions.map((dimension) => ({ ...dimension }))
});
const loadRubrics = async () => {
loading.value = true;
try {
const result = await listPracticeRubrics();
rubrics.value = result.data ?? [];
if (!selectedCode.value && rubrics.value.length > 0) {
selectRubric(rubrics.value[0]);
} else if (selectedCode.value) {
const current = rubrics.value.find((item) => item.code === selectedCode.value);
if (current) selectRubric(current);
}
} catch {
rubrics.value = [];
} finally {
loading.value = false;
}
};
const selectRubric = (item: PracticeRubricAdmin) => {
selectedCode.value = item.code;
form.value = reactive(cloneForm(item));
};
const normalizeWeights = () => {
if (!form.value) return;
const total = form.value.dimensions.reduce((sum, item) => sum + Number(item.weight || 0), 0);
if (total > 0 && Math.abs(total - 1) > 0.001) {
form.value.dimensions.forEach((item) => {
item.weight = Number((item.weight / total).toFixed(2));
});
}
};
const createVersion = () => {
const source = rubrics.value[0];
if (!source) return;
const suffix = Date.now().toString().slice(-4);
form.value = reactive({
...cloneForm(source),
code: `${source.code}-${suffix}`,
version: `v${Number.parseInt(source.version.replace(/\D/g, ''), 10) + 1}`,
enabled: false
});
selectedCode.value = '';
};
const copyVersion = () => createVersion();
const saveVersion = async () => {
if (!form.value) return;
normalizeWeights();
const total = form.value.dimensions.reduce((sum, item) => sum + Number(item.weight || 0), 0);
if (Math.abs(total - 1) > 0.011) {
ElMessage.warning('各维度权重合计必须为 100%');
return;
}
saving.value = true;
try {
const result = await savePracticeRubric(form.value);
ElMessage.success('评分规则已保存并发布');
await loadRubrics();
if (result.data) selectRubric(result.data);
} finally {
saving.value = false;
}
};
const toggleEnabled = async (item: PracticeRubricAdmin) => {
await ElMessageBox.confirm(`确定${item.enabled ? '停用' : '启用'}“${item.name} ${item.version}”吗?`, '状态确认');
await updatePracticeRubricEnabled(item.code, !item.enabled);
item.enabled = !item.enabled;
if (item.code === selectedCode.value && form.value) form.value.enabled = item.enabled;
ElMessage.success(item.enabled ? '评分规则已启用' : '评分规则已停用');
};
onMounted(loadRubrics);
</script>
<style scoped>
.rubric-page {
min-height: calc(100vh - 84px);
padding: 28px 32px 40px;
background: #f4f6fa;
color: #1d2635;
}
.page-heading,
.editor-heading,
.editor-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
.page-heading {
margin-bottom: 22px;
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: 28px;
font-weight: 600;
}
h2 {
font-size: 20px;
font-weight: 600;
}
h3 {
margin: 24px 0 18px;
font-size: 18px;
}
.page-heading p,
.editor-heading p {
margin-top: 8px;
color: #858e9e;
font-size: 13px;
}
.rubric-layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.version-card,
.editor-card {
min-height: 520px;
padding: 18px 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 8px 24px rgb(33 43 54 / 4%);
}
.version-card h2 {
margin-bottom: 16px;
}
.version-item {
display: block;
width: 100%;
margin-bottom: 9px;
padding: 16px 14px;
border: 0;
border-radius: 8px;
background: #fafbfc;
color: #253044;
text-align: left;
cursor: pointer;
}
.version-item.active {
background: #fff0f1;
color: #e60012;
}
.version-item strong,
.version-item span {
display: block;
}
.version-item span {
margin-top: 7px;
color: #858e9e;
font-size: 12px;
}
.editor-card {
padding: 20px;
}
.editor-heading {
align-items: flex-start;
}
.dimension-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-items: center;
margin: 18px 0;
}
.dimension-label strong,
.dimension-label span,
.dimension-control > span {
display: block;
}
.dimension-label span {
margin-top: 7px;
color: #858e9e;
font-size: 12px;
}
.dimension-control > span {
margin-bottom: 8px;
color: #e60012;
font-size: 12px;
text-align: right;
}
.band-card {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 24px;
padding: 16px;
border-radius: 8px;
background: #f7f9fb;
color: #758196;
font-size: 13px;
}
.band-card strong {
color: #253044;
font-size: 15px;
}
.editor-actions {
margin-top: 16px;
gap: 10px;
}
.editor-actions .el-button:last-child {
flex: 1;
}
@media (max-width: 900px) {
.rubric-layout {
grid-template-columns: 1fr;
}
.dimension-row {
grid-template-columns: 1fr;
gap: 8px;
}
}
</style>