2026-08-21 17:43:57 +08:00
|
|
|
|
import { useEffect, useState } from "react";
|
2026-08-21 18:52:24 +08:00
|
|
|
|
import { Badge, Button, Form, Input, Modal, Table, message } from "antd";
|
2026-08-21 17:43:57 +08:00
|
|
|
|
import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
|
|
|
|
|
|
import SearchForm from "~/components/SearchForm";
|
|
|
|
|
|
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
|
|
|
|
|
|
import { Connect } from "@cqsjjb/jjb-dva-runtime";
|
|
|
|
|
|
import { NS_COURSEWARE } from "~/enumerate/namespace";
|
|
|
|
|
|
import {
|
|
|
|
|
|
CLASS_STUDENT_EXAM_STATUS_OPTIONS,
|
|
|
|
|
|
CLASS_STUDENT_FACE_AUTH_MAP,
|
|
|
|
|
|
CLASS_STUDENT_STUDY_STATUS_MAP,
|
|
|
|
|
|
CLASS_STUDENT_EXAM_STATUS_MAP,
|
|
|
|
|
|
CLASS_STUDENT_STUDY_STATUS_OPTIONS,
|
|
|
|
|
|
} from "~/enumerate/constant";
|
|
|
|
|
|
|
|
|
|
|
|
/** 搜索表单值 → 查询参数:入班时间范围拆为起止字段(yyyy-MM-dd,接口暂未支持,先行透传) */
|
|
|
|
|
|
const buildQuery = (value) => {
|
|
|
|
|
|
const { entryTimeRange, ...rest } = value || {};
|
|
|
|
|
|
return {
|
|
|
|
|
|
...rest,
|
|
|
|
|
|
...(entryTimeRange?.[0]
|
|
|
|
|
|
? { entryTimeStart: entryTimeRange[0].format("YYYY-MM-DD") }
|
|
|
|
|
|
: {}),
|
|
|
|
|
|
...(entryTimeRange?.[1]
|
|
|
|
|
|
? { entryTimeEnd: entryTimeRange[1].format("YYYY-MM-DD") }
|
|
|
|
|
|
: {}),
|
|
|
|
|
|
current: 1,
|
|
|
|
|
|
size: 10,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/** 班级详情 — 学员 Tab:班级学员查询 */
|
|
|
|
|
|
function ClassStudentTab(props) {
|
|
|
|
|
|
const { classId } = props;
|
|
|
|
|
|
const [searchForm] = Form.useForm();
|
|
|
|
|
|
const [query, setQuery] = useState({ current: 1, size: 10 });
|
2026-08-21 18:52:24 +08:00
|
|
|
|
const [selectOpen, setSelectOpen] = useState(false);
|
2026-08-21 17:43:57 +08:00
|
|
|
|
const {
|
|
|
|
|
|
classStudentList: dataSource,
|
|
|
|
|
|
classStudentTotal: total,
|
|
|
|
|
|
classStudentLoading: loading,
|
|
|
|
|
|
} = props.courseware || {};
|
2026-08-21 18:52:24 +08:00
|
|
|
|
const tableSource = Array.isArray(dataSource) ? dataSource : [];
|
2026-08-21 17:43:57 +08:00
|
|
|
|
|
|
|
|
|
|
const handleSearch = (params = query) => {
|
|
|
|
|
|
props.classStudentPage({ ...params, classId });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!classId) return;
|
|
|
|
|
|
handleSearch(query);
|
|
|
|
|
|
}, [classId]);
|
|
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "序号",
|
|
|
|
|
|
width: 70,
|
|
|
|
|
|
fixed: "left",
|
|
|
|
|
|
render: (_, __, index) => (query.current - 1) * query.size + index + 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "姓名", dataIndex: "studentName", width: 100 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "参训单位",
|
|
|
|
|
|
dataIndex: "unitName",
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
render: (v) => v || "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "部门", dataIndex: "department", width: 120, render: (v) => v || "-" },
|
|
|
|
|
|
{ title: "手机号", dataIndex: "phone", width: 130, render: (v) => v || "-" },
|
|
|
|
|
|
{ title: "档案编号", dataIndex: "fileNumber", width: 220, ellipsis: true },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "人脸认证",
|
|
|
|
|
|
dataIndex: "faceAuth",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (v) => {
|
|
|
|
|
|
const item = CLASS_STUDENT_FACE_AUTH_MAP[v];
|
|
|
|
|
|
return item ? <Badge status={item.status} text={item.label} /> : "-";
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "要求学时",
|
|
|
|
|
|
dataIndex: "requiredHours",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (v) => (v == null ? "-" : v),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "已完成学时",
|
|
|
|
|
|
dataIndex: "completedHours",
|
|
|
|
|
|
width: 110,
|
|
|
|
|
|
render: (v) => (v == null ? "-" : v),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "学习状态",
|
|
|
|
|
|
dataIndex: "studyStatus",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (v) => CLASS_STUDENT_STUDY_STATUS_MAP[v] ?? "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "考试状态",
|
|
|
|
|
|
dataIndex: "examStatus",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: (v) => CLASS_STUDENT_EXAM_STATUS_MAP[v] ?? "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "操作人",
|
|
|
|
|
|
dataIndex: "createName",
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "操作",
|
|
|
|
|
|
width: 160,
|
|
|
|
|
|
fixed: "right",
|
2026-08-21 18:52:24 +08:00
|
|
|
|
render: (_, record) => (
|
2026-08-21 17:43:57 +08:00
|
|
|
|
<TableAction>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
onClick={() => message.info("学员查看功能待接入")}
|
|
|
|
|
|
>
|
|
|
|
|
|
查看
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
2026-08-21 18:52:24 +08:00
|
|
|
|
onClick={() =>
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: "确认移除该学员?",
|
|
|
|
|
|
content: `确认将学员「${record.studentName || ""}」从本班移除吗?`,
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
const res = await props.classStudentRemove({
|
|
|
|
|
|
data: record.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res?.success !== false) {
|
|
|
|
|
|
message.success("移除成功");
|
|
|
|
|
|
handleSearch();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-08-21 17:43:57 +08:00
|
|
|
|
>
|
|
|
|
|
|
从本班移除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</TableAction>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<SearchForm
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
|
|
|
|
|
form={searchForm}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
formLine={[
|
|
|
|
|
|
<Form.Item key="studentName" name="studentName">
|
|
|
|
|
|
<ControlWrapper.Input
|
|
|
|
|
|
label="姓名"
|
|
|
|
|
|
placeholder="请输入"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
maxLength={50}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>,
|
|
|
|
|
|
<Form.Item key="studyStatus" name="studyStatus">
|
|
|
|
|
|
<ControlWrapper.Select
|
|
|
|
|
|
label="学习状态"
|
|
|
|
|
|
placeholder="请选择"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
style={{ width: "100%" }}
|
|
|
|
|
|
options={CLASS_STUDENT_STUDY_STATUS_OPTIONS}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>,
|
|
|
|
|
|
<Form.Item key="examStatus" name="examStatus">
|
|
|
|
|
|
<ControlWrapper.Select
|
|
|
|
|
|
label="考试状态"
|
|
|
|
|
|
placeholder="请选择"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
style={{ width: "100%" }}
|
|
|
|
|
|
options={CLASS_STUDENT_EXAM_STATUS_OPTIONS}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>,
|
|
|
|
|
|
<Form.Item key="entryTimeRange" name="entryTimeRange">
|
|
|
|
|
|
<ControlWrapper.DatePicker.RangePicker label="入班时间" />
|
|
|
|
|
|
</Form.Item>,
|
|
|
|
|
|
]}
|
|
|
|
|
|
onReset={(value) => {
|
|
|
|
|
|
const next = buildQuery(value);
|
|
|
|
|
|
setQuery(next);
|
|
|
|
|
|
handleSearch(next);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onFinish={(value) => {
|
|
|
|
|
|
const next = buildQuery(value);
|
|
|
|
|
|
setQuery(next);
|
|
|
|
|
|
handleSearch(next);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
style={{ marginBottom: 12 }}
|
2026-08-21 18:52:24 +08:00
|
|
|
|
onClick={() => setSelectOpen(true)}
|
2026-08-21 17:43:57 +08:00
|
|
|
|
>
|
|
|
|
|
|
选择学员
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
<Table
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
columns={columns}
|
2026-08-21 18:52:24 +08:00
|
|
|
|
dataSource={tableSource}
|
2026-08-21 17:43:57 +08:00
|
|
|
|
scroll={{ x: 1400 }}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
total,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
|
|
|
|
current: query.current,
|
|
|
|
|
|
pageSize: query.size,
|
|
|
|
|
|
onChange: (page, pageSize) => {
|
|
|
|
|
|
const next = { ...query, current: page, size: pageSize };
|
|
|
|
|
|
setQuery(next);
|
|
|
|
|
|
handleSearch(next);
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-08-21 18:52:24 +08:00
|
|
|
|
|
|
|
|
|
|
<StudentSelectModal
|
|
|
|
|
|
open={selectOpen}
|
|
|
|
|
|
loading={props.courseware?.orgPersonnelLoading}
|
|
|
|
|
|
confirmLoading={props.courseware?.classStudentConfirmLoading}
|
|
|
|
|
|
dataSource={props.courseware?.orgPersonnelList}
|
|
|
|
|
|
total={props.courseware?.orgPersonnelTotal}
|
|
|
|
|
|
requestPage={props.orgPersonnelPage}
|
|
|
|
|
|
selectedPhones={tableSource.map((item) => item.phone)}
|
|
|
|
|
|
onOk={async (rows) => {
|
|
|
|
|
|
// 批量新增班级学员
|
|
|
|
|
|
const res = await props.classStudentBatchSave(
|
|
|
|
|
|
rows.map(({ userName, deptName, account }) => ({
|
|
|
|
|
|
classId,
|
|
|
|
|
|
studentName: userName,
|
|
|
|
|
|
department: deptName,
|
|
|
|
|
|
phone: account,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
if (res?.success === false) return;
|
|
|
|
|
|
message.success("添加成功");
|
|
|
|
|
|
setSelectOpen(false);
|
|
|
|
|
|
handleSearch();
|
|
|
|
|
|
}}
|
|
|
|
|
|
onCancel={() => setSelectOpen(false)}
|
|
|
|
|
|
/>
|
2026-08-21 17:43:57 +08:00
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 18:52:24 +08:00
|
|
|
|
/** 选择学员弹窗:数据源为人员信息列表 */
|
|
|
|
|
|
function StudentSelectModal({
|
|
|
|
|
|
open,
|
|
|
|
|
|
loading,
|
|
|
|
|
|
confirmLoading,
|
|
|
|
|
|
dataSource,
|
|
|
|
|
|
total,
|
|
|
|
|
|
requestPage,
|
|
|
|
|
|
selectedPhones,
|
|
|
|
|
|
onOk,
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [query, setQuery] = useState({ current: 1, size: 10 });
|
|
|
|
|
|
const [keyword, setKeyword] = useState("");
|
|
|
|
|
|
const [selectedRows, setSelectedRows] = useState([]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSearch = (params) => {
|
|
|
|
|
|
setQuery(params);
|
|
|
|
|
|
requestPage({
|
|
|
|
|
|
...params,
|
|
|
|
|
|
employmentStatusCode: 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (open) {
|
|
|
|
|
|
setKeyword("");
|
|
|
|
|
|
setSelectedRows([]);
|
|
|
|
|
|
handleSearch({ current: 1, size: 10 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
open={open}
|
|
|
|
|
|
title="选择学员"
|
|
|
|
|
|
width={860}
|
|
|
|
|
|
confirmLoading={confirmLoading}
|
|
|
|
|
|
destroyOnHidden
|
|
|
|
|
|
okText={`确定${selectedRows.length ? `(已选 ${selectedRows.length})` : ""}`}
|
|
|
|
|
|
okButtonProps={{ disabled: !selectedRows.length }}
|
|
|
|
|
|
onOk={() => onOk(selectedRows)}
|
|
|
|
|
|
onCancel={onCancel}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input.Search
|
|
|
|
|
|
placeholder="请输入学员姓名"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
style={{ width: 280, marginBottom: 12 }}
|
|
|
|
|
|
value={keyword}
|
|
|
|
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
|
|
|
|
onSearch={(value) =>
|
|
|
|
|
|
handleSearch({ current: 1, size: query.size, userName: value })
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
columns={[
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "序号",
|
|
|
|
|
|
width: 60,
|
|
|
|
|
|
render: (_, __, index) =>
|
|
|
|
|
|
(query.current - 1) * query.size + index + 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "公司名称",
|
|
|
|
|
|
dataIndex: "orgName",
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
render: (v) => v || "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "部门",
|
|
|
|
|
|
dataIndex: "deptName",
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (v) => v || "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
{ title: "学员姓名", dataIndex: "userName", width: 100 },
|
|
|
|
|
|
{
|
|
|
|
|
|
title: "手机号",
|
|
|
|
|
|
dataIndex: "account",
|
|
|
|
|
|
width: 130,
|
|
|
|
|
|
render: (v) => v || "-",
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
dataSource={Array.isArray(dataSource) ? dataSource : []}
|
|
|
|
|
|
rowSelection={{
|
|
|
|
|
|
preserveSelectedRowKeys: true,
|
|
|
|
|
|
selectedRowKeys: selectedRows.map((r) => r.id),
|
|
|
|
|
|
getCheckboxProps: (record) => ({
|
|
|
|
|
|
disabled: selectedPhones.includes(record.account),
|
|
|
|
|
|
}),
|
|
|
|
|
|
onChange: (_, rows) => setSelectedRows(rows),
|
|
|
|
|
|
}}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
total,
|
|
|
|
|
|
size: "small",
|
|
|
|
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
|
|
|
|
current: Number(query.current) || 1,
|
|
|
|
|
|
pageSize: Number(query.size) || 10,
|
|
|
|
|
|
onChange: (page, pageSize) =>
|
|
|
|
|
|
handleSearch({
|
|
|
|
|
|
current: page,
|
|
|
|
|
|
size: pageSize,
|
|
|
|
|
|
userName: query.userName,
|
|
|
|
|
|
}),
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-21 17:43:57 +08:00
|
|
|
|
export default Connect([NS_COURSEWARE], true)(ClassStudentTab);
|