feat: 添加请求验证错误详细日志
All checks were successful
continuous-integration/drone/push Build is passing

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuliang_guo
2026-01-31 10:03:54 +08:00
parent fadeaadd65
commit 0b7c07eb7f
11 changed files with 2282 additions and 2267 deletions

View File

@@ -1,154 +1,154 @@
/**
* 分数格式化工具
*
* 用于在前端显示分数时进行格式化,避免显示过长的小数
*/
/**
* 格式化分数显示
*
* @param score 分数
* @param decimalPlaces 小数位数默认1位
* @returns 格式化后的分数字符串
*
* @example
* formatScore(16.666666) // "16.7"
* formatScore(17) // "17"
* formatScore(16.5, 0) // "17"
*/
export function formatScore(score: number, decimalPlaces: number = 1): string {
// 如果是整数,直接返回
if (Number.isInteger(score)) {
return score.toString()
}
// 四舍五入到指定小数位
const rounded = Number(score.toFixed(decimalPlaces))
// 如果四舍五入后是整数,去掉小数点
if (Number.isInteger(rounded)) {
return rounded.toString()
}
return rounded.toFixed(decimalPlaces)
}
/**
* 格式化分数显示(带单位)
*
* @param score 分数
* @param unit 单位,默认"分"
* @returns 格式化后的分数字符串
*
* @example
* formatScoreWithUnit(16.7) // "16.7分"
* formatScoreWithUnit(100) // "100分"
*/
export function formatScoreWithUnit(score: number, unit: string = '分'): string {
return `${formatScore(score)}${unit}`
}
/**
* 格式化百分比
*
* @param value 值0-1 或 0-100
* @param isPercent 是否已经是百分比形式0-100默认false
* @returns 格式化后的百分比字符串
*
* @example
* formatPercent(0.8567) // "85.7%"
* formatPercent(85.67, true) // "85.7%"
*/
export function formatPercent(value: number, isPercent: boolean = false): string {
const percent = isPercent ? value : value * 100
return `${formatScore(percent)}%`
}
/**
* 计算及格分数
*
* @param totalScore 总分
* @param passRate 及格率默认0.6
* @returns 及格分数(向上取整)
*/
export function calculatePassScore(totalScore: number, passRate: number = 0.6): number {
return Math.ceil(totalScore * passRate)
}
/**
* 判断是否及格
*
* @param score 得分
* @param passScore 及格分数
* @returns 是否及格
*/
export function isPassed(score: number, passScore: number): boolean {
return score >= passScore
}
/**
* 获取分数等级
*
* @param score 得分
* @param totalScore 总分
* @returns 等级: 'excellent' | 'good' | 'pass' | 'fail'
*/
export function getScoreLevel(score: number, totalScore: number): 'excellent' | 'good' | 'pass' | 'fail' {
const ratio = score / totalScore
if (ratio >= 0.9) return 'excellent'
if (ratio >= 0.75) return 'good'
if (ratio >= 0.6) return 'pass'
return 'fail'
}
/**
* 获取分数等级对应的颜色
*
* @param level 等级
* @returns 颜色值
*/
export function getScoreLevelColor(level: 'excellent' | 'good' | 'pass' | 'fail'): string {
const colors = {
excellent: '#67c23a', // 绿色
good: '#409eff', // 蓝色
pass: '#e6a23c', // 橙色
fail: '#f56c6c', // 红色
}
return colors[level]
}
/**
* 智能分配分数(前端预览用)
*
* @param totalScore 总分
* @param questionCount 题目数量
* @returns 分数数组
*
* @example
* distributeScores(100, 6) // [17, 17, 17, 17, 16, 16]
*/
export function distributeScores(totalScore: number, questionCount: number): number[] {
if (questionCount <= 0) return []
const baseScore = Math.floor(totalScore / questionCount)
const extraCount = totalScore % questionCount
const scores: number[] = []
for (let i = 0; i < questionCount; i++) {
scores.push(i < extraCount ? baseScore + 1 : baseScore)
}
return scores
}
export default {
formatScore,
formatScoreWithUnit,
formatPercent,
calculatePassScore,
isPassed,
getScoreLevel,
getScoreLevelColor,
distributeScores,
}
/**
* 分数格式化工具
*
* 用于在前端显示分数时进行格式化,避免显示过长的小数
*/
/**
* 格式化分数显示
*
* @param score 分数
* @param decimalPlaces 小数位数默认1位
* @returns 格式化后的分数字符串
*
* @example
* formatScore(16.666666) // "16.7"
* formatScore(17) // "17"
* formatScore(16.5, 0) // "17"
*/
export function formatScore(score: number, decimalPlaces: number = 1): string {
// 如果是整数,直接返回
if (Number.isInteger(score)) {
return score.toString()
}
// 四舍五入到指定小数位
const rounded = Number(score.toFixed(decimalPlaces))
// 如果四舍五入后是整数,去掉小数点
if (Number.isInteger(rounded)) {
return rounded.toString()
}
return rounded.toFixed(decimalPlaces)
}
/**
* 格式化分数显示(带单位)
*
* @param score 分数
* @param unit 单位,默认"分"
* @returns 格式化后的分数字符串
*
* @example
* formatScoreWithUnit(16.7) // "16.7分"
* formatScoreWithUnit(100) // "100分"
*/
export function formatScoreWithUnit(score: number, unit: string = '分'): string {
return `${formatScore(score)}${unit}`
}
/**
* 格式化百分比
*
* @param value 值0-1 或 0-100
* @param isPercent 是否已经是百分比形式0-100默认false
* @returns 格式化后的百分比字符串
*
* @example
* formatPercent(0.8567) // "85.7%"
* formatPercent(85.67, true) // "85.7%"
*/
export function formatPercent(value: number, isPercent: boolean = false): string {
const percent = isPercent ? value : value * 100
return `${formatScore(percent)}%`
}
/**
* 计算及格分数
*
* @param totalScore 总分
* @param passRate 及格率默认0.6
* @returns 及格分数(向上取整)
*/
export function calculatePassScore(totalScore: number, passRate: number = 0.6): number {
return Math.ceil(totalScore * passRate)
}
/**
* 判断是否及格
*
* @param score 得分
* @param passScore 及格分数
* @returns 是否及格
*/
export function isPassed(score: number, passScore: number): boolean {
return score >= passScore
}
/**
* 获取分数等级
*
* @param score 得分
* @param totalScore 总分
* @returns 等级: 'excellent' | 'good' | 'pass' | 'fail'
*/
export function getScoreLevel(score: number, totalScore: number): 'excellent' | 'good' | 'pass' | 'fail' {
const ratio = score / totalScore
if (ratio >= 0.9) return 'excellent'
if (ratio >= 0.75) return 'good'
if (ratio >= 0.6) return 'pass'
return 'fail'
}
/**
* 获取分数等级对应的颜色
*
* @param level 等级
* @returns 颜色值
*/
export function getScoreLevelColor(level: 'excellent' | 'good' | 'pass' | 'fail'): string {
const colors = {
excellent: '#67c23a', // 绿色
good: '#409eff', // 蓝色
pass: '#e6a23c', // 橙色
fail: '#f56c6c', // 红色
}
return colors[level]
}
/**
* 智能分配分数(前端预览用)
*
* @param totalScore 总分
* @param questionCount 题目数量
* @returns 分数数组
*
* @example
* distributeScores(100, 6) // [17, 17, 17, 17, 16, 16]
*/
export function distributeScores(totalScore: number, questionCount: number): number[] {
if (questionCount <= 0) return []
const baseScore = Math.floor(totalScore / questionCount)
const extraCount = totalScore % questionCount
const scores: number[] = []
for (let i = 0; i < questionCount; i++) {
scores.push(i < extraCount ? baseScore + 1 : baseScore)
}
return scores
}
export default {
formatScore,
formatScoreWithUnit,
formatPercent,
calculatePassScore,
isPassed,
getScoreLevel,
getScoreLevelColor,
distributeScores,
}

View File

@@ -2,9 +2,9 @@
<div class="growth-path-management-container">
<!-- 路径列表视图 -->
<template v-if="!editingPath">
<div class="page-header">
<h1 class="page-title">成长路径管理</h1>
<div class="header-actions">
<div class="page-header">
<h1 class="page-title">成长路径管理</h1>
<div class="header-actions">
<el-select
v-model="filters.position_id"
placeholder="筛选岗位"
@@ -19,7 +19,7 @@
:label="pos.name"
:value="pos.id"
/>
</el-select>
</el-select>
<el-select
v-model="filters.is_active"
placeholder="状态"
@@ -34,9 +34,9 @@
<el-button type="primary" @click="handleCreatePath">
<el-icon class="el-icon--left"><Plus /></el-icon>
新建路径
</el-button>
</div>
</el-button>
</div>
</div>
<!-- 路径列表 -->
<div class="path-list-card">
@@ -157,7 +157,7 @@
multiple
collapse-tags
collapse-tags-tooltip
clearable
clearable
style="width: 100%"
>
<el-option
@@ -170,7 +170,7 @@
<el-button
link
type="primary"
size="small"
size="small"
@click="handleSelectAllPositions"
class="select-all-btn"
>
@@ -228,11 +228,11 @@
placeholder="阶段名称"
size="small"
style="width: 100px"
>
<template #prefix>
>
<template #prefix>
<span class="stage-order">{{ index + 1 }}</span>
</template>
</el-input>
</template>
</el-input>
<el-button
link
type="danger"
@@ -244,8 +244,8 @@
</el-button>
</div>
</div>
</div>
</div>
<!-- 路径统计 -->
<div class="top-section stats-section">
<h3>路径统计</h3>
@@ -253,7 +253,7 @@
<div class="stat-box">
<span class="stat-value">{{ editingPath.nodes?.length || 0 }}</span>
<span class="stat-label">课程总数</span>
</div>
</div>
<div class="stat-box">
<span class="stat-value">{{ requiredCount }}</span>
<span class="stat-label">必修课程</span>
@@ -261,10 +261,10 @@
<div class="stat-box">
<span class="stat-value">{{ totalDuration }}</span>
<span class="stat-label">总学时(h)</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 下方左右分栏 -->
<div class="editor-bottom">
@@ -303,11 +303,11 @@
<div class="course-list" v-loading="coursesLoading">
<div
v-for="course in filteredCourses"
:key="course.id"
class="course-item"
:key="course.id"
class="course-item"
:class="{ 'is-added': isNodeAdded(course.id) }"
draggable="true"
@dragstart="handleDragStart($event, course)"
draggable="true"
@dragstart="handleDragStart($event, course)"
@click="handleAddCourse(course)"
>
<div class="course-info">
@@ -320,21 +320,21 @@
<el-icon v-else><Plus /></el-icon>
</div>
<el-empty v-if="filteredCourses.length === 0" description="暂无课程" :image-size="50" />
</div>
</div>
</div>
</div>
<!-- 右侧 2/3已选课程配置 -->
<div class="selected-courses-panel">
<div class="panel-header">
<span>已选课程配置</span>
<el-tag size="small" type="success">{{ editingPath.nodes?.length || 0 }} </el-tag>
</div>
</div>
<div
class="selected-content"
:class="{ 'is-dragging-over': isDraggingOver }"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
@drop="handleDrop"
>
<template v-if="editingPath.nodes && editingPath.nodes.length > 0">
<div
@@ -347,7 +347,7 @@
<el-tag size="small" type="info">
{{ getStageNodes(stage.name).length }}
</el-tag>
</div>
</div>
<div class="stage-nodes">
<div
v-for="(node, nodeIndex) in getStageNodes(stage.name)"
@@ -356,7 +356,7 @@
>
<div class="node-drag-handle">
<el-icon><Rank /></el-icon>
</div>
</div>
<div class="node-content">
<span class="node-title">{{ node.title }}</span>
<div class="node-meta">
@@ -367,10 +367,10 @@
style="cursor: pointer"
>
{{ node.is_required ? '必修' : '选修' }}
</el-tag>
</el-tag>
<span>{{ node.estimated_days || 1 }}</span>
</div>
</div>
</div>
</div>
<div class="node-actions">
<el-select
v-model="node.stage_name"
@@ -393,16 +393,16 @@
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
<div
v-if="getStageNodes(stage.name).length === 0"
class="stage-empty"
>
拖拽或点击课程添加到此阶段
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<el-empty v-else description="请从左侧添加课程" :image-size="60">
<template #description>
@@ -413,7 +413,7 @@
</div>
</div>
</div>
</template>
</template>
</div>
</template>
@@ -768,10 +768,10 @@ const handleDeletePath = async (row: GrowthPathListItem) => {
try {
await ElMessageBox.confirm(
`确定要删除路径"${row.name}"吗?此操作不可恢复。`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
@@ -1040,9 +1040,9 @@ onMounted(() => {
flex-shrink: 0;
.top-section {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 16px;
h3 {
@@ -1059,7 +1059,7 @@ onMounted(() => {
flex: 2;
.form-row {
display: flex;
display: flex;
gap: 16px;
margin-bottom: 8px;
@@ -1113,9 +1113,9 @@ onMounted(() => {
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
h3 {
h3 {
margin: 0;
padding: 0;
border: none;
@@ -1134,7 +1134,7 @@ onMounted(() => {
.stage-order {
color: #667eea;
font-weight: 600;
font-weight: 600;
font-size: 12px;
}
}
@@ -1150,7 +1150,7 @@ onMounted(() => {
gap: 8px;
.stat-box {
flex: 1;
flex: 1;
text-align: center;
padding: 8px 4px;
background: #f5f7fa;
@@ -1188,8 +1188,8 @@ onMounted(() => {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
.panel-header {
display: flex;
@@ -1254,7 +1254,7 @@ onMounted(() => {
.course-name {
display: block;
font-size: 13px;
color: #333;
color: #333;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
@@ -1266,34 +1266,34 @@ onMounted(() => {
font-size: 11px;
color: #909399;
margin-top: 2px;
}
}
}
}
}
}
// 右侧已选课程 2/3
.selected-courses-panel {
flex: 1;
flex: 1;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fafafa;
border-radius: 8px 8px 0 0;
font-weight: 500;
border-bottom: 1px solid #ebeef5;
border-bottom: 1px solid #ebeef5;
}
.selected-content {
flex: 1;
flex: 1;
overflow-y: auto;
padding: 12px;
border: 2px dashed transparent;
@@ -1341,7 +1341,7 @@ onMounted(() => {
align-items: center;
gap: 8px;
padding: 10px;
background: #fff;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 8px;
@@ -1359,28 +1359,28 @@ onMounted(() => {
flex: 1;
min-width: 0;
.node-title {
.node-title {
display: block;
font-weight: 500;
color: #333;
font-weight: 500;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-meta {
display: flex;
align-items: center;
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
font-size: 12px;
color: #909399;
font-size: 12px;
color: #909399;
}
}
.node-actions {
display: flex;
align-items: center;
display: flex;
align-items: center;
gap: 8px;
}
}
@@ -1390,12 +1390,12 @@ onMounted(() => {
padding: 16px;
color: #909399;
font-size: 13px;
}
}
}
}
}
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
@@ -1424,10 +1424,10 @@ onMounted(() => {
}
.editor-bottom {
flex-direction: column;
flex-direction: column;
.course-library-panel {
width: 100%;
width: 100%;
max-height: 300px;
}
}