@gadmin2n/schematics 0.0.147 → 0.0.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +17 -4
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +15 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +2 -3
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +49 -11
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +49 -11
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +1 -2
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +47 -3
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  90. package/package.json +1 -1
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -0,0 +1,181 @@
1
+ import { Form, Input, Modal, Select, Spin, message } from 'antd';
2
+ import { useEffect, useState } from 'react';
3
+ import {
4
+ createPeopleGroup,
5
+ fetchPeopleGroup,
6
+ fetchPeopleGroups,
7
+ fetchPersonOptions,
8
+ replacePeopleGroupMembers,
9
+ type PeopleGroupDetail,
10
+ type PeopleGroupSummary,
11
+ type PersonOption,
12
+ updatePeopleGroup,
13
+ } from './people-groups.api';
14
+
15
+ interface Props {
16
+ open: boolean;
17
+ group?: PeopleGroupSummary | null;
18
+ onCancel(): void;
19
+ onSaved(): void;
20
+ }
21
+
22
+ export function PeopleGroupModal({ open, group, onCancel, onSaved }: Props) {
23
+ const [form] = Form.useForm();
24
+ const [detail, setDetail] = useState<PeopleGroupDetail>();
25
+ const [people, setPeople] = useState<PersonOption[]>([]);
26
+ const [groups, setGroups] = useState<PeopleGroupSummary[]>([]);
27
+ const [loading, setLoading] = useState(false);
28
+ const [peopleLoading, setPeopleLoading] = useState(false);
29
+ const [saving, setSaving] = useState(false);
30
+
31
+ useEffect(() => {
32
+ if (!open) return;
33
+ setLoading(true);
34
+ Promise.all([
35
+ fetchPeopleGroups('', false),
36
+ group ? fetchPeopleGroup(group.code) : Promise.resolve(undefined),
37
+ ])
38
+ .then(async ([groupOptions, nextDetail]) => {
39
+ const personOptions = await fetchPersonOptions(
40
+ '',
41
+ nextDetail?.directUserIds ?? [],
42
+ );
43
+ setPeople(personOptions);
44
+ setGroups(groupOptions.filter((item) => item.code !== group?.code));
45
+ setDetail(nextDetail);
46
+ form.setFieldsValue(
47
+ nextDetail
48
+ ? {
49
+ code: nextDetail.code,
50
+ name: nextDetail.name,
51
+ description: nextDetail.description ?? '',
52
+ userIds: nextDetail.directUserIds,
53
+ childGroupCodes: nextDetail.childGroupCodes,
54
+ }
55
+ : {
56
+ code: '',
57
+ name: '',
58
+ description: '',
59
+ userIds: [],
60
+ childGroupCodes: [],
61
+ },
62
+ );
63
+ })
64
+ .catch((error: any) => {
65
+ message.error(error?.message ?? '加载人员组失败');
66
+ onCancel();
67
+ })
68
+ .finally(() => setLoading(false));
69
+ }, [form, group, onCancel, open]);
70
+
71
+ async function searchPeople(query: string) {
72
+ setPeopleLoading(true);
73
+ try {
74
+ setPeople(
75
+ await fetchPersonOptions(query, form.getFieldValue('userIds') ?? []),
76
+ );
77
+ } catch (error: any) {
78
+ message.error(error?.message ?? '搜索人员失败');
79
+ } finally {
80
+ setPeopleLoading(false);
81
+ }
82
+ }
83
+
84
+ async function save() {
85
+ try {
86
+ const values = await form.validateFields();
87
+ setSaving(true);
88
+ if (!group) {
89
+ await createPeopleGroup({
90
+ ...(values.code.trim() && { code: values.code.trim() }),
91
+ name: values.name.trim(),
92
+ description: values.description.trim() || undefined,
93
+ userIds: values.userIds ?? [],
94
+ childGroupCodes: values.childGroupCodes ?? [],
95
+ });
96
+ } else if (detail) {
97
+ const metadata = await updatePeopleGroup(group.code, {
98
+ expectedRevision: detail.revision,
99
+ name: values.name.trim(),
100
+ description: values.description.trim() || null,
101
+ });
102
+ await replacePeopleGroupMembers(group.code, {
103
+ expectedRevision: metadata.revision,
104
+ userIds: values.userIds ?? [],
105
+ childGroupCodes: values.childGroupCodes ?? [],
106
+ });
107
+ }
108
+ message.success(group ? '人员组已更新' : '人员组已创建');
109
+ onSaved();
110
+ } catch (error: any) {
111
+ if (error?.errorFields) return;
112
+ message.error(error?.message ?? '保存人员组失败');
113
+ } finally {
114
+ setSaving(false);
115
+ }
116
+ }
117
+
118
+ return (
119
+ <Modal
120
+ open={open}
121
+ width={620}
122
+ title={group ? '编辑人员组' : '新建人员组'}
123
+ okText="保存"
124
+ cancelText="取消"
125
+ confirmLoading={saving}
126
+ onOk={save}
127
+ onCancel={onCancel}
128
+ destroyOnClose
129
+ >
130
+ <Spin spinning={loading}>
131
+ <Form form={form} layout="vertical" preserve={false}>
132
+ <Form.Item name="code" label="组标识">
133
+ <Input
134
+ disabled={!!group}
135
+ maxLength={21}
136
+ placeholder="可不填,由系统自动生成"
137
+ />
138
+ </Form.Item>
139
+ <Form.Item
140
+ name="name"
141
+ label="名称"
142
+ rules={[{ required: true, message: '请输入人员组名称' }]}
143
+ >
144
+ <Input maxLength={128} />
145
+ </Form.Item>
146
+ <Form.Item name="description" label="说明">
147
+ <Input.TextArea rows={3} maxLength={512} />
148
+ </Form.Item>
149
+ <Form.Item name="userIds" label="直接成员">
150
+ <Select
151
+ mode="multiple"
152
+ showSearch
153
+ filterOption={false}
154
+ loading={peopleLoading}
155
+ onSearch={searchPeople}
156
+ placeholder="选择人员"
157
+ options={people.map((person) => ({
158
+ value: person.userid,
159
+ label: person.username
160
+ ? `${person.username} (${person.userid})`
161
+ : person.userid,
162
+ }))}
163
+ />
164
+ </Form.Item>
165
+ <Form.Item name="childGroupCodes" label="包含人员组">
166
+ <Select
167
+ mode="multiple"
168
+ showSearch
169
+ optionFilterProp="label"
170
+ placeholder="可组合已有人员组"
171
+ options={groups.map((item) => ({
172
+ value: item.code,
173
+ label: `${item.name} (${item.code})`,
174
+ }))}
175
+ />
176
+ </Form.Item>
177
+ </Form>
178
+ </Spin>
179
+ </Modal>
180
+ );
181
+ }
@@ -0,0 +1,203 @@
1
+ import {
2
+ DeleteOutlined,
3
+ EditOutlined,
4
+ PlusOutlined,
5
+ ReloadOutlined,
6
+ } from '@ant-design/icons';
7
+ import {
8
+ Button,
9
+ Card,
10
+ Input,
11
+ Popconfirm,
12
+ Space,
13
+ Switch,
14
+ Table,
15
+ Tag,
16
+ Tooltip,
17
+ Typography,
18
+ message,
19
+ type TableColumnsType,
20
+ } from 'antd';
21
+ import { PageHeader } from 'components';
22
+ import { PAGE_WRAPPER_STYLE, SEARCH_CARD_PROPS } from 'constants/layout';
23
+ import { useCallback, useEffect, useState } from 'react';
24
+ import { PeopleGroupModal } from './PeopleGroupModal';
25
+ import {
26
+ deletePeopleGroup,
27
+ fetchPeopleGroup,
28
+ fetchPeopleGroups,
29
+ type PeopleGroupSummary,
30
+ setPeopleGroupActive,
31
+ } from './people-groups.api';
32
+
33
+ export default function PeopleGroupsPage() {
34
+ const [groups, setGroups] = useState<PeopleGroupSummary[]>([]);
35
+ const [query, setQuery] = useState('');
36
+ const [loading, setLoading] = useState(false);
37
+ const [deletingCode, setDeletingCode] = useState<string>();
38
+ const [editing, setEditing] = useState<
39
+ PeopleGroupSummary | null | undefined
40
+ >();
41
+
42
+ const load = useCallback(async () => {
43
+ setLoading(true);
44
+ try {
45
+ setGroups(await fetchPeopleGroups(query, true));
46
+ } catch (error: any) {
47
+ message.error(error?.message ?? '加载人员组失败');
48
+ } finally {
49
+ setLoading(false);
50
+ }
51
+ }, [query]);
52
+
53
+ useEffect(() => void load(), [load]);
54
+
55
+ async function toggle(group: PeopleGroupSummary, isActive: boolean) {
56
+ try {
57
+ const latest = await fetchPeopleGroup(group.code);
58
+ await setPeopleGroupActive(group.code, {
59
+ expectedRevision: latest.revision,
60
+ isActive,
61
+ });
62
+ message.success(isActive ? '人员组已启用' : '人员组已停用');
63
+ await load();
64
+ } catch (error: any) {
65
+ message.error(error?.message ?? '更新人员组状态失败');
66
+ }
67
+ }
68
+
69
+ async function remove(group: PeopleGroupSummary) {
70
+ setDeletingCode(group.code);
71
+ try {
72
+ await deletePeopleGroup(group.code, group.revision);
73
+ message.success('人员组已删除');
74
+ await load();
75
+ } catch (error: any) {
76
+ message.error(error?.message ?? '删除人员组失败');
77
+ } finally {
78
+ setDeletingCode(undefined);
79
+ }
80
+ }
81
+
82
+ const columns: TableColumnsType<PeopleGroupSummary> = [
83
+ {
84
+ title: '人员组',
85
+ key: 'group',
86
+ render: (_, group) => (
87
+ <div>
88
+ <Typography.Text strong>{group.name}</Typography.Text>
89
+ <br />
90
+ <Typography.Text type="secondary">{group.code}</Typography.Text>
91
+ </div>
92
+ ),
93
+ },
94
+ {
95
+ title: '说明',
96
+ dataIndex: 'description',
97
+ render: (value?: string | null) => value || '—',
98
+ },
99
+ {
100
+ title: '状态',
101
+ dataIndex: 'isActive',
102
+ width: 100,
103
+ render: (value: boolean) => (
104
+ <Tag color={value ? 'green' : 'default'}>
105
+ {value ? '已启用' : '已停用'}
106
+ </Tag>
107
+ ),
108
+ },
109
+ {
110
+ title: '操作',
111
+ width: 150,
112
+ render: (_, group) => (
113
+ <Space>
114
+ <Tooltip title="编辑">
115
+ <Button
116
+ size="small"
117
+ icon={<EditOutlined />}
118
+ aria-label={`编辑 ${group.name}`}
119
+ onClick={() => setEditing(group)}
120
+ />
121
+ </Tooltip>
122
+ <Popconfirm
123
+ title={group.isActive ? '停用这个人员组?' : '启用这个人员组?'}
124
+ onConfirm={() => toggle(group, !group.isActive)}
125
+ >
126
+ <Switch
127
+ size="small"
128
+ checked={group.isActive}
129
+ aria-label={`${group.isActive ? '停用' : '启用'} ${group.name}`}
130
+ />
131
+ </Popconfirm>
132
+ <Popconfirm
133
+ title="删除该人员组?"
134
+ description="被其他人员组、Workflow 或进行中审批引用时不能删除。"
135
+ okText="删除"
136
+ cancelText="取消"
137
+ okButtonProps={{ danger: true }}
138
+ onConfirm={() => remove(group)}
139
+ >
140
+ <Tooltip title="删除">
141
+ <Button
142
+ danger
143
+ size="small"
144
+ icon={<DeleteOutlined />}
145
+ loading={deletingCode === group.code}
146
+ aria-label={`删除 ${group.name}`}
147
+ />
148
+ </Tooltip>
149
+ </Popconfirm>
150
+ </Space>
151
+ ),
152
+ },
153
+ ];
154
+
155
+ return (
156
+ <div style={PAGE_WRAPPER_STYLE}>
157
+ <PageHeader
158
+ title="人员组"
159
+ subtitle="将人员或已有人员组组合为可复用的审批对象。"
160
+ extra={
161
+ <Space>
162
+ <Button icon={<ReloadOutlined />} onClick={load} />
163
+ <Button
164
+ type="primary"
165
+ icon={<PlusOutlined />}
166
+ onClick={() => setEditing(null)}
167
+ >
168
+ 新建人员组
169
+ </Button>
170
+ </Space>
171
+ }
172
+ />
173
+ <Card {...SEARCH_CARD_PROPS}>
174
+ <Input.Search
175
+ allowClear
176
+ placeholder="搜索名称或组标识"
177
+ style={{ width: 280 }}
178
+ onSearch={setQuery}
179
+ />
180
+ </Card>
181
+ <Card className="card-table">
182
+ <Table
183
+ rowKey="code"
184
+ loading={loading}
185
+ dataSource={groups}
186
+ columns={columns}
187
+ pagination={{ pageSize: 20, showSizeChanger: false }}
188
+ />
189
+ </Card>
190
+ {editing !== undefined && (
191
+ <PeopleGroupModal
192
+ open
193
+ group={editing}
194
+ onCancel={() => setEditing(undefined)}
195
+ onSaved={() => {
196
+ setEditing(undefined);
197
+ void load();
198
+ }}
199
+ />
200
+ )}
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,145 @@
1
+ import { customRequest } from 'helpers/http';
2
+
3
+ export interface PeopleGroupSummary {
4
+ code: string;
5
+ name: string;
6
+ description: string | null;
7
+ revision: number;
8
+ isActive: boolean;
9
+ updatedAt: string;
10
+ }
11
+
12
+ export interface PeopleGroupDetail {
13
+ code: string;
14
+ name: string;
15
+ description: string | null;
16
+ revision: number;
17
+ isActive: boolean;
18
+ directUserIds: string[];
19
+ childGroupCodes: string[];
20
+ resolvedUserCount: number;
21
+ }
22
+
23
+ export interface PersonOption {
24
+ userid: string;
25
+ username: string | null;
26
+ }
27
+
28
+ export function fetchPeopleGroups(query = '', includeInactive = true) {
29
+ return customRequest<PeopleGroupSummary[]>('user-groups', 'GET', {
30
+ q: query,
31
+ includeInactive,
32
+ });
33
+ }
34
+
35
+ export async function fetchPersonOptions(
36
+ query = '',
37
+ selectedIds: string[] = [],
38
+ ) {
39
+ const text = query.trim();
40
+ const requests = [
41
+ customRequest<{ data: PersonOption[] }>('user/findMany', 'POST', {
42
+ where: {
43
+ isActive: true,
44
+ isApiKey: false,
45
+ ...(text && {
46
+ OR: [
47
+ { userid: { contains: text, mode: 'insensitive' } },
48
+ { username: { contains: text, mode: 'insensitive' } },
49
+ ],
50
+ }),
51
+ },
52
+ select: { userid: true, username: true },
53
+ orderBy: { userid: 'asc' },
54
+ take: 50,
55
+ }),
56
+ ];
57
+ if (selectedIds.length > 0) {
58
+ requests.push(
59
+ customRequest<{ data: PersonOption[] }>('user/findMany', 'POST', {
60
+ where: {
61
+ userid: { in: selectedIds },
62
+ isActive: true,
63
+ isApiKey: false,
64
+ },
65
+ select: { userid: true, username: true },
66
+ take: selectedIds.length,
67
+ }),
68
+ );
69
+ }
70
+ const results = await Promise.all(requests);
71
+ return [
72
+ ...new Map(
73
+ results
74
+ .flatMap((result) => result.data)
75
+ .map((person) => [person.userid, person]),
76
+ ).values(),
77
+ ];
78
+ }
79
+
80
+ export function fetchPeopleGroup(code: string) {
81
+ return customRequest<PeopleGroupDetail>(
82
+ `user-groups/${encodeURIComponent(code)}/preview`,
83
+ 'GET',
84
+ );
85
+ }
86
+
87
+ export function createPeopleGroup(input: {
88
+ code?: string;
89
+ name: string;
90
+ description?: string;
91
+ userIds: string[];
92
+ childGroupCodes: string[];
93
+ }) {
94
+ return customRequest<PeopleGroupSummary>('user-groups', 'POST', input);
95
+ }
96
+
97
+ export function updatePeopleGroup(
98
+ code: string,
99
+ input: {
100
+ expectedRevision: number;
101
+ name: string;
102
+ description: string | null;
103
+ },
104
+ ) {
105
+ return customRequest<PeopleGroupSummary>(
106
+ `user-groups/${encodeURIComponent(code)}`,
107
+ 'PATCH',
108
+ input,
109
+ );
110
+ }
111
+
112
+ export function replacePeopleGroupMembers(
113
+ code: string,
114
+ input: {
115
+ expectedRevision: number;
116
+ userIds: string[];
117
+ childGroupCodes: string[];
118
+ },
119
+ ) {
120
+ return customRequest<PeopleGroupSummary>(
121
+ `user-groups/${encodeURIComponent(code)}/members`,
122
+ 'PUT',
123
+ input,
124
+ );
125
+ }
126
+
127
+ export function setPeopleGroupActive(
128
+ code: string,
129
+ input: { expectedRevision: number; isActive: boolean },
130
+ ) {
131
+ return customRequest<PeopleGroupSummary>(
132
+ `user-groups/${encodeURIComponent(code)}/active`,
133
+ 'PATCH',
134
+ input,
135
+ );
136
+ }
137
+
138
+ export function deletePeopleGroup(code: string, expectedRevision: number) {
139
+ return customRequest<{ count: number }>(
140
+ `user-groups/${encodeURIComponent(
141
+ code,
142
+ )}?expectedRevision=${expectedRevision}`,
143
+ 'DELETE',
144
+ );
145
+ }
@@ -0,0 +1,112 @@
1
+ import { Form, Input, Modal, Switch, message } from 'antd';
2
+ import { StaffSelect } from 'components';
3
+ import { useEffect, useState } from 'react';
4
+ import {
5
+ createPerson,
6
+ type PersonRecord,
7
+ updatePerson,
8
+ } from './person-directory.api';
9
+
10
+ interface Props {
11
+ open: boolean;
12
+ person?: PersonRecord | null;
13
+ onCancel(): void;
14
+ onSaved(): void;
15
+ }
16
+
17
+ export function PersonModal({ open, person, onCancel, onSaved }: Props) {
18
+ const [form] = Form.useForm();
19
+ const [saving, setSaving] = useState(false);
20
+
21
+ useEffect(() => {
22
+ if (!open) return;
23
+ form.resetFields();
24
+ form.setFieldsValue(
25
+ person
26
+ ? {
27
+ userid: person.userid,
28
+ username: person.username ?? '',
29
+ description: person.description ?? '',
30
+ isActive: person.isActive,
31
+ }
32
+ : {
33
+ userid: undefined,
34
+ username: '',
35
+ description: '',
36
+ isActive: true,
37
+ },
38
+ );
39
+ }, [form, open, person]);
40
+
41
+ async function save() {
42
+ try {
43
+ const values = await form.validateFields();
44
+ setSaving(true);
45
+ if (person) {
46
+ await updatePerson(person.id, {
47
+ username: values.username.trim() || undefined,
48
+ description: values.description.trim() || null,
49
+ isActive: values.isActive,
50
+ });
51
+ } else {
52
+ await createPerson({
53
+ userid: values.userid.trim(),
54
+ username: values.username.trim() || values.userid.trim(),
55
+ description: values.description.trim() || undefined,
56
+ isActive: values.isActive,
57
+ });
58
+ }
59
+ message.success(person ? '人员信息已更新' : '人员已添加');
60
+ onSaved();
61
+ } catch (error: any) {
62
+ if (error?.errorFields) return;
63
+ message.error(error?.message ?? '保存人员失败');
64
+ } finally {
65
+ setSaving(false);
66
+ }
67
+ }
68
+
69
+ return (
70
+ <Modal
71
+ open={open}
72
+ title={person ? '编辑人员' : '添加人员'}
73
+ okText="保存"
74
+ cancelText="取消"
75
+ confirmLoading={saving}
76
+ onOk={save}
77
+ onCancel={onCancel}
78
+ destroyOnClose
79
+ >
80
+ <Form form={form} layout="vertical" preserve={false}>
81
+ {person ? (
82
+ <Form.Item name="userid" label="账号">
83
+ <Input disabled />
84
+ </Form.Item>
85
+ ) : (
86
+ <Form.Item
87
+ name="userid"
88
+ label="人员"
89
+ rules={[{ required: true, message: '请选择人员' }]}
90
+ >
91
+ <StaffSelect />
92
+ </Form.Item>
93
+ )}
94
+ {person ? (
95
+ <Form.Item name="username" label="姓名">
96
+ <Input maxLength={128} placeholder="未填写时默认使用账号" />
97
+ </Form.Item>
98
+ ) : (
99
+ <Form.Item name="username" hidden>
100
+ <Input />
101
+ </Form.Item>
102
+ )}
103
+ <Form.Item name="description" label="备注">
104
+ <Input.TextArea rows={3} maxLength={512} />
105
+ </Form.Item>
106
+ <Form.Item name="isActive" label="启用" valuePropName="checked">
107
+ <Switch />
108
+ </Form.Item>
109
+ </Form>
110
+ </Modal>
111
+ );
112
+ }