From 18c5ad952159e1357b9561179c5455337bc29921 Mon Sep 17 00:00:00 2001 From: zhangyahui Date: Mon, 13 Apr 2026 15:23:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9F=A5=E7=9C=8B=E5=B7=B2=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/css/style.css | 48 ++++- src/assets/icons/CDownload.svg | 3 + src/directives/Loading.ts | 69 +++++++ src/lang/en.ts | 9 + src/lang/zh-cn.ts | 9 + src/main.ts | 23 +-- src/router/index.ts | 6 +- src/utils/request.ts | 9 +- src/views/AwardPage/contestants.vue | 2 + src/views/Preview/index.vue | 310 ++++++++++++++++++++++++++++ 10 files changed, 470 insertions(+), 18 deletions(-) create mode 100644 src/assets/icons/CDownload.svg create mode 100644 src/directives/Loading.ts create mode 100644 src/views/Preview/index.vue diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 47e8c86..7ebf65a 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -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'); -} \ No newline at end of file +} + +/* 遮罩层:利用 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; */ +} diff --git a/src/assets/icons/CDownload.svg b/src/assets/icons/CDownload.svg new file mode 100644 index 0000000..68e127e --- /dev/null +++ b/src/assets/icons/CDownload.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/directives/Loading.ts b/src/directives/Loading.ts new file mode 100644 index 0000000..73985c3 --- /dev/null +++ b/src/directives/Loading.ts @@ -0,0 +1,69 @@ +const vLoading = { + mounted(el, binding) { + // 1. 创建遮罩层 + const mask = document.createElement('div') + mask.className = 'custom-loading-mask' + mask.innerHTML = '
' + + // 将 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 diff --git a/src/lang/en.ts b/src/lang/en.ts index 1b7f939..d6987bf 100644 --- a/src/lang/en.ts +++ b/src/lang/en.ts @@ -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' } } diff --git a/src/lang/zh-cn.ts b/src/lang/zh-cn.ts index 67304a6..edb84f0 100644 --- a/src/lang/zh-cn.ts +++ b/src/lang/zh-cn.ts @@ -252,5 +252,14 @@ export default { nextStep: '下一步', stepTips: '请一次性完成这个表单。', backToIntroduction: '赛事介绍' + }, + Preview: { + title: 'Global Awards 数据总览', + total: '已提交申请总数', + downloadExcel: '下载全量信息表 (Excel)', + range: '设定下载区间', + startIndex: '起始序号', + endIndex: '结束序号', + download: '下载' } } diff --git a/src/main.ts b/src/main.ts index 676df7e..fa2cafb 100644 --- a/src/main.ts +++ b/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() diff --git a/src/router/index.ts b/src/router/index.ts index e3e876d..e3c0809 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -29,7 +29,11 @@ const router = createRouter({ } ] }, - + { + path: '/preview', + name: 'Preview', + component: () => import('@/views/Preview/index.vue') + }, { path: '/:pathMatch(.*)', name: '404', diff --git a/src/utils/request.ts b/src/utils/request.ts index bef5a4e..753e187 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -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) { diff --git a/src/views/AwardPage/contestants.vue b/src/views/AwardPage/contestants.vue index be2eb45..ac7bc72 100644 --- a/src/views/AwardPage/contestants.vue +++ b/src/views/AwardPage/contestants.vue @@ -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 diff --git a/src/views/Preview/index.vue b/src/views/Preview/index.vue new file mode 100644 index 0000000..2334fa9 --- /dev/null +++ b/src/views/Preview/index.vue @@ -0,0 +1,310 @@ + + + + +