@gadmin2n/schematics 0.0.147 → 0.0.149

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 (106) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +2 -1
  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 +83 -53
  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/agenda/agenda.controller.ts +4 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +31 -4
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +166 -17
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +82 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +7 -2
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +106 -37
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +59 -3
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +60 -14
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +60 -14
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +41 -1
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +46 -5
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +14 -10
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +1 -1
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +2 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +1 -1
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +2 -2
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.test.tsx +140 -0
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +124 -62
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +78 -17
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +4 -2
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  104. package/package.json +1 -1
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -6,6 +6,7 @@ import {
6
6
  Button,
7
7
  Card,
8
8
  Col,
9
+ DatePicker,
9
10
  Input,
10
11
  Modal,
11
12
  Popconfirm,
@@ -19,6 +20,8 @@ import {
19
20
  Typography,
20
21
  message,
21
22
  } from 'antd';
23
+ import type { Dayjs } from 'dayjs';
24
+ import dayjs from 'dayjs';
22
25
  import {
23
26
  DatabaseOutlined,
24
27
  DeleteOutlined,
@@ -33,6 +36,7 @@ import { PageHeader } from 'components';
33
36
  import { PAGE_WRAPPER_STYLE, SEARCH_CARD_PROPS } from 'constants/layout';
34
37
 
35
38
  const { Text } = Typography;
39
+ const { RangePicker } = DatePicker;
36
40
 
37
41
  // ─── Types ────────────────────────────────────────────────────────────────────
38
42
 
@@ -180,6 +184,14 @@ export default function AgendaJobsPage() {
180
184
  const page = Number(searchParams.get('page')) || 1;
181
185
  const PAGE_SIZE = 20;
182
186
 
187
+ // 时间过滤(按 lastRunAt):默认最近 7 天,URL 无 from/to 时使用默认区间。
188
+ const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(() => {
189
+ const from = searchParams.get('from');
190
+ const to = searchParams.get('to');
191
+ if (from && to) return [dayjs(from), dayjs(to)];
192
+ return [dayjs().subtract(7, 'day').startOf('day'), dayjs().endOf('day')];
193
+ });
194
+
183
195
  const updateParams = useCallback(
184
196
  (updates: Record<string, string | undefined>) => {
185
197
  setSearchParams(
@@ -217,6 +229,17 @@ export default function AgendaJobsPage() {
217
229
  (p: number) => updateParams({ page: p > 1 ? String(p) : undefined }),
218
230
  [updateParams],
219
231
  );
232
+ const setDateRangeAndSync = useCallback(
233
+ (range: [Dayjs, Dayjs] | null) => {
234
+ setDateRange(range);
235
+ updateParams({
236
+ from: range?.[0]?.toISOString(),
237
+ to: range?.[1]?.toISOString(),
238
+ page: undefined,
239
+ });
240
+ },
241
+ [updateParams],
242
+ );
220
243
 
221
244
  // Auto-refresh
222
245
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -232,6 +255,8 @@ export default function AgendaJobsPage() {
232
255
  if (filterToggle) params.toggle = filterToggle;
233
256
  if (filterName) params.q = filterName;
234
257
  if (filterModule) params.module = filterModule;
258
+ if (dateRange?.[0]) params.dateFrom = dateRange[0].toISOString();
259
+ if (dateRange?.[1]) params.dateTo = dateRange[1].toISOString();
235
260
 
236
261
  const [res, metrics] = await Promise.all([
237
262
  customRequest<JobsResponse>('agenda/jobs', 'GET', params),
@@ -247,7 +272,7 @@ export default function AgendaJobsPage() {
247
272
  } finally {
248
273
  setLoading(false);
249
274
  }
250
- }, [page, filterState, filterToggle, filterName, filterModule]);
275
+ }, [page, filterState, filterToggle, filterName, filterModule, dateRange]);
251
276
 
252
277
  useEffect(() => {
253
278
  fetchJobs();
@@ -715,6 +740,21 @@ export default function AgendaJobsPage() {
715
740
  style={{ width: 160 }}
716
741
  options={modules.map((m) => ({ value: m, label: m }))}
717
742
  />
743
+ <RangePicker
744
+ prefix={
745
+ <span
746
+ style={{ color: 'var(--ant-color-text)', whiteSpace: 'nowrap' }}
747
+ >
748
+ 最近执行
749
+ </span>
750
+ }
751
+ value={dateRange}
752
+ onChange={(range) =>
753
+ setDateRangeAndSync(range as [Dayjs, Dayjs] | null)
754
+ }
755
+ allowClear
756
+ style={{ width: 300 }}
757
+ />
718
758
  <Input
719
759
  placeholder="按名称过滤"
720
760
  allowClear
@@ -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
+ }