index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. import { TreeSelect as ATreeSelect } from 'antd';
  2. import { useIntl } from 'umi';
  3. import type { Field } from '@formily/core';
  4. import { createForm, registerValidateRules } from '@formily/core';
  5. import { createSchemaField } from '@formily/react';
  6. import React, { useEffect, useState } from 'react';
  7. import * as ICONS from '@ant-design/icons';
  8. import { PlusOutlined } from '@ant-design/icons';
  9. import {
  10. Form,
  11. FormGrid,
  12. FormItem,
  13. Input,
  14. Password,
  15. Select,
  16. Switch,
  17. TreeSelect,
  18. } from '@formily/antd';
  19. import type { ISchema } from '@formily/json-schema';
  20. import { action } from '@formily/reactive';
  21. import type { Response } from '@/utils/typings';
  22. import { service } from '@/pages/system/User';
  23. import { Modal, PermissionButton } from '@/components';
  24. import usePermissions from '@/hooks/permission';
  25. import { onlyMessage } from '@/utils/util';
  26. import { getMenuPathByCode } from '@/utils/menu';
  27. interface Props {
  28. model: 'add' | 'edit' | 'query';
  29. data: Partial<UserItem>;
  30. close: () => void;
  31. }
  32. const Save = (props: Props) => {
  33. const { model } = props;
  34. const intl = useIntl();
  35. const { permission: deptPermission } = usePermissions('system/Department');
  36. const { permission: rolePermission } = usePermissions('system/Role');
  37. const [data, setData] = useState<Partial<UserItem>>(props.data);
  38. const getRole = () => service.queryRoleList();
  39. const getOrg = () => service.queryOrgList({ paging: false });
  40. const departmentPath = getMenuPathByCode('system/Department');
  41. const rolePath = getMenuPathByCode('system/Role');
  42. const useAsyncDataSource = (api: any) => (field: Field) => {
  43. field.loading = true;
  44. api(field).then(
  45. action.bound!((resp: Response<any>) => {
  46. field.dataSource = resp.result?.map((item: Record<string, unknown>) => ({
  47. ...item,
  48. label: item.name,
  49. value: item.id,
  50. }));
  51. field.loading = false;
  52. }),
  53. );
  54. };
  55. registerValidateRules({
  56. checkStrength(value: string) {
  57. if (!value || value.length < 8 || value.length > 64) return true;
  58. if (/^[0-9]+$/.test(value) || /^[a-zA-Z]+$/.test(value) || /^[~!@#$%^&*]+$/.test(value)) {
  59. return {
  60. type: 'error',
  61. message: '密码强度不够',
  62. };
  63. }
  64. return true;
  65. },
  66. });
  67. const getUser = async () => {
  68. if (props.data.id) {
  69. const response: Response<UserItem> = await service.queryDetail(props.data?.id);
  70. if (response.status === 200) {
  71. const temp = response.result as UserItem;
  72. temp.orgIdList = (temp.orgList as { id: string; name: string }[]).map((item) => item.id);
  73. temp.roleIdList = (temp.roleList as { id: string; name: string }[]).map((item) => item.id);
  74. setData(temp);
  75. }
  76. }
  77. };
  78. useEffect(() => {
  79. if (model === 'edit') {
  80. getUser();
  81. } else {
  82. setData({});
  83. }
  84. }, [props.data, props.model]);
  85. const form = createForm({
  86. validateFirst: true,
  87. initialValues: data,
  88. });
  89. const SchemaField = createSchemaField({
  90. components: {
  91. FormItem,
  92. Input,
  93. Password,
  94. Switch,
  95. Select,
  96. TreeSelect,
  97. FormGrid,
  98. },
  99. scope: {
  100. icon(name: any) {
  101. return React.createElement(ICONS[name]);
  102. },
  103. },
  104. });
  105. const schema: ISchema = {
  106. type: 'object',
  107. properties: {
  108. layout: {
  109. type: 'void',
  110. 'x-decorator': 'FormGrid',
  111. 'x-decorator-props': {
  112. maxColumns: 2,
  113. minColumns: 2,
  114. columnGap: 24,
  115. },
  116. properties: {
  117. name: {
  118. title: intl.formatMessage({
  119. id: 'pages.system.name',
  120. defaultMessage: '姓名',
  121. }),
  122. type: 'string',
  123. 'x-decorator': 'FormItem',
  124. 'x-component': 'Input',
  125. 'x-decorator-props': {
  126. gridSpan: 1,
  127. },
  128. 'x-component-props': {
  129. placeholder: '请输入姓名',
  130. },
  131. name: 'name',
  132. 'x-validator': [
  133. {
  134. max: 64,
  135. message: '最多可输入64个字符',
  136. },
  137. {
  138. required: true,
  139. message: '请输入姓名',
  140. },
  141. ],
  142. // required: true,
  143. },
  144. username: {
  145. title: intl.formatMessage({
  146. id: 'pages.system.username',
  147. defaultMessage: '用户名',
  148. }),
  149. 'x-decorator-props': {
  150. gridSpan: 1,
  151. },
  152. type: 'string',
  153. 'x-decorator': 'FormItem',
  154. 'x-component': 'Input',
  155. 'x-component-props': {
  156. disabled: model === 'edit',
  157. placeholder: '请输入用户名',
  158. },
  159. 'x-validator': [
  160. {
  161. max: 64,
  162. message: '最多可输入64个字符',
  163. },
  164. {
  165. required: true,
  166. message: '请输入用户名',
  167. },
  168. {
  169. triggerType: 'onBlur',
  170. validator: (value: string) => {
  171. return new Promise((resolve) => {
  172. if (!value) resolve('');
  173. service
  174. .validateField('username', value)
  175. .then((resp) => {
  176. if (resp.status === 200) {
  177. if (resp.result.passed) {
  178. resolve('');
  179. } else {
  180. resolve(model === 'edit' ? '' : resp.result.reason);
  181. }
  182. }
  183. resolve('');
  184. })
  185. .catch(() => {
  186. return '验证失败!';
  187. });
  188. });
  189. },
  190. },
  191. ],
  192. name: 'username',
  193. required: true,
  194. },
  195. },
  196. },
  197. password: {
  198. type: 'string',
  199. title: intl.formatMessage({
  200. id: 'pages.system.password',
  201. defaultMessage: '密码',
  202. }),
  203. 'x-decorator': 'FormItem',
  204. 'x-component': 'Password',
  205. 'x-component-props': {
  206. checkStrength: true,
  207. placeholder: '请输入密码',
  208. },
  209. 'x-visible': model === 'add',
  210. // 'x-reactions': [
  211. // {
  212. // dependencies: ['.confirmPassword'],
  213. // fulfill: {
  214. // state: {
  215. // selfErrors:
  216. // '{{$deps[0].length > 8 && $deps[0].length > 8 && $self.value && $self.value !==$deps[0] ? "两次密码输入不一致" : ""}}',
  217. // },
  218. // },
  219. // },
  220. // ],
  221. name: 'password',
  222. 'x-validator': [
  223. {
  224. max: 64,
  225. message: '密码最多可输入64位',
  226. },
  227. {
  228. min: 8,
  229. message: '密码不能少于8位',
  230. },
  231. {
  232. required: model === 'add',
  233. message: '请输入密码',
  234. },
  235. {
  236. triggerType: 'onBlur',
  237. validator: (value: string) => {
  238. return new Promise((resolve) => {
  239. if (!value || value.length < 8 || value.length > 64) resolve('');
  240. service
  241. .validateField('password', value)
  242. .then((resp) => {
  243. if (resp.status === 200) {
  244. if (resp.result.passed) {
  245. resolve('');
  246. } else {
  247. resolve(model === 'edit' ? '' : resp.result.reason);
  248. }
  249. }
  250. resolve('');
  251. })
  252. .catch(() => {
  253. return '验证失败!';
  254. });
  255. });
  256. },
  257. },
  258. // {
  259. // checkStrength: true,
  260. // },
  261. ],
  262. },
  263. confirmPassword: {
  264. type: 'string',
  265. title: intl.formatMessage({
  266. id: 'pages.system.confirmPassword',
  267. defaultMessage: '确认密码?',
  268. }),
  269. 'x-decorator': 'FormItem',
  270. 'x-component': 'Password',
  271. 'x-component-props': {
  272. checkStrength: true,
  273. placeholder: '请再次输入密码',
  274. },
  275. 'x-visible': model === 'add',
  276. 'x-validator': [
  277. // {
  278. // max: 64,
  279. // message: '密码最多可输入64位',
  280. // },
  281. // {
  282. // min: 8,
  283. // message: '密码不能少于8位',
  284. // },
  285. {
  286. required: model === 'add',
  287. message: '请输入确认密码',
  288. },
  289. // {
  290. // checkStrength: true,
  291. // },
  292. ],
  293. 'x-reactions': [
  294. {
  295. dependencies: ['.password'],
  296. fulfill: {
  297. state: {
  298. selfErrors:
  299. '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "两次密码输入不一致" : ""}}',
  300. },
  301. },
  302. },
  303. ],
  304. 'x-decorator-props': {},
  305. name: 'confirmPassword',
  306. },
  307. layout2: {
  308. type: 'void',
  309. 'x-decorator': 'FormGrid',
  310. 'x-decorator-props': {
  311. maxColumns: 2,
  312. minColumns: 2,
  313. columnGap: 24,
  314. },
  315. properties: {
  316. roleIdList: {
  317. title: '角色',
  318. 'x-decorator': 'FormItem',
  319. 'x-component': 'Select',
  320. 'x-component-props': {
  321. mode: 'multiple',
  322. showArrow: true,
  323. placeholder: '请选择角色',
  324. filterOption: (input: string, option: any) =>
  325. option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
  326. },
  327. 'x-reactions': ['{{useAsyncDataSource(getRole)}}'],
  328. 'x-decorator-props': {
  329. gridSpan: 1,
  330. addonAfter: (
  331. <PermissionButton
  332. // type="link"
  333. type="primary"
  334. ghost
  335. style={{ padding: '0 8px' }}
  336. isPermission={rolePermission.add && !!rolePath}
  337. onClick={() => {
  338. const tab: any = window.open(`${origin}/#${rolePath}?save=true`);
  339. tab!.onTabSaveSuccess = (value: any) => {
  340. form.setFieldState('roleIdList', async (state) => {
  341. state.dataSource = await getRole().then((resp) =>
  342. resp.result?.map((item: Record<string, unknown>) => ({
  343. ...item,
  344. label: item.name,
  345. value: item.id,
  346. })),
  347. );
  348. state.value = [...(state.value || []), value.id];
  349. });
  350. };
  351. }}
  352. >
  353. <PlusOutlined />
  354. </PermissionButton>
  355. ),
  356. },
  357. },
  358. orgIdList: {
  359. title: '组织',
  360. 'x-decorator': 'FormItem',
  361. 'x-component': 'TreeSelect',
  362. 'x-component-props': {
  363. multiple: true,
  364. showArrow: true,
  365. placeholder: '请选择组织',
  366. showCheckedStrategy: ATreeSelect.SHOW_ALL,
  367. filterOption: (input: string, option: any) =>
  368. option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0,
  369. fieldNames: {
  370. label: 'name',
  371. value: 'id',
  372. },
  373. treeNodeFilterProp: 'name',
  374. },
  375. 'x-decorator-props': {
  376. gridSpan: 1,
  377. addonAfter: (
  378. <PermissionButton
  379. // type="link"
  380. // style={{ padding: 0 }}
  381. type="primary"
  382. ghost
  383. style={{ padding: '0 8px' }}
  384. isPermission={deptPermission.add && !!departmentPath}
  385. onClick={() => {
  386. const tab: any = window.open(`${origin}/#${departmentPath}?save=true`);
  387. tab!.onTabSaveSuccess = (value: any) => {
  388. form.setFieldState('orgIdList', async (state) => {
  389. state.dataSource = await getOrg().then((resp) =>
  390. resp.result?.map((item: Record<string, unknown>) => ({
  391. ...item,
  392. label: item.name,
  393. value: item.id,
  394. })),
  395. );
  396. state.value = [...(state.value || []), value.id];
  397. });
  398. };
  399. }}
  400. >
  401. <PlusOutlined />
  402. </PermissionButton>
  403. ),
  404. },
  405. 'x-reactions': ['{{useAsyncDataSource(getOrg)}}'],
  406. },
  407. telephone: {
  408. title: '手机号',
  409. 'x-decorator': 'FormItem',
  410. 'x-component': 'Input',
  411. 'x-validator': [
  412. {
  413. format: 'phone',
  414. message: '请输入正确的手机号',
  415. },
  416. ],
  417. 'x-decorator-props': {
  418. gridSpan: 1,
  419. },
  420. 'x-component-props': {
  421. placeholder: '请输入手机号',
  422. },
  423. },
  424. email: {
  425. title: '邮箱',
  426. 'x-decorator': 'FormItem',
  427. 'x-component': 'Input',
  428. 'x-validator': [
  429. {
  430. format: 'email',
  431. message: '请输入正确的邮箱',
  432. },
  433. ],
  434. 'x-decorator-props': {
  435. gridSpan: 1,
  436. },
  437. 'x-component-props': {
  438. placeholder: '请输入邮箱',
  439. },
  440. },
  441. },
  442. },
  443. },
  444. };
  445. const save = async () => {
  446. const value = await form.submit<UserItem>();
  447. const temp: any = {};
  448. temp.id = value.id;
  449. temp.user = value;
  450. temp.user.roleList = [];
  451. temp.orgIdList = value.orgIdList;
  452. temp.roleIdList = value.roleIdList?.length ? value.roleIdList : null;
  453. const response = await service.saveUser(temp, model);
  454. if (response.status === 200) {
  455. onlyMessage(
  456. intl.formatMessage({
  457. id: 'pages.data.option.success',
  458. defaultMessage: '操作成功',
  459. }),
  460. );
  461. props.close();
  462. } else {
  463. onlyMessage('操作失败!', 'error');
  464. }
  465. };
  466. return (
  467. <Modal
  468. title={intl.formatMessage({
  469. id: `pages.data.option.${model}`,
  470. defaultMessage: '编辑',
  471. })}
  472. maskClosable={false}
  473. visible={model !== 'query'}
  474. onCancel={props.close}
  475. onOk={save}
  476. width="35vw"
  477. permissionCode={'system/User'}
  478. permission={['update']}
  479. >
  480. <Form form={form} layout="vertical">
  481. <SchemaField schema={schema} scope={{ useAsyncDataSource, getRole, getOrg }} />
  482. </Form>
  483. </Modal>
  484. );
  485. };
  486. export default Save;