UserApply.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. import React, { useState, useEffect } from 'react';
  2. import {
  3. Card,
  4. Table,
  5. Button,
  6. Select,
  7. Modal,
  8. message,
  9. Space,
  10. Row,
  11. Col,
  12. DatePicker,
  13. Form,
  14. Tag,
  15. Input,
  16. Descriptions
  17. } from 'antd';
  18. import {
  19. PlusOutlined,
  20. SearchOutlined,
  21. ReloadOutlined,
  22. EyeOutlined,
  23. CheckOutlined,
  24. CloseOutlined
  25. } from '@ant-design/icons';
  26. import type { ColumnsType } from 'antd/es/table';
  27. import dayjs from 'dayjs';
  28. import {
  29. queryApplyLogs,
  30. getUserAuditLogDetail,
  31. submitUserApply,
  32. auditUserApply,
  33. type UserAuditLogItem,
  34. type UserApplyParams,
  35. type AuditParams
  36. } from '../../api/system';
  37. import { queryHospitals, type HospitalItem } from '../../api/hospital';
  38. import CustomPagination from '../../components/CustomPagination';
  39. const { Option } = Select;
  40. const { RangePicker } = DatePicker;
  41. const { TextArea } = Input;
  42. // 分页参数
  43. interface PaginationParams {
  44. current: number;
  45. pageSize: number;
  46. total: number;
  47. }
  48. // 审核状态选项
  49. const auditStatusOptions = [
  50. { value: 'R', label: '待审核', color: 'orange' },
  51. { value: 'Y', label: '已通过', color: 'green' },
  52. { value: 'N', label: '已驳回', color: 'red' }
  53. ];
  54. // 性别选项
  55. const sexOptions = [
  56. { value: '1', label: '男' },
  57. { value: '2', label: '女' },
  58. ];
  59. // 证件类型选项
  60. const credTypeOptions = [
  61. { value: '1', label: '身份证' },
  62. { value: '2', label: '护照' },
  63. { value: '3', label: '军官证' },
  64. { value: '4', label: '其他' },
  65. ];
  66. // 角色组选项
  67. const groupOptions = [
  68. { value: '1', label: '系统管理员' },
  69. { value: '2', label: '医院管理员' },
  70. { value: '3', label: '医生' },
  71. { value: '4', label: '护士' },
  72. ];
  73. const UserApply: React.FC = () => {
  74. const [applyForm] = Form.useForm();
  75. const [auditForm] = Form.useForm();
  76. const [data, setData] = useState<UserAuditLogItem[]>([]);
  77. const [loading, setLoading] = useState(false);
  78. const [pagination, setPagination] = useState<PaginationParams>({
  79. current: 1,
  80. pageSize: 10,
  81. total: 0
  82. });
  83. // 查询条件
  84. const [searchStatus, setSearchStatus] = useState('R'); // 默认查询待审核
  85. const [searchHosp, setSearchHosp] = useState('');
  86. const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null);
  87. // 医院列表
  88. const [hospitals, setHospitals] = useState<HospitalItem[]>([]);
  89. const [hospitalLoading, setHospitalLoading] = useState(false);
  90. // 弹窗状态
  91. const [detailVisible, setDetailVisible] = useState(false);
  92. const [detailRecord, setDetailRecord] = useState<UserAuditLogItem | null>(null);
  93. const [auditVisible, setAuditVisible] = useState(false);
  94. const [auditRecord, setAuditRecord] = useState<UserAuditLogItem | null>(null);
  95. const [applyVisible, setApplyVisible] = useState(false);
  96. // 加载医院列表
  97. const fetchHospitals = async () => {
  98. setHospitalLoading(true);
  99. try {
  100. const res = await queryHospitals({ active: 'Y' }, { pageSize: 1000, currentPage: 1 });
  101. if (String(res.errorCode) === '0' && res.result) {
  102. setHospitals(res.result.rows || []);
  103. }
  104. } catch (error) {
  105. console.error('加载医院列表失败:', error);
  106. } finally {
  107. setHospitalLoading(false);
  108. }
  109. };
  110. // 表格列定义
  111. const columns: ColumnsType<UserAuditLogItem> = [
  112. {
  113. title: '序号',
  114. key: 'index',
  115. width: 60,
  116. render: (_, __, index) => (pagination.current - 1) * pagination.pageSize + index + 1
  117. },
  118. {
  119. title: '申请编码',
  120. dataIndex: 'code',
  121. key: 'code',
  122. width: 120
  123. },
  124. {
  125. title: '姓名',
  126. dataIndex: 'descripts',
  127. key: 'descripts',
  128. width: 120
  129. },
  130. {
  131. title: '性别',
  132. dataIndex: 'sexDesc',
  133. key: 'sexDesc',
  134. width: 80
  135. },
  136. {
  137. title: '所属医院',
  138. dataIndex: 'hospDesc',
  139. key: 'hospDesc',
  140. width: 180
  141. },
  142. {
  143. title: '手机号',
  144. dataIndex: 'mobile',
  145. key: 'mobile',
  146. width: 120
  147. },
  148. {
  149. title: '证件号',
  150. dataIndex: 'credNo',
  151. key: 'credNo',
  152. width: 150,
  153. ellipsis: true
  154. },
  155. {
  156. title: '申请时间',
  157. dataIndex: 'applyDateTime',
  158. key: 'applyDateTime',
  159. width: 150
  160. },
  161. {
  162. title: '审核状态',
  163. dataIndex: 'auditStatus',
  164. key: 'auditStatus',
  165. width: 100,
  166. render: (status: string) => {
  167. const option = auditStatusOptions.find(o => o.value === status);
  168. return (
  169. <Tag color={option?.color || 'default'}>
  170. {option?.label || status}
  171. </Tag>
  172. );
  173. }
  174. },
  175. {
  176. title: '审核人',
  177. dataIndex: 'auditUserDesc',
  178. key: 'auditUserDesc',
  179. width: 100,
  180. render: (text: string) => text || '-'
  181. },
  182. {
  183. title: '操作',
  184. key: 'action',
  185. fixed: 'right',
  186. width: 180,
  187. render: (_, record) => (
  188. <Space size="small">
  189. <Button
  190. type="link"
  191. size="small"
  192. icon={<EyeOutlined />}
  193. onClick={() => handleView(record)}
  194. >
  195. 详情
  196. </Button>
  197. {record.auditStatus === 'R' && (
  198. <Button
  199. type="link"
  200. size="small"
  201. icon={<CheckOutlined />}
  202. onClick={() => handleAudit(record)}
  203. >
  204. 审核
  205. </Button>
  206. )}
  207. </Space>
  208. )
  209. }
  210. ];
  211. // 查询申请列表
  212. const fetchData = async (page = pagination.current, size = pagination.pageSize) => {
  213. setLoading(true);
  214. try {
  215. const res = await queryApplyLogs(
  216. {
  217. auditStatus: searchStatus || undefined,
  218. hospitalID: searchHosp || undefined,
  219. beginDate: dateRange?.[0] ? dateRange[0].format('YYYY-MM-DD') : undefined,
  220. endDate: dateRange?.[1] ? dateRange[1].format('YYYY-MM-DD') : undefined
  221. },
  222. { pageSize: size, currentPage: page }
  223. );
  224. if (String(res.errorCode) === '0' && res.result) {
  225. setData(res.result.rows || []);
  226. setPagination(prev => ({
  227. ...prev,
  228. total: res.result?.total || 0
  229. }));
  230. } else {
  231. message.error(res.errorMessage || '查询失败');
  232. }
  233. } catch (error) {
  234. console.error('查询申请记录失败:', error);
  235. message.error('查询申请记录失败');
  236. } finally {
  237. setLoading(false);
  238. }
  239. };
  240. // 初始化加载
  241. useEffect(() => {
  242. fetchData(1, pagination.pageSize);
  243. fetchHospitals();
  244. }, []);
  245. // 搜索
  246. const handleSearch = () => {
  247. setPagination(prev => ({ ...prev, current: 1 }));
  248. fetchData(1, pagination.pageSize);
  249. };
  250. // 重置
  251. const handleReset = () => {
  252. setSearchStatus('R');
  253. setSearchHosp('');
  254. setDateRange(null);
  255. setPagination(prev => ({ ...prev, current: 1 }));
  256. fetchData(1, pagination.pageSize);
  257. };
  258. // 查看详情
  259. const handleView = async (record: UserAuditLogItem) => {
  260. try {
  261. const res = await getUserAuditLogDetail({ userAuditLogID: record.id });
  262. if (String(res.errorCode) === '0' && res.result) {
  263. setDetailRecord(res.result);
  264. setDetailVisible(true);
  265. } else {
  266. message.error(res.errorMessage || '获取详情失败');
  267. }
  268. } catch (error) {
  269. console.error('获取详情失败:', error);
  270. message.error('获取详情失败');
  271. }
  272. };
  273. // 打开审核弹窗
  274. const handleAudit = (record: UserAuditLogItem) => {
  275. setAuditRecord(record);
  276. auditForm.resetFields();
  277. setAuditVisible(true);
  278. };
  279. // 提交审核
  280. const handleSubmitAudit = async (auditStatus: string) => {
  281. if (!auditRecord) return;
  282. try {
  283. const values = await auditForm.validateFields();
  284. const params: AuditParams = {
  285. userAuditLogID: auditRecord.id,
  286. auditStatus: auditStatus,
  287. auditRemarks: values.auditRemarks
  288. };
  289. const res = await auditUserApply(params);
  290. if (String(res.errorCode) === '0') {
  291. message.success(auditStatus === 'Y' ? '审核通过' : '已驳回');
  292. setAuditVisible(false);
  293. fetchData(pagination.current, pagination.pageSize);
  294. } else {
  295. message.error(res.errorMessage || '审核失败');
  296. }
  297. } catch (error) {
  298. console.error('审核失败:', error);
  299. message.error('审核失败');
  300. }
  301. };
  302. // 打开新增申请弹窗
  303. const handleAddApply = () => {
  304. applyForm.resetFields();
  305. setApplyVisible(true);
  306. setTimeout(() => {
  307. applyForm.setFieldsValue({
  308. sexID: '1',
  309. credTypeID: '1'
  310. });
  311. }, 0);
  312. };
  313. // 提交申请
  314. const handleSubmitApply = async () => {
  315. try {
  316. const values = await applyForm.validateFields();
  317. const params: UserApplyParams = {
  318. code: values.code,
  319. descripts: values.descripts,
  320. sexID: values.sexID,
  321. mobile: values.mobile,
  322. credTypeID: values.credTypeID,
  323. credNo: values.credNo,
  324. hospitalID: values.hospitalID,
  325. introduce: values.introduce,
  326. auditGroupID: values.auditGroupID,
  327. password: values.password
  328. };
  329. const res = await submitUserApply(params);
  330. if (String(res.errorCode) === '0') {
  331. message.success('申请提交成功');
  332. setApplyVisible(false);
  333. fetchData(pagination.current, pagination.pageSize);
  334. } else {
  335. message.error(res.errorMessage || '申请提交失败');
  336. }
  337. } catch (error) {
  338. console.error('提交申请失败:', error);
  339. message.error('提交申请失败');
  340. }
  341. };
  342. return (
  343. <div style={{ padding: 16 }}>
  344. {/* 查询条件 */}
  345. <Card size="small" style={{ marginBottom: 16 }}>
  346. <Row gutter={16} align="middle">
  347. <Col>
  348. <Select
  349. placeholder="审核状态"
  350. value={searchStatus || undefined}
  351. onChange={v => setSearchStatus(v || '')}
  352. style={{ width: 120 }}
  353. allowClear
  354. options={auditStatusOptions}
  355. />
  356. </Col>
  357. <Col>
  358. <Select
  359. placeholder="所属医院"
  360. value={searchHosp || undefined}
  361. onChange={v => setSearchHosp(v || '')}
  362. style={{ width: 180 }}
  363. allowClear
  364. loading={hospitalLoading}
  365. showSearch
  366. filterOption={(input, option) =>
  367. String(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
  368. }
  369. options={hospitals.map(h => ({
  370. value: String(h.hospitalID),
  371. label: h.descripts
  372. }))}
  373. />
  374. </Col>
  375. <Col>
  376. <RangePicker
  377. placeholder={['开始日期', '结束日期']}
  378. value={dateRange}
  379. onChange={dates => setDateRange(dates)}
  380. />
  381. </Col>
  382. <Col>
  383. <Space>
  384. <Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>查询</Button>
  385. <Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
  386. </Space>
  387. </Col>
  388. </Row>
  389. </Card>
  390. {/* 工具栏 + 表格 */}
  391. <Card size="small">
  392. <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
  393. <Button type="primary" icon={<PlusOutlined />} onClick={handleAddApply}>新增申请</Button>
  394. <span>共 {pagination.total} 条记录</span>
  395. </div>
  396. <Table
  397. columns={columns}
  398. dataSource={data}
  399. rowKey="id"
  400. loading={loading}
  401. scroll={{ x: 1400 }}
  402. size="small"
  403. pagination={false}
  404. />
  405. <div style={{ marginTop: 12, display: 'flex', justifyContent: 'flex-end' }}>
  406. <CustomPagination
  407. current={pagination.current}
  408. pageSize={pagination.pageSize}
  409. total={pagination.total}
  410. onChange={(page, size) => {
  411. setPagination(prev => ({ ...prev, current: page, pageSize: size }));
  412. fetchData(page, size);
  413. }}
  414. />
  415. </div>
  416. </Card>
  417. {/* 详情弹窗 */}
  418. <Modal
  419. title="申请详情"
  420. open={detailVisible}
  421. onCancel={() => setDetailVisible(false)}
  422. footer={[
  423. <Button key="close" onClick={() => setDetailVisible(false)}>
  424. 关闭
  425. </Button>
  426. ]}
  427. width={700}
  428. >
  429. {detailRecord && (
  430. <Descriptions bordered column={2} size="small">
  431. <Descriptions.Item label="申请编码">{detailRecord.code || '-'}</Descriptions.Item>
  432. <Descriptions.Item label="姓名">{detailRecord.descripts || '-'}</Descriptions.Item>
  433. <Descriptions.Item label="性别">{detailRecord.sexDesc || '-'}</Descriptions.Item>
  434. <Descriptions.Item label="手机号">{detailRecord.mobile || '-'}</Descriptions.Item>
  435. <Descriptions.Item label="所属医院">{detailRecord.hospDesc || '-'}</Descriptions.Item>
  436. <Descriptions.Item label="证件类型">{detailRecord.credTypeDesc || '-'}</Descriptions.Item>
  437. <Descriptions.Item label="证件号">{detailRecord.credNo || '-'}</Descriptions.Item>
  438. <Descriptions.Item label="申请时间">{detailRecord.applyDateTime || '-'}</Descriptions.Item>
  439. <Descriptions.Item label="审核状态">
  440. <Tag color={auditStatusOptions.find(o => o.value === detailRecord.auditStatus)?.color}>
  441. {auditStatusOptions.find(o => o.value === detailRecord.auditStatus)?.label}
  442. </Tag>
  443. </Descriptions.Item>
  444. <Descriptions.Item label="审核人">{detailRecord.auditUserDesc || '-'}</Descriptions.Item>
  445. <Descriptions.Item label="审核组">{detailRecord.auditGroupDesc || '-'}</Descriptions.Item>
  446. <Descriptions.Item label="审核备注">{detailRecord.auditRemarks || '-'}</Descriptions.Item>
  447. <Descriptions.Item label="简介" span={2}>{detailRecord.introduce || '-'}</Descriptions.Item>
  448. </Descriptions>
  449. )}
  450. </Modal>
  451. {/* 审核弹窗 */}
  452. <Modal
  453. title="审核用户申请"
  454. open={auditVisible}
  455. onCancel={() => setAuditVisible(false)}
  456. footer={[
  457. <Button key="reject" danger icon={<CloseOutlined />} onClick={() => handleSubmitAudit('N')}>
  458. 驳回
  459. </Button>,
  460. <Button key="approve" type="primary" icon={<CheckOutlined />} onClick={() => handleSubmitAudit('Y')}>
  461. 通过
  462. </Button>
  463. ]}
  464. width={600}
  465. >
  466. {auditRecord && (
  467. <>
  468. <Descriptions bordered column={2} size="small" style={{ marginBottom: 16 }}>
  469. <Descriptions.Item label="申请编码">{auditRecord.code || '-'}</Descriptions.Item>
  470. <Descriptions.Item label="姓名">{auditRecord.descripts || '-'}</Descriptions.Item>
  471. <Descriptions.Item label="性别">{auditRecord.sexDesc || '-'}</Descriptions.Item>
  472. <Descriptions.Item label="手机号">{auditRecord.mobile || '-'}</Descriptions.Item>
  473. <Descriptions.Item label="所属医院">{auditRecord.hospDesc || '-'}</Descriptions.Item>
  474. <Descriptions.Item label="证件号">{auditRecord.credNo || '-'}</Descriptions.Item>
  475. </Descriptions>
  476. <Form form={auditForm} layout="vertical">
  477. <Form.Item
  478. name="auditRemarks"
  479. label="审核备注"
  480. >
  481. <TextArea placeholder="请输入审核备注" maxLength={200} rows={3} />
  482. </Form.Item>
  483. </Form>
  484. </>
  485. )}
  486. </Modal>
  487. {/* 新增申请弹窗 */}
  488. <Modal
  489. title="新增用户申请"
  490. open={applyVisible}
  491. onOk={handleSubmitApply}
  492. onCancel={() => setApplyVisible(false)}
  493. okText="提交"
  494. cancelText="取消"
  495. width={600}
  496. >
  497. <Form form={applyForm} layout="vertical">
  498. <Row gutter={16}>
  499. <Col span={12}>
  500. <Form.Item
  501. name="code"
  502. label="用户编码"
  503. >
  504. <Input placeholder="请输入用户编码" maxLength={20} />
  505. </Form.Item>
  506. </Col>
  507. <Col span={12}>
  508. <Form.Item
  509. name="descripts"
  510. label="姓名"
  511. rules={[{ required: true, message: '请输入姓名' }]}
  512. >
  513. <Input placeholder="请输入姓名" maxLength={50} />
  514. </Form.Item>
  515. </Col>
  516. </Row>
  517. <Row gutter={16}>
  518. <Col span={12}>
  519. <Form.Item
  520. name="sexID"
  521. label="性别"
  522. rules={[{ required: true, message: '请选择性别' }]}
  523. >
  524. <Select placeholder="请选择性别" options={sexOptions} />
  525. </Form.Item>
  526. </Col>
  527. <Col span={12}>
  528. <Form.Item
  529. name="mobile"
  530. label="手机号"
  531. rules={[
  532. { required: true, message: '请输入手机号' },
  533. { pattern: /^1\d{10}$/, message: '请输入正确的11位手机号' }
  534. ]}
  535. >
  536. <Input placeholder="请输入手机号" maxLength={11} />
  537. </Form.Item>
  538. </Col>
  539. </Row>
  540. <Row gutter={16}>
  541. <Col span={12}>
  542. <Form.Item
  543. name="credTypeID"
  544. label="证件类型"
  545. >
  546. <Select placeholder="请选择证件类型" options={credTypeOptions} allowClear />
  547. </Form.Item>
  548. </Col>
  549. <Col span={12}>
  550. <Form.Item
  551. name="credNo"
  552. label="证件号"
  553. >
  554. <Input placeholder="请输入证件号" maxLength={30} />
  555. </Form.Item>
  556. </Col>
  557. </Row>
  558. <Row gutter={16}>
  559. <Col span={12}>
  560. <Form.Item
  561. name="hospitalID"
  562. label="所属医院"
  563. rules={[{ required: true, message: '请选择所属医院' }]}
  564. >
  565. <Select
  566. placeholder="请选择所属医院"
  567. loading={hospitalLoading}
  568. showSearch
  569. filterOption={(input, option) =>
  570. String(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
  571. }
  572. options={hospitals.map(h => ({
  573. value: String(h.hospitalID),
  574. label: h.descripts
  575. }))}
  576. />
  577. </Form.Item>
  578. </Col>
  579. <Col span={12}>
  580. <Form.Item
  581. name="auditGroupID"
  582. label="审核组"
  583. >
  584. <Select placeholder="请选择审核组" options={groupOptions} allowClear />
  585. </Form.Item>
  586. </Col>
  587. </Row>
  588. <Form.Item
  589. name="password"
  590. label="密码"
  591. >
  592. <Input.Password placeholder="请输入密码" maxLength={20} />
  593. </Form.Item>
  594. <Form.Item
  595. name="introduce"
  596. label="简介"
  597. >
  598. <TextArea placeholder="请输入简介" maxLength={200} rows={3} />
  599. </Form.Item>
  600. </Form>
  601. </Modal>
  602. </div>
  603. );
  604. };
  605. export default UserApply;