342 lines
7.7 KiB
Vue
342 lines
7.7 KiB
Vue
|
|
<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>
|
|||
|
|
|