library-picturebook-activity/java-frontend/src/views/contests/components/AddParticipantDrawer.vue

271 lines
6.4 KiB
Vue
Raw Normal View History

<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 || "-" }}
</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>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'account'">
2026-04-02 14:22:56 +08:00
{{ record.username || "-" }}
</template>
<template v-else-if="column.key === 'contact'">
2026-04-02 14:22:56 +08:00
{{ record.phone || "-" }}
</template>
<template v-else-if="column.key === 'organization'">
2026-04-02 14:22:56 +08:00
{{ record.roleNames?.join("、") || "-" }}
</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">
确定
</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"
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"
import { useListRequest } from "@/composables/useListRequest"
interface Props {
open: boolean
2026-04-02 14:22:56 +08:00
registeredUserIds?: number[]
}
interface Emits {
(e: "update:open", value: boolean): void
2026-04-02 14:22:56 +08:00
(e: "confirm", students: RegistrationCandidateUser[]): void
}
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,
})
},
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 },
]
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[]
) => {
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 }
}
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)
)
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>