laobinghu e73f62133b feat: enhance transitions and animations with UnoCSS
- Add advanced page/layout transitions (slide, zoom, fade with blur)
- Enhance UnoCSS config with keyframes, transitions, and shortcuts
- Add staggered animations for stat cards and table rows
- Improve sidebar menu animations with slide-in effects
- Add custom dialog/drawer transitions with bounce effects
- Optimize transition timing with cubic-bezier easing

Based on Element Plus transitions guide and Nuxt 4 transitions docs
2026-03-22 08:30:49 +08:00

269 lines
8.0 KiB
Vue
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.

<template>
<div class="events-container">
<el-card>
<template #header>
<div class="card-header">
<span>比赛项目管理</span>
<el-button type="primary" @click="showAddDialog = true">添加项目</el-button>
</div>
</template>
<el-form :inline="true" class="filter-form">
<!-- 类别筛选 -->
<el-form-item label="类别">
<el-select v-model="filters.category" placeholder="全部" clearable @change="loadEvents">
<el-option label="田赛" value="田赛" />
<el-option label="径赛" value="径赛" />
<el-option label="团体赛" value="团体赛" />
</el-select>
</el-form-item>
<!-- 年级筛选 -->
<el-form-item label="年级">
<el-select v-model="filters.grade" placeholder="全部" clearable @change="onFilterChange">
<el-option v-for="g in config.grades" :key="g" :label="g" :value="g" />
</el-select>
</el-form-item>
<!-- 班级类型筛选 -->
<el-form-item label="班级类型">
<el-select v-model="filters.classType" placeholder="全部" clearable :disabled="!filters.grade" @change="onFilterChange">
<el-option v-for="c in config.classTypes" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
<!-- 性别筛选 -->
<el-form-item label="性别">
<el-select v-model="filters.gender" placeholder="全部" clearable :disabled="!filters.grade" @change="onFilterChange">
<el-option v-for="g in config.genders" :key="g" :label="g" :value="g" />
</el-select>
</el-form-item>
</el-form>
<el-table :data="events" border stripe class="events-table transition-base">
<el-table-column prop="id" label="ID" width="80" class-name="col-id" />
<el-table-column prop="name" label="项目名称" />
<el-table-column prop="category" label="类别" width="120" class-name="col-category" />
<el-table-column prop="event_group" label="组别" width="150" class-name="col-group" />
<el-table-column prop="unit" label="单位" width="100" class-name="col-unit" />
<el-table-column prop="status" label="状态" width="100" class-name="col-status">
<template #default="{ row }">
<el-tag :type="row.status === 'completed' ? 'success' : 'info'">
{{ row.status === 'completed' ? '已完成' : '进行中' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="showAddDialog" title="添加比赛项目" width="500px">
<el-form :model="form" label-width="100px">
<el-form-item label="类别">
<el-select v-model="form.category" placeholder="请选择" @change="onCategoryChange">
<el-option label="田赛" value="田赛" />
<el-option label="径赛" value="径赛" />
<el-option label="团体赛" value="团体赛" />
</el-select>
</el-form-item>
<el-form-item label="项目名称">
<el-select v-model="form.name" placeholder="请选择">
<el-option
v-for="event in availableEvents"
:key="event.name"
:label="event.name"
:value="event.name"
/>
</el-select>
</el-form-item>
<el-form-item label="组别">
<el-select v-model="form.event_group" placeholder="请选择">
<el-option
v-for="g in allGroupOptions"
:key="g.value"
:label="g.label"
:value="g.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddDialog = false">取消</el-button>
<el-button type="primary" @click="addEvent">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { computed } from 'vue'
const events = ref([])
const showAddDialog = ref(false)
const config = ref({
grades: [] as string[],
classTypes: [] as string[],
genders: [] as string[],
all: [] as string[]
})
const filters = ref({
category: '',
grade: '',
classType: '',
gender: ''
})
const eventTypes = ref({})
const form = ref({
name: '',
category: '',
event_group: '',
unit: ''
})
const availableEvents = computed(() => {
if (!form.value.category) return []
return eventTypes.value[form.value.category] || []
})
// 为添加项目对话框生成所有可用的组别选项从config API获取
const allGroupOptions = computed(() => {
return (config.value.all || []).map(g => ({ value: g, label: g }))
})
const loadConfig = async () => {
try {
const res = await $fetch('/api/config')
config.value = res.data.groups
eventTypes.value = res.data.eventTypes
} catch (error) {
ElMessage.error('加载配置失败')
}
}
const loadEvents = async () => {
try {
const params = new URLSearchParams()
if (filters.value.category) params.append('category', filters.value.category)
if (filters.value.grade) params.append('grade', filters.value.grade)
if (filters.value.classType) params.append('classType', filters.value.classType)
if (filters.value.gender) params.append('gender', filters.value.gender)
const res = await $fetch(`/api/events?${params}`)
events.value = res.data
} catch (error) {
ElMessage.error('加载项目失败')
}
}
const onFilterChange = () => {
loadEvents()
}
const onCategoryChange = () => {
form.value.name = ''
form.value.unit = ''
}
const addEvent = async () => {
try {
const selectedEvent = availableEvents.value.find(e => e.name === form.value.name)
if (!selectedEvent) {
ElMessage.error('请选择项目')
return
}
await $fetch('/api/events', {
method: 'POST',
body: {
name: form.value.name,
category: form.value.category,
event_group: form.value.event_group,
unit: selectedEvent.unit
}
})
ElMessage.success('添加成功')
showAddDialog.value = false
form.value = { name: '', category: '', event_group: '', unit: '' }
loadEvents()
} catch (error) {
ElMessage.error('添加失败')
}
}
onMounted(() => {
loadConfig()
loadEvents()
})
</script>
<style scoped>
.events-container {
max-width: 1200px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.filter-form {
margin-bottom: 20px;
}
/* Table row staggered animations */
.events-table :deep(.el-table__body tr) {
animation: slide-in-up 0.3s ease-out;
animation-fill-mode: both;
}
.events-table :deep(.el-table__body tr:nth-child(1)) { animation-delay: 0.05s; }
.events-table :deep(.el-table__body tr:nth-child(2)) { animation-delay: 0.1s; }
.events-table :deep(.el-table__body tr:nth-child(3)) { animation-delay: 0.15s; }
.events-table :deep(.el-table__body tr:nth-child(4)) { animation-delay: 0.2s; }
.events-table :deep(.el-table__body tr:nth-child(5)) { animation-delay: 0.25s; }
.events-table :deep(.el-table__body tr:nth-child(6)) { animation-delay: 0.3s; }
.events-table :deep(.el-table__body tr:nth-child(7)) { animation-delay: 0.35s; }
.events-table :deep(.el-table__body tr:nth-child(8)) { animation-delay: 0.4s; }
.events-table :deep(.el-table__body tr:nth-child(9)) { animation-delay: 0.45s; }
.events-table :deep(.el-table__body tr:nth-child(10)) { animation-delay: 0.5s; }
@keyframes slide-in-up {
0% {
opacity: 0;
transform: translateY(15px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 900px) {
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.filter-form :deep(.el-form-item) {
margin-right: 0;
width: 100%;
}
.filter-form :deep(.el-select) {
width: 100%;
}
.events-table :deep(.col-id),
.events-table :deep(.col-category),
.events-table :deep(.col-unit),
.events-table :deep(.col-status) {
display: none;
}
}
</style>