ZhangQiPro/src/components/SubmissionDialog.vue
sladro 3c19173c24 feat: 优化BigScreenPortrait大屏页面显示和功能完善
🔧 显示问题修复:
- 修复参与学生人数显示布局问题,优化number-gauge-combined容器
- 调整迷你仪表盘尺寸从60px到45px,适配紧凑布局
- 优化数字显示样式,使用clamp()确保响应式适配
- 完善ECharts仪表盘配置,简化为迷你版本显示

🎯 功能按钮完善:
- 实现"生成报告"和"历史记录"按钮完整功能
- 集成权限验证系统,需要教师权限才能访问
- 添加智能权限提示,未登录时引导用户登录
- 保持按钮原有美观样式,权限不足时仅显示小锁图标

🎨 界面文本优化:
- 清理Login.vue中的"演示"字样,改为"登录"
- 移除版权信息中的"演示版本"标识
- 优化组件中的演示相关文本,提升专业性

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-16 10:23:36 +08:00

582 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>