- 修复 sys-config 接口参数对齐(configKey/configValue) - 添加 dict 字典项管理 API - 修复 logs 接口参数格式(批量删除/清理日志) - 添加 request.ts postForm/putForm 方法支持 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
|
||
|