library-picturebook-activity/.claude/skills/contest-list-page.md

450 lines
12 KiB
Markdown
Raw Normal View History

2026-01-15 16:35:00 +08:00
# 赛事列表页面生成规范
## 概述
本规范用于快速生成赛事管理系统中的列表页面,这类页面具有统一的结构:
- 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