🔧 显示问题修复: - 修复参与学生人数显示布局问题,优化number-gauge-combined容器 - 调整迷你仪表盘尺寸从60px到45px,适配紧凑布局 - 优化数字显示样式,使用clamp()确保响应式适配 - 完善ECharts仪表盘配置,简化为迷你版本显示 🎯 功能按钮完善: - 实现"生成报告"和"历史记录"按钮完整功能 - 集成权限验证系统,需要教师权限才能访问 - 添加智能权限提示,未登录时引导用户登录 - 保持按钮原有美观样式,权限不足时仅显示小锁图标 🎨 界面文本优化: - 清理Login.vue中的"演示"字样,改为"登录" - 移除版权信息中的"演示版本"标识 - 优化组件中的演示相关文本,提升专业性 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
582 lines
16 KiB
Vue
582 lines
16 KiB
Vue
<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>支持上传项目相关文档、截图、代码文件等,单个文件不超过${FILE_SIZE_LIMIT}</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="var(--warning)"
|
||
score-template="{value}分"
|
||
/>
|
||
</div>
|
||
<div class="rating-item">
|
||
<span class="rating-label">质量:</span>
|
||
<el-rate
|
||
v-model="formData.selfRating.quality"
|
||
show-score
|
||
text-color="var(--warning)"
|
||
score-template="{value}分"
|
||
/>
|
||
</div>
|
||
<div class="rating-item">
|
||
<span class="rating-label">创新性:</span>
|
||
<el-rate
|
||
v-model="formData.selfRating.innovation"
|
||
show-score
|
||
text-color="var(--warning)"
|
||
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'
|
||
|
||
// 常量配置
|
||
const FILE_SIZE_LIMIT = '10MB'
|
||
const FORM_VALIDATION = {
|
||
PROJECT_NAME: { min: 2, max: 100 },
|
||
PROJECT_DESC: { min: 10, max: 800 },
|
||
CONTRIBUTION: { min: 10, max: 600 },
|
||
REFLECTION: { min: 10, max: 600 },
|
||
IMPROVEMENTS: { min: 10, max: 600 }
|
||
}
|
||
const ANIMATION_DELAY = {
|
||
SHORT: 500,
|
||
LONG: 1500
|
||
}
|
||
|
||
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: FORM_VALIDATION.PROJECT_NAME.min, max: FORM_VALIDATION.PROJECT_NAME.max, message: `项目名称长度在${FORM_VALIDATION.PROJECT_NAME.min}-${FORM_VALIDATION.PROJECT_NAME.max}个字符`, trigger: 'blur' }
|
||
],
|
||
projectDescription: [
|
||
{ required: true, message: '请输入项目描述', trigger: 'blur' },
|
||
{ min: FORM_VALIDATION.PROJECT_DESC.min, max: FORM_VALIDATION.PROJECT_DESC.max, message: `项目描述长度在${FORM_VALIDATION.PROJECT_DESC.min}-${FORM_VALIDATION.PROJECT_DESC.max}个字符`, trigger: 'blur' }
|
||
],
|
||
contributions: [
|
||
{ required: true, message: '请描述主要贡献', trigger: 'blur' },
|
||
{ min: FORM_VALIDATION.CONTRIBUTION.min, max: FORM_VALIDATION.CONTRIBUTION.max, message: `贡献描述长度在${FORM_VALIDATION.CONTRIBUTION.min}-${FORM_VALIDATION.CONTRIBUTION.max}个字符`, trigger: 'blur' }
|
||
],
|
||
learnings: [
|
||
{ required: true, message: '请分享收获与感悟', trigger: 'blur' },
|
||
{ min: FORM_VALIDATION.REFLECTION.min, max: FORM_VALIDATION.REFLECTION.max, message: `收获感悟长度在${FORM_VALIDATION.REFLECTION.min}-${FORM_VALIDATION.REFLECTION.max}个字符`, 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) {
|
||
ElMessage.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, ANIMATION_DELAY.SHORT))
|
||
|
||
ElMessage.success('草稿保存成功')
|
||
} catch (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, ANIMATION_DELAY.LONG))
|
||
|
||
emit('submit', submitData)
|
||
|
||
// 清除草稿
|
||
const draftKey = `submission_draft_${props.student.id}`
|
||
localStorage.removeItem(draftKey)
|
||
|
||
submitting.value = false
|
||
} catch (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> |