2026-03-28 18:53:24 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<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'">
|
2026-04-02 14:22:56 +08:00
|
|
|
|
{{ record.nickname || "-" }}
|
2026-03-28 18:53:24 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'gender'">
|
2026-04-02 14:22:56 +08:00
|
|
|
|
<a-tag v-if="record.gender === 'male'" color="blue">男</a-tag>
|
|
|
|
|
|
<a-tag v-else-if="record.gender === 'female'" color="pink">女</a-tag>
|
2026-03-28 18:53:24 +08:00
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'account'">
|
2026-04-02 14:22:56 +08:00
|
|
|
|
{{ record.username || "-" }}
|
2026-03-28 18:53:24 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'contact'">
|
2026-04-02 14:22:56 +08:00
|
|
|
|
{{ record.phone || "-" }}
|
2026-03-28 18:53:24 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'organization'">
|
2026-04-02 14:22:56 +08:00
|
|
|
|
{{ record.roleNames?.join("、") || "-" }}
|
2026-03-28 18:53:24 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<a-space>
|
|
|
|
|
|
<a-button @click="handleCancel">取消</a-button>
|
2026-04-02 14:22:56 +08:00
|
|
|
|
<a-button type="primary" @click="handleSubmit">
|
2026-03-28 18:53:24 +08:00
|
|
|
|
确定
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-drawer>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-04-02 14:22:56 +08:00
|
|
|
|
import { ref, reactive, watch } from "vue"
|
2026-03-28 18:53:24 +08:00
|
|
|
|
import { message } from "ant-design-vue"
|
|
|
|
|
|
import { SearchOutlined } from "@ant-design/icons-vue"
|
|
|
|
|
|
import type { TableColumnsType } from "ant-design-vue"
|
2026-04-02 14:22:56 +08:00
|
|
|
|
import {
|
|
|
|
|
|
registrationsApi,
|
|
|
|
|
|
type RegistrationCandidateUser,
|
|
|
|
|
|
} from "@/api/contests"
|
2026-03-28 18:53:24 +08:00
|
|
|
|
import { useListRequest } from "@/composables/useListRequest"
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
open: boolean
|
2026-04-02 14:22:56 +08:00
|
|
|
|
registeredUserIds?: number[]
|
2026-03-28 18:53:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Emits {
|
|
|
|
|
|
(e: "update:open", value: boolean): void
|
2026-04-02 14:22:56 +08:00
|
|
|
|
(e: "confirm", students: RegistrationCandidateUser[]): void
|
2026-03-28 18:53:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
|
|
|
|
|
|
|
|
const visible = ref(false)
|
|
|
|
|
|
const selectedKeys = ref<number[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
const searchParams = reactive<{
|
|
|
|
|
|
username?: string
|
|
|
|
|
|
nickname?: string
|
|
|
|
|
|
}>({})
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
loading,
|
|
|
|
|
|
dataSource,
|
|
|
|
|
|
pagination,
|
|
|
|
|
|
handleTableChange,
|
|
|
|
|
|
search,
|
|
|
|
|
|
fetchList,
|
2026-04-02 14:22:56 +08:00
|
|
|
|
} = useListRequest<RegistrationCandidateUser, { username?: string; nickname?: string }>({
|
|
|
|
|
|
requestFn: async (params) => {
|
|
|
|
|
|
const kw = [params.username, params.nickname].filter(Boolean).join(" ").trim()
|
|
|
|
|
|
return registrationsApi.getCandidateUsers({
|
|
|
|
|
|
roleCode: "student",
|
|
|
|
|
|
keyword: kw || undefined,
|
|
|
|
|
|
page: params.page,
|
|
|
|
|
|
pageSize: params.pageSize,
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-03-28 18:53:24 +08:00
|
|
|
|
defaultSearchParams: {},
|
|
|
|
|
|
defaultPageSize: 10,
|
|
|
|
|
|
errorMessage: "获取学生列表失败",
|
|
|
|
|
|
immediate: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const columns: TableColumnsType = [
|
2026-04-02 14:22:56 +08:00
|
|
|
|
{ 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 },
|
2026-03-28 18:53:24 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.open,
|
|
|
|
|
|
(newVal) => {
|
|
|
|
|
|
visible.value = newVal
|
|
|
|
|
|
if (newVal) {
|
|
|
|
|
|
selectedKeys.value = []
|
|
|
|
|
|
searchParams.username = ""
|
|
|
|
|
|
searchParams.nickname = ""
|
|
|
|
|
|
fetchList()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(visible, (newVal) => {
|
|
|
|
|
|
emit("update:open", newVal)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearch = () => {
|
|
|
|
|
|
search({
|
|
|
|
|
|
username: searchParams.username || undefined,
|
|
|
|
|
|
nickname: searchParams.nickname || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectionChange = (
|
|
|
|
|
|
selectedRowKeys: number[],
|
2026-04-02 14:22:56 +08:00
|
|
|
|
_selectedRows: RegistrationCandidateUser[]
|
2026-03-28 18:53:24 +08:00
|
|
|
|
) => {
|
|
|
|
|
|
selectedKeys.value = selectedRowKeys
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 14:22:56 +08:00
|
|
|
|
const getCheckboxProps = (record: RegistrationCandidateUser) => {
|
|
|
|
|
|
const isRegistered = props.registeredUserIds?.includes(record.id) ?? false
|
|
|
|
|
|
return { disabled: isRegistered }
|
2026-03-28 18:53:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = () => {
|
|
|
|
|
|
if (selectedKeys.value.length === 0) {
|
|
|
|
|
|
message.warning("请至少选择一个参赛人")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 14:22:56 +08:00
|
|
|
|
const selectedStudents = dataSource.value.filter((row) =>
|
|
|
|
|
|
selectedKeys.value.includes(row.id)
|
2026-03-28 18:53:24 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
emit("confirm", selectedStudents)
|
|
|
|
|
|
visible.value = false
|
|
|
|
|
|
selectedKeys.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
visible.value = false
|
|
|
|
|
|
selectedKeys.value = []
|
|
|
|
|
|
searchParams.username = ""
|
|
|
|
|
|
searchParams.nickname = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
</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>
|