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:
parent
c6194ae1dd
commit
1efef41ba8
15
CLAUDE.md
15
CLAUDE.md
@ -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设计规范与教训(重要!)
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
485
src/components/EvaluationDialog.vue
Normal file
485
src/components/EvaluationDialog.vue
Normal 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>
|
||||
600
src/components/FileUpload.vue
Normal file
600
src/components/FileUpload.vue
Normal 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>
|
||||
570
src/components/SubmissionDialog.vue
Normal file
570
src/components/SubmissionDialog.vue
Normal 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>
|
||||
@ -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')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -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
847
src/views/Dashboard.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user