fix: 修复导航系统关键问题并完善功能

### 🔧 主要修复
- **导航切换问题**: 添加工作台主页导航入口,修复无法返回仪表盘的问题
- **视觉样式问题**: 移除导航文字下划线,优化选中状态显示
- **路由匹配逻辑**: 修复蓝色指示条不切换的问题,确保选中状态正确显示
- **用户体验优化**: 统一导航交互逻辑,提升操作流畅性

### 📝 详细变更
1. **Home.vue导航优化**:
   - 添加工作台(`/home`)主页导航项
   - 修复CSS中重复的`::before`伪元素冲突
   - 统一使用`exact-active-class`确保精确路由匹配
   - 添加`text-decoration: none`移除链接下划线

2. **路由结构完善**:
   - 优化嵌套路由配置,确保正确的父子关系
   - 修正路由守卫逻辑,提升权限控制精度

3. **Mock数据扩展**:
   - 增强mockData.js,支持更丰富的演示数据
   - 添加评价状态管理和项目详情数据

4. **组件功能完善**:
   - 完善Evaluate.vue评价管理页面
   - 优化样式变量统一性

### 🎯 解决的问题
-  修复点击左侧导航无法切换到仪表盘主页
-  移除"评价管理"等导航文字的下划线
-  修复蓝色指示条不随导航切换的问题
-  解决工作台始终显示透明白色选中效果的问题

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-09-15 11:06:59 +08:00
parent c6194ae1dd
commit 1efef41ba8
11 changed files with 3412 additions and 332 deletions

View File

@ -78,7 +78,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- 图表数据实时生成,确保演示效果
## 当前状态
项目目前只有PRD文档需要从零开始搭建Vue3纯前端演示项目所有数据通过Mock生成。
项目已完成基础架构搭建和核心功能开发:
- ✅ 完成Vue3 + ElementPlus基础框架搭建
- ✅ 完成登录认证系统和角色权限控制
- ✅ 完成仪表盘工作台界面
- ✅ 完成评价管理功能
- ✅ 完成学生画像功能(教师专属)
- ✅ 完成侧边栏导航系统
### 最近更新2025-01-15
修复了导航系统的关键问题:
- 添加了工作台主页导航入口
- 修复了导航选中状态的蓝色指示条显示问题
- 移除了导航文字的下划线样式
- 修复了路由匹配逻辑,确保只有当前页面显示选中状态
## UI设计规范与教训重要

View File

@ -71,8 +71,8 @@
--font-size-xxl: 1.5rem;
/* 页面布局 */
--header-height: 60px;
--sidebar-width: 240px;
--header-height: 72px;
--sidebar-width: 280px;
--container-max-width: 1200px;
/* 渐变色 */
@ -94,11 +94,6 @@
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
--glass-blur: blur(10px);
/* 布局尺寸 */
--sidebar-width: 280px;
--header-height: 72px;
--container-max: 1200px;
/* 动画 */
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);

View File

@ -0,0 +1,485 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="680px"
:before-close="handleClose"
class="evaluation-dialog"
>
<div v-if="student" class="dialog-content">
<!-- 学生信息 -->
<div class="student-info">
<div class="info-row">
<span class="info-label">学生姓名</span>
<span class="info-value">{{ student.name }}</span>
<span class="info-label">学号</span>
<span class="info-value">{{ student.studentId }}</span>
</div>
<div class="info-row">
<span class="info-label">年级班级</span>
<span class="info-value">{{ student.grade }} {{ student.class }}</span>
</div>
</div>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<!-- 企业评价表单 -->
<template v-if="role === 'company'">
<el-form-item label="实践态度" prop="attitude" required>
<el-rate
v-model="formData.attitude"
show-score
text-color="#ff9900"
score-template="实践态度: {value}分"
/>
</el-form-item>
<el-form-item label="专业技能" prop="skills" required>
<el-rate
v-model="formData.skills"
show-score
text-color="#ff9900"
score-template="专业技能: {value}分"
/>
</el-form-item>
<el-form-item label="沟通协作" prop="communication" required>
<el-rate
v-model="formData.communication"
show-score
text-color="#ff9900"
score-template="沟通协作: {value}分"
/>
</el-form-item>
<el-form-item label="问题解决" prop="problemSolving" required>
<el-rate
v-model="formData.problemSolving"
show-score
text-color="#ff9900"
score-template="问题解决: {value}分"
/>
</el-form-item>
<el-form-item label="综合评价" prop="overall" required>
<el-select v-model="formData.overall" placeholder="请选择综合评价">
<el-option label="优秀" value="excellent" />
<el-option label="良好" value="good" />
<el-option label="中等" value="average" />
<el-option label="及格" value="pass" />
<el-option label="不及格" value="fail" />
</el-select>
</el-form-item>
<el-form-item label="企业建议" prop="suggestions">
<el-input
v-model="formData.suggestions"
type="textarea"
:rows="4"
placeholder="请输入对学生的建议和意见..."
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="佐证材料">
<FileUpload @upload="handleFileUpload" />
</el-form-item>
</template>
<!-- 教师评价表单 -->
<template v-if="role === 'teacher'">
<el-form-item label="理论掌握" prop="theory" required>
<el-rate
v-model="formData.theory"
show-score
text-color="#ff9900"
score-template="理论掌握: {value}分"
/>
</el-form-item>
<el-form-item label="实践应用" prop="practice" required>
<el-rate
v-model="formData.practice"
show-score
text-color="#ff9900"
score-template="实践应用: {value}分"
/>
</el-form-item>
<el-form-item label="创新能力" prop="innovation" required>
<el-rate
v-model="formData.innovation"
show-score
text-color="#ff9900"
score-template="创新能力: {value}分"
/>
</el-form-item>
<el-form-item label="学习态度" prop="attitude" required>
<el-rate
v-model="formData.attitude"
show-score
text-color="#ff9900"
score-template="学习态度: {value}分"
/>
</el-form-item>
<el-form-item label="课程成绩" prop="courseGrade" required>
<el-input-number
v-model="formData.courseGrade"
:min="0"
:max="100"
:precision="1"
placeholder="请输入课程成绩"
style="width: 200px"
/>
<span class="unit"></span>
</el-form-item>
<el-form-item label="教师评语" prop="comments">
<el-input
v-model="formData.comments"
type="textarea"
:rows="4"
placeholder="请输入对学生的评价和建议..."
maxlength="500"
show-word-limit
/>
</el-form-item>
</template>
<!-- 专家评价表单 -->
<template v-if="role === 'expert'">
<el-form-item label="行业认知" prop="industryKnowledge" required>
<el-rate
v-model="formData.industryKnowledge"
show-score
text-color="#ff9900"
score-template="行业认知: {value}分"
/>
</el-form-item>
<el-form-item label="技术深度" prop="technicalDepth" required>
<el-rate
v-model="formData.technicalDepth"
show-score
text-color="#ff9900"
score-template="技术深度: {value}分"
/>
</el-form-item>
<el-form-item label="应用能力" prop="applicationAbility" required>
<el-rate
v-model="formData.applicationAbility"
show-score
text-color="#ff9900"
score-template="应用能力: {value}分"
/>
</el-form-item>
<el-form-item label="发展潜力" prop="potential" required>
<el-rate
v-model="formData.potential"
show-score
text-color="#ff9900"
score-template="发展潜力: {value}分"
/>
</el-form-item>
<el-form-item label="专业建议" prop="professionalAdvice">
<el-input
v-model="formData.professionalAdvice"
type="textarea"
:rows="4"
placeholder="请从行业专家角度提供专业建议..."
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="推荐指数" prop="recommendation" required>
<el-select v-model="formData.recommendation" placeholder="请选择推荐指数">
<el-option label="强烈推荐" value="strongly_recommend" />
<el-option label="推荐" value="recommend" />
<el-option label="一般推荐" value="neutral" />
<el-option label="不推荐" value="not_recommend" />
</el-select>
</el-form-item>
</template>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
提交评价
</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import FileUpload from './FileUpload.vue'
export default {
name: 'EvaluationDialog',
components: {
FileUpload
},
props: {
modelValue: {
type: Boolean,
default: false
},
student: {
type: Object,
default: null
},
role: {
type: String,
required: true
}
},
emits: ['update:modelValue', 'submit'],
setup(props, { emit }) {
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const formRef = ref(null)
const submitting = ref(false)
//
const dialogTitle = computed(() => {
const roleNames = {
company: '企业评价',
teacher: '教师评价',
expert: '专家评价'
}
return roleNames[props.role] || '评价'
})
//
const formData = reactive({
//
attitude: 0,
suggestions: '',
//
skills: 0,
communication: 0,
problemSolving: 0,
overall: '',
//
theory: 0,
practice: 0,
innovation: 0,
courseGrade: null,
comments: '',
//
industryKnowledge: 0,
technicalDepth: 0,
applicationAbility: 0,
potential: 0,
professionalAdvice: '',
recommendation: '',
//
files: []
})
//
const rules = reactive({
attitude: [{ required: true, message: '请评价学习态度', trigger: 'change' }],
skills: [{ required: true, message: '请评价专业技能', trigger: 'change' }],
communication: [{ required: true, message: '请评价沟通协作', trigger: 'change' }],
problemSolving: [{ required: true, message: '请评价问题解决能力', trigger: 'change' }],
overall: [{ required: true, message: '请选择综合评价', trigger: 'change' }],
theory: [{ required: true, message: '请评价理论掌握情况', trigger: 'change' }],
practice: [{ required: true, message: '请评价实践应用能力', trigger: 'change' }],
innovation: [{ required: true, message: '请评价创新能力', trigger: 'change' }],
courseGrade: [{ required: true, message: '请输入课程成绩', trigger: 'blur' }],
industryKnowledge: [{ required: true, message: '请评价行业认知水平', trigger: 'change' }],
technicalDepth: [{ required: true, message: '请评价技术深度', trigger: 'change' }],
applicationAbility: [{ required: true, message: '请评价应用能力', trigger: 'change' }],
potential: [{ required: true, message: '请评价发展潜力', trigger: 'change' }],
recommendation: [{ required: true, message: '请选择推荐指数', trigger: 'change' }]
})
//
const resetForm = () => {
Object.keys(formData).forEach(key => {
if (typeof formData[key] === 'number') {
formData[key] = 0
} else if (typeof formData[key] === 'string') {
formData[key] = ''
} else if (Array.isArray(formData[key])) {
formData[key] = []
} else {
formData[key] = null
}
})
}
//
watch(() => props.modelValue, (newVal) => {
if (newVal) {
resetForm()
}
})
//
const handleFileUpload = (files) => {
formData.files = files
}
//
const handleClose = () => {
visible.value = false
}
//
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
submitting.value = true
//
await new Promise(resolve => setTimeout(resolve, 1000))
//
const submitData = {
studentId: props.student.id,
role: props.role,
evaluationData: { ...formData },
timestamp: new Date().toISOString()
}
emit('submit', submitData)
submitting.value = false
} catch (error) {
console.error('表单验证失败:', error)
ElMessage.error('请完善必填项')
submitting.value = false
}
}
return {
visible,
formRef,
submitting,
dialogTitle,
formData,
rules,
handleFileUpload,
handleClose,
handleSubmit
}
}
}
</script>
<style scoped>
.evaluation-dialog {
.student-info {
background: var(--bg-secondary);
padding: var(--spacing-md);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-lg);
}
.info-row {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xs);
&:last-child {
margin-bottom: 0;
}
}
.info-label {
font-weight: 500;
color: var(--text-secondary);
min-width: 80px;
}
.info-value {
color: var(--text-primary);
font-weight: 600;
}
:deep(.el-rate) {
display: flex;
align-items: center;
.el-rate__text {
margin-left: var(--spacing-sm);
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
}
:deep(.el-textarea) {
.el-textarea__inner {
border-radius: var(--radius-md);
}
}
:deep(.el-select) {
width: 100%;
}
.unit {
margin-left: var(--spacing-xs);
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
}
:deep(.el-dialog) {
border-radius: var(--radius-lg);
}
:deep(.el-dialog__header) {
background: var(--bg-secondary);
margin: 0;
padding: var(--spacing-lg);
border-bottom: 1px solid var(--border-light);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
:deep(.el-dialog__title) {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
}
:deep(.el-dialog__body) {
padding: var(--spacing-lg);
}
:deep(.el-dialog__footer) {
padding: var(--spacing-lg);
background: var(--bg-secondary);
border-top: 1px solid var(--border-light);
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
</style>

View File

@ -0,0 +1,600 @@
<template>
<div class="file-upload">
<el-upload
ref="uploadRef"
:file-list="fileList"
:before-upload="beforeUpload"
:on-change="handleChange"
:on-remove="handleRemove"
:on-preview="handlePreview"
:auto-upload="false"
:multiple="multiple"
:accept="accept"
:show-file-list="true"
drag
class="upload-dragger"
>
<div class="upload-content">
<el-icon class="upload-icon"><UploadFilled /></el-icon>
<div class="upload-text">
<p class="upload-primary-text">点击或拖拽文件到此区域上传</p>
<p class="upload-secondary-text">
支持{{ acceptText }}格式单个文件不超过{{ maxSize }}MB
</p>
</div>
</div>
</el-upload>
<!-- 文件列表 -->
<div v-if="fileList.length > 0" class="file-list">
<div class="file-list-header">
<span class="file-count">已选择文件 ({{ fileList.length }})</span>
<el-button
v-if="fileList.length > 0"
type="danger"
icon="Delete"
size="small"
text
@click="clearAll"
>
清空所有
</el-button>
</div>
<div class="file-items">
<div
v-for="(file, index) in fileList"
:key="file.uid"
class="file-item"
:class="{ 'file-error': file.status === 'error' }"
>
<div class="file-info">
<el-icon class="file-icon">
<component :is="getFileIcon(file.name)" />
</el-icon>
<div class="file-details">
<div class="file-name" :title="file.name">{{ file.name }}</div>
<div class="file-meta">
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<span v-if="file.status === 'error'" class="file-error-text">
上传失败
</span>
<span v-else-if="file.status === 'success'" class="file-success-text">
上传成功
</span>
</div>
</div>
</div>
<div class="file-actions">
<el-button
type="primary"
icon="View"
size="small"
text
@click="handlePreview(file)"
>
预览
</el-button>
<el-button
type="danger"
icon="Delete"
size="small"
text
@click="handleRemove(file)"
>
删除
</el-button>
</div>
</div>
</div>
</div>
<!-- 预览对话框 -->
<el-dialog v-model="previewVisible" title="文件预览" width="60%">
<div v-if="previewFile" class="preview-content">
<!-- 图片预览 -->
<div v-if="isImage(previewFile.name)" class="image-preview">
<el-image
:src="previewFile.url"
fit="contain"
style="width: 100%; max-height: 500px"
/>
</div>
<!-- 文本文件预览 -->
<div v-else-if="isText(previewFile.name)" class="text-preview">
<pre>{{ previewContent || '正在加载...' }}</pre>
</div>
<!-- 其他文件显示基本信息 -->
<div v-else class="file-info-preview">
<el-icon class="preview-file-icon">
<component :is="getFileIcon(previewFile.name)" />
</el-icon>
<div class="preview-details">
<h3>{{ previewFile.name }}</h3>
<p>文件大小: {{ formatFileSize(previewFile.size) }}</p>
<p>文件类型: {{ getFileType(previewFile.name) }}</p>
<el-button type="primary" @click="downloadFile(previewFile)">
<el-icon><Download /></el-icon>
下载文件
</el-button>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
UploadFilled,
Document,
Picture,
VideoCamera,
Headset,
Files,
View,
Delete,
Download
} from '@element-plus/icons-vue'
export default {
name: 'FileUpload',
components: {
UploadFilled,
Document,
Picture,
VideoCamera,
Headset,
Files,
View,
Delete,
Download
},
props: {
accept: {
type: String,
default: '.pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.txt,.md'
},
maxSize: {
type: Number,
default: 10 // MB
},
multiple: {
type: Boolean,
default: true
},
modelValue: {
type: Array,
default: () => []
}
},
emits: ['update:modelValue', 'upload'],
setup(props, { emit }) {
const uploadRef = ref(null)
const fileList = ref([])
const previewVisible = ref(false)
const previewFile = ref(null)
const previewContent = ref('')
//
const acceptText = computed(() => {
const types = props.accept.split(',').map(type => type.trim().replace('.', ''))
return types.join('、').toUpperCase()
})
//
watch(() => props.modelValue, (newVal) => {
if (newVal && newVal.length !== fileList.value.length) {
fileList.value = [...newVal]
}
}, { immediate: true })
//
const beforeUpload = (file) => {
//
const isLt10M = file.size / 1024 / 1024 < props.maxSize
if (!isLt10M) {
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB`)
return false
}
//
if (props.accept && props.accept !== '*') {
const acceptTypes = props.accept.split(',').map(type => type.trim())
const fileExtension = '.' + file.name.split('.').pop().toLowerCase()
const isAcceptType = acceptTypes.includes(fileExtension)
if (!isAcceptType) {
ElMessage.error(`不支持 ${fileExtension} 格式的文件`)
return false
}
}
return true
}
//
const handleChange = (file, files) => {
fileList.value = files
//
setTimeout(() => {
file.status = 'success'
file.url = URL.createObjectURL(file.raw)
emitUpdate()
}, 1000)
}
//
const handleRemove = (file) => {
const index = fileList.value.findIndex(item => item.uid === file.uid)
if (index > -1) {
// URL
if (file.url && file.url.startsWith('blob:')) {
URL.revokeObjectURL(file.url)
}
fileList.value.splice(index, 1)
emitUpdate()
}
}
//
const clearAll = () => {
fileList.value.forEach(file => {
if (file.url && file.url.startsWith('blob:')) {
URL.revokeObjectURL(file.url)
}
})
fileList.value = []
if (uploadRef.value) {
uploadRef.value.clearFiles()
}
emitUpdate()
}
//
const handlePreview = async (file) => {
previewFile.value = file
previewVisible.value = true
//
if (isText(file.name) && file.raw) {
try {
const text = await readFileAsText(file.raw)
previewContent.value = text
} catch (error) {
previewContent.value = '无法读取文件内容'
}
}
}
//
const readFileAsText = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.onerror = reject
reader.readAsText(file)
})
}
//
const downloadFile = (file) => {
if (file.url) {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
}
//
const isImage = (filename) => {
const imageTypes = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const ext = '.' + filename.split('.').pop().toLowerCase()
return imageTypes.includes(ext)
}
//
const isText = (filename) => {
const textTypes = ['.txt', '.md', '.json', '.xml', '.csv', '.log']
const ext = '.' + filename.split('.').pop().toLowerCase()
return textTypes.includes(ext)
}
//
const getFileIcon = (filename) => {
const ext = filename.split('.').pop().toLowerCase()
const iconMap = {
//
jpg: 'Picture', jpeg: 'Picture', png: 'Picture', gif: 'Picture', bmp: 'Picture', webp: 'Picture',
//
mp4: 'VideoCamera', avi: 'VideoCamera', mov: 'VideoCamera', wmv: 'VideoCamera',
//
mp3: 'Headset', wav: 'Headset', flac: 'Headset',
//
pdf: 'Document', doc: 'Document', docx: 'Document', ppt: 'Document', pptx: 'Document',
xls: 'Document', xlsx: 'Document', txt: 'Document', md: 'Document'
}
return iconMap[ext] || 'Files'
}
//
const getFileType = (filename) => {
const ext = filename.split('.').pop().toLowerCase()
const typeMap = {
pdf: 'PDF文档',
doc: 'Word文档', docx: 'Word文档',
ppt: 'PPT演示文稿', pptx: 'PPT演示文稿',
xls: 'Excel表格', xlsx: 'Excel表格',
jpg: 'JPEG图片', jpeg: 'JPEG图片',
png: 'PNG图片', gif: 'GIF图片',
txt: '文本文件', md: 'Markdown文档',
zip: 'ZIP压缩包', rar: 'RAR压缩包'
}
return typeMap[ext] || ext.toUpperCase() + '文件'
}
//
const formatFileSize = (bytes) => {
if (!bytes) return '0 B'
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
}
//
const emitUpdate = () => {
const files = fileList.value.map(file => ({
uid: file.uid,
name: file.name,
size: file.size,
url: file.url,
status: file.status,
raw: file.raw
}))
emit('update:modelValue', files)
emit('upload', files)
}
return {
uploadRef,
fileList,
previewVisible,
previewFile,
previewContent,
acceptText,
beforeUpload,
handleChange,
handleRemove,
clearAll,
handlePreview,
downloadFile,
isImage,
isText,
getFileIcon,
getFileType,
formatFileSize
}
}
}
</script>
<style scoped>
.file-upload {
width: 100%;
}
:deep(.el-upload-dragger) {
border: 2px dashed var(--border);
border-radius: var(--radius-lg);
background: var(--bg-primary);
transition: var(--transition);
padding: var(--spacing-xl);
&:hover {
border-color: var(--primary);
background: var(--primary-lighter);
}
}
.upload-content {
text-align: center;
}
.upload-icon {
font-size: 48px;
color: var(--primary);
margin-bottom: var(--spacing-md);
}
.upload-primary-text {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin: 0 0 var(--spacing-xs) 0;
font-weight: 500;
}
.upload-secondary-text {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin: 0;
}
.file-list {
margin-top: var(--spacing-lg);
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-light);
}
.file-count {
font-weight: 500;
color: var(--text-primary);
}
.file-items {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-md);
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
transition: var(--transition);
&:hover {
border-color: var(--primary-light);
box-shadow: var(--shadow-sm);
}
&.file-error {
border-color: var(--danger);
background: rgba(239, 68, 68, 0.05);
}
}
.file-info {
display: flex;
align-items: center;
gap: var(--spacing-md);
flex: 1;
min-width: 0;
}
.file-icon {
font-size: 24px;
color: var(--primary);
flex-shrink: 0;
}
.file-details {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 500;
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
display: flex;
gap: var(--spacing-sm);
font-size: var(--font-size-sm);
}
.file-size {
color: var(--text-secondary);
}
.file-success-text {
color: var(--success);
}
.file-error-text {
color: var(--danger);
}
.file-actions {
display: flex;
gap: var(--spacing-xs);
flex-shrink: 0;
}
.preview-content {
max-height: 60vh;
overflow: auto;
}
.image-preview {
text-align: center;
}
.text-preview {
background: var(--bg-secondary);
padding: var(--spacing-md);
border-radius: var(--radius-md);
font-family: 'Monaco', 'Courier New', monospace;
font-size: var(--font-size-sm);
max-height: 400px;
overflow: auto;
}
.file-info-preview {
text-align: center;
padding: var(--spacing-xl);
}
.preview-file-icon {
font-size: 64px;
color: var(--primary);
margin-bottom: var(--spacing-lg);
}
.preview-details {
h3 {
color: var(--text-primary);
margin-bottom: var(--spacing-md);
word-break: break-all;
}
p {
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.el-button {
margin-top: var(--spacing-md);
}
}
:deep(.el-dialog) {
border-radius: var(--radius-lg);
}
@media (max-width: 768px) {
.file-item {
flex-direction: column;
align-items: stretch;
gap: var(--spacing-md);
}
.file-actions {
justify-content: center;
}
.upload-icon {
font-size: 36px;
}
:deep(.el-upload-dragger) {
padding: var(--spacing-lg);
}
}
</style>

View File

@ -0,0 +1,570 @@
<template>
<el-dialog
v-model="visible"
title="提交实践成果"
width="650px"
:before-close="handleClose"
class="submission-dialog"
>
<div v-if="student" class="dialog-content">
<!-- 学生信息 -->
<div class="student-info">
<div class="info-row">
<span class="info-label">学生姓名</span>
<span class="info-value">{{ student.name }}</span>
<span class="info-label">学号</span>
<span class="info-value">{{ student.studentId }}</span>
</div>
<div class="info-row">
<span class="info-label">年级班级</span>
<span class="info-value">{{ student.grade }} {{ student.class }}</span>
</div>
</div>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="项目名称" prop="projectName" required>
<el-input
v-model="formData.projectName"
placeholder="请输入实践项目名称"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="项目描述" prop="projectDescription" required>
<el-input
v-model="formData.projectDescription"
type="textarea"
:rows="4"
placeholder="请描述项目背景、目标、主要功能等..."
maxlength="800"
show-word-limit
/>
</el-form-item>
<el-form-item label="技术栈" prop="techStack">
<el-select
v-model="formData.techStack"
multiple
filterable
allow-create
placeholder="请选择或输入使用的技术栈"
style="width: 100%"
>
<el-option label="Java" value="Java" />
<el-option label="Python" value="Python" />
<el-option label="JavaScript" value="JavaScript" />
<el-option label="Vue.js" value="Vue.js" />
<el-option label="React" value="React" />
<el-option label="Spring Boot" value="Spring Boot" />
<el-option label="MySQL" value="MySQL" />
<el-option label="Redis" value="Redis" />
<el-option label="Docker" value="Docker" />
<el-option label="Git" value="Git" />
</el-select>
</el-form-item>
<el-form-item label="主要贡献" prop="contributions" required>
<el-input
v-model="formData.contributions"
type="textarea"
:rows="4"
placeholder="请详细描述你在项目中的主要贡献和完成的工作..."
maxlength="600"
show-word-limit
/>
</el-form-item>
<el-form-item label="遇到的问题" prop="challenges">
<el-input
v-model="formData.challenges"
type="textarea"
:rows="3"
placeholder="请描述在项目实践中遇到的主要问题及解决方案..."
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="收获与感悟" prop="learnings" required>
<el-input
v-model="formData.learnings"
type="textarea"
:rows="4"
placeholder="请分享通过本次实践获得的收获、技能提升和感悟..."
maxlength="600"
show-word-limit
/>
</el-form-item>
<el-form-item label="项目成果展示">
<div class="upload-section">
<div class="upload-tips">
<el-icon><InfoFilled /></el-icon>
<span>支持上传项目相关文档截图代码文件等单个文件不超过10MB</span>
</div>
<FileUpload
@upload="handleFileUpload"
:accept="'.pdf,.doc,.docx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.zip,.rar,.txt,.md'"
:max-size="10"
multiple
/>
</div>
</el-form-item>
<el-form-item label="项目链接" prop="projectLinks">
<div class="links-section">
<div class="link-input">
<el-input
v-model="linkInput.github"
placeholder="GitHub仓库链接可选"
prefix-icon="Link"
/>
</div>
<div class="link-input">
<el-input
v-model="linkInput.demo"
placeholder="演示链接(可选)"
prefix-icon="Link"
/>
</div>
<div class="link-input">
<el-input
v-model="linkInput.other"
placeholder="其他相关链接(可选)"
prefix-icon="Link"
/>
</div>
</div>
</el-form-item>
<el-form-item label="自我评分" prop="selfRating" required>
<div class="rating-section">
<div class="rating-item">
<span class="rating-label">完成度</span>
<el-rate
v-model="formData.selfRating.completion"
show-score
text-color="#ff9900"
score-template="{value}分"
/>
</div>
<div class="rating-item">
<span class="rating-label">质量</span>
<el-rate
v-model="formData.selfRating.quality"
show-score
text-color="#ff9900"
score-template="{value}分"
/>
</div>
<div class="rating-item">
<span class="rating-label">创新性</span>
<el-rate
v-model="formData.selfRating.innovation"
show-score
text-color="#ff9900"
score-template="{value}分"
/>
</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button @click="saveDraft" :loading="saving">保存草稿</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
正式提交
</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { InfoFilled, Link } from '@element-plus/icons-vue'
import FileUpload from './FileUpload.vue'
export default {
name: 'SubmissionDialog',
components: {
FileUpload,
InfoFilled,
Link
},
props: {
modelValue: {
type: Boolean,
default: false
},
student: {
type: Object,
default: null
}
},
emits: ['update:modelValue', 'submit'],
setup(props, { emit }) {
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const formRef = ref(null)
const submitting = ref(false)
const saving = ref(false)
//
const formData = reactive({
projectName: '',
projectDescription: '',
techStack: [],
contributions: '',
challenges: '',
learnings: '',
files: [],
selfRating: {
completion: 0,
quality: 0,
innovation: 0
}
})
//
const linkInput = reactive({
github: '',
demo: '',
other: ''
})
//
const rules = reactive({
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '项目名称长度在2-100个字符', trigger: 'blur' }
],
projectDescription: [
{ required: true, message: '请输入项目描述', trigger: 'blur' },
{ min: 10, max: 800, message: '项目描述长度在10-800个字符', trigger: 'blur' }
],
contributions: [
{ required: true, message: '请描述主要贡献', trigger: 'blur' },
{ min: 10, max: 600, message: '贡献描述长度在10-600个字符', trigger: 'blur' }
],
learnings: [
{ required: true, message: '请分享收获与感悟', trigger: 'blur' },
{ min: 10, max: 600, message: '收获感悟长度在10-600个字符', trigger: 'blur' }
],
'selfRating.completion': [
{ required: true, message: '请为完成度评分', trigger: 'change' }
],
'selfRating.quality': [
{ required: true, message: '请为质量评分', trigger: 'change' }
],
'selfRating.innovation': [
{ required: true, message: '请为创新性评分', trigger: 'change' }
]
})
//
const resetForm = () => {
formData.projectName = ''
formData.projectDescription = ''
formData.techStack = []
formData.contributions = ''
formData.challenges = ''
formData.learnings = ''
formData.files = []
formData.selfRating.completion = 0
formData.selfRating.quality = 0
formData.selfRating.innovation = 0
linkInput.github = ''
linkInput.demo = ''
linkInput.other = ''
}
//
watch(() => props.modelValue, (newVal) => {
if (newVal) {
resetForm()
// 稿
loadDraft()
}
})
//
const handleFileUpload = (files) => {
formData.files = files
}
// 稿
const loadDraft = () => {
if (!props.student) return
const draftKey = `submission_draft_${props.student.id}`
const draft = localStorage.getItem(draftKey)
if (draft) {
try {
const draftData = JSON.parse(draft)
Object.assign(formData, draftData.formData)
Object.assign(linkInput, draftData.linkInput)
ElMessage.success('已加载草稿内容')
} catch (error) {
console.error('加载草稿失败:', error)
}
}
}
// 稿
const saveDraft = async () => {
if (!props.student) return
saving.value = true
try {
const draftKey = `submission_draft_${props.student.id}`
const draftData = {
formData: { ...formData },
linkInput: { ...linkInput },
timestamp: new Date().toISOString()
}
localStorage.setItem(draftKey, JSON.stringify(draftData))
//
await new Promise(resolve => setTimeout(resolve, 500))
ElMessage.success('草稿保存成功')
} catch (error) {
console.error('保存草稿失败:', error)
ElMessage.error('草稿保存失败')
} finally {
saving.value = false
}
}
//
const handleClose = () => {
visible.value = false
}
//
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
submitting.value = true
//
const projectLinks = {
github: linkInput.github || null,
demo: linkInput.demo || null,
other: linkInput.other || null
}
//
const submitData = {
studentId: props.student.id,
submissionData: {
...formData,
projectLinks
},
submitted: true,
timestamp: new Date().toISOString()
}
//
await new Promise(resolve => setTimeout(resolve, 1500))
emit('submit', submitData)
// 稿
const draftKey = `submission_draft_${props.student.id}`
localStorage.removeItem(draftKey)
submitting.value = false
} catch (error) {
console.error('表单验证失败:', error)
ElMessage.error('请完善必填项')
submitting.value = false
}
}
return {
visible,
formRef,
submitting,
saving,
formData,
linkInput,
rules,
handleFileUpload,
saveDraft,
handleClose,
handleSubmit
}
}
}
</script>
<style scoped>
.submission-dialog {
.student-info {
background: var(--bg-secondary);
padding: var(--spacing-md);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-lg);
}
.info-row {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xs);
&:last-child {
margin-bottom: 0;
}
}
.info-label {
font-weight: 500;
color: var(--text-secondary);
min-width: 80px;
}
.info-value {
color: var(--text-primary);
font-weight: 600;
}
.upload-section {
.upload-tips {
display: flex;
align-items: center;
gap: var(--spacing-xs);
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-md);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--primary-lighter);
border-radius: var(--radius-sm);
.el-icon {
color: var(--primary);
}
}
}
.links-section {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
.link-input {
display: flex;
align-items: center;
}
}
.rating-section {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
.rating-item {
display: flex;
align-items: center;
gap: var(--spacing-md);
.rating-label {
min-width: 80px;
font-weight: 500;
color: var(--text-secondary);
}
:deep(.el-rate) {
.el-rate__text {
margin-left: var(--spacing-sm);
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
}
}
}
:deep(.el-textarea) {
.el-textarea__inner {
border-radius: var(--radius-md);
}
}
:deep(.el-select) {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
}
:deep(.el-dialog) {
border-radius: var(--radius-lg);
}
:deep(.el-dialog__header) {
background: var(--bg-secondary);
margin: 0;
padding: var(--spacing-lg);
border-bottom: 1px solid var(--border-light);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
:deep(.el-dialog__title) {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
}
:deep(.el-dialog__body) {
padding: var(--spacing-lg);
max-height: 60vh;
overflow-y: auto;
}
:deep(.el-dialog__footer) {
padding: var(--spacing-lg);
background: var(--bg-secondary);
border-top: 1px solid var(--border-light);
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
@media (max-width: 768px) {
.links-section {
.link-input {
flex-direction: column;
align-items: stretch;
}
}
.rating-section {
.rating-item {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-xs);
.rating-label {
min-width: auto;
}
}
}
}
</style>

View File

@ -12,25 +12,30 @@ const routes = [
path: '/home',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: true }
},
{
path: '/evaluate',
name: 'Evaluate',
component: () => import('@/views/Evaluate.vue'),
meta: { requiresAuth: true }
},
{
path: '/portrait',
name: 'Portrait',
component: () => import('@/views/Portrait.vue'),
meta: { requiresAuth: true, role: 'teacher' }
},
{
path: '/report/:studentId',
name: 'Report',
component: () => import('@/views/Report.vue'),
meta: { requiresAuth: true }
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: 'evaluate',
name: 'Evaluate',
component: () => import('@/views/Evaluate.vue')
},
{
path: 'portrait',
name: 'Portrait',
component: () => import('@/views/Portrait.vue'),
meta: { role: 'teacher' }
},
{
path: 'report/:studentId',
name: 'Report',
component: () => import('@/views/Report.vue')
}
]
}
]

View File

@ -1,41 +1,214 @@
// Mock用户数据
export const mockUsers = [
{ username: 'student1', password: '123456', role: 'student', name: '张三' },
{ username: 'teacher1', password: '123456', role: 'teacher', name: '李老师' },
{ username: 'company1', password: '123456', role: 'company', name: '企业代表' },
{ username: 'expert1', password: '123456', role: 'expert', name: '行业专家' }
{ username: 'student1', password: '123456', role: 'student', name: '张三', avatar: '/avatar/student1.jpg' },
{ username: 'teacher1', password: '123456', role: 'teacher', name: '李老师', avatar: '/avatar/teacher1.jpg' },
{ username: 'company1', password: '123456', role: 'company', name: '华为技术有限公司', avatar: '/avatar/company1.jpg' },
{ username: 'expert1', password: '123456', role: 'expert', name: '王专家', avatar: '/avatar/expert1.jpg' },
{ username: 'student2', password: '123456', role: 'student', name: '李四', avatar: '/avatar/student2.jpg' },
{ username: 'student3', password: '123456', role: 'student', name: '王五', avatar: '/avatar/student3.jpg' }
]
// Mock项目选择数据
export const mockOptions = {
semesters: ['2023-2024-1', '2023-2024-2', '2024-2025-1'],
grades: ['2021级', '2022级', '2023级'],
classes: ['计科1班', '计科2班', '软工1班', '软工2班'],
projects: ['XX公司实训', 'YY企业项目', 'ZZ技术实习']
grades: ['2021级', '2022级', '2023级', '2024级'],
classes: ['计科1班', '计科2班', '软工1班', '软工2班', '数媒1班', '人工智能1班'],
projects: [
'华为云计算实训项目',
'腾讯前端开发实习',
'阿里巴巴数据分析项目',
'百度AI算法实践',
'字节跳动移动开发实训',
'美团后端服务开发',
'滴滴出行数据挖掘项目'
]
}
// Mock学生数据
// Mock扩展学生数据
export const mockStudents = [
{ id: 1, name: '张三', studentId: '202101001', grade: '2021级', class: '计科1班' },
{ id: 2, name: '李四', studentId: '202101002', grade: '2021级', class: '计科1班' },
{ id: 3, name: '王五', studentId: '202101003', grade: '2021级', class: '计科1班' },
{ id: 4, name: '赵六', studentId: '202101004', grade: '2021级', class: '计科1班' }
{ id: 1, name: '张三', studentId: '202101001', grade: '2021级', class: '计科1班', phone: '13812345678', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', studentId: '202101002', grade: '2021级', class: '计科1班', phone: '13912345678', email: 'lisi@example.com' },
{ id: 3, name: '王五', studentId: '202101003', grade: '2021级', class: '计科1班', phone: '13712345678', email: 'wangwu@example.com' },
{ id: 4, name: '赵六', studentId: '202101004', grade: '2021级', class: '计科1班', phone: '13612345678', email: 'zhaoliu@example.com' },
{ id: 5, name: '孙七', studentId: '202101005', grade: '2021级', class: '计科1班', phone: '13512345678', email: 'sunqi@example.com' },
{ id: 6, name: '周八', studentId: '202102001', grade: '2021级', class: '计科2班', phone: '13412345678', email: 'zhouba@example.com' },
{ id: 7, name: '吴九', studentId: '202102002', grade: '2021级', class: '计科2班', phone: '13312345678', email: 'wujiu@example.com' },
{ id: 8, name: '郑十', studentId: '202201001', grade: '2022级', class: '软工1班', phone: '13212345678', email: 'zhengshi@example.com' },
{ id: 9, name: '陈一', studentId: '202201002', grade: '2022级', class: '软工1班', phone: '13112345678', email: 'chenyi@example.com' },
{ id: 10, name: '林二', studentId: '202202001', grade: '2022级', class: '软工2班', phone: '13012345678', email: 'liner@example.com' }
]
// Mock评价数据存储
export const mockEvaluationData = {
// 企业评价记录
company: {
1: {
attitude: 4.5,
skills: 4.2,
communication: 4.8,
problemSolving: 4.0,
overall: 'good',
suggestions: '学生表现良好,专业技能扎实,建议加强项目实战经验。',
evaluatedAt: '2024-12-10T10:30:00Z',
evaluatorName: '华为技术有限公司'
},
2: {
attitude: 4.8,
skills: 4.5,
communication: 4.6,
problemSolving: 4.7,
overall: 'excellent',
suggestions: '优秀学生,具备很强的学习能力和解决问题的能力。',
evaluatedAt: '2024-12-09T14:20:00Z',
evaluatorName: '华为技术有限公司'
}
},
// 教师评价记录
teacher: {
1: {
theory: 4.3,
practice: 4.1,
innovation: 3.8,
attitude: 4.5,
courseGrade: 88.5,
comments: '该学生理论基础扎实,实践能力良好,建议在创新思维方面多下功夫。',
evaluatedAt: '2024-12-08T16:45:00Z',
evaluatorName: '李老师'
},
3: {
theory: 4.6,
practice: 4.4,
innovation: 4.2,
attitude: 4.7,
courseGrade: 92.0,
comments: '表现优异,理论与实践结合能力强,具有良好的创新潜质。',
evaluatedAt: '2024-12-07T11:15:00Z',
evaluatorName: '李老师'
}
},
// 专家评价记录
expert: {
2: {
industryKnowledge: 4.2,
technicalDepth: 4.0,
applicationAbility: 4.3,
potential: 4.5,
professionalAdvice: '从行业发展角度看,该学生具备较强的技术基础和学习能力,建议重点关注新兴技术的学习。',
recommendation: 'recommend',
evaluatedAt: '2024-12-06T09:30:00Z',
evaluatorName: '王专家'
}
},
// 学生提交记录
submissions: {
1: {
submitted: true,
projectName: '在线学习管理系统',
projectDescription: '基于Vue.js和Spring Boot开发的在线学习管理系统支持课程管理、作业提交、成绩查询等功能。',
techStack: ['Vue.js', 'Spring Boot', 'MySQL', 'Redis'],
contributions: '负责前端页面开发、用户权限管理模块以及部分后端API开发。完成了用户登录、课程列表、作业管理等核心功能。',
challenges: '在开发过程中遇到了跨域问题和数据库性能优化问题通过配置CORS和添加数据库索引得到解决。',
learnings: '通过本次项目深入学习了前后端分离开发模式掌握了Vue组件化开发和Spring Boot微服务架构提升了团队协作能力。',
selfRating: { completion: 4.2, quality: 4.0, innovation: 3.8 },
submittedAt: '2024-12-05T18:20:00Z'
},
2: {
submitted: true,
projectName: '智能推荐算法研究',
projectDescription: '基于机器学习的电商推荐算法研究与实现,采用协同过滤和内容推荐相结合的混合推荐方式。',
techStack: ['Python', 'TensorFlow', 'Pandas', 'Scikit-learn'],
contributions: '负责推荐算法的设计与实现,包括数据预处理、特征工程、模型训练和效果评估。',
challenges: '数据稀疏性和冷启动问题是主要挑战,通过引入物品特征和用户画像得到缓解。',
learnings: '深入理解了推荐系统的原理和应用,掌握了机器学习在实际业务中的应用方法。',
selfRating: { completion: 4.5, quality: 4.3, innovation: 4.6 },
submittedAt: '2024-12-04T16:15:00Z'
}
}
}
// Mock图表数据生成器
export const generateChartData = () => {
return {
scoreDistribution: [
{ grade: '优秀', count: Math.floor(Math.random() * 10) + 5 },
{ grade: '良好', count: Math.floor(Math.random() * 15) + 10 },
{ grade: '中等', count: Math.floor(Math.random() * 20) + 15 },
{ grade: '及格', count: Math.floor(Math.random() * 10) + 5 },
{ grade: '不及格', count: Math.floor(Math.random() * 5) + 1 }
{ grade: '优秀(90-100)', count: Math.floor(Math.random() * 15) + 8 },
{ grade: '良好(80-89)', count: Math.floor(Math.random() * 20) + 15 },
{ grade: '中等(70-79)', count: Math.floor(Math.random() * 25) + 20 },
{ grade: '及格(60-69)', count: Math.floor(Math.random() * 10) + 5 },
{ grade: '不及格(0-59)', count: Math.floor(Math.random() * 5) + 1 }
],
abilityMatrix: {
professional: Math.floor(Math.random() * 35) + 60,
comprehensive: Math.floor(Math.random() * 35) + 60,
digital: Math.floor(Math.random() * 35) + 60
professional: Math.floor(Math.random() * 30) + 65, // 65-95
comprehensive: Math.floor(Math.random() * 30) + 65, // 65-95
digital: Math.floor(Math.random() * 30) + 65, // 65-95
innovation: Math.floor(Math.random() * 25) + 60, // 60-85
communication: Math.floor(Math.random() * 35) + 60, // 60-95
teamwork: Math.floor(Math.random() * 30) + 65 // 65-95
}
}
}
// Mock统计数据生成器
export const generateStatsData = () => {
return {
todayEvaluations: Math.floor(Math.random() * 20) + 15,
completedTasks: Math.floor(Math.random() * 50) + 120,
pendingTasks: Math.floor(Math.random() * 15) + 8,
completionRate: Math.floor(Math.random() * 20) + 75, // 75-95%
totalStudents: 156,
evaluatedStudents: Math.floor(Math.random() * 30) + 120,
averageScore: (Math.random() * 10 + 80).toFixed(1) // 80-90分
}
}
// Mock项目详细信息
export const mockProjectDetails = {
'huawei-cloud': {
id: 'huawei-cloud',
name: '华为云计算实训项目',
company: '华为技术有限公司',
duration: '3个月',
description: '基于华为云平台的企业级应用开发实训,涵盖云服务架构设计、微服务开发、容器化部署等核心技术。',
requirements: [
'掌握云计算基础概念和核心技术',
'熟悉华为云平台各项服务',
'具备微服务架构设计能力',
'掌握Docker容器化技术',
'具备团队协作和项目管理能力'
],
skills: ['华为云服务', 'Spring Cloud', 'Docker', 'Kubernetes', 'Redis', 'MySQL'],
participants: 25,
status: 'active'
},
'tencent-frontend': {
id: 'tencent-frontend',
name: '腾讯前端开发实习',
company: '腾讯科技有限公司',
duration: '4个月',
description: '参与腾讯核心产品的前端开发工作,学习大型前端项目的架构设计和性能优化技术。',
requirements: [
'熟练掌握Vue.js/React框架',
'了解前端工程化和模块化',
'具备响应式设计能力',
'掌握前端性能优化技术',
'具备良好的代码规范意识'
],
skills: ['Vue.js', 'React', 'TypeScript', 'Webpack', 'Element UI', 'Ant Design'],
participants: 20,
status: 'active'
}
}
// 获取评价状态的辅助函数
export const getEvaluationStatus = (studentId, role) => {
const hasEvaluation = mockEvaluationData[role] && mockEvaluationData[role][studentId]
return hasEvaluation ? 'completed' : 'pending'
}
// 获取学生提交状态的辅助函数
export const getSubmissionStatus = (studentId) => {
const submission = mockEvaluationData.submissions[studentId]
return submission?.submitted ? 'submitted' : 'pending'
}

847
src/views/Dashboard.vue Normal file
View File

@ -0,0 +1,847 @@
<template>
<main class="main-content">
<!-- 顶部状态栏 -->
<header class="top-bar">
<div class="page-title">
<div class="breadcrumb">
<span class="breadcrumb-item">首页</span>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active">工作台</span>
</div>
<p class="welcome-text">下午好{{ user?.name }} 👋</p>
</div>
<div class="header-actions">
<button class="header-icon-btn">
<Search :size="18" />
</button>
<button class="header-icon-btn">
<Bell :size="18" />
<span class="notification-dot"></span>
</button>
<div class="time-display">
{{ new Date().toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric',
weekday: 'short'
}) }}
</div>
</div>
</header>
<!-- 统计卡片 -->
<section class="stats-section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon primary">
<EditPen :size="18" />
</div>
<span class="stat-trend up">+12%</span>
</div>
<div class="stat-content">
<div class="stat-number">24</div>
<div class="stat-label">今日新增评价</div>
<div class="stat-chart">
<span class="mini-bar" style="height: 40%"></span>
<span class="mini-bar" style="height: 60%"></span>
<span class="mini-bar" style="height: 45%"></span>
<span class="mini-bar" style="height: 80%"></span>
<span class="mini-bar" style="height: 65%"></span>
<span class="mini-bar" style="height: 90%"></span>
<span class="mini-bar" style="height: 75%"></span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon success">
<SuccessFilled :size="18" />
</div>
<span class="stat-trend up">+8.5%</span>
</div>
<div class="stat-content">
<div class="stat-number">92.5%</div>
<div class="stat-label">本月完成率</div>
<div class="stat-progress">
<div class="progress-bar" style="width: 92.5%"></div>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon warning">
<Clock :size="18" />
</div>
<span class="stat-trend down">-2</span>
</div>
<div class="stat-content">
<div class="stat-number">8</div>
<div class="stat-label">待审核任务</div>
<div class="stat-list">
<span class="stat-item">实践报告 3</span>
<span class="stat-item">互评 5</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon info">
<TrendCharts :size="18" />
</div>
<span class="stat-trend">稳定</span>
</div>
<div class="stat-content">
<div class="stat-number">65%</div>
<div class="stat-label">学期进度</div>
<div class="stat-timeline">
<span class="timeline-dot active"></span>
<span class="timeline-dot active"></span>
<span class="timeline-dot active"></span>
<span class="timeline-dot"></span>
<span class="timeline-dot"></span>
</div>
</div>
</div>
</div>
</section>
<!-- 功能模块 -->
<section class="modules-section">
<div class="section-header">
<h2 class="section-title">快速访问</h2>
<div class="section-actions">
<button class="action-btn">全部功能</button>
<button class="action-btn primary">新建任务</button>
</div>
</div>
<div class="modules-grid">
<div class="module-card" @click="goToEvaluate">
<div class="module-accent primary"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon primary">
<EditPen :size="18" />
</div>
</div>
<div class="module-badge">核心</div>
</div>
<div class="module-content">
<h3 class="module-title">评价管理</h3>
<p class="module-description">
支持多维度评价体系包含企业实践教师点评同行互评等多角色协同评价流程实时跟踪评价进度智能提醒待办事项
</p>
<div class="module-stats">
<div class="module-stat">
<span class="stat-value">24</span>
<span class="stat-name">待评价</span>
</div>
<div class="module-stat">
<span class="stat-value">156</span>
<span class="stat-name">已完成</span>
</div>
<div class="module-stat">
<span class="stat-value">92%</span>
<span class="stat-name">完成率</span>
</div>
</div>
<div class="module-footer">
<span class="module-action">开始评价</span>
<ArrowRight :size="14" />
</div>
</div>
</div>
<div
v-if="user?.role === 'teacher'"
class="module-card"
@click="goToPortrait"
>
<div class="module-accent secondary"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon secondary">
<TrendCharts :size="18" />
</div>
</div>
<div class="module-badge special">教师专属</div>
</div>
<div class="module-content">
<h3 class="module-title">学生画像</h3>
<p class="module-description">
基于6大维度18项指标的综合能力分析系统通过AI算法生成个性化发展建议帮助教师全面了解学生成长轨迹
</p>
<div class="module-features">
<span class="feature-tag">能力雷达图</span>
<span class="feature-tag">成长曲线</span>
<span class="feature-tag">智能建议</span>
</div>
<div class="module-footer">
<span class="module-action">查看画像</span>
<ArrowRight :size="14" />
</div>
</div>
</div>
<div class="module-card coming-soon">
<div class="module-accent muted"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon muted">
<Document :size="18" />
</div>
</div>
<div class="module-badge coming">即将上线</div>
</div>
<div class="module-content">
<h3 class="module-title">报告中心</h3>
<p class="module-description">
一键生成PDF格式的学期报告实践报告能力认证等多类型文档支持自定义模板批量导出电子签章认证
</p>
<div class="module-preview">
<div class="preview-item">📊 数据报表</div>
<div class="preview-item">📝 评价报告</div>
<div class="preview-item">🎓 能力证书</div>
</div>
<div class="module-footer">
<span class="module-action muted">2024年3月上线</span>
</div>
</div>
</div>
<div class="module-card coming-soon">
<div class="module-accent muted"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon muted">
<DataAnalysis :size="18" />
</div>
</div>
<div class="module-badge coming">开发中</div>
</div>
<div class="module-content">
<h3 class="module-title">数据统计</h3>
<p class="module-description">
实时数据大屏展示多维度统计分析支持数据导出和可视化定制包含学生表现教学质量实践成果等核心指标监控
</p>
<div class="module-preview">
<div class="preview-item">📈 趋势分析</div>
<div class="preview-item">🎯 目标达成</div>
<div class="preview-item">📱 移动大屏</div>
</div>
<div class="module-footer">
<span class="module-action muted">2024年4月上线</span>
</div>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import {
EditPen,
SuccessFilled,
Clock,
TrendCharts,
Document,
DataAnalysis,
ArrowRight,
Search,
Bell
} from '@element-plus/icons-vue'
export default {
name: 'Dashboard',
components: {
EditPen,
SuccessFilled,
Clock,
TrendCharts,
Document,
DataAnalysis,
ArrowRight,
Search,
Bell
},
setup() {
const router = useRouter()
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const goToEvaluate = () => {
router.push('/home/evaluate')
}
const goToPortrait = () => {
router.push('/home/portrait')
}
return {
user,
goToEvaluate,
goToPortrait
}
}
}
</script>
<style scoped>
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.top-bar {
background: rgba(255, 255, 255, 0.98);
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: slideInUp 0.5s ease-out;
min-height: 64px;
}
.header-actions {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.breadcrumb {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-xs);
}
.breadcrumb-item {
font-size: var(--font-size-sm);
color: var(--text-muted);
font-weight: 500;
}
.breadcrumb-item.active {
color: var(--text-primary);
font-weight: 600;
}
.breadcrumb-separator {
color: var(--text-muted);
opacity: 0.5;
}
.welcome-text {
color: var(--text-secondary);
margin: 0;
font-size: var(--font-size-md);
font-weight: 500;
}
.header-icon-btn {
width: 36px;
height: 36px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
position: relative;
color: var(--text-secondary);
}
.header-icon-btn:hover {
background: var(--bg-secondary);
border-color: var(--primary-light);
color: var(--primary);
}
.notification-dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background: var(--danger);
border-radius: 50%;
border: 2px solid var(--bg-primary);
}
.time-display {
color: var(--text-secondary);
font-size: var(--font-size-sm);
background: var(--bg-secondary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
border: 1px solid var(--border);
font-weight: 500;
}
/* 统计卡片 */
.stats-section {
padding: calc(var(--spacing-xl) * 1.5) calc(var(--spacing-xxl) * 1.2);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-lg);
}
.stat-card {
background: var(--bg-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-md);
border: 1px solid var(--border);
transition: var(--transition-slow);
position: relative;
overflow: hidden;
animation: slideInUp 0.6s ease-out backwards;
min-height: 140px;
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.stat-trend {
font-size: var(--font-size-xs);
font-weight: 600;
padding: 2px 6px;
border-radius: var(--radius-sm);
background: rgba(16, 185, 129, 0.1);
color: var(--success);
}
.stat-trend.up {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
}
.stat-trend.down {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
.stat-content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.stat-card:nth-child(1) { animation-delay: 0.1s; }
.stat-card:nth-child(2) { animation-delay: 0.2s; }
.stat-card:nth-child(3) { animation-delay: 0.3s; }
.stat-card:nth-child(4) { animation-delay: 0.4s; }
.stat-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
.stat-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.stat-icon.primary {
background: var(--gradient-card-primary);
}
.stat-icon.success {
background: var(--gradient-card-success);
}
.stat-icon.warning {
background: var(--gradient-card-warning);
}
.stat-icon.info {
background: var(--gradient-card-secondary);
}
.stat-number {
font-size: calc(var(--font-size-xxl) * 1.8);
font-weight: 700;
color: var(--text-primary);
line-height: 1;
}
.stat-label {
color: var(--text-secondary);
font-size: var(--font-size-sm);
font-weight: 500;
}
.stat-chart {
display: flex;
align-items: flex-end;
gap: 3px;
height: 30px;
margin-top: auto;
}
.mini-bar {
flex: 1;
background: var(--primary-lighter);
border-radius: 2px 2px 0 0;
transition: var(--transition);
}
.stat-card:hover .mini-bar {
background: var(--primary-light);
}
.stat-progress {
height: 4px;
background: var(--gray-lighter);
border-radius: 2px;
overflow: hidden;
margin-top: auto;
}
.progress-bar {
height: 100%;
background: var(--gradient-primary);
border-radius: 2px;
transition: var(--transition);
}
.stat-list {
display: flex;
gap: var(--spacing-md);
font-size: var(--font-size-xs);
color: var(--text-muted);
}
.stat-timeline {
display: flex;
gap: var(--spacing-xs);
margin-top: auto;
}
.timeline-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--gray-lighter);
}
.timeline-dot.active {
background: var(--primary);
}
/* 功能模块 */
.modules-section {
padding: 0 calc(var(--spacing-xxl) * 1.2) calc(var(--spacing-xxl) * 1.5);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.section-title {
font-size: calc(var(--font-size-xl) * 1.3);
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.5px;
}
.section-actions {
display: flex;
gap: var(--spacing-sm);
}
.action-btn {
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-secondary);
font-size: var(--font-size-sm);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.action-btn:hover {
background: var(--bg-secondary);
border-color: var(--primary-light);
color: var(--text-primary);
}
.action-btn.primary {
background: var(--gradient-primary);
color: white;
border: none;
}
.action-btn.primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.modules-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: var(--spacing-lg);
}
.module-card {
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 0;
box-shadow: var(--shadow);
cursor: pointer;
transition: var(--transition-slow);
position: relative;
overflow: hidden;
animation: slideInUp 0.7s ease-out backwards;
min-height: 260px;
display: flex;
flex-direction: column;
}
.module-accent {
height: 3px;
width: 100%;
position: relative;
overflow: hidden;
}
.module-accent.primary {
background: var(--gradient-primary);
}
.module-accent.secondary {
background: var(--gradient-card-secondary);
}
.module-accent.muted {
background: var(--gray-light);
}
.module-card:nth-child(1) { animation-delay: 0.1s; }
.module-card:nth-child(2) { animation-delay: 0.2s; }
.module-card:nth-child(3) { animation-delay: 0.3s; }
.module-card:nth-child(4) { animation-delay: 0.4s; }
.module-card:hover {
transform: translateY(-8px);
box-shadow: var(--shadow-xl);
border-color: var(--primary-light);
}
.module-card.coming-soon {
opacity: 0.5;
cursor: not-allowed;
background: linear-gradient(135deg, rgba(243, 244, 246, 0.9) 0%, rgba(229, 231, 235, 0.9) 100%);
}
.module-card.coming-soon:hover {
transform: none;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
border-color: rgba(226, 232, 240, 0.6);
}
.module-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md) var(--spacing-lg) 0;
}
.module-content {
padding: var(--spacing-md) var(--spacing-lg) var(--spacing-lg);
flex: 1;
display: flex;
flex-direction: column;
}
.module-icon-wrapper {
position: relative;
}
.module-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
transition: var(--transition-spring);
font-size: 18px;
}
.module-icon.primary {
background: var(--gradient-card-primary);
}
.module-icon.secondary {
background: var(--gradient-card-secondary);
}
.module-icon.muted {
background: var(--gray-light);
}
.module-card:hover .module-icon {
transform: scale(1.1) rotate(5deg);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
}
.module-badge {
background: var(--primary-lighter);
color: var(--primary);
padding: 5px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: var(--shadow-sm);
}
.module-badge.special {
background: linear-gradient(135deg, var(--secondary-light) 0%, var(--secondary) 100%);
color: white;
}
.module-badge.coming {
background: var(--gray-lighter);
color: var(--gray);
border: 1px solid var(--border);
}
.module-title {
font-size: calc(var(--font-size-lg) * 1.2);
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
letter-spacing: -0.3px;
}
.module-description {
color: var(--text-secondary);
font-size: var(--font-size-sm);
line-height: 1.6;
margin-bottom: var(--spacing-lg);
flex: 1;
}
.module-stats {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
padding: var(--spacing-md) 0;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
.module-stat {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-value {
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--text-primary);
}
.stat-name {
font-size: var(--font-size-xs);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.module-features {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.feature-tag {
background: var(--bg-secondary);
color: var(--text-secondary);
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
border: 1px solid var(--border);
}
.module-preview {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.preview-item {
font-size: var(--font-size-sm);
color: var(--text-secondary);
padding-left: var(--spacing-md);
}
.module-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
}
.module-action {
color: var(--primary);
font-weight: 500;
font-size: var(--font-size-sm);
transition: var(--transition);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.module-action.muted {
color: var(--text-muted);
opacity: 0.6;
}
@keyframes slideInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@ -1,9 +1,638 @@
<template>
<div>评价页面 - 开发中</div>
<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'
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>
</script>
<style scoped>
.evaluate-page {
padding: var(--spacing-lg);
min-height: 100vh;
background: var(--bg-secondary);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-xl);
padding-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--border-light);
}
.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-bottom: var(--spacing-lg);
}
.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 {
min-height: 400px;
}
.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;
}
:deep(.el-table) {
border-radius: var(--radius-md);
overflow: hidden;
}
:deep(.el-table th) {
background: var(--bg-secondary);
color: var(--text-primary);
font-weight: 600;
}
:deep(.el-button--small) {
padding: 4px 8px;
font-size: var(--font-size-xs);
}
:deep(.el-select) {
width: 100%;
}
@media (max-width: 1024px) {
.filter-row {
flex-direction: column;
align-items: stretch;
}
.filter-item {
min-width: auto;
}
.table-actions {
justify-content: flex-start;
}
}
</style>

View File

@ -24,10 +24,23 @@
<nav class="nav-menu">
<div class="nav-group">
<div class="nav-title">工作台</div>
<button
<router-link
to="/home"
class="nav-item"
:class="{ active: $route.path === '/evaluate' }"
@click="goToEvaluate"
exact-active-class="active"
>
<div class="nav-icon">
<TrendCharts :size="16" />
</div>
<div class="nav-content">
<span class="nav-label">工作台</span>
<span class="nav-hint">总览仪表盘</span>
</div>
</router-link>
<router-link
to="/home/evaluate"
class="nav-item"
exact-active-class="active"
>
<div class="nav-icon">
<EditPen :size="16" />
@ -37,12 +50,12 @@
<span class="nav-hint">24个待处理</span>
</div>
<div class="nav-badge">24</div>
</button>
<button
</router-link>
<router-link
v-if="user?.role === 'teacher'"
to="/home/portrait"
class="nav-item"
:class="{ active: $route.path === '/portrait' }"
@click="goToPortrait"
exact-active-class="active"
>
<div class="nav-icon">
<TrendCharts :size="16" />
@ -51,7 +64,7 @@
<span class="nav-label">学生画像</span>
<span class="nav-hint">156名学生</span>
</div>
</button>
</router-link>
<button class="nav-item">
<div class="nav-icon">
<Document :size="16" />
@ -90,243 +103,9 @@
</aside>
<!-- 主内容区 -->
<main class="main-content">
<!-- 顶部状态栏 -->
<header class="top-bar">
<div class="page-title">
<div class="breadcrumb">
<span class="breadcrumb-item">首页</span>
<span class="breadcrumb-separator">/</span>
<span class="breadcrumb-item active">工作台</span>
</div>
<p class="welcome-text">下午好{{ user?.name }} 👋</p>
</div>
<div class="header-actions">
<button class="header-icon-btn">
<Search :size="18" />
</button>
<button class="header-icon-btn">
<Bell :size="18" />
<span class="notification-dot"></span>
</button>
<div class="time-display">
{{ new Date().toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric',
weekday: 'short'
}) }}
</div>
</div>
</header>
<!-- 统计卡片 -->
<section class="stats-section">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon primary">
<EditPen :size="18" />
</div>
<span class="stat-trend up">+12%</span>
</div>
<div class="stat-content">
<div class="stat-number">24</div>
<div class="stat-label">今日新增评价</div>
<div class="stat-chart">
<span class="mini-bar" style="height: 40%"></span>
<span class="mini-bar" style="height: 60%"></span>
<span class="mini-bar" style="height: 45%"></span>
<span class="mini-bar" style="height: 80%"></span>
<span class="mini-bar" style="height: 65%"></span>
<span class="mini-bar" style="height: 90%"></span>
<span class="mini-bar" style="height: 75%"></span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon success">
<CheckCircle :size="18" />
</div>
<span class="stat-trend up">+8.5%</span>
</div>
<div class="stat-content">
<div class="stat-number">92.5%</div>
<div class="stat-label">本月完成率</div>
<div class="stat-progress">
<div class="progress-bar" style="width: 92.5%"></div>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon warning">
<Clock :size="18" />
</div>
<span class="stat-trend down">-2</span>
</div>
<div class="stat-content">
<div class="stat-number">8</div>
<div class="stat-label">待审核任务</div>
<div class="stat-list">
<span class="stat-item">实践报告 3</span>
<span class="stat-item">互评 5</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div class="stat-icon info">
<TrendUp :size="18" />
</div>
<span class="stat-trend">稳定</span>
</div>
<div class="stat-content">
<div class="stat-number">65%</div>
<div class="stat-label">学期进度</div>
<div class="stat-timeline">
<span class="timeline-dot active"></span>
<span class="timeline-dot active"></span>
<span class="timeline-dot active"></span>
<span class="timeline-dot"></span>
<span class="timeline-dot"></span>
</div>
</div>
</div>
</div>
</section>
<!-- 功能模块 -->
<section class="modules-section">
<div class="section-header">
<h2 class="section-title">快速访问</h2>
<div class="section-actions">
<button class="action-btn">全部功能</button>
<button class="action-btn primary">新建任务</button>
</div>
</div>
<div class="modules-grid">
<div class="module-card" @click="goToEvaluate">
<div class="module-accent primary"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon primary">
<EditPen :size="18" />
</div>
</div>
<div class="module-badge">核心</div>
</div>
<div class="module-content">
<h3 class="module-title">评价管理</h3>
<p class="module-description">
支持多维度评价体系包含企业实践教师点评同行互评等多角色协同评价流程实时跟踪评价进度智能提醒待办事项
</p>
<div class="module-stats">
<div class="module-stat">
<span class="stat-value">24</span>
<span class="stat-name">待评价</span>
</div>
<div class="module-stat">
<span class="stat-value">156</span>
<span class="stat-name">已完成</span>
</div>
<div class="module-stat">
<span class="stat-value">92%</span>
<span class="stat-name">完成率</span>
</div>
</div>
<div class="module-footer">
<span class="module-action">开始评价</span>
<ArrowRight :size="14" />
</div>
</div>
</div>
<div
v-if="user?.role === 'teacher'"
class="module-card"
@click="goToPortrait"
>
<div class="module-accent secondary"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon secondary">
<TrendCharts :size="18" />
</div>
</div>
<div class="module-badge special">教师专属</div>
</div>
<div class="module-content">
<h3 class="module-title">学生画像</h3>
<p class="module-description">
基于6大维度18项指标的综合能力分析系统通过AI算法生成个性化发展建议帮助教师全面了解学生成长轨迹
</p>
<div class="module-features">
<span class="feature-tag">能力雷达图</span>
<span class="feature-tag">成长曲线</span>
<span class="feature-tag">智能建议</span>
</div>
<div class="module-footer">
<span class="module-action">查看画像</span>
<ArrowRight :size="14" />
</div>
</div>
</div>
<div class="module-card coming-soon">
<div class="module-accent muted"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon muted">
<Document :size="18" />
</div>
</div>
<div class="module-badge coming">即将上线</div>
</div>
<div class="module-content">
<h3 class="module-title">报告中心</h3>
<p class="module-description">
一键生成PDF格式的学期报告实践报告能力认证等多类型文档支持自定义模板批量导出电子签章认证
</p>
<div class="module-preview">
<div class="preview-item">📊 数据报表</div>
<div class="preview-item">📝 评价报告</div>
<div class="preview-item">🎓 能力证书</div>
</div>
<div class="module-footer">
<span class="module-action muted">2024年3月上线</span>
</div>
</div>
</div>
<div class="module-card coming-soon">
<div class="module-accent muted"></div>
<div class="module-header">
<div class="module-icon-wrapper">
<div class="module-icon muted">
<DataAnalysis :size="18" />
</div>
</div>
<div class="module-badge coming">开发中</div>
</div>
<div class="module-content">
<h3 class="module-title">数据统计</h3>
<p class="module-description">
实时数据大屏展示多维度统计分析支持数据导出和可视化定制包含学生表现教学质量实践成果等核心指标监控
</p>
<div class="module-preview">
<div class="preview-item">📈 趋势分析</div>
<div class="preview-item">🎯 目标达成</div>
<div class="preview-item">📱 移动大屏</div>
</div>
<div class="module-footer">
<span class="module-action muted">2024年4月上线</span>
</div>
</div>
</div>
</div>
</section>
</main>
<div class="content-area">
<router-view />
</div>
</div>
</template>
@ -354,13 +133,6 @@ export default {
return roleMap[role] || role
}
const goToEvaluate = () => {
router.push('/evaluate')
}
const goToPortrait = () => {
router.push('/portrait')
}
const handleLogout = () => {
authStore.logout()
@ -371,8 +143,6 @@ export default {
return {
user,
getRoleText,
goToEvaluate,
goToPortrait,
handleLogout
}
}
@ -634,6 +404,7 @@ export default {
transition: var(--transition);
margin-bottom: var(--spacing-xs);
position: relative;
text-decoration: none;
}
.nav-icon {
@ -697,12 +468,13 @@ export default {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
top: 50%;
transform: translateY(-50%) scaleY(0);
width: 3px;
background: var(--gradient-primary);
transform: scaleY(0);
transition: transform 0.3s;
height: 20px;
background: var(--primary-light);
border-radius: 0 2px 2px 0;
transition: transform 0.3s ease;
}
.nav-item:hover {
@ -721,15 +493,7 @@ export default {
}
.nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
background: var(--primary-light);
border-radius: 0 2px 2px 0;
transform: translateY(-50%) scaleY(1);
}
.nav-item.active .nav-icon {
@ -795,13 +559,12 @@ export default {
}
/* 主内容区 */
.main-content {
.content-area {
flex: 1;
margin-left: 320px;
display: flex;
flex-direction: column;
min-height: 100vh;
overflow-x: hidden;
background: var(--bg-secondary);
position: relative;
}
.top-bar {

View File

@ -6,7 +6,7 @@
<!-- Logo和标题 -->
<div class="brand-header">
<div class="logo">
<School :size="48" />
<School :size="24" />
</div>
<h1 class="brand-title">实践教学评价平台</h1>
<p class="brand-subtitle">智能化多角色协同评价系统</p>
@ -241,8 +241,8 @@ export default {
}
.logo {
width: 80px;
height: 80px;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.1);
border-radius: var(--radius-xl);
display: flex;
@ -254,7 +254,7 @@ export default {
}
.brand-title {
font-size: calc(var(--font-size-xxl) * 2);
font-size: calc(var(--font-size-xxl) * 1.3);
font-weight: 800;
margin-bottom: var(--spacing-md);
line-height: 1.2;
@ -289,7 +289,7 @@ export default {
}
.stat-number {
font-size: calc(var(--font-size-xxl) * 1.5);
font-size: var(--font-size-xxl);
font-weight: 700;
margin-bottom: var(--spacing-xs);
color: white;
@ -316,7 +316,7 @@ export default {
/* 右侧登录区域 */
.login-section {
flex: 0 0 480px;
flex: 0 0 450px;
background: var(--bg-primary);
display: flex;
align-items: center;
@ -335,7 +335,7 @@ export default {
}
.login-header h2 {
font-size: calc(var(--font-size-xxl) * 1.5);
font-size: var(--font-size-xxl);
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
@ -370,7 +370,7 @@ export default {
.login-btn {
width: 100%;
height: 52px;
height: 44px;
background: var(--gradient-primary);
border: none;
border-radius: var(--radius-lg);
@ -458,7 +458,7 @@ export default {
/* 响应式设计 */
@media (max-width: 1024px) {
.login-section {
flex: 0 0 400px;
flex: 0 0 380px;
}
}
@ -468,7 +468,7 @@ export default {
}
.brand-section {
flex: 0 0 300px;
flex: 0 0 280px;
}
.stats-grid {