feat(dashboard): 新增比赛管理仪表盘功能

添加比赛管理仪表盘页面,包含比赛卡片列表、状态标签和新增功能
移除旧的mine页面和相关路由配置
强制设置i18n语言为中文并更新系统标题
更新VSCode设置中的typescript格式化工具
This commit is contained in:
2026-01-16 16:47:50 +08:00
parent 1e510efe8e
commit b7ec446f19
15 changed files with 280 additions and 90 deletions

View File

@ -13,7 +13,7 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint"

View File

@ -1,12 +1,17 @@
import type { App } from 'vue' import type { App } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import { localStg } from '@/utils/storage'
import messages from './locale' import messages from './locale'
const i18n = createI18n({ const i18n = createI18n({
locale: localStg.get('lang') || 'zh-CN', // locale: localStg.get('lang') || 'zh-CN',
fallbackLocale: 'en', // fallbackLocale: 'en',
// messages,
// legacy: false,
locale: 'zh-CN', // 强制设为中文
fallbackLocale: 'zh-CN', // 回退语言也设为中文
allowComposition: true,
messages, messages,
// 建议关闭旧版兼容模式,减少控制台警告
legacy: false, legacy: false,
}) })

View File

@ -1,6 +1,6 @@
const local: App.I18n.Schema = { const local: App.I18n.Schema = {
system: { system: {
title: 'reader-star 管理系统', title: '阅读比赛配置后台',
updateTitle: '系统版本更新通知', updateTitle: '系统版本更新通知',
updateContent: '检测到系统有新版本发布,是否立即刷新页面?', updateContent: '检测到系统有新版本发布,是否立即刷新页面?',
updateConfirm: '立即刷新', updateConfirm: '立即刷新',

View File

@ -20,6 +20,6 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
500: () => import("@/views/_builtin/500/index.vue"), 500: () => import("@/views/_builtin/500/index.vue"),
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"), "iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"), login: () => import("@/views/_builtin/login/index.vue"),
dashboard: () => import("@/views/dashboard/index.vue"),
home: () => import("@/views/home/index.vue"), home: () => import("@/views/home/index.vue"),
mine: () => import("@/views/mine/index.vue"),
}; };

View File

@ -39,6 +39,15 @@ export const generatedRoutes: GeneratedRoute[] = [
hideInMenu: true hideInMenu: true
} }
}, },
{
name: 'dashboard',
path: '/dashboard',
component: 'layout.base$view.dashboard',
meta: {
title: 'dashboard',
i18nKey: 'route.dashboard'
}
},
{ {
name: 'home', name: 'home',
path: '/home', path: '/home',
@ -74,14 +83,5 @@ export const generatedRoutes: GeneratedRoute[] = [
constant: true, constant: true,
hideInMenu: true hideInMenu: true
} }
},
{
name: 'mine',
path: '/mine',
component: 'layout.base$view.mine',
meta: {
title: 'mine',
i18nKey: 'route.mine'
}
} }
]; ];

View File

@ -166,10 +166,10 @@ const routeMap: RouteMap = {
"403": "/403", "403": "/403",
"404": "/404", "404": "/404",
"500": "/500", "500": "/500",
"dashboard": "/dashboard",
"home": "/home", "home": "/home",
"iframe-page": "/iframe-page/:url", "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)?"
"mine": "/mine"
}; };
/** /**

View File

@ -1,84 +1,66 @@
/** Default theme settings */ /** Default theme settings */
export const themeSettings: App.Theme.ThemeSetting = { export const themeSettings: App.Theme.ThemeSetting = {
themeScheme: 'light', themeScheme: 'light', // light | dark
grayscale: false, grayscale: false, // 是否开启灰度模式
colourWeakness: false, colourWeakness: false, // 是否开启色弱模式
recommendColor: false, recommendColor: false, // 是否开启推荐颜色
themeColor: '#646cff', themeColor: '#646cff', // 主题颜色
themeRadius: 6, themeRadius: 6, // 主题圆角
otherColor: { otherColor: {
info: '#2080f0', info: '#2080f0',
success: '#52c41a', success: '#52c41a',
warning: '#faad14', warning: '#faad14',
error: '#f5222d', error: '#f5222d',
}, },
isInfoFollowPrimary: true, isInfoFollowPrimary: true, // 是否开启信息类颜色跟随主题颜色
layout: { layout: { mode: 'horizontal', scrollMode: 'content' }, // 布局模式 mode: horizontal | vertical
mode: 'vertical', page: { animate: true, animateMode: 'fade-slide' }, // 页面动画模式
scrollMode: 'content',
},
page: {
animate: true,
animateMode: 'fade-slide',
},
header: { header: {
height: 56, height: 56, // 头部高度
breadcrumb: { breadcrumb: { visible: true, showIcon: true }, // 是否显示面包屑
visible: true, multilingual: { visible: true }, // 是否显示多语言切换
showIcon: true, globalSearch: { visible: true }, // 是否显示全局搜索
},
multilingual: {
visible: true,
},
globalSearch: {
visible: true,
},
}, },
tab: { tab: {
visible: true, visible: true, // 是否显示标签页
cache: true, cache: true, // 是否开启标签页缓存
height: 44, height: 44, // 标签页高度
mode: 'chrome', mode: 'chrome', // 标签页模式 chrome | default
closeTabByMiddleClick: false, closeTabByMiddleClick: false, // 是否开启通过中键关闭标签页
}, },
fixedHeaderAndTab: true, fixedHeaderAndTab: true, // 是否固定头部和标签页
sider: { sider: {
inverted: false, inverted: false, // 是否开启侧边栏反色
width: 220, width: 220, // 侧边栏宽度
collapsedWidth: 64, collapsedWidth: 64, // 侧边栏折叠宽度
mixWidth: 90, mixWidth: 90, // 侧边栏混合宽度
mixCollapsedWidth: 64, mixCollapsedWidth: 64, // 侧边栏混合折叠宽度
mixChildMenuWidth: 200, mixChildMenuWidth: 200, // 侧边栏混合子菜单宽度
autoSelectFirstMenu: false, autoSelectFirstMenu: false, // 是否自动选择第一个菜单
},
footer: {
visible: true,
fixed: false,
height: 48,
right: true,
}, },
footer: { visible: true, fixed: false, height: 48, right: true }, // 是否固定底部
watermark: { watermark: {
visible: false, visible: false, // 是否显示水印
text: 'reader-starAdmin', text: 'reader-starAdmin', // 水印文本
enableUserName: false, enableUserName: false, // 是否开启显示用户名
enableTime: false, enableTime: false, // 是否开启显示时间
timeFormat: 'YYYY-MM-DD HH:mm', timeFormat: 'YYYY-MM-DD HH:mm', // 时间格式
}, },
tokens: { tokens: {
light: { light: { // 亮色主题
colors: { colors: {
'container': 'rgb(255, 255, 255)', 'container': 'rgb(255, 255, 255)',
'layout': 'rgb(247, 250, 252)', 'layout': 'rgb(247, 250, 252)',
'inverted': 'rgb(0, 20, 40)', 'inverted': 'rgb(0, 20, 40)',
'base-text': 'rgb(31, 31, 31)', 'base-text': 'rgb(31, 31, 31)',
}, },
boxShadow: { boxShadow: { // 亮色主题阴影
header: '0 1px 2px rgb(0, 21, 41, 0.08)', header: '0 1px 2px rgb(0, 21, 41, 0.08)',
sider: '2px 0 8px 0 rgb(29, 35, 41, 0.05)', sider: '2px 0 8px 0 rgb(29, 35, 41, 0.05)',
tab: '0 1px 2px rgb(0, 21, 41, 0.08)', tab: '0 1px 2px rgb(0, 21, 41, 0.08)',
}, },
}, },
dark: { dark: { // 暗色主题
colors: { colors: {
'container': 'rgb(28, 28, 28)', 'container': 'rgb(28, 28, 28)',
'layout': 'rgb(18, 18, 18)', 'layout': 'rgb(18, 18, 18)',

View File

@ -68,6 +68,7 @@ declare module 'vue' {
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab'] NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs'] NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing'] NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip'] NTooltip: typeof import('naive-ui')['NTooltip']
NWatermark: typeof import('naive-ui')['NWatermark'] NWatermark: typeof import('naive-ui')['NWatermark']
@ -143,6 +144,7 @@ declare global {
const NSwitch: typeof import('naive-ui')['NSwitch'] const NSwitch: typeof import('naive-ui')['NSwitch']
const NTab: typeof import('naive-ui')['NTab'] const NTab: typeof import('naive-ui')['NTab']
const NTabs: typeof import('naive-ui')['NTabs'] const NTabs: typeof import('naive-ui')['NTabs']
const NTag: typeof import('naive-ui')['NTag']
const NThing: typeof import('naive-ui')['NThing'] const NThing: typeof import('naive-ui')['NThing']
const NTooltip: typeof import('naive-ui')['NTooltip'] const NTooltip: typeof import('naive-ui')['NTooltip']
const NWatermark: typeof import('naive-ui')['NWatermark'] const NWatermark: typeof import('naive-ui')['NWatermark']

View File

@ -20,10 +20,10 @@ declare module "@elegant-router/types" {
"403": "/403"; "403": "/403";
"404": "/404"; "404": "/404";
"500": "/500"; "500": "/500";
"dashboard": "/dashboard";
"home": "/home"; "home": "/home";
"iframe-page": "/iframe-page/:url"; "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)?";
"mine": "/mine";
}; };
/** /**
@ -58,10 +58,10 @@ declare module "@elegant-router/types" {
| "403" | "403"
| "404" | "404"
| "500" | "500"
| "dashboard"
| "home" | "home"
| "iframe-page" | "iframe-page"
| "login" | "login"
| "mine"
>; >;
/** /**
@ -83,8 +83,8 @@ declare module "@elegant-router/types" {
| "500" | "500"
| "iframe-page" | "iframe-page"
| "login" | "login"
| "dashboard"
| "home" | "home"
| "mine"
>; >;
/** /**

View File

@ -4,7 +4,6 @@ import { getPaletteColorByNumber, mixColor } from '@sa/color'
import { computed } from 'vue' import { computed } from 'vue'
import { loginModuleRecord } from '@/constants/app' import { loginModuleRecord } from '@/constants/app'
import { $t } from '@/locales' import { $t } from '@/locales'
import { useAppStore } from '@/store/modules/app'
import { useThemeStore } from '@/store/modules/theme' import { useThemeStore } from '@/store/modules/theme'
import BindWechat from './modules/bind-wechat.vue' import BindWechat from './modules/bind-wechat.vue'
import CodeLogin from './modules/code-login.vue' import CodeLogin from './modules/code-login.vue'
@ -19,7 +18,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const appStore = useAppStore() // const appStore = useAppStore()
const themeStore = useThemeStore() const themeStore = useThemeStore()
interface LoginModule { interface LoginModule {
@ -67,13 +66,13 @@ const bgColor = computed(() => {
class="text-20px lt-sm:text-18px" class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme" @switch="themeStore.toggleThemeScheme"
/> />
<LangSwitch <!-- <LangSwitch
v-if="themeStore.header.multilingual.visible" v-if="themeStore.header.multilingual.visible"
:lang="appStore.locale" :lang="appStore.locale"
:lang-options="appStore.localeOptions" :lang-options="appStore.localeOptions"
:show-tooltip="false" :show-tooltip="false"
@change-lang="appStore.changeLocale" @change-lang="appStore.changeLocale"
/> /> -->
</div> </div>
</header> </header>
<main class="pt-24px"> <main class="pt-24px">

View File

@ -48,6 +48,7 @@ interface Account {
password: string password: string
} }
// eslint-disable-next-line unused-imports/no-unused-vars
const accounts = computed<Account[]>(() => [ const accounts = computed<Account[]>(() => [
{ {
key: 'super', key: 'super',
@ -69,6 +70,7 @@ const accounts = computed<Account[]>(() => [
}, },
]) ])
// eslint-disable-next-line unused-imports/no-unused-vars
async function handleAccountLogin(account: Account) { async function handleAccountLogin(account: Account) {
await authStore.login(account.userName, account.password) await authStore.login(account.userName, account.password)
} }
@ -105,14 +107,14 @@ async function handleAccountLogin(account: Account) {
{{ $t(loginModuleRecord.register) }} {{ $t(loginModuleRecord.register) }}
</NButton> </NButton>
</div> </div>
<NDivider class="text-14px text-#666 !m-0"> <!-- <NDivider class="text-14px text-#666 !m-0">
{{ $t('page.login.pwdLogin.otherAccountLogin') }} {{ $t('page.login.pwdLogin.otherAccountLogin') }}
</NDivider> </NDivider> -->
<div class="flex-center gap-12px"> <!-- <div class="flex-center gap-12px">
<NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)"> <NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)">
{{ item.label }} {{ item.label }}
</NButton> </NButton>
</div> </div> -->
</NSpace> </NSpace>
</NForm> </NForm>
</template> </template>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import { NButton, NGi, NGrid, NSpace } from 'naive-ui'
import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app'
import CompetitionCard from './modules/competition-card.vue'
import { competitionList } from './modules/data'
// 引入 store 用于判断移动端布局 (与你提供的风格保持一致)
const appStore = useAppStore()
const gap = computed(() => (appStore.isMobile ? 12 : 16))
// 模拟新增操作
function handleAdd() {
window.$message?.success('点击了新增比赛活动')
}
</script>
<template>
<NSpace vertical :size="24">
<!-- 顶部 Header 区域 -->
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
<div>
<h2 class="m-0 text-xl text-gray-800 font-bold dark:text-white">
比赛列表
</h2>
<p class="mt-1 text-sm text-gray-500">
管理并配置所有阅读大比拼活动
</p>
</div>
<NButton type="primary" size="medium" class="px-6 font-medium" @click="handleAdd">
<template #icon>
<div class="i-carbon-add text-lg" />
</template>
新增比赛活动
</NButton>
</div>
<!-- 卡片列表区域 -->
<!-- 响应式布局手机1列平板2列中屏3列大屏4列 -->
<NGrid :x-gap="gap" :y-gap="gap" responsive="screen" item-responsive>
<NGi
v-for="item in competitionList"
:key="item.id"
span="24 s:12 m:8 l:6"
>
<CompetitionCard :item="item" />
</NGi>
</NGrid>
</NSpace>
</template>
<style scoped>
/* 针对不同屏幕微调间距 */
</style>

View File

@ -0,0 +1,84 @@
<script setup lang="ts">
import type { CompetitionItem } from './data'
import { NButton, NCard, NTag } from 'naive-ui'
import { computed } from 'vue'
interface Props {
item: CompetitionItem
}
const props = defineProps<Props>()
// 状态配置:颜色和文本
const statusConfig = computed(() => {
if (props.item.status === 'ongoing') {
return { type: 'success', text: '进行中', bgClass: 'bg-green-100 text-green-600' }
}
return { type: 'default', text: '未开始', bgClass: 'bg-gray-100 text-gray-500' }
})
// 处理标题换行
const formattedTitle = computed(() => props.item.title.replace(/\n/g, '<br/>'))
</script>
<template>
<NCard
:bordered="false"
class="group relative h-full overflow-hidden rounded-2xl transition-all duration-300 hover:shadow-lg"
content-style="padding: 24px; display: flex; flex-direction: column; height: 100%;"
>
<!-- 特殊背景装饰 (仅高亮卡片显示) -->
<div
v-if="item.isHighlight"
class="pointer-events-none absolute right-0 top-0 z-0 h-32 w-32 rounded-bl-full bg-blue-50/80 -mr-8 -mt-8"
/>
<div class="relative z-10 h-full flex flex-col justify-between">
<!-- 顶部内容 -->
<div>
<!-- 状态标签 -->
<div class="mb-4">
<NTag
:bordered="false"
size="small"
round
class="px-3 font-medium" :class="[statusConfig.bgClass]"
>
{{ statusConfig.text }}
</NTag>
</div>
<!-- 标题 -->
<h3
class="mb-3 min-h-[3rem] text-lg text-gray-800 font-bold leading-tight dark:text-white"
v-html="formattedTitle"
/>
<!-- 日期 -->
<div class="mb-6 flex items-center text-sm text-gray-400">
<div class="i-carbon-calendar mr-2 text-base" />
<span>{{ item.date }}</span>
</div>
</div>
<!-- 底部内容 -->
<div class="flex items-center justify-between border-t border-gray-50 pt-4 dark:border-gray-800">
<span class="text-xs text-gray-500 font-medium">
{{ item.teamCount }} 支参赛队伍
</span>
<NButton
text
type="primary"
size="small"
class="font-bold hover:underline"
>
查看配置详情
</NButton>
</div>
</div>
</NCard>
</template>
<style scoped>
/* 可选:微调样式 */
</style>

View File

@ -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',
},
]

View File

@ -1,9 +0,0 @@
<script lang="ts" setup></script>
<template>
<div>
<h1>Mine</h1>
</div>
</template>
<style scoped></style>