library-picturebook-activity/java-frontend/src/views/contests/components/AddParticipantDrawer.vue
En 48fc71b41d fix: 前后端接口对齐修复
- 修复 sys-config 接口参数对齐(configKey/configValue)
- 添加 dict 字典项管理 API
- 修复 logs 接口参数格式(批量删除/清理日志)
- 添加 request.ts postForm/putForm 方法支持

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 18:53:24 +08:00

342 lines
7.7 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>
<a-drawer
v-model:open="visible"
title="添加参赛人"
placement="right"
width="850px"
:footer-style="{ textAlign: 'right', padding: '16px 24px' }"
@close="handleCancel"
>
<template #title>
<div class="drawer-title">
<span>添加参赛人</span>
</div>
</template>
<!-- 搜索/筛选区域 -->
<div class="search-section">
<div class="search-item">
<span class="search-label">账号</span>
<a-input
v-model:value="searchParams.username"
placeholder="请输入"
allow-clear
style="width: 200px"
@press-enter="handleSearch"
>
<template #suffix>
<SearchOutlined />
</template>
</a-input>
</div>
<div class="search-item">
<span class="search-label">姓名</span>
<a-input
v-model:value="searchParams.nickname"
placeholder="请输入"
allow-clear
style="width: 200px"
@press-enter="handleSearch"
>
<template #suffix>
<SearchOutlined />
</template>
</a-input>
</div>
<div class="search-item">
<span class="search-label">机构信息</span>
<a-select
v-model:value="searchParams.classId"
placeholder="年份+班级"
allow-clear
style="width: 200px"
@change="handleSearch"
>
<a-select-option
v-for="cls in classOptions"
:key="cls.id"
:value="cls.id"
>
{{ cls.grade?.name }} - {{ cls.name }}
</a-select-option>
</a-select>
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
:row-selection="{
type: 'checkbox',
selectedRowKeys: selectedKeys,
onChange: handleSelectionChange,
getCheckboxProps: getCheckboxProps,
}"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
{{ record.user?.nickname || "-" }}
</template>
<template v-else-if="column.key === 'gender'">
<a-tag v-if="record.gender === 1" color="blue">男</a-tag>
<a-tag v-else-if="record.gender === 2" color="pink">女</a-tag>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'account'">
{{ record.user?.username || "-" }}
</template>
<template v-else-if="column.key === 'contact'">
{{ record.user?.phone || record.phone || "-" }}
</template>
<template v-else-if="column.key === 'organization'">
{{ record.class?.grade?.name }} - {{ record.class?.name }}
</template>
</template>
</a-table>
</div>
<template #footer>
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
确定
</a-button>
</a-space>
</template>
</a-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from "vue"
import { message } from "ant-design-vue"
import { SearchOutlined } from "@ant-design/icons-vue"
import type { TableColumnsType } from "ant-design-vue"
import { studentsApi, type Student } from "@/api/students"
import { classesApi, type Class } from "@/api/classes"
import { useListRequest } from "@/composables/useListRequest"
interface Props {
open: boolean
registeredUserIds?: number[] // 已报名的用户ID列表
}
interface Emits {
(e: "update:open", value: boolean): void
(e: "confirm", students: Student[]): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = ref(false)
const submitLoading = ref(false)
const selectedKeys = ref<number[]>([])
const classOptions = ref<Class[]>([])
// 搜索参数
const searchParams = reactive<{
username?: string
nickname?: string
classId?: number
}>({})
// 列表请求
const {
loading,
dataSource,
pagination,
handleTableChange,
search,
fetchList,
} = useListRequest<Student, { username?: string; nickname?: string; classId?: number }>({
requestFn: studentsApi.getList,
defaultSearchParams: {},
defaultPageSize: 10,
errorMessage: "获取学生列表失败",
immediate: false,
})
// 表格列定义
const columns: TableColumnsType = [
{
title: "姓名",
key: "name",
width: 120,
},
{
title: "性别",
key: "gender",
width: 80,
align: "center",
},
{
title: "账号",
key: "account",
width: 120,
},
{
title: "联系方式",
key: "contact",
width: 120,
},
{
title: "机构信息",
key: "organization",
width: 200,
},
]
// 监听 open 变化
watch(
() => props.open,
(newVal) => {
visible.value = newVal
if (newVal) {
// 打开时重置状态
selectedKeys.value = []
searchParams.username = ""
searchParams.nickname = ""
searchParams.classId = undefined
fetchClasses()
fetchList()
}
},
{ immediate: true }
)
// 监听 visible 变化,同步到父组件
watch(visible, (newVal) => {
emit("update:open", newVal)
})
// 搜索处理
const handleSearch = () => {
search({
username: searchParams.username || undefined,
nickname: searchParams.nickname || undefined,
classId: searchParams.classId || undefined,
})
}
// 选择变化处理
const handleSelectionChange = (
selectedRowKeys: number[],
selectedRows: Student[]
) => {
selectedKeys.value = selectedRowKeys
}
// 获取复选框属性,禁用已报名的学生
const getCheckboxProps = (record: Student) => {
const isRegistered = props.registeredUserIds?.includes(record.userId) ?? false
return {
disabled: isRegistered,
}
}
// 加载班级列表
const fetchClasses = async () => {
try {
const response = await classesApi.getList({
page: 1,
pageSize: 100,
type: 1,
})
classOptions.value = response.list
} catch (error) {
console.error("获取班级列表失败:", error)
}
}
// 提交
const handleSubmit = () => {
if (selectedKeys.value.length === 0) {
message.warning("请至少选择一个参赛人")
return
}
// 获取选中的学生
const selectedStudents = dataSource.value.filter((student) =>
selectedKeys.value.includes(student.id)
)
emit("confirm", selectedStudents)
visible.value = false
selectedKeys.value = []
}
// 取消
const handleCancel = () => {
visible.value = false
selectedKeys.value = []
searchParams.username = ""
searchParams.nickname = ""
searchParams.classId = undefined
}
onMounted(() => {
fetchClasses()
})
</script>
<style scoped lang="scss">
.drawer-title {
font-size: 16px;
font-weight: 500;
color: #303133;
}
:deep(.ant-drawer-body) {
padding: 24px;
}
.search-section {
margin-bottom: 24px;
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
flex-wrap: nowrap;
.search-item {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
.search-label {
font-size: 14px;
color: #303133;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
}
}
.table-section {
margin-bottom: 24px;
:deep(.ant-table-thead > tr > th) {
background-color: #f5f7fa;
font-weight: 500;
color: #303133;
}
:deep(.ant-table-tbody > tr:hover > td) {
background-color: #f5f7fa;
}
:deep(.ant-table-tbody > tr:nth-child(even) > td) {
background-color: #fafafa;
}
}
</style>