feat: 查看已提交数据
This commit is contained in:
@@ -28,7 +28,6 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -98,4 +97,49 @@ body,
|
||||
@font-face {
|
||||
font-family: 'InstrumentBold';
|
||||
src: url('./fonts/InstrumentSans-Bold.ttf') format('truetype');
|
||||
}
|
||||
}
|
||||
|
||||
/* 遮罩层:利用 inherit 完美适配父元素形状 */
|
||||
.custom-loading-mask {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
/* 核心修正:继承父元素的圆角,防止遮罩层直角溢出 */
|
||||
border-radius: inherit;
|
||||
/* 确保 top/left 0 包含在 border 之内 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 加载图标 */
|
||||
.custom-loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 状态类:强制父元素定位 */
|
||||
.v-loading-parent--relative {
|
||||
position: relative !important;
|
||||
/* 如果父元素内容非常多且溢出,开启此项可防止 loading 跟着滚动 */
|
||||
/* overflow: hidden !important; */
|
||||
}
|
||||
|
||||
3
src/assets/icons/CDownload.svg
Normal file
3
src/assets/icons/CDownload.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 2.75C11.5063 2.75 11.9167 3.16041 11.9167 3.66667V11.537L14.0185 9.43515C14.3765 9.07717 14.9569 9.07717 15.3148 9.43515C15.6728 9.79313 15.6728 10.3735 15.3148 10.7315L11.6482 14.3982C11.2902 14.7562 10.7098 14.7562 10.3518 14.3982L6.68515 10.7315C6.32717 10.3735 6.32717 9.79313 6.68515 9.43515C7.04313 9.07717 7.62354 9.07717 7.98152 9.43515L10.0833 11.537V3.66667C10.0833 3.16041 10.4937 2.75 11 2.75ZM3.66667 12.8333C4.17293 12.8333 4.58333 13.2437 4.58333 13.75V13.9333C4.58333 14.7185 4.58405 15.2523 4.61776 15.6649C4.65059 16.0668 4.71011 16.2723 4.78316 16.4156C4.95892 16.7606 5.23939 17.0411 5.58435 17.2168C5.72772 17.2899 5.93324 17.3494 6.33512 17.3822C6.7477 17.416 7.28147 17.4167 8.06667 17.4167H13.9333C14.7185 17.4167 15.2523 17.416 15.6649 17.3822C16.0668 17.3494 16.2723 17.2899 16.4157 17.2168C16.7606 17.0411 17.0411 16.7606 17.2168 16.4156C17.2899 16.2723 17.3494 16.0668 17.3822 15.6649C17.416 15.2523 17.4167 14.7185 17.4167 13.9333V13.75C17.4167 13.2437 17.8271 12.8333 18.3333 12.8333C18.8396 12.8333 19.25 13.2437 19.25 13.75V13.9712C19.25 14.7091 19.25 15.3181 19.2095 15.8142C19.1674 16.3294 19.077 16.8031 18.8504 17.248C18.4988 17.9379 17.9379 18.4988 17.248 18.8504C16.8031 19.077 16.3294 19.1674 15.8142 19.2095C15.3181 19.25 14.7091 19.25 13.9712 19.25H8.02879C7.29091 19.25 6.68192 19.25 6.18583 19.2095C5.67057 19.1674 5.19693 19.077 4.75204 18.8504C4.06211 18.4988 3.50118 17.9379 3.14964 17.248C2.92296 16.8031 2.83261 16.3294 2.79051 15.8142C2.74998 15.3181 2.74999 14.7091 2.75 13.9712L2.75 13.75C2.75 13.2437 3.16041 12.8333 3.66667 12.8333Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
69
src/directives/Loading.ts
Normal file
69
src/directives/Loading.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
const vLoading = {
|
||||
mounted(el, binding) {
|
||||
// 1. 创建遮罩层
|
||||
const mask = document.createElement('div')
|
||||
mask.className = 'custom-loading-mask'
|
||||
mask.innerHTML = '<div class="custom-loading-spinner"></div>'
|
||||
|
||||
// 将 mask 存入 el 方便后续调用
|
||||
el.instance = mask
|
||||
|
||||
// 2. 初始判断
|
||||
if (binding.value) {
|
||||
appendMask(el)
|
||||
}
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
if (binding.value !== binding.oldValue) {
|
||||
if (binding.value) {
|
||||
appendMask(el)
|
||||
} else {
|
||||
removeMask(el)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
unmounted(el) {
|
||||
removeMask(el)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入遮罩并修正尺寸
|
||||
*/
|
||||
function appendMask(el) {
|
||||
// 添加相对定位类
|
||||
el.classList.add('v-loading-parent--relative')
|
||||
|
||||
// 尺寸优化逻辑:
|
||||
// 如果父元素高度或宽度小于 40px,动态缩小图标
|
||||
const spinner = el.instance.querySelector('.custom-loading-spinner')
|
||||
if (spinner) {
|
||||
const minSize = Math.min(el.offsetWidth, el.offsetHeight)
|
||||
if (minSize > 0 && minSize < 40) {
|
||||
spinner.style.width = '16px'
|
||||
spinner.style.height = '16px'
|
||||
spinner.style.borderWidth = '2px'
|
||||
} else {
|
||||
// 恢复默认尺寸
|
||||
spinner.style.width = '30px'
|
||||
spinner.style.height = '30px'
|
||||
}
|
||||
}
|
||||
|
||||
// 插入 DOM
|
||||
el.appendChild(el.instance)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除遮罩
|
||||
*/
|
||||
function removeMask(el) {
|
||||
el.classList.remove('v-loading-parent--relative')
|
||||
if (el.instance && el.contains(el.instance)) {
|
||||
el.removeChild(el.instance)
|
||||
}
|
||||
}
|
||||
|
||||
export default vLoading
|
||||
@@ -263,5 +263,14 @@ export default {
|
||||
nextStep: 'Next Step',
|
||||
stepTips: 'Please complete this form in one sitting.',
|
||||
backToIntroduction: 'Back to Introduction'
|
||||
},
|
||||
Preview: {
|
||||
title: 'Global Awards Preview',
|
||||
total: 'Total Applications Submitted',
|
||||
downloadExcel: 'Download Full Information Table (Excel)',
|
||||
range: 'Set the download range',
|
||||
startIndex:'start ID',
|
||||
endIndex:'end ID',
|
||||
download: 'Download'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,5 +252,14 @@ export default {
|
||||
nextStep: '下一步',
|
||||
stepTips: '请一次性完成这个表单。',
|
||||
backToIntroduction: '赛事介绍'
|
||||
},
|
||||
Preview: {
|
||||
title: 'Global Awards 数据总览',
|
||||
total: '已提交申请总数',
|
||||
downloadExcel: '下载全量信息表 (Excel)',
|
||||
range: '设定下载区间',
|
||||
startIndex: '起始序号',
|
||||
endIndex: '结束序号',
|
||||
download: '下载'
|
||||
}
|
||||
}
|
||||
|
||||
23
src/main.ts
23
src/main.ts
@@ -5,24 +5,19 @@ import router from './router'
|
||||
import store from './stores/index'
|
||||
import 'normalize.css'
|
||||
import './assets/css/style.css'
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import "virtual:svg-icons-register";
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import 'virtual:svg-icons-register'
|
||||
import messagePlugin from './components/Message/message'
|
||||
import vLoading from '@/directives/Loading'
|
||||
|
||||
import i18n from "./lang/index";
|
||||
import i18n from './lang/index'
|
||||
|
||||
import flexible from "./utils/flexible.js";
|
||||
import flexible from './utils/flexible.js'
|
||||
|
||||
import "./router/router-config" // 路由守卫,做动态路由的地方
|
||||
import './router/router-config' // 路由守卫,做动态路由的地方
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
.use(store)
|
||||
.component("SvgIcon", SvgIcon)
|
||||
.use(i18n)
|
||||
.use(messagePlugin)
|
||||
.mount('#app')
|
||||
|
||||
|
||||
flexible();
|
||||
app.directive('loading', vLoading)
|
||||
app.use(router).use(store).component('SvgIcon', SvgIcon).use(i18n).use(messagePlugin).mount('#app')
|
||||
|
||||
flexible()
|
||||
|
||||
@@ -29,7 +29,11 @@ const router = createRouter({
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/preview',
|
||||
name: 'Preview',
|
||||
component: () => import('@/views/Preview/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
|
||||
@@ -59,6 +59,10 @@ axios.interceptors.response.use(
|
||||
let url = binaryToUrl(res.data, res.config.env.binaryType, res)
|
||||
return Promise.resolve({ url, data: res.data })
|
||||
}
|
||||
if (res.data instanceof Blob) {
|
||||
return Promise.resolve(res)
|
||||
}
|
||||
|
||||
if (res?.data) {
|
||||
if (res?.data?.errCode === 0) {
|
||||
// message.error(res?.data?.errMsg)
|
||||
@@ -130,7 +134,10 @@ export const Https = {
|
||||
uploadPDFComplete: '/api/global-award/uploads/pdf/complete', // 上传pdf完成
|
||||
uploadVideoComplete: '/api/global-award/uploads/video/complete', // 上传video完成
|
||||
submitForm: '/api/global-award/contestants/save', // 提交表单
|
||||
getContestantByID: '/api/global-award/contestants/' // 获取表单
|
||||
getContestantByID: '/api/global-award/contestants/', // 获取表单
|
||||
getContestCount: '/api/global-award/contestants/count', // 获取已提交申请总数
|
||||
getExcel: '/api/global-award/contestants/export', // 导出excel
|
||||
postExportFile: '/api/global-award/contestants/export/files' // 下载指定范围文件
|
||||
},
|
||||
|
||||
axiosGet(url, config) {
|
||||
|
||||
@@ -448,6 +448,8 @@ const handleTestComplete = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const contestantId = computed(() => route.query.id || route.params.id)
|
||||
|
||||
const readOnly = computed(() => {
|
||||
if (route.query.id && !hasValidEmail.value) {
|
||||
return true
|
||||
|
||||
310
src/views/Preview/index.vue
Normal file
310
src/views/Preview/index.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="card">
|
||||
<section class="stats-section">
|
||||
<h1 class="title">{{ $t('Preview.title') }}</h1>
|
||||
<div class="count-badge">
|
||||
<span class="label">{{ $t('Preview.total') }}</span>
|
||||
<span class="number">{{ submittedCount }}</span>
|
||||
</div>
|
||||
<button v-loading="excelLoading" @click="handleDownloadAll" class="download-all-btn flex">
|
||||
<SvgIcon name="CDownload" size="22" color="#fff" />
|
||||
{{ $t('Preview.downloadExcel') }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<section class="download-section">
|
||||
<h2 class="subtitle">{{ $t('Preview.range') }}</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="field">
|
||||
<label>{{ $t('Preview.startIndex') }}</label>
|
||||
<input v-model.number="range.start" type="number" placeholder="10000" min="1" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ $t('Preview.endIndex') }}</label>
|
||||
<input v-model.number="range.end" type="number" :placeholder="maxIndex" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button v-loading="fileDownloading" @click="handleDownload" :disabled="!isValid" class="download-btn">
|
||||
<span class="btn-text">{{ $t('Preview.download') }}</span>
|
||||
</button>
|
||||
|
||||
<p v-if="errorMsg" class="status-text error">{{ errorMsg }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Https } from '@/utils/request'
|
||||
import { debounce } from 'lodash-es'
|
||||
|
||||
// 模拟已提交人数
|
||||
const submittedCount = ref(0)
|
||||
|
||||
// 输入框绑定的数据模型
|
||||
const range = ref({
|
||||
start: null,
|
||||
end: null
|
||||
})
|
||||
const maxIndex = ref(10000)
|
||||
const handleFetchSubmittedCount = async () => {
|
||||
Https.axiosGet(Https.httpUrls.getContestCount).then((res) => {
|
||||
submittedCount.value = res.count
|
||||
maxIndex.value = res.maxContestantNumber
|
||||
})
|
||||
}
|
||||
|
||||
// 逻辑判断:区间是否合法
|
||||
const isValid = computed(() => {
|
||||
const { start, end } = range.value
|
||||
// 必须是数字,且起始>0,结束>=起始,结束不超过当前总数
|
||||
return start !== null && end !== null && start > 0 && end >= start && end <= maxIndex.value
|
||||
})
|
||||
|
||||
// 错误提示文案
|
||||
const errorMsg = computed(() => {
|
||||
const { start, end } = range.value
|
||||
if (start === null || end === null) return ''
|
||||
if (start < 10000) return '起始序号必须大于 10000'
|
||||
if (end < start) return '结束序号不能小于起始序号'
|
||||
if (end > maxIndex.value) return `结束序号不能超过最大值 ${maxIndex.value}`
|
||||
return ''
|
||||
})
|
||||
|
||||
const excelLoading = ref(false)
|
||||
const handleDownloadAll = debounce(() => {
|
||||
excelLoading.value = true
|
||||
|
||||
Https.axiosGet(Https.httpUrls.getExcel, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
// 创建下载链接
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([res.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', `Global_Awards_Applications_${Date.now()}.xlsx`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
.finally(() => {
|
||||
excelLoading.value = false
|
||||
})
|
||||
}, 500)
|
||||
|
||||
const fileDownloading = ref(false)
|
||||
// 下载执行函数
|
||||
const handleDownload = debounce(() => {
|
||||
if (!isValid.value) return
|
||||
fileDownloading.value = true
|
||||
Https.axiosPost(
|
||||
Https.httpUrls.postExportFile,
|
||||
{
|
||||
minContestantNumber: range.value.start,
|
||||
maxContestantNumber: range.value.end
|
||||
},
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
.then((res) => {
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(new Blob([res.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute(
|
||||
'download',
|
||||
`Global_Awards_Applications_${range.value.start}_to_${range.value.end}.zip`
|
||||
)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
.finally(() => {
|
||||
fileDownloading.value = false
|
||||
})
|
||||
}, 500)
|
||||
|
||||
onMounted(() => {
|
||||
handleFetchSubmittedCount()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 全局背景容器 */
|
||||
.page-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
/* 渐变背景:深邃蓝紫色调 */
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* 核心卡片样式 */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
width: 80rem;
|
||||
padding: 4rem;
|
||||
border-radius: 2.4rem;
|
||||
box-shadow: 0 2rem 4rem rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 统计区域 */
|
||||
.title {
|
||||
margin: 0 0 2.4rem 0;
|
||||
font-size: 2.4rem;
|
||||
text-align: center;
|
||||
color: #2d3436;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
background: #f8f9fa;
|
||||
border: 0.2rem solid #edf2f7;
|
||||
border-radius: 1.6rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.count-badge .label {
|
||||
display: block;
|
||||
font-size: 1.4rem;
|
||||
color: #636e72;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.count-badge .number {
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
color: #4834d4;
|
||||
}
|
||||
|
||||
.download-all-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.2rem;
|
||||
background-color: #00b894;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
column-gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
.c-svg {
|
||||
width: initial;
|
||||
height: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 0.1rem;
|
||||
background: #eee;
|
||||
margin: 3.2rem 0;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.subtitle {
|
||||
font-size: 1.6rem;
|
||||
color: #2d3436;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
color: #b2bec3;
|
||||
margin-bottom: 0.6rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.field input {
|
||||
width: 100%;
|
||||
padding: 1.2rem 1.6rem;
|
||||
border: 0.2rem solid #dfe6e9;
|
||||
border-radius: 1.2rem;
|
||||
font-size: 1.6rem;
|
||||
color: #2d3436;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
outline: none;
|
||||
border-color: #6c5ce7;
|
||||
box-shadow: 0 0 0 4px rgba(108, 92, 231, 0.1);
|
||||
}
|
||||
|
||||
/* 按钮逻辑 */
|
||||
.download-btn {
|
||||
width: 100%;
|
||||
padding: 1.6rem;
|
||||
border: none;
|
||||
border-radius: 1.2rem;
|
||||
background: #6c5ce7;
|
||||
color: white;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.download-btn:hover:not(:disabled) {
|
||||
background: #5849be;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0.8rem 1.5rem rgba(108, 92, 231, 0.3);
|
||||
}
|
||||
|
||||
.download-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.download-btn:disabled {
|
||||
background: #dfe6e9;
|
||||
color: #b2bec3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 状态文本 */
|
||||
.status-text {
|
||||
text-align: center;
|
||||
font-size: 1.3rem;
|
||||
margin-top: 1.6rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-text.error {
|
||||
color: #ff7675;
|
||||
}
|
||||
|
||||
.status-text.success {
|
||||
color: #00b894;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user