library-picturebook-activity/.claude/skills/contest-list-page.md
aid 418aa57ea8 Day4: 超管端设计优化 + UGC绘本创作社区P0实现
一、超管端设计优化
- 文档管理SOP体系建立,docs目录重组
- 统一用户管理:跨租户全局视角,合并用户管理+公众用户
- 活动监管全模块重构:全部活动(统计卡片+阶段筛选+SuperDetail详情页)、报名数据/作品数据/评审进度(两层合一扁平列表)、成果发布(去Tab+统计+隐藏写操作)
- 菜单精简:移除评委管理/评审规则/通知管理
- Bug修复:租户编辑丢失隐藏菜单、pageSize限制、主色统一

二、UGC绘本创作社区P0
- 数据库:10张新表(user_works/user_work_pages/work_tags等)
- 子女账号独立化:Child升级为独立User,家长切换+独立登录
- 用户作品库:CRUD+发布审核,8个API
- AI创作流程:提交→生成→保存到作品库,4个API
- 作品广场:首页改造为推荐流,标签+搜索+排序
- 内容审核(超管端):作品审核+作品管理+标签管理
- 活动联动:WorkSelector作品选择器
- 布局改造:底部5Tab(发现/创作/活动/作品库/我的)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:20:25 +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