chore(frontend): 移除孤儿路由 /upload 与 Upload.vue(BACKLOG B2)
定性:/upload 是全仓零 UI 入口的孤儿路由。
- Dashboard.enterContest() 对所有组别统一 router.push('/competition');
全仓 push('/upload') / replace('/upload') / to="/upload" / name:'Upload' 0 命中,
Upload.vue 只被 router 一处 import。
- 本科/教师的答案文件上传链路早已由 Competition.vue 的 exam-upload-mode 完整承担
(zip 独占 / pdf-docx 互斥 / 组合校验 / 覆盖式重传 / 删除)。
- 该页对中职/高职会调用后端不支持的分组接口
(GET /api/upload/{zhongzhi,gaozhi}/status|submissions → 「无效的组别,仅支持 benke/jiaoshi」),
因守卫只弹 warning 且注释明写"不强制跳转",随后仍无条件发请求 ⇒ 3 条 console 报错。
处置(三方案中的 ①;②接线会使 Competition.vue 上传模式被孤立,属功能倒退;③被①涵盖):
- src/router/index.js: 删 import Upload 与 /upload 路由块(-9 行)
- src/views/Upload.vue: git rm(-735 行)
- 保留 src/api/upload.js:stores/exam.js 与 Competition.vue 仍在用
getUploadStatus / uploadWork / deleteWork / getSubmissions
验证:
- 前置:全项目(docs/deploy/nginx/bat/ps1/后端)零外部引用;Upload.vue 无具名导出
- vite build exit 0;dist 异步分片列表已无 Upload-*.js(残留 upload-*.js 是 MD5 库分片,0 页面文案)
- TDZ 扫描 37 文件 PASS(原 38,正好少一)
- 真浏览器:/login 9/9 · /upload 现落 404 页且 0 pageerror / 0 console error
(修复前:渲染旧上传页 + 3 条 console 报错)
This commit is contained in:
@@ -16,7 +16,6 @@ const Competition = () => import('@/views/Competition.vue')
|
||||
const MockExam = () => import('@/views/MockExam.vue')
|
||||
const NmeaParser = () => import('@/views/NmeaParser.vue')
|
||||
const AtSimulator = () => import('@/views/AtSimulator.vue')
|
||||
const Upload = () => import('@/views/Upload.vue')
|
||||
const Materials = () => import('@/views/Materials.vue')
|
||||
const Score = () => import('@/views/Score.vue')
|
||||
|
||||
@@ -106,14 +105,6 @@ const routes = [
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
|
||||
// 作品上传页
|
||||
{
|
||||
path: '/upload',
|
||||
name: 'Upload',
|
||||
component: Upload,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
|
||||
// 资料下载页
|
||||
{
|
||||
path: '/materials',
|
||||
|
||||
@@ -1,735 +0,0 @@
|
||||
<template>
|
||||
<div class="upload-page">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="upload-header">
|
||||
<div class="upload-header-left">
|
||||
<el-button text @click="$router.push('/')" class="back-btn">
|
||||
<el-icon><HomeFilled /></el-icon><span>返回首页</span>
|
||||
</el-button>
|
||||
<el-button text @click="$router.push('/dashboard')" class="back-btn">
|
||||
<el-icon><ArrowLeft /></el-icon><span>返回工作台</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<h2 class="upload-title">作品提交</h2>
|
||||
<span class="group-tag">{{ groupLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div class="upload-content">
|
||||
<!-- 截止时间 -->
|
||||
<div class="deadline-card" v-if="deadline">
|
||||
<div class="deadline-info">
|
||||
<el-icon :size="20"><Clock /></el-icon>
|
||||
<span>提交截止时间:</span>
|
||||
<strong :class="{ 'deadline-near': isDeadlineNear }">{{ formatDeadline(deadline) }}</strong>
|
||||
</div>
|
||||
<div class="deadline-countdown" v-if="deadlineCountdown > 0">
|
||||
<span>剩余时间:</span>
|
||||
<strong>{{ formatCountdown(deadlineCountdown) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 教师组双上传区 -->
|
||||
<template v-if="isTeacher">
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="upload-card">
|
||||
<h3 class="upload-card-title">教学方案</h3>
|
||||
<p class="upload-card-desc">请上传教学方案文件(PDF、Word 格式)</p>
|
||||
<file-upload-area
|
||||
group="jiaoshi"
|
||||
file-type="word"
|
||||
:max-size="100 * 1024 * 1024"
|
||||
accept=".pdf,.doc,.docx"
|
||||
@upload-success="onUploadSuccess('word', $event)"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="upload-card">
|
||||
<h3 class="upload-card-title">说课 PPT</h3>
|
||||
<p class="upload-card-desc">请上传说课 PPT 文件</p>
|
||||
<file-upload-area
|
||||
group="jiaoshi"
|
||||
file-type="ppt"
|
||||
:max-size="100 * 1024 * 1024"
|
||||
accept=".ppt,.pptx"
|
||||
@upload-success="onUploadSuccess('ppt', $event)"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 本科组双上传区(Word + PPT) -->
|
||||
<template v-else>
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="upload-card">
|
||||
<h3 class="upload-card-title">参赛作品(Word)</h3>
|
||||
<p class="upload-card-desc">请上传参赛作品文档(PDF、Word 格式)</p>
|
||||
<file-upload-area
|
||||
:group="userGroup"
|
||||
file-type="word"
|
||||
:max-size="50 * 1024 * 1024"
|
||||
accept=".pdf,.doc,.docx"
|
||||
@upload-success="onUploadSuccess('word', $event)"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="upload-card">
|
||||
<h3 class="upload-card-title">参赛作品(PPT)</h3>
|
||||
<p class="upload-card-desc">请上传参赛作品演示文稿(PPT 格式)</p>
|
||||
<file-upload-area
|
||||
:group="userGroup"
|
||||
file-type="ppt"
|
||||
:max-size="50 * 1024 * 1024"
|
||||
accept=".ppt,.pptx"
|
||||
@upload-success="onUploadSuccess('ppt', $event)"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 上传历史 -->
|
||||
<div class="history-card">
|
||||
<h3 class="history-title">提交记录</h3>
|
||||
<div v-loading="historyLoading">
|
||||
<el-table :data="submissionHistory" stripe empty-text="暂无提交记录">
|
||||
<el-table-column prop="fileName" label="文件名" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="fileSize" label="大小" width="100">
|
||||
<template #default="{ row }">{{ formatSize(row.fileSize) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="170">
|
||||
<template #default="{ row }">{{ formatDate(row.uploadTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
text
|
||||
type="danger"
|
||||
size="small"
|
||||
:loading="deletingId === row.id"
|
||||
v-guard="() => deleteSubmission(row)"
|
||||
>删除</el-button>
|
||||
<el-button text type="primary" size="small" @click="reupload">重新上传</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* Upload.vue - 作品上传页面(需要登录,benke/jiaoshi 组)
|
||||
*
|
||||
* 功能:
|
||||
* 1. 截止时间倒计时
|
||||
* 2. 拖拽上传区域
|
||||
* 3. 文件类型/大小校验
|
||||
* 4. 分片上传 + 进度条
|
||||
* 5. MD5 哈希显示
|
||||
* 6. 上传历史列表
|
||||
* 7. 教师组双上传区(教学方案 + 说课PPT)
|
||||
*/
|
||||
import { ref, computed, onMounted, onUnmounted, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox, ElUpload, ElProgress, ElButton, ElTag, ElIcon } from 'element-plus'
|
||||
import { ArrowLeft, HomeFilled, Clock, UploadFilled, Delete } from '@element-plus/icons-vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import * as uploadApi from '@/api/upload'
|
||||
import { chunkFile, calculateMd5, uploadWithRetry, formatFileSize } from '@/utils/upload'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
const userGroup = computed(() => authStore.userGroup)
|
||||
|
||||
const groupLabel = computed(() => {
|
||||
const map = { benke: '本科组', jiaoshi: '教师组' }
|
||||
return map[userGroup.value] || userGroup.value
|
||||
})
|
||||
|
||||
const isTeacher = computed(() => userGroup.value === 'jiaoshi')
|
||||
|
||||
const isDeadlineNear = computed(() => {
|
||||
if (!deadline.value) return false
|
||||
const diff = new Date(deadline.value) - new Date()
|
||||
return diff > 0 && diff < 3 * 24 * 60 * 60 * 1000
|
||||
})
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
const deadline = ref('')
|
||||
const deadlineCountdown = ref(0)
|
||||
const submissionHistory = ref([])
|
||||
const historyLoading = ref(false)
|
||||
const deletingId = ref(null)
|
||||
let deadlineTimer = null
|
||||
|
||||
// ==================== 文件上传子组件(内联实现) ====================
|
||||
|
||||
// 使用简单的内联上传逻辑替代子组件
|
||||
const uploadStates = reactive({})
|
||||
|
||||
// ==================== 方法 ====================
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return dateStr
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function formatDeadline(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return dateStr
|
||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日 ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
return formatFileSize(bytes || 0)
|
||||
}
|
||||
|
||||
function formatCountdown(seconds) {
|
||||
const d = Math.floor(seconds / 86400)
|
||||
const h = Math.floor((seconds % 86400) / 3600)
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
const s = seconds % 60
|
||||
if (d > 0) return `${d}天 ${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** 启动截止倒计时 */
|
||||
function startDeadlineCountdown() {
|
||||
clearInterval(deadlineTimer)
|
||||
if (!deadline.value) return
|
||||
deadlineTimer = setInterval(() => {
|
||||
const diff = Math.floor((new Date(deadline.value) - new Date()) / 1000)
|
||||
deadlineCountdown.value = Math.max(0, diff)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/** 获取提交历史 */
|
||||
async function fetchHistory() {
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const data = await uploadApi.getSubmissions(userGroup.value)
|
||||
if (Array.isArray(data)) {
|
||||
submissionHistory.value = data
|
||||
} else if (data?.list) {
|
||||
submissionHistory.value = data.list
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取提交历史失败', err)
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取上传状态 */
|
||||
async function fetchUploadStatus() {
|
||||
try {
|
||||
const data = await uploadApi.getUploadStatus(userGroup.value)
|
||||
if (data?.deadline) {
|
||||
deadline.value = data.deadline
|
||||
startDeadlineCountdown()
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取上传状态失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
function onUploadSuccess(type, result) {
|
||||
ElMessage.success('文件上传成功')
|
||||
fetchHistory()
|
||||
}
|
||||
|
||||
/** 删除已上传的作品文件(本科/教师组) */
|
||||
async function deleteSubmission(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除「${row.fileName}」吗?删除后可重新上传进行替换。`,
|
||||
'删除确认',
|
||||
{ confirmButtonText: '确认删除', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
deletingId.value = row.id
|
||||
try {
|
||||
await uploadApi.deleteWork(userGroup.value, row.id)
|
||||
ElMessage.success('文件已删除')
|
||||
fetchHistory()
|
||||
} catch (err) {
|
||||
ElMessage.error('删除失败,请重试')
|
||||
} finally {
|
||||
deletingId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 滚动到上传区以便重新上传 */
|
||||
function reupload() {
|
||||
const el = document.querySelector('.upload-card')
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
|
||||
// ==================== 生命周期 ====================
|
||||
|
||||
onMounted(() => {
|
||||
// 组别检查
|
||||
if (!['benke', 'jiaoshi'].includes(authStore.userGroup)) {
|
||||
ElMessage.warning('您所在的组别不需要提交作品')
|
||||
// 不强制跳转,避免闪烁
|
||||
}
|
||||
|
||||
fetchUploadStatus()
|
||||
fetchHistory()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (deadlineTimer) clearInterval(deadlineTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- ==================== 文件上传子区域组件模板 ==================== -->
|
||||
|
||||
<script>
|
||||
/**
|
||||
* file-upload-area 是一个内联组件
|
||||
* 为了保持 Vue SFC 的简洁性,我们用 defineComponent
|
||||
*/
|
||||
import { defineComponent, ref, reactive, h, computed } from 'vue'
|
||||
import { ElMessage, ElUpload, ElProgress, ElButton, ElTag, ElIcon } from 'element-plus'
|
||||
import { UploadFilled, Delete } from '@element-plus/icons-vue'
|
||||
import * as uploadApi from '@/api/upload'
|
||||
import { chunkFile, calculateMd5, uploadWithRetry, formatFileSize } from '@/utils/upload'
|
||||
|
||||
const FileUploadArea = defineComponent({
|
||||
name: 'FileUploadArea',
|
||||
props: {
|
||||
group: { type: String, required: true },
|
||||
fileType: { type: String, default: 'work' },
|
||||
maxSize: { type: Number, default: 50 * 1024 * 1024 },
|
||||
accept: { type: String, default: '.pdf,.doc,.docx,.ppt,.pptx,.zip' },
|
||||
},
|
||||
emits: ['upload-success'],
|
||||
setup(props, { emit }) {
|
||||
const fileList = ref([])
|
||||
const uploading = ref(false)
|
||||
const uploadPercent = ref(0)
|
||||
const uploadedInfo = ref(null)
|
||||
|
||||
function validateFile(file) {
|
||||
const raw = file.raw || file
|
||||
if (raw.size > props.maxSize) {
|
||||
const maxMB = props.maxSize / (1024 * 1024)
|
||||
ElMessage.error(`文件大小不能超过 ${maxMB}MB`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function handleFileChange(file) {
|
||||
if (!validateFile(file)) {
|
||||
fileList.value = []
|
||||
return
|
||||
}
|
||||
fileList.value = [file]
|
||||
uploadedInfo.value = null
|
||||
// 选择文件后自动开始上传,提供即时反馈
|
||||
startUpload()
|
||||
}
|
||||
|
||||
function removeFile() {
|
||||
if (uploading.value) {
|
||||
ElMessage.warning('文件正在上传中,请等待完成后再操作')
|
||||
return
|
||||
}
|
||||
fileList.value = []
|
||||
uploadedInfo.value = null
|
||||
uploadPercent.value = 0
|
||||
}
|
||||
|
||||
async function startUpload() {
|
||||
// 防重入:选择文件会自动触发一次上传,用户若再点「开始上传」会造成同一文件
|
||||
// 双份分片流并发,既浪费连接池也可能让后端合并出错。
|
||||
if (uploading.value) return
|
||||
if (fileList.value.length === 0) {
|
||||
ElMessage.warning('请先选择文件')
|
||||
return
|
||||
}
|
||||
const rawFile = fileList.value[0]?.raw || fileList.value[0]
|
||||
if (!rawFile) return
|
||||
|
||||
uploading.value = true
|
||||
uploadPercent.value = 0
|
||||
|
||||
try {
|
||||
// 计算 MD5 作为 fileId
|
||||
let fileId = ''
|
||||
try {
|
||||
fileId = await calculateMd5(rawFile)
|
||||
} catch (e) {
|
||||
console.warn('MD5 计算失败', e)
|
||||
fileId = rawFile.name + '_' + Date.now()
|
||||
}
|
||||
|
||||
// 分片上传
|
||||
const chunks = chunkFile(rawFile)
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const formData = new FormData()
|
||||
// ⚠️ Egg stream 模式下 stream.fields 仅解析「出现在文件字段之前」的普通字段。
|
||||
// 必须把文本字段放在 chunk(文件)之前,否则后端收不到 chunkIndex/fileId/fileName -> 400 缺参。
|
||||
formData.append('chunkIndex', chunks[i].index)
|
||||
formData.append('totalChunks', chunks[i].total)
|
||||
formData.append('fileId', fileId)
|
||||
formData.append('fileName', rawFile.name)
|
||||
formData.append('chunk', chunks[i].chunk, rawFile.name)
|
||||
|
||||
await uploadWithRetry(
|
||||
() => uploadApi.uploadChunk(props.group, formData),
|
||||
3, 1000
|
||||
)
|
||||
|
||||
uploadPercent.value = Math.round(((i + 1) / chunks.length) * 100)
|
||||
}
|
||||
|
||||
// 所有分片上传完成,调用合并接口(透传 file_type:word/ppt)
|
||||
await uploadApi.mergeChunks(props.group, {
|
||||
fileId,
|
||||
fileName: rawFile.name,
|
||||
totalChunks: chunks.length,
|
||||
work_name: rawFile.name,
|
||||
file_type: props.fileType,
|
||||
})
|
||||
|
||||
uploadedInfo.value = {
|
||||
fileName: rawFile.name,
|
||||
fileSize: rawFile.size,
|
||||
fileId,
|
||||
}
|
||||
|
||||
ElMessage.success('上传成功')
|
||||
emit('upload-success', { type: props.fileType, info: uploadedInfo.value })
|
||||
} catch (err) {
|
||||
ElMessage.error('上传失败,请重试')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
return h('div', { class: 'file-upload-wrapper' }, [
|
||||
// 拖拽上传区
|
||||
!uploadedInfo.value ? [
|
||||
h('div', {
|
||||
class: 'drag-area',
|
||||
onClick: () => {
|
||||
// 触发文件选择
|
||||
},
|
||||
}, [
|
||||
h('input', {
|
||||
type: 'file',
|
||||
accept: props.accept,
|
||||
style: { display: 'none' },
|
||||
ref: 'fileInput',
|
||||
onChange: (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
handleFileChange({ raw: file, name: file.name, size: file.size })
|
||||
}
|
||||
e.target.value = ''
|
||||
},
|
||||
}),
|
||||
h('div', {
|
||||
class: 'drag-content',
|
||||
onClick: (e) => {
|
||||
// 点击时触发隐藏的 file input
|
||||
e.currentTarget.previousElementSibling?.click()
|
||||
},
|
||||
}, [
|
||||
h(ElIcon, { size: 48 }, () => h(UploadFilled)),
|
||||
h('div', { class: 'drag-text' }, '拖拽文件到此处或 '),
|
||||
h('em', null, '点击上传'),
|
||||
h('div', { class: 'drag-hint' },
|
||||
`支持 ${props.accept.replace(/\./g, '')} 格式,最大 ${props.maxSize / (1024 * 1024)}MB`
|
||||
),
|
||||
]),
|
||||
]),
|
||||
|
||||
// 已选文件
|
||||
fileList.value.length > 0 ? h('div', { class: 'selected-file' }, [
|
||||
h('span', { class: 'selected-name' }, fileList.value[0].name || fileList.value[0].fileName),
|
||||
h(ElButton, {
|
||||
type: 'danger',
|
||||
size: 'small',
|
||||
icon: h(Delete),
|
||||
circle: true,
|
||||
onClick: removeFile,
|
||||
}),
|
||||
]) : null,
|
||||
|
||||
// 上传按钮
|
||||
fileList.value.length > 0 ? h('div', { class: 'upload-actions' }, [
|
||||
h(ElButton, {
|
||||
type: 'primary',
|
||||
loading: uploading.value,
|
||||
onClick: startUpload,
|
||||
}, { default: () => '开始上传' }),
|
||||
]) : null,
|
||||
] : null,
|
||||
|
||||
// 上传进度
|
||||
uploading.value ? h('div', { class: 'upload-progress' }, [
|
||||
h(ElProgress, {
|
||||
percentage: uploadPercent.value,
|
||||
status: uploadPercent.value >= 100 ? 'success' : undefined,
|
||||
}),
|
||||
]) : null,
|
||||
|
||||
// 已上传文件信息
|
||||
uploadedInfo.value ? h('div', { class: 'uploaded-card' }, [
|
||||
h(ElTag, { type: 'success', size: 'small' }, () => '上传成功'),
|
||||
h('div', { class: 'uploaded-name' }, uploadedInfo.value.fileName),
|
||||
h('div', { class: 'uploaded-meta' }, [
|
||||
h('span', null, formatFileSize(uploadedInfo.value.fileSize)),
|
||||
uploadedInfo.value.md5 ? h('span', null, `MD5: ${uploadedInfo.value.md5.slice(0, 16)}...`) : null,
|
||||
].filter(Boolean)),
|
||||
h(ElButton, {
|
||||
text: true,
|
||||
type: 'danger',
|
||||
size: 'small',
|
||||
onClick: removeFile,
|
||||
}, { default: () => '重新上传' }),
|
||||
]) : null,
|
||||
])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
components: { FileUploadArea },
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
/* ==================== 顶部导航 ==================== */
|
||||
.upload-header {
|
||||
background: #1a3a5c;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
height: 60px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.back-btn { color: rgba(255,255,255,0.85); white-space: nowrap; }
|
||||
.back-btn:hover { color: #c9a84c; }
|
||||
|
||||
.upload-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.upload-header-left {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.group-tag {
|
||||
background: rgba(201,168,76,0.2);
|
||||
color: #c9a84c;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ==================== 内容 ==================== */
|
||||
.upload-content {
|
||||
max-width: 1000px;
|
||||
margin: 24px auto 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/* ==================== 截止时间 ==================== */
|
||||
.deadline-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.deadline-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.deadline-near { color: #f56c6c; }
|
||||
|
||||
.deadline-countdown {
|
||||
font-size: 18px;
|
||||
color: #1a3a5c;
|
||||
}
|
||||
|
||||
/* ==================== 上传卡片 ==================== */
|
||||
.upload-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.upload-card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a3a5c;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-card-desc {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* ==================== 拖拽上传区 ==================== */
|
||||
.drag-area {
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 12px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.drag-area:hover {
|
||||
border-color: #1a3a5c;
|
||||
background: #f0f4f8;
|
||||
}
|
||||
|
||||
.drag-content {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.drag-text {
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ==================== 已选文件 ==================== */
|
||||
.selected-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f0f4f8;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.selected-name {
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* ==================== 上传成功信息 ==================== */
|
||||
.uploaded-card {
|
||||
border: 1px solid #bbf7d0;
|
||||
background: #f0fdf4;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uploaded-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.uploaded-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ==================== 历史记录 ==================== */
|
||||
.history-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
.history-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a3a5c;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ==================== Responsive ==================== */
|
||||
@media (max-width: 480px) {
|
||||
.upload-header { padding: 0 12px; }
|
||||
.deadline-card { flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user