dev_1.0.1
tangjie 2026-07-07 09:07:19 +08:00
parent 0c9e2d62d3
commit 950980d48b
10 changed files with 927 additions and 741 deletions

View File

@ -1,55 +1,53 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime"; import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
import { import { Get } from "@cqsjjb/jjb-common-lib/http";
fromPageResponse,
fromSingleResponse,
fromStaffCertForm,
toPageQuery,
toStaffCertForm,
} from "../enterpriseInfo/adapter";
import { apiGet, apiPost, apiPostDelete, safeAction, safePageResult } from "../enterpriseInfo/http";
export const staffCertificateList = declareRequest("staffCertificateLoading", safePageResult(async (params) => { export const staffCertificateList = declareRequest(
const query = toPageQuery(params, { "staffCertificateLoading",
eqStaffId: "personnelId", "Get > /safetyEval/org-personnel-cert/page",
likeCertNo: "certNo", "staffCertificateList: [] | res.data || [] & staffCertificateTotal: 0 | res.data?.total || 0",
}); );
const res = await apiGet("/safetyEval/org-personnel-cert/page", query);
return fromPageResponse(res, toStaffCertForm);
}));
export const staffCertificateInfo = declareRequest("staffCertificateLoading", safeAction(async (params) => { export const staffCertificateInfo = declareRequest(
const res = await apiGet("/safetyEval/org-personnel-cert/get", { id: params.id }); "staffCertificateLoading",
return fromSingleResponse(res, toStaffCertForm); "Get > /safetyEval/org-personnel-cert/get",
})); "staffCertificateDetail: {} | res.data || {}",
);
export const staffCertificateAdd = declareRequest("staffCertificateLoading", safeAction(async (values) => { export const staffCertificateAdd = declareRequest(
const res = await apiPost("/safetyEval/org-personnel-cert/save", fromStaffCertForm(values)); "staffCertificateLoading",
return fromSingleResponse(res, toStaffCertForm); "Post > @/safetyEval/org-personnel-cert/save",
})); );
export const staffCertificateEdit = declareRequest("staffCertificateLoading", safeAction(async (values) => { export const staffCertificateEdit = declareRequest(
const res = await apiPost("/safetyEval/org-personnel-cert/modify", fromStaffCertForm(values)); "staffCertificateLoading",
return fromSingleResponse(res, toStaffCertForm); "Post > @/safetyEval/org-personnel-cert/modify",
})); );
export const staffCertificateRemove = declareRequest("staffCertificateLoading", safeAction(async ({ id }) => { export const staffCertificateRemove = declareRequest(
return apiPostDelete("/safetyEval/org-personnel-cert/delete", id); "staffCertificateLoading",
})); "Post > @/safetyEval/org-personnel-cert/delete",
);
/** StaffViewModal 直接调用的辅助函数 — 获取人员下所有证书列表 */
export async function fetchStaffCertListByPersonnelId(personnelId) { export async function fetchStaffCertListByPersonnelId(personnelId) {
const query = toPageQuery({ try {
pageIndex: 1, const res = await Get("/safetyEval/org-personnel-cert/page", {
pageSize: 999, current: 1,
eqStaffId: personnelId, size: 999,
}, { personnelId,
eqStaffId: "personnelId", });
}); return res?.data || [];
const res = await apiGet("/safetyEval/org-personnel-cert/page", query); } catch {
const page = fromPageResponse(res, toStaffCertForm); return [];
return page?.data || []; }
} }
/** StaffViewModal 直接调用的辅助函数 — 获取证书详情 */
export async function fetchStaffCertDetail({ id }) { export async function fetchStaffCertDetail({ id }) {
const res = await apiGet("/safetyEval/org-personnel-cert/get", { id }); try {
return fromSingleResponse(res, toStaffCertForm); const res = await Get("/safetyEval/org-personnel-cert/get", { id });
} return res?.data || null;
} catch {
return null;
}
}

View File

@ -1,42 +1,29 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime"; import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
import {
fromPageResponse,
fromResignAuditForm,
fromSingleResponse,
toChangeLogForm,
toPageQuery,
toResignApplyForm,
toStaffChangeSummary,
} from "../enterpriseInfo/adapter";
import { apiGet, apiPost, apiPostDelete, safeAction, safePageResult } from "../enterpriseInfo/http";
export const staffChangeLogStaffList = declareRequest("staffChangeLogLoading", safePageResult(async (params) => { export const staffChangeLogStaffList = declareRequest(
const query = toPageQuery(params, { "staffChangeLogLoading",
likeAccount: "account", "Get > /safetyEval/org-personnel/page",
likeStaffName: "userName", "staffChangeLogStaffList: [] | res.data || [] & staffChangeLogStaffTotal: 0 | res.total || 0",
employmentStatus: "employmentStatusCode", );
resignAuditStatus: "resignAuditStatus",
});
const res = await apiGet("/safetyEval/org-personnel/page", query);
return fromPageResponse(res, toStaffChangeSummary);
}));
export const staffChangeLogList = declareRequest("staffChangeLogLoading", safePageResult(async (params) => { export const staffChangeLogList = declareRequest(
const query = toPageQuery(params, { eqStaffId: "personnelId" }); "staffChangeLogLoading",
const res = await apiGet("/safetyEval/org-personnel-change/page", query); "Get > /safetyEval/org-personnel-change/page",
return fromPageResponse(res, toChangeLogForm); "staffChangeLogChangeList: [] | res.data || [] & staffChangeLogChangeTotal: 0 | res.total || 0",
})); );
export const staffChangeLogRemove = declareRequest("staffChangeLogLoading", safeAction(async ({ id }) => { export const staffChangeLogRemove = declareRequest(
return apiPostDelete("/safetyEval/org-personnel-change/delete", id); "staffChangeLogLoading",
})); "Post > @/safetyEval/org-personnel-change/delete",
);
export const staffResignationAudit = declareRequest("staffChangeLogLoading", safeAction(async (params) => { export const staffResignationAudit = declareRequest(
const res = await apiPost("/safetyEval/org-resign-apply/modify", fromResignAuditForm(params)); "staffChangeLogLoading",
return fromSingleResponse(res, toResignApplyForm); "Post > @/safetyEval/org-resign-apply/modify",
})); );
export const staffResignationInfo = declareRequest("staffChangeLogLoading", safeAction(async (params) => { export const staffResignationInfo = declareRequest(
const res = await apiGet("/safetyEval/org-resign-apply/get", { id: params.id }); "staffChangeLogLoading",
return fromSingleResponse(res, toResignApplyForm); "Get > /safetyEval/org-resign-apply/get",
})); "staffResignationDetail: {} | res.data || {}",
);

View File

@ -1,43 +1,33 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime"; import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
import {
fromPageResponse,
fromSingleResponse,
fromStaffForm,
toPageQuery,
toStaffForm,
} from "../enterpriseInfo/adapter";
import { apiGet, apiPost, apiPostDelete, safeAction, safePageResult } from "../enterpriseInfo/http";
export const staffInfoList = declareRequest("staffInfoLoading", safePageResult(async (params) => { export const staffInfoList = declareRequest(
const query = toPageQuery(params, { "staffInfoLoading",
likeStaffName: "userName", "Get > /safetyEval/org-personnel/page",
eqDeptId: "deptId", "staffInfoList: [] | res.data || [] & staffInfoTotal: 0 | res.data?.total || 0",
eqPositionId: "postId", );
});
const res = await apiGet("/safetyEval/org-personnel/page", query);
return fromPageResponse(res, toStaffForm);
}));
export const staffInfoGet = declareRequest("staffInfoLoading", safeAction(async (params) => { export const staffInfoGet = declareRequest(
const res = await apiGet("/safetyEval/org-personnel/get", { id: params.id }); "staffInfoLoading",
return fromSingleResponse(res, toStaffForm); "Get > /safetyEval/org-personnel/get",
})); "staffInfoDetail: {} | res.data || {}",
);
export const staffInfoAdd = declareRequest("staffInfoLoading", safeAction(async (values) => { export const staffInfoAdd = declareRequest(
const res = await apiPost("/safetyEval/org-personnel/save", fromStaffForm(values)); "staffInfoLoading",
return fromSingleResponse(res, toStaffForm); "Post > @/safetyEval/org-personnel/save",
})); );
export const staffInfoEdit = declareRequest("staffInfoLoading", safeAction(async (values) => { export const staffInfoEdit = declareRequest(
const res = await apiPost("/safetyEval/org-personnel/modify", fromStaffForm(values)); "staffInfoLoading",
return fromSingleResponse(res, toStaffForm); "Post > @/safetyEval/org-personnel/modify",
})); );
export const staffInfoRemove = declareRequest("staffInfoLoading", safeAction(async ({ id }) => { export const staffInfoRemove = declareRequest(
return apiPostDelete("/safetyEval/org-personnel/delete", id); "staffInfoLoading",
})); "Post > @/safetyEval/org-personnel/delete",
);
/** 后端暂未提供重置密码接口 */ export const staffInfoResetPassword = declareRequest(
export const staffInfoResetPassword = declareRequest("staffInfoLoading", safeAction(async ({ id }) => { "staffInfoLoading",
return apiPost(`/safetyEval/org-personnel/reset-password?id=${id}`); "Get > /safetyEval/org-personnel/reset-password",
})); );

View File

@ -1,28 +1,18 @@
import { declareRequest } from "@cqsjjb/jjb-dva-runtime"; import { declareRequest } from "@cqsjjb/jjb-dva-runtime";
import {
fromPageResponse,
fromResignApplyForm,
fromSingleResponse,
toPageQuery,
toResignApplyForm,
} from "../enterpriseInfo/adapter";
import { apiGet, apiPost, safeAction, safePageResult } from "../enterpriseInfo/http";
export const staffResignationApplyList = declareRequest("staffResignationApplyLoading", safePageResult(async (params) => { export const staffResignationApplyList = declareRequest(
const query = toPageQuery(params, { "staffResignationApplyLoading",
likeStaffName: "applicantName", "Get > /safetyEval/org-resign-apply/page",
auditStatus: "auditStatusCode", "staffResignationApplyList: [] | res.data || [] & staffResignationApplyTotal: 0 | res.total || 0",
}); );
const res = await apiGet("/safetyEval/org-resign-apply/page", query);
return fromPageResponse(res, toResignApplyForm);
}));
export const staffResignationApplyInfo = declareRequest("staffResignationApplyLoading", safeAction(async (params) => { export const staffResignationApplyInfo = declareRequest(
const res = await apiGet("/safetyEval/org-resign-apply/get", { id: params.id }); "staffResignationApplyLoading",
return fromSingleResponse(res, toResignApplyForm); "Get > /safetyEval/org-resign-apply/get",
})); "resignApplyDetail: {} | res.data || {}",
);
export const staffResignationApplyAdd = declareRequest("staffResignationApplyLoading", safeAction(async (values) => { export const staffResignationApplyAdd = declareRequest(
const res = await apiPost("/safetyEval/org-resign-apply/save", fromResignApplyForm(values)); "staffResignationApplyLoading",
return fromSingleResponse(res, toResignApplyForm); "Post > @/safetyEval/org-resign-apply/save",
})); );

View File

@ -1,5 +1,6 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { Button, Empty, Form, Input, message, Modal, Row, Col, Select, Space, Tree } from "antd"; import { Button, Empty, Form, Input, message, Modal, Row, Col, Select, Space, Tree } from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import AddIcon from "zy-react-library/components/Icon/AddIcon"; import AddIcon from "zy-react-library/components/Icon/AddIcon";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
@ -271,10 +272,10 @@ function DepartmentPositionPage(props) {
title: "操作", title: "操作",
width: 160, width: 160,
render: (_, record) => ( render: (_, record) => (
<Space> <TableAction>
<Button type="link" onClick={() => openPositionModal(record)}>编辑</Button> <Button type="link" onClick={() => openPositionModal(record)}>编辑</Button>
<Button danger type="link" onClick={() => onDeletePosition(record.id)}>删除</Button> <Button danger type="link" onClick={() => onDeletePosition(record.id)}>删除</Button>
</Space> </TableAction>
), ),
}, },
]} ]}

View File

@ -1,18 +1,30 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { Button, Descriptions, Form, Input, message, Modal, Select, Space, Typography } from "antd"; import { Button, Descriptions, Form, Input, message, Modal, Select, Space, Table, Tag, Typography } from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import Search from "zy-react-library/components/Search"; import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import Table from "zy-react-library/components/Table"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import useTable from "zy-react-library/hooks/useTable"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { tools } from "@cqsjjb/jjb-common-lib";
import { NS_STAFF_CHANGE_LOG, NS_STAFF_INFO } from "~/enumerate/namespace"; import { NS_STAFF_CHANGE_LOG, NS_STAFF_INFO } from "~/enumerate/namespace";
import { safeListRequest, safeRequest } from "~/utils"; import {
import { CHANGE_COUNT_STYLE, formSelectField, getChangeCount, getEmploymentStatusLabel, getResignAuditStatusLabel } from "~/utils/enterpriseForm"; CHANGE_COUNT_STYLE,
RESIGN_AUDIT_STATUS_LABEL,
AUDIT_STATUS_COLOR,
} from "~/utils/enterpriseForm";
const { router } = tools;
const EMPLOYMENT_STATUS = { 1: "在职", 2: "离职" }; const EMPLOYMENT_STATUS_OPTIONS = [
const RESIGN_AUDIT_STATUS = { 0: "未审核", 1: "已审核", 2: "已退回" }; { value: 1, label: "在职" },
const SEARCH_COL = { xs: 24, sm: 12, md: 8, lg: 6 }; { value: 2, label: "离职" },
];
const RESIGN_AUDIT_OPTIONS = Object.entries(RESIGN_AUDIT_STATUS_LABEL).map(([value, label]) => ({
value: Number(value),
label,
}));
function PersonnelChangePage(props) { function PersonnelChangePage(props) {
const [viewMode, setViewMode] = useState("list"); const [viewMode, setViewMode] = useState("list");
@ -22,27 +34,34 @@ function PersonnelChangePage(props) {
const [resignInfo, setResignInfo] = useState({}); const [resignInfo, setResignInfo] = useState({});
const [rejectReason, setRejectReason] = useState(""); const [rejectReason, setRejectReason] = useState("");
const [searchForm] = Form.useForm(); const [searchForm] = Form.useForm();
const [logForm] = Form.useForm();
const { tableProps, getData } = useTable(safeListRequest(props.staffChangeLogStaffList), { const { staffChangeLog } = props;
form: searchForm, const {
}); staffChangeLogStaffList: staffList,
staffChangeLogStaffTotal: staffTotal,
staffChangeLogLoading: loading,
staffChangeLogChangeList: changeList,
staffChangeLogChangeTotal: changeTotal,
} = staffChangeLog || {};
const { tableProps: logTableProps, getData: getLogData } = useTable( const handleSearch = () => {
safeListRequest(props.staffChangeLogList), props.staffChangeLogStaffList({ ...router.query });
{ };
form: logForm,
transform: () => ({ eqStaffId: selectedStaff?.staffId }),
},
);
useEffect(() => { useEffect(() => {
getData(); searchForm.setFieldsValue(router.query);
handleSearch();
}, []); }, []);
const handleLogSearch = () => {
if (selectedStaff?.staffId) {
props.staffChangeLogList({ eqStaffId: selectedStaff.staffId });
}
};
useEffect(() => { useEffect(() => {
if (viewMode === "log" && selectedStaff?.staffId) { if (viewMode === "log" && selectedStaff?.staffId) {
getLogData(); handleLogSearch();
} }
}, [viewMode, selectedStaff?.staffId]); }, [viewMode, selectedStaff?.staffId]);
@ -56,9 +75,8 @@ function PersonnelChangePage(props) {
const res = await props.staffInfoRemove?.({ id: record.staffId || record.id }); const res = await props.staffInfoRemove?.({ id: record.staffId || record.id });
if (res?.success !== false) { if (res?.success !== false) {
message.success("删除成功"); message.success("删除成功");
getData(); handleSearch();
} } else {
else {
message.error(res?.message || "删除失败"); message.error(res?.message || "删除失败");
} }
}, },
@ -67,26 +85,23 @@ function PersonnelChangePage(props) {
const openResignAudit = async (record) => { const openResignAudit = async (record) => {
try { try {
const res = await safeRequest(props.staffResignationInfo, { id: record.resignApplyId }); const res = await props.staffResignationInfo({ id: record.resignApplyId });
setResignInfo(res?.data || {}); const data = res?.data || {};
setResignInfo(data);
setSelectedStaff(record); setSelectedStaff(record);
setRejectReason(""); setRejectReason("");
setAuditModalOpen(true); setAuditModalOpen(true);
} } catch {
catch (err) {
console.warn("[PersonnelChange] openResignAudit failed:", err);
message.error("获取离职申请详情失败"); message.error("获取离职申请详情失败");
} }
}; };
const openResignView = async (record) => { const openResignView = async (record) => {
try { try {
const res = await safeRequest(props.staffResignationInfo, { id: record.resignApplyId }); const res = await props.staffResignationInfo({ id: record.resignApplyId });
setResignInfo(res?.data || {}); setResignInfo(res?.data || {});
setViewResignOpen(true); setViewResignOpen(true);
} } catch {
catch (err) {
console.warn("[PersonnelChange] openResignView failed:", err);
message.error("获取离职申请详情失败"); message.error("获取离职申请详情失败");
} }
}; };
@ -99,39 +114,121 @@ function PersonnelChangePage(props) {
try { try {
const res = await props.staffResignationAudit({ const res = await props.staffResignationAudit({
id: resignInfo.id, id: resignInfo.id,
auditStatus: passed ? 1 : 2, auditStatusCode: passed ? 1 : 2,
auditStatusName: passed ? "已审核" : "已退回",
rejectReason: passed ? undefined : rejectReason, rejectReason: passed ? undefined : rejectReason,
}); });
if (res?.success !== false) { if (res?.success !== false) {
message.success(passed ? "审核通过" : "已退回"); message.success(passed ? "审核通过" : "已退回");
setAuditModalOpen(false); setAuditModalOpen(false);
getData(); handleSearch();
} } else {
else {
message.error(res?.message || "审核操作失败"); message.error(res?.message || "审核操作失败");
} }
} } catch {
catch (err) {
console.warn("[PersonnelChange] submitAudit failed:", err);
message.error("审核操作失败"); message.error("审核操作失败");
} }
}; };
const staffColumns = [
{ title: "账户", dataIndex: "account", width: 130 },
{ title: "姓名", dataIndex: "userName", width: 100 },
{ title: "部门", dataIndex: "deptName", width: 120 },
{
title: "信息变更数",
dataIndex: "changeCount",
width: 110,
render: (_, record) => {
const n = Number(record.changeCount ?? 0);
const style = n > 0 ? CHANGE_COUNT_STYLE.active : CHANGE_COUNT_STYLE.zero;
if (n <= 0) {
return <span style={style}>{n}</span>;
}
return (
<Typography.Link
style={style}
onClick={() => {
setSelectedStaff(record);
setViewMode("log");
}}
>
{n}
</Typography.Link>
);
},
},
{
title: "就职状态",
dataIndex: "employmentStatusCode",
width: 90,
render: (_, record) => {
const code = record.employmentStatusCode ?? record.employmentStatus;
return <Tag color={code === 2 ? "error" : "success"}>{code === 2 ? "离职" : "在职"}</Tag>;
},
},
{
title: "离职申请审核状态",
dataIndex: "resignAuditStatus",
width: 140,
render: (_, record) => {
const code = record.resignAuditStatus ?? record.auditStatus ?? record.auditStatusCode;
const label = RESIGN_AUDIT_STATUS_LABEL[code] ?? "-";
return <Tag color={AUDIT_STATUS_COLOR[code]}>{label}</Tag>;
},
},
{
title: "操作",
width: 200,
fixed: "right",
render: (_, record) => (
<TableAction>
<Button type="link" size="small" onClick={() => openResignView(record)}>
查看
</Button>
{record.resignApplyId && Number(record.resignAuditStatus) === 0 && (
<Button type="link" size="small" onClick={() => openResignAudit(record)}>
离职审核
</Button>
)}
<Button danger type="link" size="small" onClick={() => onDelete(record)}>
删除
</Button>
</TableAction>
),
},
];
const logColumns = [
{ title: "变更事项", dataIndex: "changeItem", width: 200 },
{ title: "变更时间", dataIndex: "changeTime", width: 160 },
{ title: "操作人", dataIndex: "createBy", width: 100 },
];
if (viewMode === "log") { if (viewMode === "log") {
return ( return (
<PageLayout title="人员变更次数"> <PageLayout title="人员变更次数">
<Button style={{ marginBottom: 16 }} onClick={() => setViewMode("list")}>返回</Button> <Button style={{ marginBottom: 16 }} onClick={() => setViewMode("list")}>
<div style={{ marginBottom: 8 }}> 返回
人员 </Button>
{selectedStaff?.staffName} <div style={{ marginBottom: 8 }}>人员{selectedStaff?.staffName || selectedStaff?.userName}</div>
</div>
<Table <Table
columns={[ rowKey="id"
{ title: "变更事项", dataIndex: "changeItem" }, columns={logColumns}
{ title: "变更时间", dataIndex: "changeTime" }, dataSource={changeList}
{ title: "操作人", dataIndex: "createBy" }, scroll={{ y: props.scrollY }}
]} loading={loading}
{...logTableProps} pagination={{
total: changeTotal,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
current: router.query.current || 1,
pageSize: router.query.size || 10,
onChange: (page, pageSize) => {
router.query = { ...router.query, current: page, size: pageSize };
handleLogSearch();
},
}}
/> />
</PageLayout> </PageLayout>
); );
@ -148,82 +245,60 @@ function PersonnelChangePage(props) {
</div> </div>
} }
> >
<Search <SearchForm
style={{ marginBottom: 24 }}
form={searchForm} form={searchForm}
options={[ loading={loading}
{ name: "likeAccount", label: "账户", placeholder: "请输入账户", colProps: SEARCH_COL }, formLine={[
{ name: "likeStaffName", label: "姓名", placeholder: "请输入姓名", colProps: SEARCH_COL }, <Form.Item key="account" name="account">
formSelectField( <ControlWrapper.Input label="账户" placeholder="请输入" allowClear />
"employmentStatus", </Form.Item>,
"就职状态", <Form.Item key="userName" name="userName">
Object.entries(EMPLOYMENT_STATUS).map(([value, label]) => ({ label, value: Number(value) })), <ControlWrapper.Input label="姓名" placeholder="请输入" allowClear />
{ colProps: SEARCH_COL }, </Form.Item>,
), <Form.Item key="employmentStatusCode" name="employmentStatusCode">
formSelectField( <ControlWrapper.Select label="就职状态" placeholder="请选择" allowClear style={{ width: "100%" }}>
"resignAuditStatus", {EMPLOYMENT_STATUS_OPTIONS.map((o) => (
"复核状态", <Select.Option key={o.value} value={o.value}>{o.label}</Select.Option>
Object.entries(RESIGN_AUDIT_STATUS).map(([value, label]) => ({ label, value: Number(value) })), ))}
{ colProps: SEARCH_COL, placeholder: "请选择复核状态" }, </ControlWrapper.Select>
), </Form.Item>,
<Form.Item key="resignAuditStatus" name="resignAuditStatus">
<ControlWrapper.Select label="复核状态" placeholder="请选择" allowClear style={{ width: "100%" }}>
{RESIGN_AUDIT_OPTIONS.map((o) => (
<Select.Option key={o.value} value={o.value}>{o.label}</Select.Option>
))}
</ControlWrapper.Select>
</Form.Item>,
]} ]}
onFinish={getData} onReset={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
onFinish={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
/> />
<Table <Table
columns={[ rowKey={(record) => record.id || record.staffId}
{ title: "账户", dataIndex: "account", width: 130 }, columns={staffColumns}
{ title: "姓名", dataIndex: "staffName", width: 100 }, dataSource={staffList}
{ title: "部门", dataIndex: "deptName", width: 120 }, scroll={{ y: props.scrollY }}
{ loading={loading}
title: "信息变更数", pagination={{
dataIndex: "changeCount", total: staffTotal,
width: 110, showSizeChanger: true,
render: (_, record) => { showQuickJumper: true,
const n = getChangeCount(record); showTotal: (t) => `${t}`,
const style = n > 0 ? CHANGE_COUNT_STYLE.active : CHANGE_COUNT_STYLE.zero; current: router.query.current || 1,
if (n <= 0) { pageSize: router.query.size || 10,
return <span style={style}>{n}</span>; onChange: (page, pageSize) => {
} router.query = { ...router.query, current: page, size: pageSize };
return ( handleSearch();
<Typography.Link
style={style}
onClick={() => {
setSelectedStaff(record);
setViewMode("log");
}}
>
{n}
</Typography.Link>
);
},
}, },
{ }}
title: "就职状态",
dataIndex: "employmentStatusName",
width: 90,
render: (_, record) => getEmploymentStatusLabel(record),
},
{
title: "离职申请审核状态",
dataIndex: "resignAuditStatus",
width: 140,
render: (_, record) => getResignAuditStatusLabel(record),
},
{
title: "操作",
width: 220,
fixed: "right",
render: (_, record) => (
<Space>
<Button type="link" style={{ padding: 0 }} onClick={() => openResignView(record)}>查看</Button>
{record.resignApplyId && Number(record.resignAuditStatus) === 0 && (
<Button type="link" style={{ padding: 0 }} onClick={() => openResignAudit(record)}>离职审核</Button>
)}
<Button danger type="link" style={{ padding: 0 }} onClick={() => onDelete(record)}>删除</Button>
</Space>
),
},
]}
{...tableProps}
/> />
<Modal <Modal
@ -231,20 +306,20 @@ function PersonnelChangePage(props) {
destroyOnHidden destroyOnHidden
title="审核离职申请" title="审核离职申请"
width={720} width={720}
footer={( footer={
<Space> <Space>
<Button onClick={() => setAuditModalOpen(false)}>取消</Button> <Button onClick={() => setAuditModalOpen(false)}>取消</Button>
<Button danger onClick={() => submitAudit(false)}>退回</Button> <Button danger onClick={() => submitAudit(false)}>退回</Button>
<Button type="primary" onClick={() => submitAudit(true)}>通过</Button> <Button type="primary" onClick={() => submitAudit(true)}>通过</Button>
</Space> </Space>
)} }
onCancel={() => setAuditModalOpen(false)} onCancel={() => setAuditModalOpen(false)}
> >
<Descriptions bordered column={1} labelStyle={{ width: 120 }}> <Descriptions bordered column={1} labelStyle={{ width: 120 }}>
<Descriptions.Item label="申请人">{resignInfo.applicantName}</Descriptions.Item> <Descriptions.Item label="申请人">{resignInfo.applicantName}</Descriptions.Item>
<Descriptions.Item label="申请时间">{resignInfo.applyTime}</Descriptions.Item> <Descriptions.Item label="申请时间">{resignInfo.applyTime}</Descriptions.Item>
<Descriptions.Item label="预计离职日期">{resignInfo.expectedLeaveDate}</Descriptions.Item> <Descriptions.Item label="预计离职日期">{resignInfo.expectedResignDate}</Descriptions.Item>
<Descriptions.Item label="离职原因">{resignInfo.leaveReason}</Descriptions.Item> <Descriptions.Item label="离职原因">{resignInfo.resignReason}</Descriptions.Item>
<Descriptions.Item label="备注">{resignInfo.remark}</Descriptions.Item> <Descriptions.Item label="备注">{resignInfo.remark}</Descriptions.Item>
</Descriptions> </Descriptions>
<Form.Item label="退回原因" style={{ marginTop: 16 }}> <Form.Item label="退回原因" style={{ marginTop: 16 }}>
@ -264,8 +339,8 @@ function PersonnelChangePage(props) {
<Descriptions bordered column={1} labelStyle={{ width: 120 }}> <Descriptions bordered column={1} labelStyle={{ width: 120 }}>
<Descriptions.Item label="申请人">{resignInfo.applicantName}</Descriptions.Item> <Descriptions.Item label="申请人">{resignInfo.applicantName}</Descriptions.Item>
<Descriptions.Item label="申请时间">{resignInfo.applyTime}</Descriptions.Item> <Descriptions.Item label="申请时间">{resignInfo.applyTime}</Descriptions.Item>
<Descriptions.Item label="预计离职日期">{resignInfo.expectedLeaveDate}</Descriptions.Item> <Descriptions.Item label="预计离职日期">{resignInfo.expectedResignDate}</Descriptions.Item>
<Descriptions.Item label="离职原因">{resignInfo.leaveReason}</Descriptions.Item> <Descriptions.Item label="离职原因">{resignInfo.resignReason}</Descriptions.Item>
<Descriptions.Item label="备注">{resignInfo.remark}</Descriptions.Item> <Descriptions.Item label="备注">{resignInfo.remark}</Descriptions.Item>
</Descriptions> </Descriptions>
</Modal> </Modal>
@ -273,4 +348,4 @@ function PersonnelChangePage(props) {
); );
} }
export default Connect([NS_STAFF_CHANGE_LOG, NS_STAFF_INFO], true)(PersonnelChangePage); export default Connect([NS_STAFF_CHANGE_LOG, NS_STAFF_INFO], true)(AntdTableFuncControl(PersonnelChangePage));

View File

@ -1,45 +1,42 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { Button, Descriptions, Form, message, Modal, Space } from "antd"; import { Button, DatePicker, Descriptions, Form, Input, message, Modal, Select, Table, Upload } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import FormBuilder from "zy-react-library/components/FormBuilder";
import AddIcon from "zy-react-library/components/Icon/AddIcon";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import CertPreviewImg from "~/components/CertPreviewImg"; import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import Search from "zy-react-library/components/Search"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import Table from "zy-react-library/components/Table"; import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import Upload from "zy-react-library/components/Upload"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { FORM_ITEM_RENDER_ENUM } from "zy-react-library/enum/formItemRender"; import { tools } from "@cqsjjb/jjb-common-lib";
import useGetFile from "zy-react-library/hooks/useGetFile";
import useGetUrlQuery from "zy-react-library/hooks/useGetUrlQuery";
import useTable from "zy-react-library/hooks/useTable";
import { NS_STAFF_CERTIFICATE } from "~/enumerate/namespace"; import { NS_STAFF_CERTIFICATE } from "~/enumerate/namespace";
import { safeGetFiles, safeListRequest, safeRequest } from "~/utils"; import CertPreviewImg from "~/components/CertPreviewImg";
import { mockUploadFileList, resolveUploadFileId } from "~/utils/mockUpload";
import { dateRangeRule } from "~/utils/validators"; import { dateRangeRule } from "~/utils/validators";
const { router } = tools;
function StaffCertificatePage(props) { function StaffCertificatePage(props) {
const query = useGetUrlQuery(); const urlParams = Object.fromEntries(new URLSearchParams(window.location.search));
const staffId = query.staffId; const staffId = urlParams.staffId;
const staffName = query.staffName; const staffName = urlParams.staffName ? decodeURIComponent(urlParams.staffName) : "";
const [addModalOpen, setAddModalOpen] = useState(false); const [addModalOpen, setAddModalOpen] = useState(false);
const [viewModalOpen, setViewModalOpen] = useState(false); const [viewModalOpen, setViewModalOpen] = useState(false);
const [currentId, setCurrentId] = useState(""); const [currentId, setCurrentId] = useState("");
const [form] = Form.useForm(); const [searchForm] = Form.useForm();
const { staffCertificate } = props;
const { tableProps, getData } = useTable(safeListRequest(props.staffCertificateList), { const {
form, staffCertificateList: dataSource,
transform: (formData) => ({ staffCertificateTotal: total,
...formData, staffCertificateLoading: loading,
eqStaffId: staffId, } = staffCertificate || {};
}),
});
useEffect(() => { useEffect(() => {
if (staffId) { searchForm.setFieldsValue(router.query);
getData(); handleSearch();
} }, []);
}, [staffId]);
const handleSearch = () => {
props.staffCertificateList({ ...router.query, personnelId: staffId });
};
const goBack = () => { const goBack = () => {
window.location.href = window.location.pathname.replace(/\/Certificate.*$/, "/List"); window.location.href = window.location.pathname.replace(/\/Certificate.*$/, "/List");
@ -55,7 +52,7 @@ function StaffCertificatePage(props) {
const res = await props.staffCertificateRemove({ id }); const res = await props.staffCertificateRemove({ id });
if (res?.success !== false) { if (res?.success !== false) {
message.success("删除成功"); message.success("删除成功");
getData(); handleSearch();
} }
}, },
}); });
@ -64,28 +61,32 @@ function StaffCertificatePage(props) {
return ( return (
<PageLayout title={`人员证书 - ${staffName || ""}`}> <PageLayout title={`人员证书 - ${staffName || ""}`}>
<Button style={{ marginBottom: 16 }} onClick={goBack}>返回</Button> <Button style={{ marginBottom: 16 }} onClick={goBack}>返回</Button>
<Search <SearchForm
form={form} style={{ marginBottom: 24 }}
options={[ form={searchForm}
{ name: "likeCertNo", label: "证书编号", placeholder: "请输入证书编号" }, loading={loading}
{ name: "certCategory", label: "证书类别" }, formLine={[
{ name: "certWorkCategory", label: "证书作业类别" }, <Form.Item key="certNo" name="certNo">
<ControlWrapper.Input label="证书编号" placeholder="请输入证书编号" allowClear />
</Form.Item>,
<Form.Item key="certCategory" name="certCategory">
<ControlWrapper.Input label="证书类别" placeholder="请输入证书类别" allowClear />
</Form.Item>,
<Form.Item key="certWorkCategory" name="certWorkCategory">
<ControlWrapper.Input label="证书作业类别" placeholder="请输入证书作业类别" allowClear />
</Form.Item>,
]} ]}
onFinish={getData} onReset={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
onFinish={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
/> />
<Table <Table
toolBarRender={() => ( rowKey="id"
<Button
type="primary"
icon={<AddIcon />}
onClick={() => {
setCurrentId("");
setAddModalOpen(true);
}}
>
添加证书
</Button>
)}
columns={[ columns={[
{ title: "证书类型", dataIndex: "certCategory" }, { title: "证书类型", dataIndex: "certCategory" },
{ title: "证书作业类别", dataIndex: "certWorkCategory" }, { title: "证书作业类别", dataIndex: "certWorkCategory" },
@ -94,19 +95,33 @@ function StaffCertificatePage(props) {
title: "操作", title: "操作",
width: 200, width: 200,
render: (_, record) => ( render: (_, record) => (
<Space> <TableAction>
<Button type="link" onClick={() => { setCurrentId(record.id); setViewModalOpen(true); }}> <Button type="link" size="small" onClick={() => { setCurrentId(record.id); setViewModalOpen(true); }}>
查看 查看
</Button> </Button>
<Button type="link" onClick={() => { setCurrentId(record.id); setAddModalOpen(true); }}> <Button type="link" size="small" onClick={() => { setCurrentId(record.id); setAddModalOpen(true); }}>
编辑 编辑
</Button> </Button>
<Button danger type="link" onClick={() => onDelete(record.id)}>删除</Button> <Button danger type="link" size="small" onClick={() => onDelete(record.id)}>删除</Button>
</Space> </TableAction>
), ),
}, },
]} ]}
{...tableProps} dataSource={dataSource}
scroll={{ y: props.scrollY }}
loading={loading}
pagination={{
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
current: Number(router.query.current) || 1,
pageSize: Number(router.query.size) || 10,
onChange: (page, pageSize) => {
router.query = { ...router.query, current: page, size: pageSize };
handleSearch();
},
}}
/> />
{addModalOpen && ( {addModalOpen && (
@ -114,21 +129,21 @@ function StaffCertificatePage(props) {
open={addModalOpen} open={addModalOpen}
staffId={staffId} staffId={staffId}
currentId={currentId} currentId={currentId}
requestAdd={props.staffCertificateAdd} staffCertificateAdd={props.staffCertificateAdd}
requestEdit={props.staffCertificateEdit} staffCertificateEdit={props.staffCertificateEdit}
requestDetails={props.staffCertificateInfo} staffCertificateInfo={props.staffCertificateInfo}
onCancel={() => { onCancel={() => {
setAddModalOpen(false); setAddModalOpen(false);
setCurrentId(""); setCurrentId("");
}} }}
onSuccess={getData} onSuccess={handleSearch}
/> />
)} )}
{viewModalOpen && ( {viewModalOpen && (
<ViewModal <ViewModal
open={viewModalOpen} open={viewModalOpen}
currentId={currentId} currentId={currentId}
requestDetails={props.staffCertificateInfo} staffCertificateInfo={props.staffCertificateInfo}
onCancel={() => { onCancel={() => {
setViewModalOpen(false); setViewModalOpen(false);
setCurrentId(""); setCurrentId("");
@ -140,83 +155,91 @@ function StaffCertificatePage(props) {
} }
function CertModal({ function CertModal({
open, staffId, currentId, requestAdd, requestEdit, requestDetails, onCancel, onSuccess, open, staffId, currentId, staffCertificateAdd, staffCertificateEdit, staffCertificateInfo, onCancel, onSuccess,
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const { getFile } = useGetFile(); const [uploadFileList, setUploadFileList] = useState([]);
useEffect(() => { useEffect(() => {
if (!open) { if (!open) return;
return;
}
if (!currentId) { if (!currentId) {
form.resetFields(); form.resetFields();
form.setFieldsValue({ certImgs: mockUploadFileList("证书附件.jpg") }); setUploadFileList([]);
return; return;
} }
let cancelled = false; let cancelled = false;
(async () => { (async () => {
setDetailLoading(true); setDetailLoading(true);
try { try {
const res = await safeRequest(requestDetails, { id: currentId }); const res = await staffCertificateInfo({ id: currentId });
if (cancelled || !res?.data) { if (cancelled || !res?.data) return;
return;
}
const data = { ...res.data }; const data = { ...res.data };
data.validDate = [data.validStartDate, data.validEndDate]; // 将 validStartDate/validEndDate 合并为 validDateRangePicker
data.certImgs = data.certImgFiles?.length ? data.certImgFiles : mockUploadFileList("证书附件.jpg"); if (data.validStartDate || data.validEndDate) {
data.validDate = [data.validStartDate, data.validEndDate];
}
// 处理证书附件 fileList
let fileList = [];
if (Array.isArray(data.certImgFiles)) {
fileList = data.certImgFiles.map((f, i) => ({
uid: f.uid || `-${i}`,
name: f.fileName || f.name || `证书附件${i + 1}`,
status: "done",
url: f.url,
}));
} else if (data.certAttachmentUrl) {
fileList = data.certAttachmentUrl.split(",").filter(Boolean).map((url, i) => ({
uid: `-${i}`,
name: url.split("/").pop() || `证书附件${i + 1}`,
status: "done",
url,
}));
}
data.certAttachmentUrl = fileList;
setUploadFileList(fileList);
form.setFieldsValue(data); form.setFieldsValue(data);
setDetailLoading(false); } catch (err) {
const files = await safeGetFiles(
getFile,
{ eqType: "staff_cert", eqForeignKey: data.id },
data.certImgFiles || [],
);
if (!cancelled && files.length) {
form.setFieldValue("certImgs", files);
}
}
catch (err) {
console.warn("[StaffCertificate] load detail failed:", err); console.warn("[StaffCertificate] load detail failed:", err);
} } finally {
finally { if (!cancelled) setDetailLoading(false);
if (!cancelled) {
setDetailLoading(false);
}
} }
})(); })();
return () => { return () => { cancelled = true; };
cancelled = true;
};
}, [open, currentId]); }, [open, currentId]);
const handleCancel = () => { const handleCancel = () => {
form.resetFields(); form.resetFields();
setUploadFileList([]);
onCancel(); onCancel();
}; };
const handleSubmit = async (values) => { const handleSubmit = async (values) => {
try { try {
setSubmitting(true); setSubmitting(true);
values.validStartDate = values.validDate?.[0]; const payload = {
values.validEndDate = values.validDate?.[1]; ...values,
values.staffId = staffId; personnelId: staffId,
values.certAttachmentUrl = resolveUploadFileId(values.certImgs); validStartDate: values.validDate?.[0],
const request = currentId ? requestEdit : requestAdd; validEndDate: values.validDate?.[1],
if (currentId) { certAttachmentUrl: Array.isArray(values.certAttachmentUrl)
values.id = currentId; ? values.certAttachmentUrl.map((f) => f.url || f.response?.url).filter(Boolean).join(",")
} : "",
const res = await request(values); };
delete payload.validDate;
delete payload.certImgFiles;
if (currentId) payload.id = currentId;
const request = currentId ? staffCertificateEdit : staffCertificateAdd;
const res = await request(payload);
if (res?.success !== false) { if (res?.success !== false) {
message.success(currentId ? "编辑成功" : "添加成功"); message.success(currentId ? "编辑成功" : "添加成功");
handleCancel(); handleCancel();
onSuccess(); onSuccess();
} else {
message.error(res?.message || "操作失败");
} }
} } finally {
finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
@ -232,46 +255,60 @@ function CertModal({
onCancel={handleCancel} onCancel={handleCancel}
onOk={form.submit} onOk={form.submit}
> >
<FormBuilder <Form form={form} layout="vertical" onFinish={handleSubmit}>
form={form} <Form.Item name="certName" label="证照名称" rules={[{ required: true, message: "请输入证照名称" }]}>
span={24} <Input placeholder="请输入证照名称" />
showActionButtons={false} </Form.Item>
onFinish={handleSubmit} <Form.Item name="certCategory" label="证书类别" rules={[{ required: true, message: "请选择证书类别" }]}>
options={[ <Input placeholder="请输入证书类别" />
{ name: "certName", label: "证照名称", rules: [{ required: true, message: "请输入证照名称" }] }, </Form.Item>
{ name: "certCategory", label: "证书类别", rules: [{ required: true, message: "请选择证书类别" }] }, <Form.Item name="certWorkCategory" label="证书作业类别">
{ name: "certWorkCategory", label: "证书作业类别" }, <Input placeholder="请输入证书作业类别" />
{ name: "certNo", label: "证书编号", rules: [{ required: true, message: "请输入证书编号" }] }, </Form.Item>
{ name: "issueOrg", label: "发证机关", rules: [{ required: true, message: "请输入发证机关" }] }, <Form.Item name="certNo" label="证书编号" rules={[{ required: true, message: "请输入证书编号" }]}>
{ <Input placeholder="请输入证书编号" />
name: "validDate", </Form.Item>
label: "证书有效开始/结束日期", <Form.Item name="issueOrg" label="发证机关" rules={[{ required: true, message: "请输入发证机关" }]}>
render: FORM_ITEM_RENDER_ENUM.DATE_RANGE, <Input placeholder="请输入发证机关" />
rules: [{ required: true, message: "请选择证书有效期" }, dateRangeRule()], </Form.Item>
}, <Form.Item
{ name: "reviewDate", label: "复合日期", render: FORM_ITEM_RENDER_ENUM.DATE }, name="validDate"
{ label="证书有效开始/结束日期"
name: "certImgs", rules={[{ required: true, message: "请选择证书有效期" }, dateRangeRule()]}
label: "证书附件", >
render: ( <DatePicker.RangePicker style={{ width: "100%" }} />
<Upload </Form.Item>
maxCount={3} <Form.Item name="reviewDate" label="复合日期">
onGetRemoveFile={(file) => setDeleteFiles(prev => [...prev, file])} <DatePicker style={{ width: "100%" }} />
/> </Form.Item>
), <Form.Item
}, name="certAttachmentUrl"
]} label="证书附件"
labelCol={{ span: 24 }} valuePropName="fileList"
wrapperCol={{ span: 24 }} getValueFromEvent={(e) => {
/> if (Array.isArray(e)) return e;
return e?.fileList;
}}
>
<Upload
action="/api/upload"
maxCount={3}
fileList={uploadFileList}
onChange={(info) => {
setUploadFileList(info.fileList);
}}
>
<Button icon={<UploadOutlined />}>上传文件</Button>
</Upload>
</Form.Item>
</Form>
</Modal> </Modal>
); );
} }
function ViewModal({ open, currentId, requestDetails, onCancel }) { function ViewModal({ open, currentId, staffCertificateInfo, onCancel }) {
const [info, setInfo] = useState({}); const [info, setInfo] = useState({});
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const { getFile } = useGetFile();
useEffect(() => { useEffect(() => {
if (!open || !currentId) { if (!open || !currentId) {
@ -282,36 +319,29 @@ function ViewModal({ open, currentId, requestDetails, onCancel }) {
(async () => { (async () => {
setDetailLoading(true); setDetailLoading(true);
try { try {
const res = await safeRequest(requestDetails, { id: currentId }); const res = await staffCertificateInfo({ id: currentId });
if (cancelled || !res?.data) { if (cancelled || !res?.data) return;
return;
}
const data = { ...res.data }; const data = { ...res.data };
data.certImgs = data.certImgFiles || []; // 处理 certImgFiles
if (Array.isArray(data.certImgFiles)) {
data.certDisplayFiles = data.certImgFiles;
} else if (data.certAttachmentUrl) {
data.certDisplayFiles = data.certAttachmentUrl.split(",").filter(Boolean).map((url, i) => ({
uid: `-${i}`,
fileName: url.split("/").pop() || `证书附件${i + 1}`,
url,
}));
} else {
data.certDisplayFiles = [];
}
setInfo(data); setInfo(data);
setDetailLoading(false); } catch (err) {
const files = await safeGetFiles(
getFile,
{ eqType: "staff_cert", eqForeignKey: data.id },
data.certImgFiles || [],
);
if (!cancelled && files.length) {
setInfo((prev) => ({ ...prev, certImgs: files }));
}
}
catch (err) {
console.warn("[StaffCertificate] load view failed:", err); console.warn("[StaffCertificate] load view failed:", err);
} } finally {
finally { if (!cancelled) setDetailLoading(false);
if (!cancelled) {
setDetailLoading(false);
}
} }
})(); })();
return () => { return () => { cancelled = true; };
cancelled = true;
};
}, [open, currentId]); }, [open, currentId]);
return ( return (
@ -333,10 +363,10 @@ function ViewModal({ open, currentId, requestDetails, onCancel }) {
<Descriptions.Item label="证书有效开始日期">{info.validStartDate}</Descriptions.Item> <Descriptions.Item label="证书有效开始日期">{info.validStartDate}</Descriptions.Item>
<Descriptions.Item label="证书有效结束日期">{info.validEndDate}</Descriptions.Item> <Descriptions.Item label="证书有效结束日期">{info.validEndDate}</Descriptions.Item>
<Descriptions.Item label="复合日期">{info.reviewDate}</Descriptions.Item> <Descriptions.Item label="复合日期">{info.reviewDate}</Descriptions.Item>
<Descriptions.Item label="证书图片"><CertPreviewImg files={info.certImgs} /></Descriptions.Item> <Descriptions.Item label="证书图片"><CertPreviewImg files={info.certDisplayFiles} /></Descriptions.Item>
</Descriptions> </Descriptions>
</Modal> </Modal>
); );
} }
export default Connect([NS_STAFF_CERTIFICATE], true)(StaffCertificatePage); export default Connect([NS_STAFF_CERTIFICATE], true)(AntdTableFuncControl(StaffCertificatePage));

View File

@ -1,13 +1,16 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { Button, DatePicker, Form, Input, message, Modal, Row, Col, Select, Space } from "antd"; import { Button, DatePicker, Form, Input, message, Modal, Row, Col, Select, Table, Upload } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import AddIcon from "zy-react-library/components/Icon/AddIcon"; import dayjs from "dayjs";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import Search from "zy-react-library/components/Search"; import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import Table from "zy-react-library/components/Table"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import Upload from "zy-react-library/components/Upload"; import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import useTable from "zy-react-library/hooks/useTable"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import { NS_ORG_DEPARTMENT, NS_ORG_POSITION, NS_STAFF_INFO } from "~/enumerate/namespace"; import { tools } from "@cqsjjb/jjb-common-lib";
import { Get } from "@cqsjjb/jjb-common-lib/http";
import { NS_STAFF_INFO } from "~/enumerate/namespace";
import { import {
EDUCATION_LEVEL_OPTIONS, EDUCATION_LEVEL_OPTIONS,
EDUCATION_TYPE_OPTIONS, EDUCATION_TYPE_OPTIONS,
@ -16,21 +19,11 @@ import {
QUALIFICATION_INDUSTRY_OPTIONS, QUALIFICATION_INDUSTRY_OPTIONS,
REGISTER_ENGINEER_OPTIONS, REGISTER_ENGINEER_OPTIONS,
} from "~/enumerate/enterpriseOptions"; } from "~/enumerate/enterpriseOptions";
import { ensureOrgContext, fetchOrgDepartmentPage, fetchOrgPositionPage } from "~/api/enterpriseInfo/orgBootstrap"; import { getBirthDateFromIdCard } from "~/utils";
import { asId, sameId } from "~/api/enterpriseInfo/idUtil";
import { getBirthDateFromIdCard, safeListRequest, safeRequest } from "~/utils";
import { toDayjs } from "~/utils/dateFormat";
import { formSelectField } from "~/utils/enterpriseForm";
import { idCardRule, mobileRule } from "~/utils/validators"; import { idCardRule, mobileRule } from "~/utils/validators";
import StaffViewModal from "../StaffViewModal"; import StaffViewModal from "../StaffViewModal";
function mapPositionOptions(list = []) { const { router } = tools;
return list.map((p) => ({
label: p.positionName,
value: asId(p.id),
deptId: asId(p.deptId),
}));
}
function PersonnelInfoPage(props) { function PersonnelInfoPage(props) {
const [formModalOpen, setFormModalOpen] = useState(false); const [formModalOpen, setFormModalOpen] = useState(false);
@ -39,43 +32,47 @@ function PersonnelInfoPage(props) {
const [searchForm] = Form.useForm(); const [searchForm] = Form.useForm();
const [deptOptions, setDeptOptions] = useState([]); const [deptOptions, setDeptOptions] = useState([]);
const [positionOptions, setPositionOptions] = useState([]); const [positionOptions, setPositionOptions] = useState([]);
const { staffInfo } = props;
const {
staffInfoList: dataSource,
staffInfoTotal: total,
staffInfoLoading: loading,
} = staffInfo || {};
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
(async () => { (async () => {
try { try {
await ensureOrgContext();
const [deptRes, posRes] = await Promise.all([ const [deptRes, posRes] = await Promise.all([
fetchOrgDepartmentPage({ pageIndex: 1, pageSize: 200 }), Get("/safetyEval/org-department/page", { current: 1, size: 200 }),
fetchOrgPositionPage({ pageIndex: 1, pageSize: 200 }), Get("/safetyEval/org-position/page", { current: 1, size: 200 }),
]); ]);
if (cancelled) { if (cancelled) return;
return; setDeptOptions((deptRes?.data || []).map((d) => ({ label: d.deptName, value: d.id })));
} setPositionOptions((posRes?.data || []).map((p) => ({
setDeptOptions((deptRes?.data || []).map((d) => ({ label: d.deptName, value: asId(d.id) }))); label: p.positionName,
setPositionOptions(mapPositionOptions(posRes?.data)); value: p.id,
} deptId: p.deptId,
catch (err) { })));
} catch (err) {
console.warn("[PersonnelInfo] load dept/position options failed:", err); console.warn("[PersonnelInfo] load dept/position options failed:", err);
} }
})(); })();
return () => { return () => { cancelled = true; };
cancelled = true;
};
}, []); }, []);
const { tableProps, getData } = useTable(safeListRequest(props.staffInfoList), { useEffect(() => {
form: searchForm, searchForm.setFieldsValue(router.query);
transform: (formData) => ({ handleSearch();
...formData, }, []);
eqDeptId: formData.deptId,
eqPositionId: formData.positionId,
}),
});
const goCertificate = (staffId, staffName) => { const handleSearch = () => {
props.staffInfoList({ ...router.query });
};
const goCertificate = (id, staffName) => {
const base = window.location.pathname.replace(/\/List.*$/, ""); const base = window.location.pathname.replace(/\/List.*$/, "");
window.location.href = `${base}/Certificate?staffId=${staffId}&staffName=${encodeURIComponent(staffName || "")}`; window.location.href = `${base}/Certificate?staffId=${id}&staffName=${encodeURIComponent(staffName || "")}`;
}; };
const onDelete = (id) => { const onDelete = (id) => {
@ -88,7 +85,7 @@ function PersonnelInfoPage(props) {
const res = await props.staffInfoRemove({ id }); const res = await props.staffInfoRemove({ id });
if (res?.success !== false) { if (res?.success !== false) {
message.success("删除成功"); message.success("删除成功");
getData(); handleSearch();
} }
}, },
}); });
@ -120,30 +117,42 @@ function PersonnelInfoPage(props) {
</div> </div>
} }
> >
<Search <SearchForm
style={{ marginBottom: 24 }}
form={searchForm} form={searchForm}
options={[ loading={loading}
{ name: "likeStaffName", label: "用户名称", placeholder: "请输入用户名称" }, formLine={[
formSelectField("deptId", "部门", deptOptions), <Form.Item key="userName" name="userName">
formSelectField("positionId", "岗位", positionOptions), <ControlWrapper.Input label="用户名称" placeholder="请输入用户名称" allowClear />
</Form.Item>,
<Form.Item key="deptId" name="deptId">
<ControlWrapper.Select label="部门" placeholder="请选择部门" allowClear style={{ width: "100%" }}>
{deptOptions.map((d) => (
<Select.Option key={d.value} value={d.value}>{d.label}</Select.Option>
))}
</ControlWrapper.Select>
</Form.Item>,
<Form.Item key="postId" name="postId">
<ControlWrapper.Select label="岗位" placeholder="请选择岗位" allowClear style={{ width: "100%" }}>
{positionOptions.map((p) => (
<Select.Option key={p.value} value={p.value}>{p.label}</Select.Option>
))}
</ControlWrapper.Select>
</Form.Item>,
]} ]}
onFinish={getData} onReset={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
onFinish={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
/> />
<Table <Table
toolBarRender={() => ( rowKey="id"
<Button
type="primary"
icon={<AddIcon />}
onClick={() => {
setCurrentId("");
setFormModalOpen(true);
}}
>
新增人员
</Button>
)}
columns={[ columns={[
{ title: "用户名称", dataIndex: "staffName" }, { title: "用户名称", dataIndex: "userName" },
{ title: "账号", dataIndex: "account" }, { title: "账号", dataIndex: "account" },
{ title: "部门", dataIndex: "deptName" }, { title: "部门", dataIndex: "deptName" },
{ title: "岗位", dataIndex: "positionName" }, { title: "岗位", dataIndex: "positionName" },
@ -157,38 +166,53 @@ function PersonnelInfoPage(props) {
title: "操作", title: "操作",
width: 320, width: 320,
render: (_, record) => ( render: (_, record) => (
<Space wrap> <TableAction>
<Button type="link" onClick={() => { setCurrentId(asId(record.id)); setViewModalOpen(true); }}> <Button type="link" size="small" onClick={() => { setCurrentId(record.id); setViewModalOpen(true); }}>
查看 查看
</Button> </Button>
<Button type="link" onClick={() => { setCurrentId(asId(record.id)); setFormModalOpen(true); }}> <Button type="link" size="small" onClick={() => { setCurrentId(record.id); setFormModalOpen(true); }}>
编辑 编辑
</Button> </Button>
<Button type="link" onClick={() => goCertificate(asId(record.id), record.staffName)}> <Button type="link" size="small" onClick={() => { setCurrentId(record.id); goCertificate(record.id, record.userName); }}>
证书 证书
</Button> </Button>
<Button type="link" onClick={() => onResetPassword(asId(record.id))}>重置密码</Button> <Button type="link" size="small" onClick={() => onResetPassword(record.id)}>重置密码</Button>
<Button danger type="link" onClick={() => onDelete(asId(record.id))}>删除</Button> <Button danger type="link" size="small" onClick={() => onDelete(record.id)}>删除</Button>
</Space> </TableAction>
), ),
}, },
]} ]}
{...tableProps} dataSource={dataSource}
scroll={{ y: props.scrollY }}
loading={loading}
pagination={{
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
current: Number(router.query.current) || 1,
pageSize: Number(router.query.size) || 10,
onChange: (page, pageSize) => {
router.query = { ...router.query, current: page, size: pageSize };
handleSearch();
},
}}
/> />
{formModalOpen && ( {formModalOpen && (
<StaffFormModal <StaffFormModal
open={formModalOpen} open={formModalOpen}
currentId={currentId} currentId={currentId}
requestAdd={props.staffInfoAdd} staffInfoGet={props.staffInfoGet}
requestEdit={props.staffInfoEdit} staffInfoAdd={props.staffInfoAdd}
requestDetails={props.staffInfoGet} staffInfoEdit={props.staffInfoEdit}
deptOptions={deptOptions} deptOptions={deptOptions}
positionOptions={positionOptions}
onCancel={() => { onCancel={() => {
setFormModalOpen(false); setFormModalOpen(false);
setCurrentId(""); setCurrentId("");
}} }}
onSuccess={getData} onSuccess={handleSearch}
/> />
)} )}
{viewModalOpen && ( {viewModalOpen && (
@ -207,60 +231,41 @@ function PersonnelInfoPage(props) {
} }
function StaffFormModal({ function StaffFormModal({
open, currentId, requestAdd, requestEdit, requestDetails, deptOptions, onCancel, onSuccess, open, currentId, staffInfoGet, staffInfoAdd, staffInfoEdit, deptOptions, positionOptions, onCancel, onSuccess,
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const [deptPositionOptions, setDeptPositionOptions] = useState([]); const [deptPositionOptions, setDeptPositionOptions] = useState([]);
const [positionLoading, setPositionLoading] = useState(false); const [positionLoading, setPositionLoading] = useState(false);
const [uploadFileList, setUploadFileList] = useState([]);
const watchedDeptId = Form.useWatch("deptId", form); const watchedDeptId = Form.useWatch("deptId", form);
const wasOpenRef = useRef(false); const wasOpenRef = useRef(false);
const inflightDeptRef = useRef(null); const inflightDeptRef = useRef(null);
const requestDetailsRef = useRef(requestDetails);
requestDetailsRef.current = requestDetails;
const loadPositionsByDept = async (deptId, currentStaff = null) => { const loadPositionsByDept = async (deptId) => {
const id = asId(deptId); const id = deptId;
if (!id) { if (!id) {
setDeptPositionOptions([]); setDeptPositionOptions([]);
return []; return [];
} }
const key = id; if (inflightDeptRef.current === id) return deptPositionOptions;
if (inflightDeptRef.current === key) { inflightDeptRef.current = id;
return deptPositionOptions;
}
inflightDeptRef.current = key;
setPositionLoading(true); setPositionLoading(true);
try { try {
let res = await fetchOrgPositionPage({ const res = await Get("/safetyEval/org-position/page", { current: 1, size: 200, deptId: id });
pageIndex: 1, const options = (res?.data || []).map((p) => ({
pageSize: 200, label: p.positionName,
eqDeptId: id, value: p.id,
}); }));
let options = mapPositionOptions(res?.data);
// 兼容历史岗位 dept_id 因 Number() 精度丢失与部门 id 不完全一致
if (!options.length) {
const allRes = await fetchOrgPositionPage({ pageIndex: 1, pageSize: 500 });
options = mapPositionOptions(allRes?.data).filter((p) => sameId(p.deptId, id));
}
const positionId = asId(currentStaff?.positionId);
const positionName = currentStaff?.positionName;
if (positionId && positionName && !options.some((item) => sameId(item.value, positionId))) {
options = [{ label: positionName, value: positionId, deptId: id }, ...options];
}
setDeptPositionOptions(options); setDeptPositionOptions(options);
return options; return options;
} } catch (err) {
catch (err) {
console.warn("[PersonnelInfo] load positions by dept failed:", err); console.warn("[PersonnelInfo] load positions by dept failed:", err);
setDeptPositionOptions([]); setDeptPositionOptions([]);
return []; return [];
} } finally {
finally { if (inflightDeptRef.current === id) inflightDeptRef.current = null;
if (inflightDeptRef.current === key) {
inflightDeptRef.current = null;
}
setPositionLoading(false); setPositionLoading(false);
} }
}; };
@ -270,51 +275,53 @@ function StaffFormModal({
if (!currentId) { if (!currentId) {
form.resetFields(); form.resetFields();
setDeptPositionOptions([]); setDeptPositionOptions([]);
setUploadFileList([]);
form.setFieldsValue({ form.setFieldsValue({
personType: "基础人员",
registerEngineerFlag: 2, registerEngineerFlag: 2,
}); });
} }
} }
if (!open) { if (!open) {
setDeptPositionOptions([]); setDeptPositionOptions([]);
setUploadFileList([]);
} }
wasOpenRef.current = open; wasOpenRef.current = open;
}, [open, currentId, form]); }, [open, currentId, form]);
useEffect(() => { useEffect(() => {
if (!open || !currentId) { if (!open || !currentId) return;
return;
}
let cancelled = false; let cancelled = false;
setDetailLoading(true); setDetailLoading(true);
safeRequest(requestDetailsRef.current, { id: currentId }).then(async (res) => { staffInfoGet({ id: currentId }).then((res) => {
if (!cancelled && res?.data) { if (cancelled || !res?.data) return;
const data = res.data; const data = { ...res.data };
await loadPositionsByDept(data.deptId, data); loadPositionsByDept(data.deptId);
if (!cancelled) { if (cancelled) return;
form.setFieldsValue({ const setFields = {
...data, ...data,
deptId: asId(data.deptId), };
positionId: asId(data.positionId), if (data.birthDate) setFields.birthDate = dayjs(data.birthDate);
birthDate: toDayjs(data.birthDate), const fileList = data.proofMaterialUrl
proofMaterials: data.proofMaterials?.length ? data.proofMaterials : [], ? data.proofMaterialUrl.split(",").filter(Boolean).map((url, i) => ({
}); uid: `-${i}`,
} name: url.split("/").pop() || `附件${i + 1}`,
} status: "done",
url,
}))
: [];
setFields.proofMaterialUrl = fileList;
setUploadFileList(fileList);
form.setFieldsValue(setFields);
}).finally(() => { }).finally(() => {
if (!cancelled) { if (!cancelled) setDetailLoading(false);
setDetailLoading(false);
}
}); });
return () => { return () => { cancelled = true; };
cancelled = true;
};
}, [open, currentId]); }, [open, currentId]);
const handleCancel = () => { const handleCancel = () => {
form.resetFields(); form.resetFields();
setDeptPositionOptions([]); setDeptPositionOptions([]);
setUploadFileList([]);
onCancel(); onCancel();
}; };
@ -322,11 +329,11 @@ function StaffFormModal({
if ("idCardNo" in changed && changed.idCardNo) { if ("idCardNo" in changed && changed.idCardNo) {
const birth = getBirthDateFromIdCard(changed.idCardNo); const birth = getBirthDateFromIdCard(changed.idCardNo);
if (birth) { if (birth) {
form.setFieldValue("birthDate", toDayjs(birth)); form.setFieldValue("birthDate", dayjs(birth));
} }
} }
if ("deptId" in changed) { if ("deptId" in changed) {
form.setFieldValue("positionId", undefined); form.setFieldValue("postId", undefined);
loadPositionsByDept(changed.deptId); loadPositionsByDept(changed.deptId);
} }
}; };
@ -334,21 +341,37 @@ function StaffFormModal({
const handleSubmit = async (values) => { const handleSubmit = async (values) => {
try { try {
setSubmitting(true); setSubmitting(true);
const request = currentId ? requestEdit : requestAdd; const payload = {
if (currentId) { ...values,
values.id = currentId; genderName: values.genderCode === 1 ? "男" : values.genderCode === 2 ? "女" : undefined,
personTypeCode: values.personTypeName,
personTypeName: values.personTypeName,
professionalLevelCode: values.professionalLevelName,
professionalLevelName: values.professionalLevelName,
educationTypeCode: values.educationTypeName,
educationTypeName: values.educationTypeName,
educationLevelCode: values.educationLevelName,
educationLevelName: values.educationLevelName,
proofMaterialUrl: Array.isArray(values.proofMaterialUrl)
? values.proofMaterialUrl.map((f) => f.url || f.response?.url).filter(Boolean).join(",")
: "",
};
if (payload.birthDate && typeof payload.birthDate !== "string") {
payload.birthDate = payload.birthDate.format("YYYY-MM-DD");
} }
const res = await request(values); if (currentId) payload.id = currentId;
delete payload.proofMaterials;
const request = currentId ? staffInfoEdit : staffInfoAdd;
const res = await request(payload);
if (res?.success !== false) { if (res?.success !== false) {
message.success(currentId ? "编辑成功" : "添加成功"); message.success(currentId ? "编辑成功" : "添加成功");
handleCancel(); handleCancel();
onSuccess(); onSuccess();
} } else {
else {
message.error(res?.message || "操作失败"); message.error(res?.message || "操作失败");
} }
} } finally {
finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
@ -372,12 +395,12 @@ function StaffFormModal({
> >
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Form.Item name="staffName" label="姓名" rules={[{ required: true, message: "请输入姓名" }]}> <Form.Item name="userName" label="姓名" rules={[{ required: true, message: "请输入姓名" }]}>
<Input placeholder="请输入姓名" /> <Input placeholder="请输入姓名" />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="gender" label="性别" rules={[{ required: true, message: "请选择性别" }]}> <Form.Item name="genderCode" label="性别" rules={[{ required: true, message: "请选择性别" }]}>
<Select <Select
placeholder="请选择性别" placeholder="请选择性别"
options={[{ label: "男", value: 1 }, { label: "女", value: 2 }]} options={[{ label: "男", value: 1 }, { label: "女", value: 2 }]}
@ -400,7 +423,7 @@ function StaffFormModal({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="positionId" label="岗位" rules={[{ required: true, message: "请选择岗位" }]}> <Form.Item name="postId" label="岗位" rules={[{ required: true, message: "请选择岗位" }]}>
<Select <Select
placeholder="请选择岗位" placeholder="请选择岗位"
options={deptPositionOptions} options={deptPositionOptions}
@ -418,7 +441,7 @@ function StaffFormModal({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="personType" label="人员类型" initialValue="基础人员"> <Form.Item name="personTypeName" label="人员类型">
<Select placeholder="请选择人员类型" options={PERSON_TYPE_OPTIONS} /> <Select placeholder="请选择人员类型" options={PERSON_TYPE_OPTIONS} />
</Form.Item> </Form.Item>
</Col> </Col>
@ -428,7 +451,7 @@ function StaffFormModal({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="professionalLevel" label="职业等级"> <Form.Item name="professionalLevelName" label="职业等级">
<Select placeholder="请选择职业等级" allowClear options={PROFESSIONAL_LEVEL_OPTIONS} /> <Select placeholder="请选择职业等级" allowClear options={PROFESSIONAL_LEVEL_OPTIONS} />
</Form.Item> </Form.Item>
</Col> </Col>
@ -438,12 +461,12 @@ function StaffFormModal({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="educationType" label="学历类型"> <Form.Item name="educationTypeName" label="学历类型">
<Select placeholder="请选择学历类型" allowClear options={EDUCATION_TYPE_OPTIONS} /> <Select placeholder="请选择学历类型" allowClear options={EDUCATION_TYPE_OPTIONS} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="educationLevel" label="学历层次"> <Form.Item name="educationLevelName" label="学历层次">
<Select placeholder="请选择学历层次" allowClear options={EDUCATION_LEVEL_OPTIONS} /> <Select placeholder="请选择学历层次" allowClear options={EDUCATION_LEVEL_OPTIONS} />
</Form.Item> </Form.Item>
</Col> </Col>
@ -473,12 +496,24 @@ function StaffFormModal({
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Form.Item name="proofMaterials" label="申报专业能力证明材料"> <Form.Item name="proofMaterialUrl" label="申报专业能力证明材料" valuePropName="fileList" getValueFromEvent={(e) => {
<Upload maxCount={5} /> if (Array.isArray(e)) return e;
return e?.fileList;
}}>
<Upload
action="/api/upload"
maxCount={5}
fileList={uploadFileList}
onChange={(info) => {
setUploadFileList(info.fileList);
}}
>
<Button icon={<UploadOutlined />}>上传文件</Button>
</Upload>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item name="homeAddress" label="现住地址"> <Form.Item name="currentAddress" label="现住地址">
<Input placeholder="请输入现住地址" /> <Input placeholder="请输入现住地址" />
</Form.Item> </Form.Item>
</Col> </Col>
@ -497,15 +532,10 @@ function StaffFormModal({
<Input placeholder="请输入专业" /> <Input placeholder="请输入专业" />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}>
<Form.Item name="joinWorkDate" label="参加工作日期">
<DatePicker placeholder="请选择参加工作日期" />
</Form.Item>
</Col>
</Row> </Row>
</Form> </Form>
</Modal> </Modal>
); );
} }
export default Connect([NS_STAFF_INFO, NS_ORG_DEPARTMENT, NS_ORG_POSITION], true)(PersonnelInfoPage); export default Connect([NS_STAFF_INFO], true)(AntdTableFuncControl(PersonnelInfoPage));

View File

@ -1,10 +1,9 @@
import { Button, Descriptions, Modal, Space, Table } from "antd"; import { Button, Descriptions, Modal, Table } from "antd";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import FilePreviewModal, { FilePreviewContent } from "~/components/FilePreviewModal"; import FilePreviewModal, { FilePreviewContent } from "~/components/FilePreviewModal";
import { resolvePreviewSrc } from "~/components/CertPreviewImg"; import { resolvePreviewSrc } from "~/components/CertPreviewImg";
import { fetchStaffCertDetail, fetchStaffCertListByPersonnelId } from "~/api/staffCertificate"; import { fetchStaffCertDetail, fetchStaffCertListByPersonnelId } from "~/api/staffCertificate";
import { fetchOrgPersonnelDetail } from "~/api/qualFiling/personnelHelper"; import { fetchOrgPersonnelDetail } from "~/api/qualFiling/personnelHelper";
import { safeRequest } from "~/utils";
const GENDER_MAP = { 1: "男", 2: "女" }; const GENDER_MAP = { 1: "男", 2: "女" };
const REGISTER_ENGINEER_MAP = { 1: "是", 2: "否" }; const REGISTER_ENGINEER_MAP = { 1: "是", 2: "否" };
@ -39,9 +38,13 @@ export default function StaffViewModal({
} }
let cancelled = false; let cancelled = false;
setDetailLoading(true); setDetailLoading(true);
const loadCertList = requestCertListRef.current; const loadCertList = requestCertListRef.current;
Promise.all([ Promise.all([
safeRequest(requestDetailsRef.current, { id: currentId }), Promise.resolve(requestDetailsRef.current({ id: currentId })).catch((err) => {
console.warn("[StaffViewModal] load detail failed:", err);
return { data: {} };
}),
typeof loadCertList === "function" typeof loadCertList === "function"
? Promise.resolve(loadCertList(currentId)).catch((err) => { ? Promise.resolve(loadCertList(currentId)).catch((err) => {
console.warn("[StaffViewModal] load certificates failed:", err); console.warn("[StaffViewModal] load certificates failed:", err);
@ -72,10 +75,12 @@ export default function StaffViewModal({
} }
setCertPreviewLoading(true); setCertPreviewLoading(true);
try { try {
const res = await safeRequest(requestCertDetailsRef.current, { id: record.id }); const res = await requestCertDetailsRef.current({ id: record.id });
setCertPreviewInfo(res?.data || record); setCertPreviewInfo(res?.data || record);
} } catch (err) {
finally { console.warn("[StaffViewModal] load cert detail failed:", err);
setCertPreviewInfo(record);
} finally {
setCertPreviewLoading(false); setCertPreviewLoading(false);
} }
}; };
@ -133,11 +138,11 @@ export default function StaffViewModal({
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="申报专业能力证明材料" span={2}> <Descriptions.Item label="申报专业能力证明材料" span={2}>
{proofFiles.length ? ( {proofFiles.length ? (
<Space direction="vertical" size={4}> <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
{proofFiles.map((file, index) => { {proofFiles.map((file, index) => {
const fileName = file.name || file.fileName || `专业能力证明${index + 1}.pdf`; const fileName = file.name || file.fileName || `专业能力证明${index + 1}.pdf`;
return ( return (
<Space key={`${fileName}-${index}`}> <div key={`${fileName}-${index}`} style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Button <Button
type="link" type="link"
size="small" size="small"
@ -147,10 +152,10 @@ export default function StaffViewModal({
查看附件 查看附件
</Button> </Button>
<span>{fileName}</span> <span>{fileName}</span>
</Space> </div>
); );
})} })}
</Space> </div>
) : "-"} ) : "-"}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
@ -226,4 +231,4 @@ export default function StaffViewModal({
</Modal> </Modal>
</> </>
); );
} }

View File

@ -1,20 +1,35 @@
import { Connect } from "@cqsjjb/jjb-dva-runtime"; import { Connect } from "@cqsjjb/jjb-dva-runtime";
import { Button, DatePicker, Descriptions, Form, Input, message, Modal, Select } from "antd"; import {
Button,
DatePicker,
Descriptions,
Form,
Input,
message,
Modal,
Select,
Table,
Tag,
Upload,
} from "antd";
import TableAction from "@cqsjjb/jjb-react-admin-component/TableAction";
import { UploadOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AddIcon from "zy-react-library/components/Icon/AddIcon";
import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout"; import PageLayout from "@cqsjjb/jjb-react-admin-component/PageLayout";
import Search from "zy-react-library/components/Search"; import SearchForm from "@cqsjjb/jjb-react-admin-component/SearchForm";
import Table from "zy-react-library/components/Table"; import ControlWrapper from "@cqsjjb/jjb-react-admin-component/ControlWrapper";
import Upload from "zy-react-library/components/Upload"; import { AntdTableFuncControl } from "@cqsjjb/jjb-common-decorator/antd";
import useTable from "zy-react-library/hooks/useTable"; import { tools } from "@cqsjjb/jjb-common-lib";
import { asId } from "~/api/enterpriseInfo/idUtil"; import {
import { NS_STAFF_INFO, NS_STAFF_RESIGNATION_APPLY } from "~/enumerate/namespace"; NS_STAFF_INFO,
import { safeListRequest, safeRequest } from "~/utils"; NS_STAFF_RESIGNATION_APPLY,
import { formSelectField, getResignAuditStatusLabel } from "~/utils/enterpriseForm"; } from "~/enumerate/namespace";
import { DEFAULT_UPLOAD_FILE_URL, mockUploadFileList, resolveUploadFileId } from "~/utils/mockUpload"; import {
RESIGN_AUDIT_STATUS_LABEL,
AUDIT_STATUS_COLOR,
} from "~/utils/enterpriseForm";
const { router } = tools;
const RESIGN_AUDIT_STATUS = { 0: "未审核", 1: "已审核", 2: "已退回" };
function ResignationApplyPage(props) { function ResignationApplyPage(props) {
const [addModalOpen, setAddModalOpen] = useState(false); const [addModalOpen, setAddModalOpen] = useState(false);
@ -22,27 +37,39 @@ function ResignationApplyPage(props) {
const [currentId, setCurrentId] = useState(""); const [currentId, setCurrentId] = useState("");
const [staffOptions, setStaffOptions] = useState([]); const [staffOptions, setStaffOptions] = useState([]);
const [searchForm] = Form.useForm(); const [searchForm] = Form.useForm();
const { staffResignationApply } = props;
const {
staffResignationApplyList: dataSource,
staffResignationApplyTotal: total,
staffResignationApplyLoading: loading,
} = staffResignationApply || {};
const { tableProps, getData } = useTable(safeListRequest(props.staffResignationApplyList), { form: searchForm }); const handleSearch = () => {
props.staffResignationApplyList({ ...router.query });
};
useEffect(() => { useEffect(() => {
getData(); searchForm.setFieldsValue(router.query);
handleSearch();
}, []); }, []);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
props.staffInfoList?.({ pageIndex: 1, pageSize: 500 }).then((res) => { props
if (!cancelled) { .staffInfoList?.({ current: 1, size: 500 })
setStaffOptions((res?.data || []).map((s) => ({ .then((res) => {
label: `${s.staffName}${s.account}`, if (!cancelled) {
value: asId(s.id), setStaffOptions(
staffName: s.staffName, (res?.data || []).map((s) => ({
account: s.account, label: `${s.userName ?? s.staffName}${s.account}`,
}))); value: s.id,
} staffName: s.userName ?? s.staffName,
}).catch((err) => { account: s.account,
console.warn("[ResignationApply] load staff options failed:", err); })),
}); );
}
})
.catch(() => {});
return () => { return () => {
cancelled = true; cancelled = true;
}; };
@ -58,82 +85,126 @@ function ResignationApplyPage(props) {
</div> </div>
</div> </div>
} }
extra={
<Button
type="primary"
style={{ marginTop: 16 }}
onClick={() => {
setCurrentId("");
setAddModalOpen(true);
}}
>
新增离职申请
</Button>
}
> >
<Search <SearchForm
style={{ marginBottom: 24 }}
form={searchForm} form={searchForm}
options={[ loading={loading}
{ name: "likeStaffName", label: "姓名", placeholder: "请输入姓名", colProps: { xs: 24, sm: 12, md: 8, lg: 6 } }, formLine={[
formSelectField( <Form.Item key="applicantName" name="applicantName">
"auditStatus", <ControlWrapper.Input
"离职申请审核状态", label="姓名"
Object.entries(RESIGN_AUDIT_STATUS).map(([value, label]) => ({ label, value: Number(value) })), placeholder="请输入"
{ colProps: { xs: 24, sm: 12, md: 8, lg: 8 }, placeholder: "请选择审核状态" }, allowClear
), />
</Form.Item>,
<Form.Item key="auditStatusCode" name="auditStatusCode">
<ControlWrapper.Select
label="离职申请审核状态"
placeholder="请选择"
allowClear
style={{ width: "100%" }}
>
{Object.entries(RESIGN_AUDIT_STATUS_LABEL).map(
([value, label]) => (
<Select.Option key={value} value={Number(value)}>
{label}
</Select.Option>
),
)}
</ControlWrapper.Select>
</Form.Item>,
]} ]}
onFinish={getData} onReset={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
onFinish={() => {
router.query = { ...router.query, current: 1, size: 10 };
handleSearch();
}}
/> />
<Table <Table
toolBarRender={() => ( rowKey="id"
<Button
type="primary"
icon={<AddIcon />}
onClick={() => {
setCurrentId("");
setAddModalOpen(true);
}}
>
新增离职申请
</Button>
)}
columns={[ columns={[
{ title: "账户", dataIndex: "account" }, { title: "账户", dataIndex: "account", width: 140 },
{ title: "姓名", dataIndex: "applicantName" }, { title: "姓名", dataIndex: "applicantName", width: 100 },
{ title: "部门", dataIndex: "deptName" }, { title: "部门", dataIndex: "deptName", width: 160 },
{ title: "申请时间", dataIndex: "applyTime" }, { title: "申请时间", dataIndex: "applyTime", width: 160 },
{ {
title: "离职申请审核状态", title: "离职申请审核状态",
dataIndex: "auditStatus", dataIndex: "auditStatusCode",
render: (_, record) => getResignAuditStatusLabel(record), width: 140,
render: (_, record) => {
const code = record.auditStatusCode ?? record.auditStatus;
const label = RESIGN_AUDIT_STATUS_LABEL[code] ?? "-";
return <Tag color={AUDIT_STATUS_COLOR[code]}>{label}</Tag>;
},
}, },
{ {
title: "操作", title: "操作",
width: 100, width: 80,
render: (_, record) => ( render: (_, record) => (
<Button <TableAction>
type="link" <Button
onClick={() => { type="link"
setCurrentId(asId(record.id)); size="small"
setViewModalOpen(true); onClick={() => {
}} setCurrentId(record.id);
> setViewModalOpen(true);
查看 }}
</Button> >
查看
</Button>
</TableAction>
), ),
}, },
]} ]}
{...tableProps} dataSource={dataSource}
scroll={{ y: props.scrollY }}
loading={loading}
pagination={{
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (t) => `${t}`,
current: router.query.current || 1,
pageSize: router.query.size || 10,
onChange: (page, pageSize) => {
router.query = { ...router.query, current: page, size: pageSize };
handleSearch();
},
}}
/> />
{addModalOpen && ( <AddModal
<AddModal open={addModalOpen}
open={addModalOpen} staffOptions={staffOptions}
staffOptions={staffOptions} requestAdd={props.staffResignationApplyAdd}
requestAdd={props.staffResignationApplyAdd} onCancel={() => setAddModalOpen(false)}
onCancel={() => setAddModalOpen(false)} onSuccess={handleSearch}
onSuccess={getData} />
/> <ViewModal
)} open={viewModalOpen}
{viewModalOpen && ( currentId={currentId}
<ViewModal requestDetail={props.staffResignationApplyInfo}
open={viewModalOpen} onCancel={() => {
currentId={currentId} setViewModalOpen(false);
requestDetails={props.staffResignationApplyInfo} setCurrentId("");
onCancel={() => { }}
setViewModalOpen(false); />
setCurrentId("");
}}
/>
)}
</PageLayout> </PageLayout>
); );
} }
@ -141,36 +212,33 @@ function ResignationApplyPage(props) {
function AddModal({ open, staffOptions, requestAdd, onCancel, onSuccess }) { function AddModal({ open, staffOptions, requestAdd, onCancel, onSuccess }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [uploading, setUploading] = useState(false);
const handleCancel = () => { const handleCancel = () => {
form.resetFields(); form.resetFields();
onCancel(); onCancel();
}; };
const onApplicantChange = (personnelId) => { const handleSubmit = async () => {
const staff = staffOptions.find((s) => String(s.value) === String(personnelId));
form.setFieldsValue({
applicantName: staff?.staffName || "",
});
};
const handleSubmit = async (values) => {
try { try {
const values = await form.validateFields();
setSubmitting(true); setSubmitting(true);
const staff = staffOptions.find((s) => String(s.value) === String(values.personnelId)); values.applyTime =
values.applicantName = staff?.staffName || values.applicantName; values.applyTime?.format?.("YYYY-MM-DD HH:mm:ss") || values.applyTime;
values.attachmentId = resolveUploadFileId(values.resignReport); values.expectedResignDate =
values.expectedResignDate?.format?.("YYYY-MM-DD") ||
values.expectedResignDate;
const res = await requestAdd(values); const res = await requestAdd(values);
if (res?.success !== false) { if (res?.success !== false) {
message.success("提交成功"); message.success("提交成功");
handleCancel(); handleCancel();
onSuccess(); onSuccess();
} } else {
else {
message.error(res?.message || "提交失败"); message.error(res?.message || "提交失败");
} }
} } catch {
finally { // validation error
} finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
@ -183,9 +251,9 @@ function AddModal({ open, staffOptions, requestAdd, onCancel, onSuccess }) {
width={720} width={720}
confirmLoading={submitting} confirmLoading={submitting}
onCancel={handleCancel} onCancel={handleCancel}
onOk={form.submit} onOk={handleSubmit}
> >
<Form form={form} layout="vertical" onFinish={handleSubmit}> <Form form={form} layout="vertical">
<Form.Item <Form.Item
name="personnelId" name="personnelId"
label="申请人" label="申请人"
@ -196,12 +264,8 @@ function AddModal({ open, staffOptions, requestAdd, onCancel, onSuccess }) {
placeholder="请选择申请人" placeholder="请选择申请人"
options={staffOptions} options={staffOptions}
optionFilterProp="label" optionFilterProp="label"
onChange={onApplicantChange}
/> />
</Form.Item> </Form.Item>
<Form.Item name="applicantName" hidden>
<Input />
</Form.Item>
<Form.Item <Form.Item
name="applyTime" name="applyTime"
label="申请时间" label="申请时间"
@ -210,14 +274,14 @@ function AddModal({ open, staffOptions, requestAdd, onCancel, onSuccess }) {
<DatePicker showTime style={{ width: "100%" }} /> <DatePicker showTime style={{ width: "100%" }} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="expectedLeaveDate" name="expectedResignDate"
label="预计离职日期" label="预计离职日期"
rules={[{ required: true, message: "请选择预计离职日期" }]} rules={[{ required: true, message: "请选择预计离职日期" }]}
> >
<DatePicker style={{ width: "100%" }} /> <DatePicker style={{ width: "100%" }} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="leaveReason" name="resignReason"
label="离职原因" label="离职原因"
rules={[{ required: true, message: "请输入离职原因" }]} rules={[{ required: true, message: "请输入离职原因" }]}
> >
@ -227,25 +291,40 @@ function AddModal({ open, staffOptions, requestAdd, onCancel, onSuccess }) {
<Input.TextArea rows={2} placeholder="请输入备注" /> <Input.TextArea rows={2} placeholder="请输入备注" />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="resignReport" name="reportFileUrl"
label="离职通知报告" label="离职通知报告"
rules={[{ required: true, message: "请上传离职通知报告" }]} rules={[{ required: true, message: "请上传离职通知报告" }]}
valuePropName="fileList"
getValueFromEvent={(e) => e?.fileList}
initialValue={mockUploadFileList("离职通知报告.pdf")}
> >
<Upload <Upload
accept=".pdf" action={`${window.process.env.app.API_HOST}/safetyEval/file/upload`}
showUploadList={false}
maxCount={1} maxCount={1}
tip="联调阶段默认通过,使用固定文件地址" accept=".pdf"
/> onChange={(info) => {
if (info.file.status === "uploading") {
setUploading(true);
} else {
setUploading(false);
if (info.file.status === "done") {
const data = info.file.response?.data;
if (data) {
form.setFieldsValue({ reportFileUrl: data.url || data });
}
}
}
}}
>
<Button loading={uploading} icon={<UploadOutlined />}>
上传文件
</Button>
</Upload>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
} }
function ViewModal({ open, currentId, requestDetails, onCancel }) { function ViewModal({ open, currentId, requestDetail, onCancel }) {
const [info, setInfo] = useState({}); const [info, setInfo] = useState({});
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
@ -256,21 +335,13 @@ function ViewModal({ open, currentId, requestDetails, onCancel }) {
} }
let cancelled = false; let cancelled = false;
setDetailLoading(true); setDetailLoading(true);
safeRequest(requestDetails, { id: currentId }).then((res) => { requestDetail({ id: currentId })
if (!cancelled) { .then((res) => {
const data = res?.data || {}; if (!cancelled) setInfo(res?.data || {});
setInfo({ })
...data, .finally(() => {
resignReport: data.attachmentId if (!cancelled) setDetailLoading(false);
? [{ name: "离职通知报告.pdf", fileName: "离职通知报告.pdf", url: data.attachmentId || DEFAULT_UPLOAD_FILE_URL }] });
: [],
});
}
}).finally(() => {
if (!cancelled) {
setDetailLoading(false);
}
});
return () => { return () => {
cancelled = true; cancelled = true;
}; };
@ -288,17 +359,26 @@ function ViewModal({ open, currentId, requestDetails, onCancel }) {
onCancel={onCancel} onCancel={onCancel}
> >
<Descriptions bordered column={1} labelStyle={{ width: 120 }}> <Descriptions bordered column={1} labelStyle={{ width: 120 }}>
<Descriptions.Item label="申请人">{info.applicantName}</Descriptions.Item> <Descriptions.Item label="申请人">
{info.applicantName}
</Descriptions.Item>
<Descriptions.Item label="申请时间">{info.applyTime}</Descriptions.Item> <Descriptions.Item label="申请时间">{info.applyTime}</Descriptions.Item>
<Descriptions.Item label="预计离职日期">{info.expectedLeaveDate}</Descriptions.Item> <Descriptions.Item label="预计离职日期">
<Descriptions.Item label="离职原因">{info.leaveReason}</Descriptions.Item> {info.expectedResignDate}
</Descriptions.Item>
<Descriptions.Item label="离职原因">
{info.resignReason}
</Descriptions.Item>
<Descriptions.Item label="备注">{info.remark}</Descriptions.Item> <Descriptions.Item label="备注">{info.remark}</Descriptions.Item>
<Descriptions.Item label="离职通知报告"> <Descriptions.Item label="离职通知报告">
{info.resignReport?.[0]?.fileName || info.resignReport?.[0]?.name || "-"} {info.reportFileUrl || "-"}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</Modal> </Modal>
); );
} }
export default Connect([NS_STAFF_RESIGNATION_APPLY, NS_STAFF_INFO], true)(ResignationApplyPage); export default Connect(
[NS_STAFF_RESIGNATION_APPLY, NS_STAFF_INFO],
true,
)(AntdTableFuncControl(ResignationApplyPage));