refactor: 将记分板功能模块化为可复用组件

- 创建 app/modules/scoreboard 模块目录
- 添加配置常量 (EVENT_CATEGORIES, TEAM_GROUPS, EVENT_TYPES)
- 添加 API 工具函数 (fetchEvents, fetchTeams, fetchResults 等)
- 添加可复用组件 (ModuleLayout, StatCard, DataTable)
- 添加模块导出文件 (mod.ts)
- 添加模块使用文档 (README.md)
- 更新首页使用模块 API 函数

后续可通过 import { xxx } from '~/modules/scoreboard' 导入使用
This commit is contained in:
Administrator 2026-03-17 22:33:01 +08:00
parent 22f073d8e7
commit 94dbd0d34c
7 changed files with 583 additions and 0 deletions

View File

@ -0,0 +1,51 @@
<template>
<el-table :data="data" border stripe class="scoreboard-table">
<el-table-column
v-for="column in columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:width="column.width"
:class-name="column.className"
:sortable="column.sortable"
>
<template v-if="column.slot" #default="{ row }">
<slot :name="column.slot" :row="row">
{{ row[column.prop] }}
</slot>
</template>
</el-table-column>
</el-table>
</template>
<script setup lang="ts">
interface Column {
prop: string
label: string
width?: number | string
className?: string
sortable?: boolean
slot?: string
}
interface Props {
data: any[]
columns: Column[]
}
defineProps<Props>()
</script>
<style scoped>
.scoreboard-table {
width: 100%;
}
@media (max-width: 900px) {
.scoreboard-table :deep(.col-group),
.scoreboard-table :deep(.col-medal),
.scoreboard-table :deep(.col-time) {
display: none;
}
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<div class="scoreboard-module">
<slot name="header">
<header class="module-header">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</header>
</slot>
<div class="module-content">
<slot />
</div>
<footer class="module-footer">
<slot name="footer">
<p>© {{ currentYear }} 运动会记分板系统 - 版权所有</p>
</slot>
</footer>
</div>
</template>
<script setup lang="ts">
interface Props {
title?: string
description?: string
}
const props = withDefaults(defineProps<Props>(), {
title: '运动会记分板',
description: '运动会管理系统'
})
const currentYear = new Date().getFullYear()
</script>
<style scoped>
.scoreboard-module {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.module-header {
background-color: #304156;
color: #fff;
padding: 20px;
text-align: center;
}
.module-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
}
.module-header p {
margin: 0;
opacity: 0.8;
font-size: 14px;
}
.module-content {
flex: 1;
background-color: #f0f2f5;
}
.module-footer {
background-color: #fff;
border-top: 1px solid #e6e6e6;
padding: 12px;
text-align: center;
}
.module-footer p {
margin: 0;
color: #909399;
font-size: 12px;
}
</style>

View File

@ -0,0 +1,155 @@
# 运动会记分板模块
可复用的运动会管理模块,提供完整的比赛项目、队伍、成绩管理功能。
## 目录结构
```
modules/scoreboard/
├── index.ts # 模块配置和常量
├── api.ts # API 工具函数
├── mod.ts # 模块导出
├── ModuleLayout.vue # 模块布局组件
├── StatCard.vue # 统计卡片组件
└── DataTable.vue # 数据表格组件
```
## 使用方式
### 1. 导入配置常量
```ts
import {
EVENT_CATEGORIES,
TEAM_GROUPS,
EVENT_TYPES,
SCORING_RULES
} from '~/modules/scoreboard'
// 使用示例
const categories = Object.values(EVENT_CATEGORIES)
// ['田赛', '径赛', '团体赛']
```
### 2. 使用 API 函数
```ts
import {
fetchEvents,
fetchTeams,
fetchResults,
fetchScoreboard,
createResult
} from '~/modules/scoreboard'
// 获取所有比赛项目
const events = await fetchEvents()
// 按类别筛选
const trackEvents = await fetchEvents({ category: '径赛' })
// 获取队伍
const teams = await fetchTeams({ group: '文化班甲组' })
// 录入成绩
await createResult({
event_id: 1,
team_id: 1,
score: '10.5',
rank: 1
})
// 获取记分板
const scoreboard = await fetchScoreboard()
```
### 3. 使用 UI 组件
```vue
<template>
<ModuleLayout title="运动会" description="管理系统">
<el-row :gutter="20">
<el-col :span="6">
<StatCard
icon="Trophy"
label="比赛项目"
:value="12"
color="#409eff"
/>
</el-col>
</el-row>
<DataTable :data="scoreboard" :columns="columns" />
</ModuleLayout>
</template>
<script setup>
import { ModuleLayout, StatCard, DataTable } from '~/modules/scoreboard'
const columns = [
{ prop: 'name', label: '队伍名称' },
{ prop: 'total_score', label: '总分', sortable: true }
]
</script>
```
## 积分规则
| 名次 | 积分 | 奖牌 |
|------|------|------|
| 第 1 名 | 7 分 | 金牌 |
| 第 2 名 | 5 分 | 银牌 |
| 第 3 名 | 3 分 | 铜牌 |
## 比赛项目
### 田赛
- 跳高(米)
- 跳远(米)
- 掷铅球(米)
### 径赛
- 100m
- 200m
- 400m
- 4×100m
- 4×400m
- 20×50m
### 团体赛
- 旱地龙舟(秒)
- 跳长绳(次)
- 折返跑(秒)
## 组别
- 教师组
- 航空班组
- 体育班组
- 文化班甲组
- 文化班乙组
## API 接口
| 接口 | 方法 | 描述 |
|------|------|------|
| `/api/config` | GET | 获取系统配置 |
| `/api/events` | GET/POST | 比赛项目管理 |
| `/api/teams` | GET/POST | 队伍管理 |
| `/api/results` | GET/POST | 成绩管理 |
| `/api/scoreboard` | GET | 记分板数据 |
| `/api/seed` | POST | 初始化数据 |
## 扩展模块
可以通过以下方式扩展模块功能:
1. 添加新的比赛项目类型
2. 自定义积分规则
3. 添加数据导出功能
4. 集成图表展示
5. 添加实时通知
## License
MIT

View File

@ -0,0 +1,66 @@
<template>
<el-card class="stat-card">
<div class="stat-content">
<el-icon class="stat-icon" :color="color">
<component :is="icon" />
</el-icon>
<div class="stat-info">
<div class="stat-value">{{ value }}</div>
<div class="stat-label">{{ label }}</div>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
interface Props {
icon: string
label: string
value?: string | number
color?: string
}
withDefaults(defineProps<Props>(), {
value: '0',
color: '#409eff'
})
</script>
<style scoped>
.stat-card {
text-align: center;
}
.stat-content {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.stat-icon {
font-size: 40px;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #303133;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-top: 5px;
}
@media (max-width: 900px) {
.stat-value {
font-size: 24px;
}
.stat-icon {
font-size: 28px;
}
}
</style>

View File

@ -0,0 +1,106 @@
/**
* API
*/
/**
*
*/
export const fetchConfig = async () => {
const res = await $fetch('/api/config')
return res.data
}
/**
*
*/
export const fetchEvents = async (params?: { category?: string; group?: string }) => {
const query = new URLSearchParams()
if (params?.category) query.append('category', params.category)
if (params?.group) query.append('group', params.group)
const res = await $fetch(`/api/events?${query}`)
return res.data
}
/**
*
*/
export const createEvent = async (data: {
name: string
category: string
event_group: string
unit: string
}) => {
const res = await $fetch('/api/events', {
method: 'POST',
body: data
})
return res.data
}
/**
*
*/
export const fetchTeams = async (params?: { group?: string }) => {
const query = new URLSearchParams()
if (params?.group) query.append('group', params.group)
const res = await $fetch(`/api/teams?${query}`)
return res.data
}
/**
*
*/
export const createTeam = async (data: { name: string; team_group: string }) => {
const res = await $fetch('/api/teams', {
method: 'POST',
body: data
})
return res.data
}
/**
*
*/
export const fetchResults = async (params?: { event_id?: number; team_id?: number }) => {
const query = new URLSearchParams()
if (params?.event_id) query.append('event_id', String(params.event_id))
if (params?.team_id) query.append('team_id', String(params.team_id))
const res = await $fetch(`/api/results?${query}`)
return res.data
}
/**
*
*/
export const createResult = async (data: {
event_id: number
team_id: number
score: string
rank?: number
}) => {
const res = await $fetch('/api/results', {
method: 'POST',
body: data
})
return res.data
}
/**
*
*/
export const fetchScoreboard = async (params?: { group?: string }) => {
const query = new URLSearchParams()
if (params?.group) query.append('group', params.group)
const res = await $fetch(`/api/scoreboard?${query}`)
return res.data
}
/**
*
*/
export const seedData = async () => {
const res = await $fetch('/api/seed', {
method: 'POST'
})
return res
}

View File

@ -0,0 +1,77 @@
/**
*
*
*
*
*/
// 模块配置
export const scoreboardModule = {
name: 'scoreboard',
version: '1.0.0',
description: '运动会记分板管理模块'
}
// 比赛类别配置
export const EVENT_CATEGORIES = {
FIELD: '田赛',
TRACK: '径赛',
TEAM: '团体赛'
} as const
// 组别配置
export const TEAM_GROUPS = {
TEACHER: '教师组',
AVIATION: '航空班组',
SPORTS: '体育班组',
CULTURE_A: '文化班甲组',
CULTURE_B: '文化班乙组'
} as const
// 项目配置
export const EVENT_TYPES = {
[EVENT_CATEGORIES.FIELD]: [
{ name: '跳高', unit: '米' },
{ name: '跳远', unit: '米' },
{ name: '掷铅球', unit: '米' }
],
[EVENT_CATEGORIES.TRACK]: [
{ name: '100m', unit: '秒' },
{ name: '200m', unit: '秒' },
{ name: '400m', unit: '秒' },
{ name: '4×100m', unit: '秒' },
{ name: '4×400m', unit: '秒' },
{ name: '20×50m', unit: '秒' }
],
[EVENT_CATEGORIES.TEAM]: [
{ name: '旱地龙舟', unit: '秒' },
{ name: '跳长绳', unit: '次' },
{ name: '折返跑', unit: '秒' }
]
}
// 积分规则
export const SCORING_RULES = {
GOLD: { rank: 1, points: 7, medal: 'gold' },
SILVER: { rank: 2, points: 5, medal: 'silver' },
BRONZE: { rank: 3, points: 3, medal: 'bronze' }
}
// API 路由
export const API_ROUTES = {
EVENTS: '/api/events',
TEAMS: '/api/teams',
RESULTS: '/api/results',
SCOREBOARD: '/api/scoreboard',
CONFIG: '/api/config',
SEED: '/api/seed'
} as const
// 页面路由
export const PAGE_ROUTES = {
HOME: '/',
EVENTS: '/events',
TEAMS: '/teams',
RESULTS: '/results',
SCOREBOARD: '/scoreboard'
} as const

View File

@ -0,0 +1,50 @@
/**
*
*
* 使:
* ```ts
* import {
* EVENT_CATEGORIES,
* TEAM_GROUPS,
* EVENT_TYPES,
* SCORING_RULES,
* API_ROUTES,
* fetchEvents,
* fetchTeams,
* fetchResults,
* fetchScoreboard
* } from '~/modules/scoreboard'
* ```
*/
// 配置常量
export {
EVENT_CATEGORIES,
TEAM_GROUPS,
EVENT_TYPES,
SCORING_RULES,
API_ROUTES,
PAGE_ROUTES,
scoreboardModule
} from './index'
// API 函数
export {
fetchConfig,
fetchEvents,
createEvent,
fetchTeams,
createTeam,
fetchResults,
createResult,
fetchScoreboard,
seedData
} from './api'
// Vue 组件
export { default as ModuleLayout } from './ModuleLayout.vue'
export { default as StatCard } from './StatCard.vue'
export { default as DataTable } from './DataTable.vue'
// 类型导出
export type { Column } from './DataTable.vue'