450 lines
12 KiB
Markdown
450 lines
12 KiB
Markdown
|
|
# 赛事列表页面生成规范
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
本规范用于快速生成赛事管理系统中的列表页面,这类页面具有统一的结构:
|
|||
|
|
- Tab 切换(个人赛/团队赛)
|
|||
|
|
- 搜索表单
|
|||
|
|
- 数据表格
|
|||
|
|
- 操作按钮
|
|||
|
|
|
|||
|
|
## 页面结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────┐
|
|||
|
|
│ 标题卡片 (a-card) │
|
|||
|
|
├─────────────────────────────────────────────────┤
|
|||
|
|
│ Tab栏: [个人赛] [团队赛] │
|
|||
|
|
├─────────────────────────────────────────────────┤
|
|||
|
|
│ 搜索表单: [搜索条件...] [搜索] [重置] │
|
|||
|
|
├─────────────────────────────────────────────────┤
|
|||
|
|
│ 数据表格 │
|
|||
|
|
│ ┌───┬──────┬──────┬──────┬──────┐ │
|
|||
|
|
│ │序号│ 列1 │ 列2 │ 列3 │ 操作 │ │
|
|||
|
|
│ ├───┼──────┼──────┼──────┼──────┤ │
|
|||
|
|
│ │ 1 │ ... │ ... │ ... │ 详情 │ │
|
|||
|
|
│ │ 2 │ ... │ ... │ ... │ 详情 │ │
|
|||
|
|
│ └───┴──────┴──────┴──────┴──────┘ │
|
|||
|
|
└─────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 配置参数
|
|||
|
|
|
|||
|
|
生成页面时需要提供以下配置:
|
|||
|
|
|
|||
|
|
### 1. 基础配置
|
|||
|
|
|
|||
|
|
| 参数 | 类型 | 必填 | 说明 |
|
|||
|
|
|------|------|------|------|
|
|||
|
|
| pageName | string | 是 | 页面名称,如"赛果发布"、"报名管理" |
|
|||
|
|
| pageClass | string | 是 | CSS类名,如"results-page" |
|
|||
|
|
| routePrefix | string | 是 | 路由前缀,如"results"、"registrations" |
|
|||
|
|
| detailRouteName | string | 否 | 详情页路由名称 |
|
|||
|
|
|
|||
|
|
### 2. 搜索条件配置
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface SearchField {
|
|||
|
|
field: string // 字段名
|
|||
|
|
label: string // 标签文本
|
|||
|
|
type: 'input' | 'select' | 'date' | 'dateRange' // 控件类型
|
|||
|
|
placeholder?: string // 占位文本
|
|||
|
|
width?: string // 宽度,默认 "200px"
|
|||
|
|
options?: Array<{ label: string; value: string }> // select 类型的选项
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**示例**:
|
|||
|
|
```typescript
|
|||
|
|
const searchFields = [
|
|||
|
|
{ field: 'contestName', label: '赛事名称', type: 'input', placeholder: '请输入赛事名称' },
|
|||
|
|
{ field: 'status', label: '状态', type: 'select', options: [
|
|||
|
|
{ label: '已发布', value: 'published' },
|
|||
|
|
{ label: '未发布', value: 'unpublished' }
|
|||
|
|
]}
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 表格列配置
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface TableColumn {
|
|||
|
|
title: string // 列标题
|
|||
|
|
key: string // 列标识
|
|||
|
|
dataIndex?: string // 数据字段(简单字段直接映射)
|
|||
|
|
width?: number // 列宽度
|
|||
|
|
fixed?: 'left' | 'right' // 固定列
|
|||
|
|
render?: 'index' | 'link' | 'count' | 'tag' | 'date' | 'dateRange' | 'custom' // 渲染类型
|
|||
|
|
renderConfig?: {
|
|||
|
|
// link 类型
|
|||
|
|
clickHandler?: string // 点击处理函数名
|
|||
|
|
// count 类型
|
|||
|
|
countField?: string // _count 下的字段名
|
|||
|
|
// tag 类型
|
|||
|
|
colorMap?: Record<string, string> // 值到颜色的映射
|
|||
|
|
textMap?: Record<string, string> // 值到文本的映射
|
|||
|
|
// dateRange 类型
|
|||
|
|
startField?: string // 开始时间字段
|
|||
|
|
endField?: string // 结束时间字段
|
|||
|
|
startLabel?: string // 开始标签
|
|||
|
|
endLabel?: string // 结束标签
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**常用渲染类型**:
|
|||
|
|
|
|||
|
|
| 类型 | 说明 | 示例 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| index | 序号列 | 自动计算 (current-1)*pageSize+index+1 |
|
|||
|
|
| link | 可点击链接 | 赛事名称点击跳转 |
|
|||
|
|
| count | 统计数量 | record._count?.registrations |
|
|||
|
|
| tag | 标签展示 | 状态标签(不同颜色) |
|
|||
|
|
| date | 单个日期 | YYYY-MM-DD HH:mm |
|
|||
|
|
| dateRange | 日期范围 | 开始:xxx / 结束:xxx |
|
|||
|
|
| custom | 自定义渲染 | 需要单独写模板 |
|
|||
|
|
|
|||
|
|
### 4. 操作按钮配置
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface ActionButton {
|
|||
|
|
text: string // 按钮文本
|
|||
|
|
type?: 'link' | 'primary' | 'default' // 按钮类型
|
|||
|
|
danger?: boolean // 是否危险按钮
|
|||
|
|
permission?: string // 权限标识
|
|||
|
|
handler: string // 处理函数名
|
|||
|
|
disabled?: (record: any) => boolean // 禁用条件
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 页面模板
|
|||
|
|
|
|||
|
|
### 完整模板代码
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<div class="{{pageClass}}">
|
|||
|
|
<a-card class="mb-4">
|
|||
|
|
<template #title>{{pageName}}</template>
|
|||
|
|
</a-card>
|
|||
|
|
|
|||
|
|
<!-- Tab栏切换 -->
|
|||
|
|
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
|
|||
|
|
<a-tab-pane key="individual" tab="个人赛" />
|
|||
|
|
<a-tab-pane key="team" tab="团队赛" />
|
|||
|
|
</a-tabs>
|
|||
|
|
|
|||
|
|
<!-- 搜索表单 -->
|
|||
|
|
<a-form
|
|||
|
|
:model="searchParams"
|
|||
|
|
layout="inline"
|
|||
|
|
class="search-form"
|
|||
|
|
@finish="handleSearch"
|
|||
|
|
>
|
|||
|
|
<!-- 根据 searchFields 配置生成 -->
|
|||
|
|
<a-form-item v-for="field in searchFields" :key="field.field" :label="field.label">
|
|||
|
|
<!-- input 类型 -->
|
|||
|
|
<a-input
|
|||
|
|
v-if="field.type === 'input'"
|
|||
|
|
v-model:value="searchParams[field.field]"
|
|||
|
|
:placeholder="field.placeholder"
|
|||
|
|
allow-clear
|
|||
|
|
:style="{ width: field.width || '200px' }"
|
|||
|
|
/>
|
|||
|
|
<!-- select 类型 -->
|
|||
|
|
<a-select
|
|||
|
|
v-else-if="field.type === 'select'"
|
|||
|
|
v-model:value="searchParams[field.field]"
|
|||
|
|
:placeholder="field.placeholder"
|
|||
|
|
allow-clear
|
|||
|
|
:style="{ width: field.width || '150px' }"
|
|||
|
|
>
|
|||
|
|
<a-select-option
|
|||
|
|
v-for="opt in field.options"
|
|||
|
|
:key="opt.value"
|
|||
|
|
:value="opt.value"
|
|||
|
|
>
|
|||
|
|
{{ opt.label }}
|
|||
|
|
</a-select-option>
|
|||
|
|
</a-select>
|
|||
|
|
</a-form-item>
|
|||
|
|
<a-form-item>
|
|||
|
|
<a-button type="primary" html-type="submit">
|
|||
|
|
<template #icon><SearchOutlined /></template>
|
|||
|
|
搜索
|
|||
|
|
</a-button>
|
|||
|
|
<a-button style="margin-left: 8px" @click="handleReset">
|
|||
|
|
<template #icon><ReloadOutlined /></template>
|
|||
|
|
重置
|
|||
|
|
</a-button>
|
|||
|
|
</a-form-item>
|
|||
|
|
</a-form>
|
|||
|
|
|
|||
|
|
<!-- 数据表格 -->
|
|||
|
|
<a-table
|
|||
|
|
:columns="columns"
|
|||
|
|
:data-source="dataSource"
|
|||
|
|
:loading="loading"
|
|||
|
|
:pagination="pagination"
|
|||
|
|
row-key="id"
|
|||
|
|
@change="handleTableChange"
|
|||
|
|
>
|
|||
|
|
<template #bodyCell="{ column, record, index }">
|
|||
|
|
<!-- 根据列配置渲染 -->
|
|||
|
|
</template>
|
|||
|
|
</a-table>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, reactive, onMounted } from "vue"
|
|||
|
|
import { useRouter, useRoute } from "vue-router"
|
|||
|
|
import { message } from "ant-design-vue"
|
|||
|
|
import { SearchOutlined, ReloadOutlined } from "@ant-design/icons-vue"
|
|||
|
|
import { contestsApi, type Contest, type QueryContestParams } from "@/api/contests"
|
|||
|
|
import dayjs from "dayjs"
|
|||
|
|
|
|||
|
|
const router = useRouter()
|
|||
|
|
const route = useRoute()
|
|||
|
|
const tenantCode = route.params.tenantCode as string
|
|||
|
|
|
|||
|
|
// Tab状态
|
|||
|
|
const activeTab = ref<"individual" | "team">("individual")
|
|||
|
|
|
|||
|
|
// 列表状态
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const dataSource = ref<Contest[]>([])
|
|||
|
|
const pagination = reactive({
|
|||
|
|
current: 1,
|
|||
|
|
pageSize: 10,
|
|||
|
|
total: 0,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 搜索参数
|
|||
|
|
const searchParams = reactive<QueryContestParams>({
|
|||
|
|
contestName: "",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 表格列定义
|
|||
|
|
const columns = [
|
|||
|
|
// 根据配置生成
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
const formatDateTime = (dateStr?: string) => {
|
|||
|
|
if (!dateStr) return "-"
|
|||
|
|
return dayjs(dateStr).format("YYYY-MM-DD HH:mm")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取列表数据
|
|||
|
|
const fetchList = async () => {
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const params: QueryContestParams = {
|
|||
|
|
...searchParams,
|
|||
|
|
contestType: activeTab.value,
|
|||
|
|
page: pagination.current,
|
|||
|
|
pageSize: pagination.pageSize,
|
|||
|
|
}
|
|||
|
|
const response = await contestsApi.getList(params)
|
|||
|
|
dataSource.value = response.list
|
|||
|
|
pagination.total = response.total
|
|||
|
|
} catch (error) {
|
|||
|
|
message.error("获取列表失败")
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Tab切换
|
|||
|
|
const handleTabChange = () => {
|
|||
|
|
pagination.current = 1
|
|||
|
|
fetchList()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 搜索
|
|||
|
|
const handleSearch = () => {
|
|||
|
|
pagination.current = 1
|
|||
|
|
fetchList()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重置
|
|||
|
|
const handleReset = () => {
|
|||
|
|
// 重置所有搜索参数
|
|||
|
|
pagination.current = 1
|
|||
|
|
fetchList()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 表格变化
|
|||
|
|
const handleTableChange = (pag: any) => {
|
|||
|
|
pagination.current = pag.current
|
|||
|
|
pagination.pageSize = pag.pageSize
|
|||
|
|
fetchList()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 查看详情
|
|||
|
|
const handleViewDetail = (record: Contest) => {
|
|||
|
|
router.push(`/${tenantCode}/contests/{{routePrefix}}/${record.id}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
fetchList()
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.{{pageClass}} {
|
|||
|
|
.search-form {
|
|||
|
|
margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 使用示例
|
|||
|
|
|
|||
|
|
### 示例1:赛果发布列表
|
|||
|
|
|
|||
|
|
**配置**:
|
|||
|
|
```yaml
|
|||
|
|
pageName: 赛果发布
|
|||
|
|
pageClass: results-page
|
|||
|
|
routePrefix: results
|
|||
|
|
|
|||
|
|
searchFields:
|
|||
|
|
- field: contestName
|
|||
|
|
label: 赛事名称
|
|||
|
|
type: input
|
|||
|
|
placeholder: 请输入赛事名称
|
|||
|
|
|
|||
|
|
columns:
|
|||
|
|
- title: 序号
|
|||
|
|
key: index
|
|||
|
|
width: 70
|
|||
|
|
render: index
|
|||
|
|
|
|||
|
|
- title: 赛事名称
|
|||
|
|
key: contestName
|
|||
|
|
dataIndex: contestName
|
|||
|
|
width: 200
|
|||
|
|
|
|||
|
|
- title: 报名人数
|
|||
|
|
key: registrationCount
|
|||
|
|
width: 100
|
|||
|
|
render: count
|
|||
|
|
renderConfig:
|
|||
|
|
countField: registrations
|
|||
|
|
|
|||
|
|
- title: 提交作品数
|
|||
|
|
key: worksCount
|
|||
|
|
width: 100
|
|||
|
|
render: count
|
|||
|
|
renderConfig:
|
|||
|
|
countField: works
|
|||
|
|
|
|||
|
|
- title: 操作
|
|||
|
|
key: action
|
|||
|
|
width: 100
|
|||
|
|
fixed: right
|
|||
|
|
|
|||
|
|
actions:
|
|||
|
|
- text: 详情
|
|||
|
|
handler: handleViewDetail
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例2:报名管理列表
|
|||
|
|
|
|||
|
|
**配置**:
|
|||
|
|
```yaml
|
|||
|
|
pageName: 报名管理
|
|||
|
|
pageClass: registrations-page
|
|||
|
|
routePrefix: registrations
|
|||
|
|
|
|||
|
|
searchFields:
|
|||
|
|
- field: contestName
|
|||
|
|
label: 赛事名称
|
|||
|
|
type: input
|
|||
|
|
placeholder: 请输入赛事名称
|
|||
|
|
|
|||
|
|
columns:
|
|||
|
|
- title: 序号
|
|||
|
|
key: index
|
|||
|
|
width: 70
|
|||
|
|
render: index
|
|||
|
|
|
|||
|
|
- title: 赛事名称
|
|||
|
|
key: contestName
|
|||
|
|
dataIndex: contestName
|
|||
|
|
width: 250
|
|||
|
|
|
|||
|
|
- title: 主办单位
|
|||
|
|
key: organizers
|
|||
|
|
width: 200
|
|||
|
|
render: custom # 需要格式化JSON数组
|
|||
|
|
|
|||
|
|
- title: 报名人数
|
|||
|
|
key: registrationCount
|
|||
|
|
width: 120
|
|||
|
|
render: count
|
|||
|
|
renderConfig:
|
|||
|
|
countField: registrations
|
|||
|
|
|
|||
|
|
- title: 报名时间
|
|||
|
|
key: registerTime
|
|||
|
|
width: 200
|
|||
|
|
render: dateRange
|
|||
|
|
renderConfig:
|
|||
|
|
startField: registerStartTime
|
|||
|
|
endField: registerEndTime
|
|||
|
|
startLabel: 开始
|
|||
|
|
endLabel: 结束
|
|||
|
|
|
|||
|
|
- title: 操作
|
|||
|
|
key: action
|
|||
|
|
width: 220
|
|||
|
|
fixed: right
|
|||
|
|
|
|||
|
|
actions:
|
|||
|
|
- text: 报名记录
|
|||
|
|
handler: handleViewRecords
|
|||
|
|
- text: 启动报名
|
|||
|
|
handler: handleStartRegistration
|
|||
|
|
permission: contest:update
|
|||
|
|
- text: 关闭报名
|
|||
|
|
handler: handleStopRegistration
|
|||
|
|
permission: contest:update
|
|||
|
|
danger: true
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 快速生成指令
|
|||
|
|
|
|||
|
|
向 Claude 提供以下格式的指令即可快速生成页面:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
请生成一个赛事列表页面:
|
|||
|
|
|
|||
|
|
页面名称:赛果发布
|
|||
|
|
路由前缀:results
|
|||
|
|
文件路径:frontend/src/views/contests/results/Index.vue
|
|||
|
|
|
|||
|
|
搜索条件:
|
|||
|
|
- 赛事名称(输入框)
|
|||
|
|
|
|||
|
|
表格列:
|
|||
|
|
- 序号
|
|||
|
|
- 赛事名称
|
|||
|
|
- 报名人数(_count.registrations)
|
|||
|
|
- 提交作品数(_count.works)
|
|||
|
|
- 操作(详情按钮)
|
|||
|
|
|
|||
|
|
详情跳转:/${tenantCode}/contests/results/${record.id}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 注意事项
|
|||
|
|
|
|||
|
|
1. **API 数据**:确保后端 API 返回了所需的统计字段(如 `_count`)
|
|||
|
|
2. **路由配置**:生成页面后需要在 `router/index.ts` 中配置路由
|
|||
|
|
3. **权限控制**:操作按钮需要配置 `v-permission` 指令
|
|||
|
|
4. **样式一致性**:使用 Ant Design Vue 组件,保持系统风格统一
|
|||
|
|
5. **团队赛差异**:如果个人赛和团队赛的列不同,需要定义两套 columns
|