library-picturebook-activity/.claude/skills/contest-list-page.md
2026-01-15 16:35:00 +08:00

450 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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