feat(school): 家长绑定孩子支持多选并修复表格勾选回显

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-24 17:07:09 +08:00
parent c8f97c45d4
commit 94ea219f2f

View File

@ -242,7 +242,8 @@
<!-- 学生表格 --> <!-- 学生表格 -->
<a-table :columns="studentTableColumns" :data-source="studentTableData" :loading="studentsLoading" <a-table :columns="studentTableColumns" :data-source="studentTableData" :loading="studentsLoading"
:pagination="studentPagination" :row-selection="studentRowSelection" row-key="id" size="small" :pagination="studentPagination" :row-selection="studentRowSelection"
:row-key="(r: Student) => studentIdNum(r)" size="small"
@change="handleStudentTableChange" style="margin-top: 16px;"> @change="handleStudentTableChange" style="margin-top: 16px;">
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'gender'"> <template v-if="column.dataIndex === 'gender'">
@ -255,11 +256,17 @@
</a-table> </a-table>
<!-- 选择关系并确认 --> <!-- 选择关系并确认 -->
<div class="select-footer" v-if="selectedStudent"> <div class="select-footer" v-if="selectedStudentIds.length > 0">
<div class="selected-info"> <div class="selected-info">
<span>已选择</span> <span class="selected-count">已选择 {{ selectedStudentIds.length }} </span>
<a-tag color="orange">{{ selectedStudent.name }}</a-tag> <a-space wrap class="selected-tags">
<span class="selected-class">{{ selectedStudent.className }}</span> <a-tag v-for="sid in selectedStudentIds" :key="sid" color="orange">
{{ selectedStudentMeta[sid]?.name ?? sid }}
<span v-if="selectedStudentMeta[sid]?.className" class="selected-class">
{{ selectedStudentMeta[sid]?.className }}
</span>
</a-tag>
</a-space>
</div> </div>
<div class="select-actions"> <div class="select-actions">
<a-select v-model:value="addChildForm.relationship" style="width: 100px; margin-right: 12px;"> <a-select v-model:value="addChildForm.relationship" style="width: 100px; margin-right: 12px;">
@ -302,7 +309,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, watch, onMounted } from 'vue';
import { import {
IdcardOutlined, IdcardOutlined,
PlusOutlined, PlusOutlined,
@ -378,7 +385,6 @@ const addChildLoading = ref(false);
const studentsLoading = ref(false); const studentsLoading = ref(false);
const addChildForm = reactive({ const addChildForm = reactive({
studentId: undefined as number | undefined,
relationship: 'FATHER', relationship: 'FATHER',
}); });
@ -386,7 +392,10 @@ const addChildForm = reactive({
const selectStudentModalVisible = ref(false); const selectStudentModalVisible = ref(false);
const studentSearchKeyword = ref(''); const studentSearchKeyword = ref('');
const studentClassFilter = ref<number | undefined>(); const studentClassFilter = ref<number | undefined>();
const selectedStudent = ref<Student | null>(null); /** 跨页保留:已勾选的学生 id */
const selectedStudentIds = ref<number[]>([]);
/** 用于底部展示姓名、班级(在勾选时写入) */
const selectedStudentMeta = ref<Record<number, { name: string; className?: string | null }>>({});
const studentTableData = ref<Student[]>([]); const studentTableData = ref<Student[]>([]);
const studentPagination = reactive({ const studentPagination = reactive({
current: 1, current: 1,
@ -613,24 +622,53 @@ const studentTableColumns = [
{ title: '班级', dataIndex: 'className', key: 'className', width: 120 }, { title: '班级', dataIndex: 'className', key: 'className', width: 120 },
]; ];
const studentRowSelection = computed(() => ({ const studentIdNum = (s: Student) => Number(s.id);
type: 'radio' as 'radio',
selectedRowKeys: selectedStudent.value ? [selectedStudent.value.id] : [], /** 合并当前页勾选与其它页已选实现表格多选跨页保留id 统一为 number避免与 rowKey 类型不一致导致无法回显) */
onChange: (selectedRowKeys: (string | number)[]) => { const mergeStudentTableSelection = (keysFromThisPage: (string | number)[]) => {
if (selectedRowKeys.length > 0) { const currentPageIds = studentTableData.value.map(studentIdNum);
const student = studentTableData.value.find(s => s.id === selectedRowKeys[0]); const keysNum = keysFromThisPage.map(Number);
selectedStudent.value = student || null; const fromOtherPages = selectedStudentIds.value.filter((id) => !currentPageIds.includes(id));
} else { selectedStudentIds.value = [...new Set([...fromOtherPages, ...keysNum])];
selectedStudent.value = null; studentTableData.value.forEach((s) => {
const sid = studentIdNum(s);
if (keysNum.includes(sid)) {
selectedStudentMeta.value[sid] = { name: s.name, className: s.className };
} else if (currentPageIds.includes(sid)) {
delete selectedStudentMeta.value[sid];
} }
});
syncStudentTableRowSelectionKeys();
};
/** 当前页应勾选的 keys须与表格 row-key 字段类型一致,否则复选框不回显 */
const syncStudentTableRowSelectionKeys = () => {
const ids = new Set(selectedStudentIds.value);
/** 与 :row-key 一致使用 number否则受控勾选无法与行匹配 */
studentRowSelection.selectedRowKeys = studentTableData.value
.filter((s) => ids.has(studentIdNum(s)))
.map((s) => studentIdNum(s));
};
const studentRowSelection = reactive({
type: 'checkbox' as const,
selectedRowKeys: [] as (string | number)[],
onChange: (selectedRowKeys: (string | number)[]) => {
mergeStudentTableSelection(selectedRowKeys);
}, },
})); });
/** 翻页、筛选后数据源变化时同步复选框回显 */
watch(studentTableData, () => {
syncStudentTableRowSelectionKeys();
});
const openSelectStudentModal = async () => { const openSelectStudentModal = async () => {
selectStudentModalVisible.value = true; selectStudentModalVisible.value = true;
studentSearchKeyword.value = ''; studentSearchKeyword.value = '';
studentClassFilter.value = undefined; studentClassFilter.value = undefined;
selectedStudent.value = null; selectedStudentIds.value = [];
selectedStudentMeta.value = {};
studentPagination.current = 1; studentPagination.current = 1;
await loadStudentsForSelect(); await loadStudentsForSelect();
await loadClassOptions(); await loadClassOptions();
@ -652,6 +690,7 @@ const loadStudentsForSelect = async () => {
studentTableData.value = []; studentTableData.value = [];
} finally { } finally {
studentsLoading.value = false; studentsLoading.value = false;
syncStudentTableRowSelectionKeys();
} }
}; };
@ -677,26 +716,42 @@ const handleStudentTableChange = (pagination: any) => {
const cancelSelectStudent = () => { const cancelSelectStudent = () => {
selectStudentModalVisible.value = false; selectStudentModalVisible.value = false;
selectedStudent.value = null; selectedStudentIds.value = [];
selectedStudentMeta.value = {};
}; };
const confirmAddChild = async () => { const confirmAddChild = async () => {
if (!selectedStudent.value || !currentParent.value) return; if (selectedStudentIds.value.length === 0 || !currentParent.value) return;
addChildLoading.value = true; addChildLoading.value = true;
let success = 0;
let lastError = '';
try { try {
await addChildToParent(currentParent.value.id, { for (const studentId of selectedStudentIds.value) {
studentId: selectedStudent.value.id, try {
relationship: addChildForm.relationship, await addChildToParent(currentParent.value.id, {
}); studentId,
message.success('添加成功'); relationship: addChildForm.relationship,
selectStudentModalVisible.value = false; });
selectedStudent.value = null; success++;
addChildForm.relationship = 'FATHER'; } catch (e: any) {
await loadParentChildren(currentParent.value.id); lastError = e?.response?.data?.message || e?.message || '添加失败';
loadParents(); // }
} catch (error: any) { }
message.error(error?.response?.data?.message || '添加失败'); if (success > 0) {
const fail = selectedStudentIds.value.length - success;
message.success(
fail > 0 ? `成功关联 ${success} 个孩子,${fail} 个未添加(可能已关联)` : `成功关联 ${success} 个孩子`
);
selectStudentModalVisible.value = false;
selectedStudentIds.value = [];
selectedStudentMeta.value = {};
addChildForm.relationship = 'FATHER';
await loadParentChildren(currentParent.value.id);
loadParents();
} else {
message.error(lastError || '添加失败');
}
} finally { } finally {
addChildLoading.value = false; addChildLoading.value = false;
} }
@ -1154,8 +1209,10 @@ onMounted(() => {
.select-footer { .select-footer {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
gap: 12px;
margin-top: 16px; margin-top: 16px;
padding: 16px; padding: 16px;
background: #F8F9FA; background: #F8F9FA;
@ -1164,8 +1221,20 @@ onMounted(() => {
.selected-info { .selected-info {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex: 1;
min-width: 0;
}
.selected-count {
flex-shrink: 0;
}
.selected-tags {
flex: 1;
min-width: 0;
} }
.selected-class { .selected-class {