ZhangQiPro/src/views/Evaluate.vue

749 lines
19 KiB
Vue

<template>
<div class="evaluate-page">
<!-- 页面标题 -->
<div class="page-header">
<h1 class="page-title">实践教学评价</h1>
<div class="page-actions">
<el-button v-if="canExport" type="primary" icon="Download" @click="exportData">
导出评价结果
</el-button>
</div>
</div>
<!-- 筛选区域 -->
<BaseCard class="filter-section">
<div class="filter-row">
<div class="filter-item">
<label class="filter-label">学期选择</label>
<el-select v-model="selectedSemester" placeholder="请选择学期" clearable>
<el-option
v-for="semester in semesters"
:key="semester"
:label="semester"
:value="semester"
/>
</el-select>
</div>
<div class="filter-item">
<label class="filter-label">年级选择</label>
<el-select v-model="selectedGrade" placeholder="请选择年级" clearable>
<el-option
v-for="grade in grades"
:key="grade"
:label="grade"
:value="grade"
/>
</el-select>
</div>
<div class="filter-item">
<label class="filter-label">班级选择</label>
<el-select v-model="selectedClass" placeholder="请选择班级" clearable>
<el-option
v-for="className in classes"
:key="className"
:label="className"
:value="className"
/>
</el-select>
</div>
<div class="filter-item">
<label class="filter-label">实践项目</label>
<el-select v-model="selectedProject" placeholder="请选择项目" clearable>
<el-option
v-for="project in projects"
:key="project"
:label="project"
:value="project"
/>
</el-select>
</div>
<div class="filter-actions">
<el-button type="primary" icon="Search" @click="filterStudents">
查询学生
</el-button>
<el-button icon="Refresh" @click="resetFilters">
重置
</el-button>
</div>
</div>
</BaseCard>
<!-- 学生列表区域 -->
<BaseCard class="student-list-section">
<div class="section-header">
<h3 class="section-title">
<el-icon><User /></el-icon>
学生列表
<span v-if="filteredStudents.length" class="count-badge">{{ filteredStudents.length }}人</span>
</h3>
<div class="list-actions">
<el-button
v-if="userRole === 'teacher'"
type="success"
icon="View"
size="small"
@click="viewAllPortraits"
>
批量查看画像
</el-button>
</div>
</div>
<el-table
:data="filteredStudents"
stripe
border
empty-text="请先设置筛选条件查询学生"
table-layout="fixed"
>
<el-table-column prop="studentId" label="学号" width="120" />
<el-table-column prop="name" label="姓名" width="100" />
<el-table-column prop="grade" label="年级" width="100" />
<el-table-column prop="class" label="班级" width="120" />
<!-- 根据角色显示不同的评价状态和操作 -->
<el-table-column
v-if="userRole === 'company'"
label="企业评价"
width="120"
align="center"
>
<template #default="{ row }">
<el-tag
:type="getEvaluationStatus(row.id, 'company').type"
size="small"
>
{{ getEvaluationStatus(row.id, 'company').text }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="userRole === 'teacher'"
label="教师评价"
width="120"
align="center"
>
<template #default="{ row }">
<el-tag
:type="getEvaluationStatus(row.id, 'teacher').type"
size="small"
>
{{ getEvaluationStatus(row.id, 'teacher').text }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="userRole === 'expert'"
label="专家评价"
width="120"
align="center"
>
<template #default="{ row }">
<el-tag
:type="getEvaluationStatus(row.id, 'expert').type"
size="small"
>
{{ getEvaluationStatus(row.id, 'expert').text }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="userRole === 'student'"
label="提交状态"
width="120"
align="center"
>
<template #default="{ row }">
<el-tag
:type="getSubmissionStatus(row.id).type"
size="small"
>
{{ getSubmissionStatus(row.id).text }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="200" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<!-- 企业角色操作 -->
<template v-if="userRole === 'company'">
<el-button
type="primary"
icon="Edit"
size="small"
@click="openEvaluationDialog(row, 'company')"
>
企业评价
</el-button>
<el-button
type="info"
icon="View"
size="small"
@click="viewSubmission(row)"
>
查看成果
</el-button>
</template>
<!-- 教师角色操作 -->
<template v-if="userRole === 'teacher'">
<el-button
type="primary"
icon="Edit"
size="small"
@click="openEvaluationDialog(row, 'teacher')"
>
教师评价
</el-button>
<el-button
type="success"
icon="User"
size="small"
@click="viewPortrait(row)"
>
查看画像
</el-button>
<el-button
type="warning"
icon="Document"
size="small"
@click="viewReport(row)"
>
生成报告
</el-button>
</template>
<!-- 专家角色操作 -->
<template v-if="userRole === 'expert'">
<el-button
type="primary"
icon="Edit"
size="small"
@click="openEvaluationDialog(row, 'expert')"
>
专家评价
</el-button>
<el-button
type="info"
icon="View"
size="small"
@click="viewSubmission(row)"
>
查看材料
</el-button>
</template>
<!-- 学生角色操作 -->
<template v-if="userRole === 'student'">
<el-button
type="primary"
icon="Upload"
size="small"
@click="openSubmissionDialog(row)"
>
提交成果
</el-button>
<el-button
type="success"
icon="Edit"
size="small"
@click="openSelfEvaluation(row)"
>
自我评价
</el-button>
<el-button
type="warning"
icon="Star"
size="small"
@click="openPeerEvaluation(row)"
>
互评他人
</el-button>
</template>
</div>
</template>
</el-table-column>
</el-table>
</BaseCard>
<!-- 评价对话框 -->
<EvaluationDialog
v-model="evaluationDialogVisible"
:student="selectedStudent"
:role="userRole"
@submit="handleEvaluationSubmit"
/>
<!-- 成果提交对话框 -->
<SubmissionDialog
v-model="submissionDialogVisible"
:student="selectedStudent"
@submit="handleSubmissionSubmit"
/>
</div>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { mockOptions, mockStudents, mockEvaluationData } from '@/utils/mockData'
import { ElMessage } from 'element-plus'
import { User, Search, Refresh, Download, Edit, View, Upload, Star, Document } from '@element-plus/icons-vue'
import BaseCard from '@/components/BaseCard.vue'
import EvaluationDialog from '@/components/EvaluationDialog.vue'
import SubmissionDialog from '@/components/SubmissionDialog.vue'
export default {
name: 'Evaluate',
components: {
BaseCard,
EvaluationDialog,
SubmissionDialog,
User,
Search,
Refresh,
Download,
Edit,
View,
Upload,
Star,
Document
},
setup() {
const router = useRouter()
const authStore = useAuthStore()
// 响应式数据
const selectedSemester = ref('')
const selectedGrade = ref('')
const selectedClass = ref('')
const selectedProject = ref('')
const filteredStudents = ref([])
const evaluationDialogVisible = ref(false)
const submissionDialogVisible = ref(false)
const selectedStudent = ref(null)
// 基础数据
const semesters = ref(mockOptions.semesters)
const grades = ref(mockOptions.grades)
const classes = ref(mockOptions.classes)
const projects = ref(mockOptions.projects)
// 计算属性
const userRole = computed(() => authStore.user?.role)
const canExport = computed(() => ['teacher', 'company'].includes(userRole.value))
// 使用Mock评价状态数据
const evaluationData = reactive({
company: { ...mockEvaluationData.company },
teacher: { ...mockEvaluationData.teacher },
expert: { ...mockEvaluationData.expert },
submissions: { ...mockEvaluationData.submissions }
})
// 筛选学生
const filterStudents = () => {
if (!selectedSemester.value || !selectedGrade.value || !selectedClass.value) {
ElMessage.warning('请选择学期、年级和班级')
return
}
// 根据筛选条件过滤学生
filteredStudents.value = mockStudents.filter(student => {
return student.grade === selectedGrade.value &&
student.class === selectedClass.value
})
ElMessage.success(`查询到 ${filteredStudents.value.length} 名学生`)
}
// 重置筛选条件
const resetFilters = () => {
selectedSemester.value = ''
selectedGrade.value = ''
selectedClass.value = ''
selectedProject.value = ''
filteredStudents.value = []
}
// 获取评价状态
const getEvaluationStatus = (studentId, role) => {
const hasEvaluation = evaluationData[role][studentId]
if (hasEvaluation) {
return { type: 'success', text: '已评价' }
}
return { type: 'warning', text: '待评价' }
}
// 获取学生提交状态
const getSubmissionStatus = (studentId) => {
const submission = evaluationData.submissions[studentId]
if (submission?.submitted) {
return { type: 'success', text: '已提交' }
}
return { type: 'warning', text: '未提交' }
}
// 打开评价对话框
const openEvaluationDialog = (student, role) => {
selectedStudent.value = student
evaluationDialogVisible.value = true
}
// 打开成果提交对话框
const openSubmissionDialog = (student) => {
selectedStudent.value = student
submissionDialogVisible.value = true
}
// 处理评价提交
const handleEvaluationSubmit = (data) => {
evaluationData[userRole.value][selectedStudent.value.id] = data
ElMessage.success('评价提交成功')
evaluationDialogVisible.value = false
}
// 处理成果提交
const handleSubmissionSubmit = (data) => {
evaluationData.submissions[selectedStudent.value.id] = data
ElMessage.success('成果提交成功')
submissionDialogVisible.value = false
}
// 查看学生画像
const viewPortrait = (student) => {
router.push(`/portrait?studentId=${student.id}`)
}
// 查看评价报告
const viewReport = (student) => {
router.push(`/report/${student.id}`)
}
// 查看成果材料
const viewSubmission = (student) => {
ElMessage.info('查看成果材料功能')
}
// 自我评价
const openSelfEvaluation = (student) => {
ElMessage.info('自我评价功能')
}
// 互评功能
const openPeerEvaluation = (student) => {
ElMessage.info('互评功能')
}
// 批量查看画像
const viewAllPortraits = () => {
router.push('/portrait')
}
// 导出数据
const exportData = () => {
ElMessage.success('评价结果导出成功')
}
return {
// 响应式数据
selectedSemester,
selectedGrade,
selectedClass,
selectedProject,
filteredStudents,
evaluationDialogVisible,
submissionDialogVisible,
selectedStudent,
// 基础数据
semesters,
grades,
classes,
projects,
// 计算属性
userRole,
canExport,
// 方法
filterStudents,
resetFilters,
getEvaluationStatus,
getSubmissionStatus,
openEvaluationDialog,
openSubmissionDialog,
handleEvaluationSubmit,
handleSubmissionSubmit,
viewPortrait,
viewReport,
viewSubmission,
openSelfEvaluation,
openPeerEvaluation,
viewAllPortraits,
exportData
}
}
}
</script>
<style scoped>
.evaluate-page {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: hidden;
background: var(--bg-secondary);
}
.page-header {
background: var(--white-alpha-high);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
padding: var(--spacing-lg) calc(var(--spacing-xxl) * 1.2);
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: 40;
animation: slideInDown 0.5s ease-out;
min-height: 64px;
}
.page-title {
font-size: calc(var(--font-size-xxl) * 1.2);
font-weight: 700;
color: var(--text-primary);
margin: 0;
background: var(--gradient-brand);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-actions {
display: flex;
gap: var(--spacing-sm);
}
.filter-section {
margin: var(--spacing-lg) calc(var(--spacing-xxl) * 1.2) var(--spacing-lg);
animation: slideInUp 0.6s ease-out 0.1s backwards;
}
.filter-row {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-lg);
align-items: end;
}
.filter-item {
display: flex;
flex-direction: column;
min-width: 180px;
gap: var(--spacing-xs);
}
.filter-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
font-weight: 500;
}
.filter-actions {
display: flex;
gap: var(--spacing-sm);
}
.student-list-section {
margin: 0 calc(var(--spacing-xxl) * 1.2) calc(var(--spacing-xxl) * 1.5);
min-height: 400px;
animation: slideInUp 0.6s ease-out 0.2s backwards;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-light);
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin: 0;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.count-badge {
background: var(--primary-lighter);
color: var(--primary);
padding: 2px 8px;
border-radius: 50px;
font-size: var(--font-size-xs);
font-weight: 500;
margin-left: var(--spacing-xs);
}
.list-actions {
display: flex;
gap: var(--spacing-sm);
}
.table-actions {
display: flex;
gap: var(--spacing-xs);
flex-wrap: wrap;
align-items: center;
}
:deep(.el-table) {
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow);
border: 1px solid var(--border);
}
:deep(.el-table th) {
background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(248, 250, 252, 0.8) 100%);
color: var(--text-primary);
font-weight: 600;
font-size: var(--font-size-sm);
border-bottom: 2px solid var(--border);
padding: var(--spacing-md) var(--spacing-sm);
}
:deep(.el-table td) {
padding: var(--spacing-md) var(--spacing-sm);
border-bottom: 1px solid var(--border-light);
transition: var(--transition);
}
:deep(.el-table tr:hover) {
background: rgba(59, 130, 246, 0.02);
}
:deep(.el-button--small) {
padding: 6px 12px;
font-size: var(--font-size-xs);
border-radius: var(--radius-sm);
font-weight: 500;
transition: var(--transition);
min-width: auto;
}
:deep(.el-button--small:hover) {
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-select .el-input__wrapper) {
border-radius: var(--radius-md);
border: 1px solid var(--border);
transition: var(--transition);
}
:deep(.el-select .el-input__wrapper:hover) {
border-color: var(--primary-light);
}
:deep(.el-tag) {
border-radius: var(--radius-sm);
font-weight: 500;
padding: 4px 8px;
font-size: var(--font-size-xs);
}
/* 动画效果 */
@keyframes slideInDown {
from { opacity: 0; transform: translateY(-30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 1024px) {
.page-header {
padding: var(--spacing-md) var(--spacing-lg);
min-height: 56px;
}
.filter-section,
.student-list-section {
margin-left: var(--spacing-lg);
margin-right: var(--spacing-lg);
}
.filter-row {
flex-direction: column;
align-items: stretch;
}
.filter-item {
min-width: auto;
}
.table-actions {
justify-content: flex-start;
gap: 4px;
}
:deep(.el-button--small) {
padding: 4px 8px;
font-size: 11px;
}
:deep(.el-table) {
font-size: var(--font-size-xs);
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: var(--spacing-sm);
align-items: flex-start;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-sm);
}
.table-actions {
flex-direction: column;
align-items: stretch;
width: 100%;
}
:deep(.el-button--small) {
width: 100%;
justify-content: center;
}
}
</style>