From 89773bd49bc0cab8d99b7dd463726a3163f1de66 Mon Sep 17 00:00:00 2001 From: rjl <98n9@163.com> Date: Wed, 21 Jan 2026 16:29:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=96=B0=E5=A2=9E=E9=A2=98?= =?UTF-8?q?=E5=BA=93=E6=A8=A1=E5=9D=97=E5=B9=B6=E4=BC=98=E5=8C=96=E6=AF=94?= =?UTF-8?q?=E8=B5=9B=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增题库模块,包含题目管理页面和Excel导入导出功能 优化比赛管理功能,包括分组管理、硬件绑定和题目配置 重构比赛添加流程,增加重置功能和数据校验 添加Excel工具函数,支持读取和导出Excel文件 更新依赖,添加xlsx库支持 --- apps/admin/package.json | 3 +- apps/admin/src/locales/langs/en-us.ts | 1 + apps/admin/src/locales/langs/zh-cn.ts | 1 + apps/admin/src/router/elegant/imports.ts | 1 + apps/admin/src/router/elegant/routes.ts | 9 + apps/admin/src/router/elegant/transform.ts | 3 +- apps/admin/src/typings/components.d.ts | 46 +- apps/admin/src/typings/elegant-router.d.ts | 3 + .../competition/competition-add/index.vue | 131 +++--- .../competition-add/modules/BasicInfo.vue | 81 +--- .../modules/GroupManagement.vue | 108 ++--- .../modules/HardwareBinding.vue | 103 ++--- .../modules/QuestionConfig.vue | 402 +++++++++--------- .../competition-add/modules/useBasicInfo.ts | 107 +++++ .../modules/useGroupManagement.ts | 287 +++++++++++++ .../modules/useHardwareBinding.ts | 286 +++++++++++++ .../modules/useQuestionConfig.ts | 170 ++++++++ .../competition/competition-list/index.vue | 48 ++- .../modules/competition-card.vue | 15 +- .../competition-list/modules/data.ts | 9 + apps/admin/src/views/question/index.vue | 46 ++ .../src/views/question/modules/card-data.vue | 112 +++++ .../question/modules/creativity-banner.vue | 17 + .../views/question/modules/header-banner.vue | 68 +++ .../src/views/question/modules/line-chart.vue | 152 +++++++ .../src/views/question/modules/pie-chart.vue | 109 +++++ .../views/question/modules/project-news.vue | 40 ++ eslint.config.js | 1 + package.json | 2 +- packages/utils/package.json | 7 +- packages/utils/src/excel.ts | 40 ++ packages/utils/src/index.ts | 1 + pnpm-lock.yaml | 75 ++++ 33 files changed, 1988 insertions(+), 496 deletions(-) create mode 100644 apps/admin/src/views/competition/competition-add/modules/useBasicInfo.ts create mode 100644 apps/admin/src/views/competition/competition-add/modules/useGroupManagement.ts create mode 100644 apps/admin/src/views/competition/competition-add/modules/useHardwareBinding.ts create mode 100644 apps/admin/src/views/competition/competition-add/modules/useQuestionConfig.ts create mode 100644 apps/admin/src/views/question/index.vue create mode 100644 apps/admin/src/views/question/modules/card-data.vue create mode 100644 apps/admin/src/views/question/modules/creativity-banner.vue create mode 100644 apps/admin/src/views/question/modules/header-banner.vue create mode 100644 apps/admin/src/views/question/modules/line-chart.vue create mode 100644 apps/admin/src/views/question/modules/pie-chart.vue create mode 100644 apps/admin/src/views/question/modules/project-news.vue create mode 100644 packages/utils/src/excel.ts diff --git a/apps/admin/package.json b/apps/admin/package.json index 18b5b05..3870bfd 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -44,7 +44,8 @@ "vue": "3.5.26", "vue-draggable-plus": "0.6.0", "vue-i18n": "11.2.7", - "vue-router": "4.6.4" + "vue-router": "4.6.4", + "xlsx": "^0.18.5" }, "devDependencies": { "@elegant-router/vue": "0.3.8", diff --git a/apps/admin/src/locales/langs/en-us.ts b/apps/admin/src/locales/langs/en-us.ts index f790b81..e5f5c44 100644 --- a/apps/admin/src/locales/langs/en-us.ts +++ b/apps/admin/src/locales/langs/en-us.ts @@ -230,6 +230,7 @@ const local: App.I18n.Schema = { 'iframe-page': 'Iframe', 'home': 'Home', 'competition': 'Competition', + 'question': 'Question', 'competition_competition-add': 'Competition Add', 'competition_competition-detail': 'Competition Detail', 'competition_competition-list': 'Competition List', diff --git a/apps/admin/src/locales/langs/zh-cn.ts b/apps/admin/src/locales/langs/zh-cn.ts index 516f495..6b6a6e1 100644 --- a/apps/admin/src/locales/langs/zh-cn.ts +++ b/apps/admin/src/locales/langs/zh-cn.ts @@ -226,6 +226,7 @@ const local: App.I18n.Schema = { 'iframe-page': '外链页面', 'home': '首页', 'competition': '比赛', + 'question': '题库', 'competition_competition-add': '比赛添加', 'competition_competition-detail': '比赛详情', 'competition_competition-list': '比赛列表', diff --git a/apps/admin/src/router/elegant/imports.ts b/apps/admin/src/router/elegant/imports.ts index 541cd60..32dd131 100644 --- a/apps/admin/src/router/elegant/imports.ts +++ b/apps/admin/src/router/elegant/imports.ts @@ -24,4 +24,5 @@ export const views: Record Promise import("@/views/competition/competition-detail/index.vue"), "competition_competition-list": () => import("@/views/competition/competition-list/index.vue"), home: () => import("@/views/home/index.vue"), + question: () => import("@/views/question/index.vue"), }; diff --git a/apps/admin/src/router/elegant/routes.ts b/apps/admin/src/router/elegant/routes.ts index 8078c46..ccbd75b 100644 --- a/apps/admin/src/router/elegant/routes.ts +++ b/apps/admin/src/router/elegant/routes.ts @@ -116,5 +116,14 @@ export const generatedRoutes: GeneratedRoute[] = [ constant: true, hideInMenu: true } + }, + { + name: 'question', + path: '/question', + component: 'layout.base$view.question', + meta: { + title: 'question', + i18nKey: 'route.question' + } } ]; diff --git a/apps/admin/src/router/elegant/transform.ts b/apps/admin/src/router/elegant/transform.ts index 84d1462..e8d1396 100644 --- a/apps/admin/src/router/elegant/transform.ts +++ b/apps/admin/src/router/elegant/transform.ts @@ -172,7 +172,8 @@ const routeMap: RouteMap = { "competition_competition-list": "/competition/competition-list", "home": "/home", "iframe-page": "/iframe-page/:url", - "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?" + "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", + "question": "/question" }; /** diff --git a/apps/admin/src/typings/components.d.ts b/apps/admin/src/typings/components.d.ts index 172971a..98d8f00 100644 --- a/apps/admin/src/typings/components.d.ts +++ b/apps/admin/src/typings/components.d.ts @@ -15,7 +15,6 @@ declare module 'vue' { AppProvider: typeof import('./../components/common/app-provider.vue')['default'] BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default'] ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] - copy: typeof import('./../components/custom/wave-bg copy.vue')['default'] CountTo: typeof import('./../components/custom/count-to.vue')['default'] DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default'] ExceptionBase: typeof import('./../components/common/exception-base.vue')['default'] @@ -26,23 +25,34 @@ declare module 'vue' { IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] IconIcBaselineAdd: typeof import('~icons/ic/baseline-add')['default'] IconIcBaselineArrowBack: typeof import('~icons/ic/baseline-arrow-back')['default'] - IconIcBaselineCalendar: typeof import('~icons/ic/baseline-calendar')['default'] + IconIcBaselineArrowForward: typeof import('~icons/ic/baseline-arrow-forward')['default'] IconIcBaselineCalendarMonth: typeof import('~icons/ic/baseline-calendar-month')['default'] + IconIcBaselineCheck: typeof import('~icons/ic/baseline-check')['default'] IconIcBaselineClose: typeof import('~icons/ic/baseline-close')['default'] IconIcBaselineContentCopy: typeof import('~icons/ic/baseline-content-copy')['default'] - IconIcBaselineCopy: typeof import('~icons/ic/baseline-copy')['default'] IconIcBaselineDelete: typeof import('~icons/ic/baseline-delete')['default'] + IconIcBaselineDownload: typeof import('~icons/ic/baseline-download')['default'] IconIcBaselineEdit: typeof import('~icons/ic/baseline-edit')['default'] + IconIcBaselineFlash: typeof import('~icons/ic/baseline-flash')['default'] IconIcBaselineGroup: typeof import('~icons/ic/baseline-group')['default'] + IconIcBaselineRefresh: typeof import('~icons/ic/baseline-refresh')['default'] IconIcBaselineRocketLaunch: typeof import('~icons/ic/baseline-rocket-launch')['default'] IconIcBaselineSave: typeof import('~icons/ic/baseline-save')['default'] + IconIcBaselineScan: typeof import('~icons/ic/baseline-scan')['default'] + IconIcBaselineSettingsBackupRestore: typeof import('~icons/ic/baseline-settings-backup-restore')['default'] + IconIcBaselineUpload: typeof import('~icons/ic/baseline-upload')['default'] IconIcBaselineUploadFile: typeof import('~icons/ic/baseline-upload-file')['default'] + IconIcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default'] + IconIcOutlineAddToPhotos: typeof import('~icons/ic/outline-add-to-photos')['default'] IconIcOutlineArchive: typeof import('~icons/ic/outline-archive')['default'] + IconIcOutlineDelete: typeof import('~icons/ic/outline-delete')['default'] + IconIcOutlineFolderOff: typeof import('~icons/ic/outline-folder-off')['default'] + IconIcOutlineFormatListNumbered: typeof import('~icons/ic/outline-format-list-numbered')['default'] IconIcOutlineTimer: typeof import('~icons/ic/outline-timer')['default'] + IconIcRoundLibraryAdd: typeof import('~icons/ic/round-library-add')['default'] IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default'] - IconLocalActivity: typeof import('~icons/local/activity')['default'] + IconIcRoundStarBorder: typeof import('~icons/ic/round-star-border')['default'] IconLocalBanner: typeof import('~icons/local/banner')['default'] - IconLocalReadLogo: typeof import('~icons/local/read-logo')['default'] IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] @@ -70,7 +80,6 @@ declare module 'vue' { NFormItem: typeof import('naive-ui')['NFormItem'] NGi: typeof import('naive-ui')['NGi'] NGrid: typeof import('naive-ui')['NGrid'] - NIcon: typeof import('naive-ui')['NIcon'] NInput: typeof import('naive-ui')['NInput'] NInputGroup: typeof import('naive-ui')['NInputGroup'] NInputNumber: typeof import('naive-ui')['NInputNumber'] @@ -78,7 +87,6 @@ declare module 'vue' { NListItem: typeof import('naive-ui')['NListItem'] NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider'] NMenu: typeof import('naive-ui')['NMenu'] - NMessage: typeof import('naive-ui')['NMessage'] NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NModal: typeof import('naive-ui')['NModal'] NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] @@ -89,7 +97,6 @@ declare module 'vue' { NSwitch: typeof import('naive-ui')['NSwitch'] NTab: typeof import('naive-ui')['NTab'] NTabs: typeof import('naive-ui')['NTabs'] - NTag: typeof import('naive-ui')['NTag'] NThing: typeof import('naive-ui')['NThing'] NTooltip: typeof import('naive-ui')['NTooltip'] NWatermark: typeof import('naive-ui')['NWatermark'] @@ -113,7 +120,6 @@ declare global { const AppProvider: typeof import('./../components/common/app-provider.vue')['default'] const BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default'] const ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] - const copy: typeof import('./../components/custom/wave-bg copy.vue')['default'] const CountTo: typeof import('./../components/custom/count-to.vue')['default'] const DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default'] const ExceptionBase: typeof import('./../components/common/exception-base.vue')['default'] @@ -124,23 +130,34 @@ declare global { const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] const IconIcBaselineAdd: typeof import('~icons/ic/baseline-add')['default'] const IconIcBaselineArrowBack: typeof import('~icons/ic/baseline-arrow-back')['default'] - const IconIcBaselineCalendar: typeof import('~icons/ic/baseline-calendar')['default'] + const IconIcBaselineArrowForward: typeof import('~icons/ic/baseline-arrow-forward')['default'] const IconIcBaselineCalendarMonth: typeof import('~icons/ic/baseline-calendar-month')['default'] + const IconIcBaselineCheck: typeof import('~icons/ic/baseline-check')['default'] const IconIcBaselineClose: typeof import('~icons/ic/baseline-close')['default'] const IconIcBaselineContentCopy: typeof import('~icons/ic/baseline-content-copy')['default'] - const IconIcBaselineCopy: typeof import('~icons/ic/baseline-copy')['default'] const IconIcBaselineDelete: typeof import('~icons/ic/baseline-delete')['default'] + const IconIcBaselineDownload: typeof import('~icons/ic/baseline-download')['default'] const IconIcBaselineEdit: typeof import('~icons/ic/baseline-edit')['default'] + const IconIcBaselineFlash: typeof import('~icons/ic/baseline-flash')['default'] const IconIcBaselineGroup: typeof import('~icons/ic/baseline-group')['default'] + const IconIcBaselineRefresh: typeof import('~icons/ic/baseline-refresh')['default'] const IconIcBaselineRocketLaunch: typeof import('~icons/ic/baseline-rocket-launch')['default'] const IconIcBaselineSave: typeof import('~icons/ic/baseline-save')['default'] + const IconIcBaselineScan: typeof import('~icons/ic/baseline-scan')['default'] + const IconIcBaselineSettingsBackupRestore: typeof import('~icons/ic/baseline-settings-backup-restore')['default'] + const IconIcBaselineUpload: typeof import('~icons/ic/baseline-upload')['default'] const IconIcBaselineUploadFile: typeof import('~icons/ic/baseline-upload-file')['default'] + const IconIcOutlineAccessTime: typeof import('~icons/ic/outline-access-time')['default'] + const IconIcOutlineAddToPhotos: typeof import('~icons/ic/outline-add-to-photos')['default'] const IconIcOutlineArchive: typeof import('~icons/ic/outline-archive')['default'] + const IconIcOutlineDelete: typeof import('~icons/ic/outline-delete')['default'] + const IconIcOutlineFolderOff: typeof import('~icons/ic/outline-folder-off')['default'] + const IconIcOutlineFormatListNumbered: typeof import('~icons/ic/outline-format-list-numbered')['default'] const IconIcOutlineTimer: typeof import('~icons/ic/outline-timer')['default'] + const IconIcRoundLibraryAdd: typeof import('~icons/ic/round-library-add')['default'] const IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default'] - const IconLocalActivity: typeof import('~icons/local/activity')['default'] + const IconIcRoundStarBorder: typeof import('~icons/ic/round-star-border')['default'] const IconLocalBanner: typeof import('~icons/local/banner')['default'] - const IconLocalReadLogo: typeof import('~icons/local/read-logo')['default'] const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default'] const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default'] const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default'] @@ -168,7 +185,6 @@ declare global { const NFormItem: typeof import('naive-ui')['NFormItem'] const NGi: typeof import('naive-ui')['NGi'] const NGrid: typeof import('naive-ui')['NGrid'] - const NIcon: typeof import('naive-ui')['NIcon'] const NInput: typeof import('naive-ui')['NInput'] const NInputGroup: typeof import('naive-ui')['NInputGroup'] const NInputNumber: typeof import('naive-ui')['NInputNumber'] @@ -176,7 +192,6 @@ declare global { const NListItem: typeof import('naive-ui')['NListItem'] const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider'] const NMenu: typeof import('naive-ui')['NMenu'] - const NMessage: typeof import('naive-ui')['NMessage'] const NMessageProvider: typeof import('naive-ui')['NMessageProvider'] const NModal: typeof import('naive-ui')['NModal'] const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] @@ -187,7 +202,6 @@ declare global { const NSwitch: typeof import('naive-ui')['NSwitch'] const NTab: typeof import('naive-ui')['NTab'] const NTabs: typeof import('naive-ui')['NTabs'] - const NTag: typeof import('naive-ui')['NTag'] const NThing: typeof import('naive-ui')['NThing'] const NTooltip: typeof import('naive-ui')['NTooltip'] const NWatermark: typeof import('naive-ui')['NWatermark'] diff --git a/apps/admin/src/typings/elegant-router.d.ts b/apps/admin/src/typings/elegant-router.d.ts index 9b33d6d..090fbbb 100644 --- a/apps/admin/src/typings/elegant-router.d.ts +++ b/apps/admin/src/typings/elegant-router.d.ts @@ -27,6 +27,7 @@ declare module "@elegant-router/types" { "home": "/home"; "iframe-page": "/iframe-page/:url"; "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; + "question": "/question"; }; /** @@ -65,6 +66,7 @@ declare module "@elegant-router/types" { | "home" | "iframe-page" | "login" + | "question" >; /** @@ -90,6 +92,7 @@ declare module "@elegant-router/types" { | "competition_competition-detail" | "competition_competition-list" | "home" + | "question" >; /** diff --git a/apps/admin/src/views/competition/competition-add/index.vue b/apps/admin/src/views/competition/competition-add/index.vue index 5ee98e2..2c3e022 100644 --- a/apps/admin/src/views/competition/competition-add/index.vue +++ b/apps/admin/src/views/competition/competition-add/index.vue @@ -31,27 +31,17 @@ const competitionData = ref({ groupCount: 10, // 组别数量 teamCount: 4, // 每组队伍数量 poster: '', // 竞赛海报 - questions: { // 赛题包配置 - regular: [// 主赛环节 - { value: 'hanzi', score: 1, time: 15 }, - { value: 'tongyin', score: 1, time: 15 }, - { value: 'pianpang', score: 1, time: 30 }, - { value: 'ciyu', score: 1, time: 30 }, - { value: 'chengyu', score: 1, time: 30 }, - { value: 'fanyi', score: 1, time: 60 }, - { value: 'jinyi', score: 1, time: 30 }, - { value: 'kantu', score: 1, time: 30 }, - { value: 'gushi', score: 1, time: 60 }, - { value: 'mingju', score: 1, time: 60 }, - ], - pk: [ // 加时赛环节 - { value: 'speed10', score: 1, time: 60 }, - { value: 'speed10', score: 1, time: 60 }, - { value: 'speed10', score: 1, time: 60 }, - { value: 'speed10', score: 1, time: 60 }, - { value: 'speed10', score: 3, time: 60 }, - ], - }, + rounds: [ // 赛题包配置 + { + id: 'round_default', + name: '第一轮:主赛环节', + questions: Array.from({ length: 10 }).map(() => ({ + value: null, + score: 5, + time: 30, + })), + }, + ], // 名单录入 groupManagement: [], // 组别管理 // 绑定硬件 @@ -88,6 +78,20 @@ function nextStep() { currentStep.value++ } else { + // 汇总整理数据 + + const { name, startTime, endTime, groupCount, teamCount, poster, rounds } = competitionData.value + // eslint-disable-next-line unused-imports/no-unused-vars + const params = { + name, + startTime, + endTime, + groupCount, + teamCount, + poster, + rounds, + } + // 提交逻辑 window.$message?.success('比赛发布成功') emit('success') @@ -100,12 +104,28 @@ function prevStep() { currentStep.value -= 1 } } + +function handleReset() { + window.$dialog?.warning({ + title: '重置确认', + content: '确定要重置当前步骤的数据吗?此操作无法撤销。', + positiveText: '确定重置', + negativeText: '取消', + onPositiveClick: () => { + const componentInstance = stepComponentRef.value + if (componentInstance && typeof componentInstance.reset === 'function') { + componentInstance.reset() + window.$message?.success('重置成功') + } + }, + }) +}