diff --git a/.vscode/settings.json b/.vscode/settings.json index b76ed51..70ad0b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,7 @@ // 3. 必须:强制所有相关文件使用 ESLint 插件作为格式化器 "[vue]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "Vue.volar" }, "[typescript]": { "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" diff --git a/apps/admin/build/plugins/unplugin.ts b/apps/admin/build/plugins/unplugin.ts index fd3f470..aa45072 100644 --- a/apps/admin/build/plugins/unplugin.ts +++ b/apps/admin/build/plugins/unplugin.ts @@ -8,12 +8,13 @@ import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' import Components from 'unplugin-vue-components/vite' import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +// 配置 unplugin 插件 export function setupUnplugin(viteEnv: Env.ImportMeta) { const { VITE_ICON_PREFIX, VITE_ICON_LOCAL_PREFIX } = viteEnv const localIconPath = path.join(process.cwd(), 'src/assets/svg-icon') - /** The name of the local icon collection */ + // 本地图标集合名称 const collectionName = VITE_ICON_LOCAL_PREFIX.replace(`${VITE_ICON_PREFIX}-`, '') const plugins: PluginOption[] = [ diff --git a/apps/admin/src/assets/svg-icon/back.svg b/apps/admin/src/assets/svg-icon/back.svg new file mode 100644 index 0000000..9b9be1b --- /dev/null +++ b/apps/admin/src/assets/svg-icon/back.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/src/assets/svg-icon/read-logo.svg b/apps/admin/src/assets/svg-icon/read-logo.svg new file mode 100644 index 0000000..84dacf2 --- /dev/null +++ b/apps/admin/src/assets/svg-icon/read-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/src/components/common/system-logo.vue b/apps/admin/src/components/common/system-logo.vue index 7777825..61fc708 100644 --- a/apps/admin/src/components/common/system-logo.vue +++ b/apps/admin/src/components/common/system-logo.vue @@ -1,150 +1,17 @@ diff --git a/apps/admin/src/components/custom/svg-icon.vue b/apps/admin/src/components/custom/svg-icon.vue index a64c508..ef13290 100644 --- a/apps/admin/src/components/custom/svg-icon.vue +++ b/apps/admin/src/components/custom/svg-icon.vue @@ -7,15 +7,15 @@ defineOptions({ name: 'SvgIcon', inheritAttrs: false }) const props = defineProps() /** - * Props + * 属性 * - * - Support iconify and local svg icon - * - If icon and localIcon are passed at the same time, localIcon will be rendered first + * - 支持 iconify 和本地 svg 图标 + * - 如果同时传递了 icon 和 localIcon,将优先渲染 localIcon */ interface Props { - /** Iconify icon name */ + /** Iconify 图标名称 */ icon?: string - /** Local svg icon name */ + /** 本地 svg 图标名称 */ localIcon?: string } @@ -26,8 +26,9 @@ const bindAttrs = computed<{ class: string, style: string }>(() => ({ style: (attrs.style as string) || '', })) +// 本地 svg 图标 id 格式:#icon-local-{icon} const symbolId = computed(() => { - const { VITE_ICON_LOCAL_PREFIX: prefix } = import.meta.env + const { VITE_ICON_LOCAL_PREFIX: prefix } = import.meta.env // icon-local const defaultLocalIcon = 'no-icon' @@ -36,17 +37,19 @@ const symbolId = computed(() => { return `#${prefix}-${icon}` }) -/** If localIcon is passed, render localIcon first */ +/** 如果传递了 localIcon,则优先渲染 localIcon */ const renderLocalIcon = computed(() => props.localIcon || !props.icon) diff --git a/apps/admin/src/hooks/common/router.ts b/apps/admin/src/hooks/common/router.ts index 0efd913..ea01be2 100644 --- a/apps/admin/src/hooks/common/router.ts +++ b/apps/admin/src/hooks/common/router.ts @@ -2,13 +2,14 @@ import type { RouteKey } from '@elegant-router/types' import type { RouteLocationRaw } from 'vue-router' import { useRouter } from 'vue-router' import { router as globalRouter } from '@/router' +import { useRouteStore } from '@/store/modules/route' /** - * Router push + * 路由跳转 * - * Jump to the specified route, it can replace function router.push + * 跳转到指定路由,可替换函数 router.push * - * @param inSetup Whether is in vue script setup + * @param inSetup 是否在 vue script setup 中 */ export function useRouterPush(inSetup = true) { const router = inSetup ? useRouter() : globalRouter @@ -36,10 +37,27 @@ export function useRouterPush(inSetup = true) { return routerPush(routeLocation) } + /** + * 路由跳转并携带 meta 中的 query 参数 + * + * @param key 路由名称 + */ function routerPushByKeyWithMetaQuery(key: RouteKey) { + const routeStore = useRouteStore() const allRoutes = router.getRoutes() const meta = allRoutes.find(item => item.name === key)?.meta || null + const menu = routeStore.menus.find(item => item.key === key) + if (menu?.children && menu.children.length > 0) { + const visibleChildren = menu.children.filter((child) => { + const childRoute = allRoutes.find(r => r.name === child.key) + return childRoute && !childRoute.meta?.hideInMenu + }) + if (visibleChildren.length === 1) { + return routerPushByKey(visibleChildren[0].key as RouteKey) + } + } + const query: Record = {} meta?.query?.forEach((item) => { @@ -54,10 +72,10 @@ export function useRouterPush(inSetup = true) { } /** - * Navigate to login page + * 跳转到登录页 * - * @param loginModule The login module - * @param redirectUrl The redirect url, if not specified, it will be the current route fullPath + * @param loginModule 登录模块 + * @param redirectUrl 重定向地址,若未指定,则为当前路由 fullPath */ async function toLogin(loginModule?: UnionKey.LoginModule, redirectUrl?: string) { const module = loginModule || 'pwd-login' @@ -78,7 +96,7 @@ export function useRouterPush(inSetup = true) { } /** - * Toggle login module + * 切换登录模块 * * @param module */ @@ -89,9 +107,9 @@ export function useRouterPush(inSetup = true) { } /** - * Redirect from login + * 从登录页重定向 * - * @param [needRedirect] Whether to redirect after login. Default is `true` + * @param [needRedirect] 登录后是否需要重定向。默认为 `true` */ async function redirectFromLogin(needRedirect = true) { const redirect = route.value.query?.redirect as string diff --git a/apps/admin/src/router/elegant/routes.ts b/apps/admin/src/router/elegant/routes.ts index a48541a..8078c46 100644 --- a/apps/admin/src/router/elegant/routes.ts +++ b/apps/admin/src/router/elegant/routes.ts @@ -55,8 +55,8 @@ export const generatedRoutes: GeneratedRoute[] = [ meta: { title: 'competition_competition-add', i18nKey: 'route.competition_competition-add', - multiTab: true,// 新增选项卡 - hideInMenu: true,// 隐藏在菜单中 + multiTab: true, + hideInMenu: true } }, { @@ -66,8 +66,8 @@ export const generatedRoutes: GeneratedRoute[] = [ meta: { title: 'competition_competition-detail', i18nKey: 'route.competition_competition-detail', - multiTab: true,// 新增选项卡 - hideInMenu: true,// 隐藏在菜单中 + multiTab: true, + hideInMenu: true } }, { diff --git a/apps/admin/src/typings/components.d.ts b/apps/admin/src/typings/components.d.ts index b95bac9..1ca9c07 100644 --- a/apps/admin/src/typings/components.d.ts +++ b/apps/admin/src/typings/components.d.ts @@ -15,6 +15,7 @@ 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'] @@ -23,7 +24,11 @@ declare module 'vue' { IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default'] IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default'] IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] + IconIcBaselineArrowBack: typeof import('~icons/ic/baseline-arrow-back')['default'] + IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default'] + IconLocalActivity: typeof import('~icons/local/activity')['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'] @@ -51,6 +56,7 @@ 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'] @@ -91,6 +97,7 @@ 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'] @@ -99,7 +106,11 @@ declare global { const IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default'] const IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default'] const IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default'] + const IconIcBaselineArrowBack: typeof import('~icons/ic/baseline-arrow-back')['default'] + const IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default'] + const IconLocalActivity: typeof import('~icons/local/activity')['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'] @@ -127,6 +138,7 @@ 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'] diff --git a/apps/admin/src/views/_builtin/login/index.vue b/apps/admin/src/views/_builtin/login/index.vue index 0a4d3f5..80f291e 100644 --- a/apps/admin/src/views/_builtin/login/index.vue +++ b/apps/admin/src/views/_builtin/login/index.vue @@ -56,6 +56,7 @@ const bgColor = computed(() => {
+

{{ $t('system.title') }}

diff --git a/apps/admin/src/views/competition/competition-add/index.vue b/apps/admin/src/views/competition/competition-add/index.vue index 6c4e715..8fd3de5 100644 --- a/apps/admin/src/views/competition/competition-add/index.vue +++ b/apps/admin/src/views/competition/competition-add/index.vue @@ -1,19 +1,69 @@ - diff --git a/apps/admin/src/views/competition/competition-add/modules/BasicInfo.vue b/apps/admin/src/views/competition/competition-add/modules/BasicInfo.vue new file mode 100644 index 0000000..01a6373 --- /dev/null +++ b/apps/admin/src/views/competition/competition-add/modules/BasicInfo.vue @@ -0,0 +1,57 @@ + + + diff --git a/apps/admin/src/views/competition/competition-add/modules/GroupManagement.vue b/apps/admin/src/views/competition/competition-add/modules/GroupManagement.vue new file mode 100644 index 0000000..ab54f60 --- /dev/null +++ b/apps/admin/src/views/competition/competition-add/modules/GroupManagement.vue @@ -0,0 +1,60 @@ + + + diff --git a/apps/admin/src/views/competition/competition-add/modules/HardwareBinding.vue b/apps/admin/src/views/competition/competition-add/modules/HardwareBinding.vue new file mode 100644 index 0000000..43a0d62 --- /dev/null +++ b/apps/admin/src/views/competition/competition-add/modules/HardwareBinding.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue b/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue new file mode 100644 index 0000000..bbad1cc --- /dev/null +++ b/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue @@ -0,0 +1,95 @@ + + + diff --git a/apps/admin/src/views/competition/competition-add/modules/data.ts b/apps/admin/src/views/competition/competition-add/modules/data.ts new file mode 100644 index 0000000..dbdf4de --- /dev/null +++ b/apps/admin/src/views/competition/competition-add/modules/data.ts @@ -0,0 +1,70 @@ +// src/views/competition/modules/data.ts + +export interface CompetitionItem { + id: string + title: string + date: string + teamCount: number + status: 'ongoing' | 'pending' // ongoing: 进行中, pending: 未开始 + isHighlight?: boolean // 用于标记第一个卡片的特殊背景 +} + +export const competitionList: CompetitionItem[] = [ + { + id: '1', + title: '阅读之星大赛\n辞海遨游环节', // 使用换行符或在组件中处理换行 + date: '2026.01.12', + teamCount: 40, + status: 'ongoing', + isHighlight: true, + }, + { + id: '2', + title: '诗词大会总决赛', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '3', + title: '趣味百科大作战', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '4', + title: '经典文学研读周', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '5', + title: '未来科学家阅读赛', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '6', + title: '历史人文知识赛', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '7', + title: '少儿绘本创意营', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '8', + title: '唐诗三百首挑战', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, +] diff --git a/apps/admin/src/views/competition/competition-detail/index.vue b/apps/admin/src/views/competition/competition-detail/index.vue index f2ef503..2ed7549 100644 --- a/apps/admin/src/views/competition/competition-detail/index.vue +++ b/apps/admin/src/views/competition/competition-detail/index.vue @@ -1,5 +1,98 @@ + + + + diff --git a/apps/admin/src/views/competition/competition-detail/modules/BasicInfoCard.vue b/apps/admin/src/views/competition/competition-detail/modules/BasicInfoCard.vue new file mode 100644 index 0000000..6716e7a --- /dev/null +++ b/apps/admin/src/views/competition/competition-detail/modules/BasicInfoCard.vue @@ -0,0 +1,80 @@ + + + diff --git a/apps/admin/src/views/competition/competition-detail/modules/QuestionConfigCard.vue b/apps/admin/src/views/competition/competition-detail/modules/QuestionConfigCard.vue new file mode 100644 index 0000000..6777494 --- /dev/null +++ b/apps/admin/src/views/competition/competition-detail/modules/QuestionConfigCard.vue @@ -0,0 +1,101 @@ + + + diff --git a/apps/admin/src/views/competition/competition-detail/modules/SideMenu.vue b/apps/admin/src/views/competition/competition-detail/modules/SideMenu.vue new file mode 100644 index 0000000..4571738 --- /dev/null +++ b/apps/admin/src/views/competition/competition-detail/modules/SideMenu.vue @@ -0,0 +1,53 @@ + + + diff --git a/apps/admin/src/views/competition/competition-detail/modules/TeamListCard.vue b/apps/admin/src/views/competition/competition-detail/modules/TeamListCard.vue new file mode 100644 index 0000000..afa9e29 --- /dev/null +++ b/apps/admin/src/views/competition/competition-detail/modules/TeamListCard.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/admin/src/views/competition/competition-detail/modules/data.ts b/apps/admin/src/views/competition/competition-detail/modules/data.ts new file mode 100644 index 0000000..dbdf4de --- /dev/null +++ b/apps/admin/src/views/competition/competition-detail/modules/data.ts @@ -0,0 +1,70 @@ +// src/views/competition/modules/data.ts + +export interface CompetitionItem { + id: string + title: string + date: string + teamCount: number + status: 'ongoing' | 'pending' // ongoing: 进行中, pending: 未开始 + isHighlight?: boolean // 用于标记第一个卡片的特殊背景 +} + +export const competitionList: CompetitionItem[] = [ + { + id: '1', + title: '阅读之星大赛\n辞海遨游环节', // 使用换行符或在组件中处理换行 + date: '2026.01.12', + teamCount: 40, + status: 'ongoing', + isHighlight: true, + }, + { + id: '2', + title: '诗词大会总决赛', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '3', + title: '趣味百科大作战', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '4', + title: '经典文学研读周', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '5', + title: '未来科学家阅读赛', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '6', + title: '历史人文知识赛', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '7', + title: '少儿绘本创意营', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, + { + id: '8', + title: '唐诗三百首挑战', + date: '2026.01.12', + teamCount: 32, + status: 'pending', + }, +] diff --git a/apps/admin/src/views/competition/competition-list/index.vue b/apps/admin/src/views/competition/competition-list/index.vue index 179d0f6..dc9174f 100644 --- a/apps/admin/src/views/competition/competition-list/index.vue +++ b/apps/admin/src/views/competition/competition-list/index.vue @@ -1,17 +1,19 @@ @@ -39,11 +41,7 @@ function handleAdd() { - + diff --git a/apps/admin/src/views/competition/competition-list/modules/competition-card.vue b/apps/admin/src/views/competition/competition-list/modules/competition-card.vue index 29b9ca2..b1ae253 100644 --- a/apps/admin/src/views/competition/competition-list/modules/competition-card.vue +++ b/apps/admin/src/views/competition/competition-list/modules/competition-card.vue @@ -2,13 +2,16 @@ import type { CompetitionItem } from './data' import { NButton, NCard, NTag } from 'naive-ui' import { computed } from 'vue' +import { useRouterPush } from '@/hooks/common/router' + +const props = defineProps() + +const { routerPushByKey } = useRouterPush() interface Props { item: CompetitionItem } -const props = defineProps() - // 状态配置:颜色和文本 const statusConfig = computed(() => { if (props.item.status === 'ongoing') { @@ -19,13 +22,17 @@ const statusConfig = computed(() => { // 处理标题换行 const formattedTitle = computed(() => props.item.title.replace(/\n/g, '
')) + +function toDetail(item: CompetitionItem) { + routerPushByKey('competition_competition-detail', { query: { id: item.id } }) +}