Compare commits

...

3 Commits

Author SHA1 Message Date
yuliang_guo
fca82e2d44 fix: 优化路由加载失败的错误处理
All checks were successful
continuous-integration/drone/push Build is passing
- 检测chunk加载失败(部署后旧文件被清理)
- 自动刷新页面加载最新资源
- 改进错误提示,告知用户正在刷新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 13:42:42 +08:00
yuliang_guo
99ded54616 style: 优化错题卡片操作按钮UI
All checks were successful
continuous-integration/drone/push Build is passing
- 操作按钮改为实心渐变样式,更加醒目
- 添加View和Check图标增强辨识度
- 按钮hover时上浮+阴影效果
- 查看解析:蓝色渐变,hover变深蓝
- 标记已掌握:绿色渐变,hover变深绿
- 表格视图按钮同步优化

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 13:19:43 +08:00
yuliang_guo
c66b355a5a style: 优化错题分析筛选框UI设计
All checks were successful
continuous-integration/drone/push Build is passing
- 重新设计筛选工具栏布局,采用flex横向排列
- 搜索框改为圆角胶囊形状,添加hover聚焦效果
- 下拉选择框使用统一的圆角灰底设计
- 添加emoji图标前缀增加辨识度
- 筛选标签改为胶囊形状
- 重置按钮仅在有筛选条件时显示
- 整体视觉更加现代简洁

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 11:16:46 +08:00
2 changed files with 296 additions and 63 deletions

View File

@@ -55,11 +55,31 @@ export function setupRouterGuard(router: Router) {
} }
}) })
// 路由错误处理 // 路由错误处理 - 处理懒加载组件失败
router.onError((error) => { router.onError((error) => {
console.error('Router error:', error) console.error('Router error:', error)
ElMessage.error('路由加载失败')
loadingManager.stop('page-loading') loadingManager.stop('page-loading')
// 检测是否是chunk加载失败通常是部署后旧文件被清理
const isChunkLoadError =
error.message?.includes('Loading chunk') ||
error.message?.includes('Failed to fetch') ||
error.message?.includes('dynamically imported module') ||
error.name === 'ChunkLoadError'
if (isChunkLoadError) {
ElMessage({
type: 'warning',
message: '页面资源已更新,正在刷新...',
duration: 2000
})
// 延迟刷新页面以加载最新资源
setTimeout(() => {
window.location.reload()
}, 1000)
} else {
ElMessage.error('页面加载失败,请刷新重试')
}
}) })
} }

View File

@@ -58,93 +58,113 @@
<!-- 搜索和筛选 --> <!-- 搜索和筛选 -->
<div class="filter-section card"> <div class="filter-section card">
<el-form :inline="true" :model="filterForm" class="filter-form"> <div class="filter-toolbar">
<el-form-item label="关键词"> <!-- 搜索框 -->
<div class="search-box">
<el-input <el-input
v-model="filterForm.keyword" v-model="filterForm.keyword"
placeholder="搜索错题内容或知识点" placeholder="搜索错题内容或知识点..."
clearable clearable
@input="handleRealTimeSearch" @input="handleRealTimeSearch"
style="width: 200px" class="search-input"
> >
<template #prefix> <template #prefix>
<el-icon><Search /></el-icon> <el-icon class="search-icon"><Search /></el-icon>
</template> </template>
</el-input> </el-input>
</el-form-item> </div>
<el-form-item label="题目类型">
<!-- 筛选项 -->
<div class="filter-items">
<el-select <el-select
v-model="filterForm.type" v-model="filterForm.type"
placeholder="全部类型" placeholder="题目类型"
clearable clearable
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 120px" class="filter-select"
> >
<template #prefix>
<span class="select-prefix-icon">📝</span>
</template>
<el-option label="单选题" value="single" /> <el-option label="单选题" value="single" />
<el-option label="多选题" value="multiple" /> <el-option label="多选题" value="multiple" />
<el-option label="判断题" value="judge" /> <el-option label="判断题" value="judge" />
<el-option label="填空题" value="fill" /> <el-option label="填空题" value="fill" />
<el-option label="简答题" value="essay" /> <el-option label="简答题" value="essay" />
</el-select> </el-select>
</el-form-item>
<el-form-item label="难度等级">
<el-select <el-select
v-model="filterForm.difficulty" v-model="filterForm.difficulty"
placeholder="全部难度" placeholder="难度等级"
clearable clearable
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 120px" class="filter-select"
> >
<template #prefix>
<span class="select-prefix-icon"></span>
</template>
<el-option label="简单" value="easy" /> <el-option label="简单" value="easy" />
<el-option label="中等" value="medium" /> <el-option label="中等" value="medium" />
<el-option label="困难" value="hard" /> <el-option label="困难" value="hard" />
</el-select> </el-select>
</el-form-item>
<el-form-item label="掌握状态">
<el-select <el-select
v-model="filterForm.status" v-model="filterForm.status"
placeholder="全部状态" placeholder="掌握状态"
clearable clearable
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 120px" class="filter-select"
> >
<template #prefix>
<span class="select-prefix-icon">📊</span>
</template>
<el-option label="未掌握" value="unmastered" /> <el-option label="未掌握" value="unmastered" />
<el-option label="已掌握" value="mastered" /> <el-option label="已掌握" value="mastered" />
<el-option label="需巩固" value="review" /> <el-option label="需巩固" value="review" />
</el-select> </el-select>
</el-form-item>
<el-form-item label="时间周期">
<el-select <el-select
v-model="filterForm.timePeriod" v-model="filterForm.timePeriod"
placeholder="全部时间" placeholder="时间周期"
clearable clearable
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 120px" class="filter-select"
> >
<template #prefix>
<span class="select-prefix-icon">📅</span>
</template>
<el-option label="最近一周" value="week" /> <el-option label="最近一周" value="week" />
<el-option label="最近一月" value="month" /> <el-option label="最近一月" value="month" />
<el-option label="最近三月" value="quarter" /> <el-option label="最近三月" value="quarter" />
<el-option label="自定义" value="custom" /> <el-option label="自定义" value="custom" />
</el-select> </el-select>
</el-form-item>
<el-form-item v-if="filterForm.timePeriod === 'custom'" label="自定义时间">
<el-date-picker <el-date-picker
v-if="filterForm.timePeriod === 'custom'"
v-model="customDateRange" v-model="customDateRange"
type="daterange" type="daterange"
range-separator="" range-separator="~"
start-placeholder="开始日期" start-placeholder="开始"
end-placeholder="结束日期" end-placeholder="结束"
@change="handleRealTimeSearch" @change="handleRealTimeSearch"
style="width: 240px" class="date-picker"
format="MM/DD"
value-format="YYYY-MM-DD"
/> />
</el-form-item> </div>
<el-form-item>
<el-button @click="handleReset"> <!-- 重置按钮 -->
<el-icon class="el-icon--left"><Refresh /></el-icon> <el-button
重置 v-if="hasActiveFilters"
</el-button> @click="handleReset"
</el-form-item> class="reset-btn"
</el-form> type="info"
plain
>
<el-icon><Refresh /></el-icon>
重置
</el-button>
</div>
<!-- 当前筛选条件显示 --> <!-- 当前筛选条件显示 -->
<div v-if="hasActiveFilters" class="filter-tags"> <div v-if="hasActiveFilters" class="filter-tags">
@@ -302,10 +322,22 @@
</div> </div>
<div class="card-actions"> <div class="card-actions">
<el-button link type="primary" size="small" @click="viewDetail(mistake)"> <el-button
type="primary"
plain
@click="viewDetail(mistake)"
class="action-btn view-btn"
>
<el-icon class="btn-icon"><View /></el-icon>
查看解析 查看解析
</el-button> </el-button>
<el-button link type="success" size="small" @click="markMastered(mistake)"> <el-button
type="success"
plain
@click="markMastered(mistake)"
class="action-btn master-btn"
>
<el-icon class="btn-icon"><Check /></el-icon>
标记已掌握 标记已掌握
</el-button> </el-button>
</div> </div>
@@ -339,14 +371,28 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="150"> <el-table-column label="操作" width="220">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" size="small" @click="viewDetail(scope.row)"> <div class="table-actions">
查看解析 <el-button
</el-button> type="primary"
<el-button link type="success" size="small" @click="markMastered(scope.row)"> size="small"
标记已掌握 @click="viewDetail(scope.row)"
</el-button> class="table-action-btn"
>
<el-icon><View /></el-icon>
解析
</el-button>
<el-button
type="success"
size="small"
@click="markMastered(scope.row)"
class="table-action-btn"
>
<el-icon><Check /></el-icon>
已掌握
</el-button>
</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -386,7 +432,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted, h } from 'vue' import { ref, reactive, computed, onMounted, h } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, Grid, List, Search } from '@element-plus/icons-vue' import { Refresh, Grid, List, Search, View, Check } from '@element-plus/icons-vue'
import { getMistakesList, getMistakesStatistics, markMistakeMastered } from '@/api/exam' import { getMistakesList, getMistakesStatistics, markMistakeMastered } from '@/api/exam'
import type { MistakeListItem, MistakesStatisticsResponse } from '@/api/exam' import type { MistakeListItem, MistakesStatisticsResponse } from '@/api/exam'
@@ -882,15 +928,105 @@ console.log('错题分析页面已加载')
} }
.filter-section { .filter-section {
.filter-form { padding: 16px 20px;
.el-form-item {
margin-bottom: 0; .filter-toolbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
.search-box {
flex: 0 0 280px;
.search-input {
:deep(.el-input__wrapper) {
border-radius: 20px;
background: #f5f7fa;
box-shadow: none;
border: 1px solid transparent;
padding: 4px 16px;
transition: all 0.3s ease;
&:hover, &:focus-within {
background: #fff;
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.15);
}
}
.search-icon {
color: #909399;
font-size: 16px;
}
}
}
.filter-items {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
flex: 1;
.filter-select {
width: 130px;
:deep(.el-select__wrapper) {
border-radius: 8px;
background: #f5f7fa;
box-shadow: none;
border: 1px solid transparent;
transition: all 0.2s ease;
min-height: 36px;
&:hover {
background: #fff;
border-color: #dcdfe6;
}
&.is-focused {
background: #fff;
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
}
}
.select-prefix-icon {
font-size: 14px;
margin-right: 4px;
}
}
.date-picker {
:deep(.el-input__wrapper) {
border-radius: 8px;
background: #f5f7fa;
box-shadow: none;
border: 1px solid transparent;
&:hover {
background: #fff;
border-color: #dcdfe6;
}
}
}
}
.reset-btn {
border-radius: 8px;
padding: 8px 16px;
font-size: 13px;
.el-icon {
margin-right: 4px;
}
} }
} }
.filter-tags { .filter-tags {
margin-top: 16px; margin-top: 14px;
padding-top: 16px; padding-top: 14px;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -898,42 +1034,62 @@ console.log('错题分析页面已加载')
gap: 8px; gap: 8px;
.filter-label { .filter-label {
color: #666; color: #606266;
font-size: 14px; font-size: 13px;
margin-right: 8px; font-weight: 500;
}
.el-tag {
border-radius: 16px;
padding: 0 12px;
height: 28px;
line-height: 26px;
font-size: 12px;
} }
.clear-all-btn { .clear-all-btn {
margin-left: 8px; margin-left: auto;
font-size: 12px;
} }
} }
.search-result-info { .search-result-info {
margin-top: 12px; margin-top: 14px;
padding-top: 12px; padding-top: 14px;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.result-count { .result-count {
color: #666; color: #606266;
font-size: 14px; font-size: 14px;
display: flex;
align-items: center;
gap: 4px;
strong { strong {
color: #409eff; color: #409eff;
font-weight: 600; font-weight: 600;
font-size: 18px;
} }
} }
.filter-hint { .filter-hint {
color: #e6a23c; color: #e6a23c;
font-size: 12px; font-size: 12px;
background: #fdf6ec;
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
} }
.category-info { .category-info {
color: #666; color: #606266;
font-size: 14px; font-size: 13px;
background: #f0f9eb;
padding: 4px 12px;
border-radius: 16px;
strong { strong {
color: #67c23a; color: #67c23a;
@@ -1045,6 +1201,21 @@ console.log('错题分析页面已加载')
.mistake-table { .mistake-table {
margin-top: 20px; margin-top: 20px;
.table-actions {
display: flex;
gap: 8px;
.table-action-btn {
border-radius: 6px;
font-size: 12px;
padding: 6px 12px;
.el-icon {
margin-right: 4px;
}
}
}
} }
.mistake-cards { .mistake-cards {
@@ -1211,11 +1382,53 @@ console.log('错题分析页面已加载')
} }
.card-actions { .card-actions {
padding: 12px 16px; padding: 14px 16px;
background: #fafafa; background: linear-gradient(135deg, #f8f9fa 0%, #f0f2f5 100%);
border-top: 1px solid #e4e7ed; border-top: 1px solid #e4e7ed;
display: flex; display: flex;
gap: 12px; gap: 12px;
.action-btn {
flex: 1;
border-radius: 8px;
font-weight: 500;
padding: 10px 16px;
transition: all 0.25s ease;
.btn-icon {
margin-right: 6px;
font-size: 16px;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.view-btn {
background: linear-gradient(135deg, #e6f4ff 0%, #bae0ff 100%);
border-color: #91caff;
color: #1677ff;
&:hover {
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
border-color: #1677ff;
color: #fff;
}
}
.master-btn {
background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
border-color: #b7eb8f;
color: #52c41a;
&:hover {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-color: #52c41a;
color: #fff;
}
}
} }
} }
} }