openxiangda-cli 2.0.0-alpha.89 → 2.0.0-alpha.90
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.
- package/README.md +2 -2
- package/bin/run.js +1 -0
- package/dist/commands/skill.d.ts +26 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +43 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +3 -3
- package/dist/create-workspace.js.map +1 -1
- package/dist/toolchain-capsule.d.ts.map +1 -1
- package/dist/toolchain-capsule.js +4 -8
- package/dist/toolchain-capsule.js.map +1 -1
- package/package.json +6 -7
- package/template/AGENTS.md +2 -1
- package/template/README.md +3 -3
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -1
- package/template/apps/server/src/main.ts +1 -1
- package/template/apps/web/e2e/client-fixture.ts +5 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +19 -14
- package/template/apps/web/e2e/field-protocol-fixture.tsx +87 -20
- package/template/apps/web/e2e/field-protocol.spec.ts +86 -0
- package/template/apps/web/e2e/resources.spec.ts +20 -181
- package/template/apps/web/field-protocol.e2e.html +3 -0
- package/template/apps/web/package.json +2 -5
- package/template/apps/web/src/main.tsx +12 -60
- package/template/apps/web/test/contracts.test.ts +52 -104
- package/template/openxiangda.config.ts +1 -1
- package/template/package.json +1 -3
- package/template/packages/contracts/src/generated.ts +1 -0
- package/template/scripts/verify-template-budget.mjs +2 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +0 -661
- package/template/apps/web/src/FilePreviewPage.tsx +0 -324
- package/template/apps/web/src/RoleSubjectSelect.tsx +0 -128
- package/template/apps/web/src/Shell.tsx +0 -445
- package/template/apps/web/src/appearance.tsx +0 -147
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +0 -338
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +0 -245
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +0 -49
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +0 -148
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +0 -77
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +0 -164
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +0 -673
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +0 -67
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +0 -196
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +0 -315
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +0 -553
- package/template/apps/web/src/components/platform-fields/address-value.ts +0 -80
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +0 -66
- package/template/apps/web/src/components/platform-fields/directory-value.ts +0 -53
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +0 -98
- package/template/apps/web/src/components/platform-fields/location-value.ts +0 -87
- package/template/apps/web/src/components/platform-fields/resource-query.ts +0 -131
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +0 -59
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +0 -187
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +0 -1184
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +0 -326
- package/template/apps/web/src/components/resource/StandardResourcePages.tsx +0 -392
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +0 -915
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +0 -18
- package/template/apps/web/src/components/resource/resource-import.ts +0 -205
- package/template/apps/web/src/data-provider.ts +0 -72
- package/template/apps/web/src/platform-client.ts +0 -708
- package/template/apps/web/src/runtime-meta.ts +0 -75
- package/template/apps/web/src/runtime.tsx +0 -227
- package/template/apps/web/src/styles.css +0 -1941
- package/template/apps/web/test/address-field.test.ts +0 -66
- package/template/apps/web/test/cascade-value.test.ts +0 -68
- package/template/apps/web/test/directory-value.test.ts +0 -55
- package/template/apps/web/test/field-bounds.test.ts +0 -17
- package/template/apps/web/test/field-form-codec.test.ts +0 -85
- package/template/apps/web/test/location-field.test.ts +0 -83
- package/template/apps/web/test/resource-import.test.ts +0 -143
- package/template/apps/web/test/resource-query.test.ts +0 -144
- package/template/apps/web/test/rich-json-field.test.ts +0 -53
- package/template/apps/web/test/signature-serial-field.test.ts +0 -40
- package/template/apps/web/test/subtable-value.test.ts +0 -144
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CloseOutlined,
|
|
3
|
-
DownloadOutlined,
|
|
4
|
-
FileExcelOutlined,
|
|
5
|
-
FileImageOutlined,
|
|
6
|
-
FilePdfOutlined,
|
|
7
|
-
FileWordOutlined,
|
|
8
|
-
PaperClipOutlined,
|
|
9
|
-
} from '@ant-design/icons';
|
|
10
|
-
import { Alert, Button, Empty, Result, Segmented, Spin, Typography } from 'antd';
|
|
11
|
-
import type { DataFilePreview } from 'openxiangda-contracts/browser';
|
|
12
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
-
import { useParams } from 'react-router-dom';
|
|
14
|
-
import {
|
|
15
|
-
downloadDataFile,
|
|
16
|
-
fetchDataFileBlob,
|
|
17
|
-
loadDataFilePreview,
|
|
18
|
-
} from './platform-client';
|
|
19
|
-
import { formatManagedFileSize } from './components/platform-fields/AttachmentFileList';
|
|
20
|
-
|
|
21
|
-
const MAX_SPREADSHEET_ROWS = 5_000;
|
|
22
|
-
const MAX_SPREADSHEET_COLUMNS = 200;
|
|
23
|
-
|
|
24
|
-
function DocxDocument({ blob }: { blob: Blob }) {
|
|
25
|
-
const styleRef = useRef<HTMLDivElement>(null);
|
|
26
|
-
const bodyRef = useRef<HTMLDivElement>(null);
|
|
27
|
-
const [error, setError] = useState('');
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
let active = true;
|
|
30
|
-
const body = bodyRef.current;
|
|
31
|
-
const style = styleRef.current;
|
|
32
|
-
if (!body || !style) return;
|
|
33
|
-
body.replaceChildren();
|
|
34
|
-
style.replaceChildren();
|
|
35
|
-
setError('');
|
|
36
|
-
void import('docx-preview')
|
|
37
|
-
.then(({ renderAsync }) =>
|
|
38
|
-
renderAsync(blob, body, style, {
|
|
39
|
-
className: 'oxa-docx-document',
|
|
40
|
-
inWrapper: true,
|
|
41
|
-
ignoreFonts: false,
|
|
42
|
-
ignoreHeight: false,
|
|
43
|
-
ignoreWidth: false,
|
|
44
|
-
renderChanges: false,
|
|
45
|
-
renderHeaders: true,
|
|
46
|
-
renderFooters: true,
|
|
47
|
-
})
|
|
48
|
-
)
|
|
49
|
-
.catch(reason => {
|
|
50
|
-
if (active) {
|
|
51
|
-
setError(reason instanceof Error ? reason.message : String(reason));
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
return () => {
|
|
55
|
-
active = false;
|
|
56
|
-
body.replaceChildren();
|
|
57
|
-
style.replaceChildren();
|
|
58
|
-
};
|
|
59
|
-
}, [blob]);
|
|
60
|
-
if (error) {
|
|
61
|
-
return <Alert title="Word 文档解析失败" description={error} type="error" showIcon />;
|
|
62
|
-
}
|
|
63
|
-
return (
|
|
64
|
-
<div className="oxa-docx-preview">
|
|
65
|
-
<div ref={styleRef} />
|
|
66
|
-
<div ref={bodyRef} />
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function SpreadsheetDocument({ blob }: { blob: Blob }) {
|
|
72
|
-
const [sheetName, setSheetName] = useState('');
|
|
73
|
-
const [sheets, setSheets] = useState<Record<string, unknown[][]>>({});
|
|
74
|
-
const [error, setError] = useState('');
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
let active = true;
|
|
77
|
-
setError('');
|
|
78
|
-
setSheets({});
|
|
79
|
-
void Promise.all([blob.arrayBuffer(), import('xlsx')])
|
|
80
|
-
.then(([buffer, XLSX]) => {
|
|
81
|
-
const workbook = XLSX.read(buffer, {
|
|
82
|
-
type: 'array',
|
|
83
|
-
cellFormula: false,
|
|
84
|
-
cellHTML: false,
|
|
85
|
-
cellText: true,
|
|
86
|
-
});
|
|
87
|
-
if (!active) return;
|
|
88
|
-
setSheets(
|
|
89
|
-
Object.fromEntries(
|
|
90
|
-
workbook.SheetNames.slice(0, 50).map(name => [
|
|
91
|
-
name,
|
|
92
|
-
(
|
|
93
|
-
XLSX.utils.sheet_to_json<unknown[]>(workbook.Sheets[name]!, {
|
|
94
|
-
header: 1,
|
|
95
|
-
raw: false,
|
|
96
|
-
blankrows: false,
|
|
97
|
-
defval: '',
|
|
98
|
-
}) as unknown[][]
|
|
99
|
-
)
|
|
100
|
-
.slice(0, MAX_SPREADSHEET_ROWS)
|
|
101
|
-
.map(row => row.slice(0, MAX_SPREADSHEET_COLUMNS)),
|
|
102
|
-
])
|
|
103
|
-
)
|
|
104
|
-
);
|
|
105
|
-
setSheetName(workbook.SheetNames[0] || '');
|
|
106
|
-
})
|
|
107
|
-
.catch(reason => {
|
|
108
|
-
if (active) {
|
|
109
|
-
setError(reason instanceof Error ? reason.message : String(reason));
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
return () => {
|
|
113
|
-
active = false;
|
|
114
|
-
};
|
|
115
|
-
}, [blob]);
|
|
116
|
-
const rows = sheets[sheetName] || [];
|
|
117
|
-
if (error) {
|
|
118
|
-
return <Alert title="Excel 文件解析失败" description={error} type="error" showIcon />;
|
|
119
|
-
}
|
|
120
|
-
const sheetNames = Object.keys(sheets);
|
|
121
|
-
if (!sheetNames.length) return <Spin description="正在解析 Excel 文件" />;
|
|
122
|
-
return (
|
|
123
|
-
<div className="oxa-spreadsheet-preview">
|
|
124
|
-
{sheetNames.length > 1 && (
|
|
125
|
-
<Segmented
|
|
126
|
-
block
|
|
127
|
-
onChange={value => setSheetName(String(value))}
|
|
128
|
-
options={sheetNames}
|
|
129
|
-
value={sheetName}
|
|
130
|
-
/>
|
|
131
|
-
)}
|
|
132
|
-
<div className="oxa-spreadsheet-scroll">
|
|
133
|
-
{rows.length ? (
|
|
134
|
-
<table>
|
|
135
|
-
<tbody>
|
|
136
|
-
{rows.map((row, rowIndex) => (
|
|
137
|
-
<tr key={rowIndex}>
|
|
138
|
-
<th>{rowIndex + 1}</th>
|
|
139
|
-
{row.map((cell, columnIndex) => (
|
|
140
|
-
<td key={columnIndex}>{String(cell ?? '')}</td>
|
|
141
|
-
))}
|
|
142
|
-
</tr>
|
|
143
|
-
))}
|
|
144
|
-
</tbody>
|
|
145
|
-
</table>
|
|
146
|
-
) : (
|
|
147
|
-
<Empty description="当前工作表没有可显示的数据" />
|
|
148
|
-
)}
|
|
149
|
-
</div>
|
|
150
|
-
{(rows.length >= MAX_SPREADSHEET_ROWS ||
|
|
151
|
-
rows.some(row => row.length >= MAX_SPREADSHEET_COLUMNS)) && (
|
|
152
|
-
<Alert
|
|
153
|
-
banner
|
|
154
|
-
title={`为保证浏览器稳定,仅展示前 ${MAX_SPREADSHEET_ROWS} 行、${MAX_SPREADSHEET_COLUMNS} 列`}
|
|
155
|
-
type="warning"
|
|
156
|
-
/>
|
|
157
|
-
)}
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function PreviewBody({ preview, blob }: { preview: DataFilePreview; blob?: Blob }) {
|
|
163
|
-
const objectUrl = useMemo(() => (blob ? URL.createObjectURL(blob) : ''), [blob]);
|
|
164
|
-
useEffect(
|
|
165
|
-
() => () => {
|
|
166
|
-
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
167
|
-
},
|
|
168
|
-
[objectUrl]
|
|
169
|
-
);
|
|
170
|
-
if (!preview.canPreview || preview.renderMode === 'download') {
|
|
171
|
-
return (
|
|
172
|
-
<Empty
|
|
173
|
-
description={
|
|
174
|
-
<div>
|
|
175
|
-
<Typography.Text strong>当前文件无法在线预览</Typography.Text>
|
|
176
|
-
<br />
|
|
177
|
-
<Typography.Text type="secondary">
|
|
178
|
-
{preview.unsupportedReason || '请下载后使用本地应用打开'}
|
|
179
|
-
</Typography.Text>
|
|
180
|
-
</div>
|
|
181
|
-
}
|
|
182
|
-
/>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
if (!blob) return <Spin description="正在读取文件内容" />;
|
|
186
|
-
if (preview.previewType === 'image') {
|
|
187
|
-
return <img alt={preview.file.name} className="oxa-preview-image" src={objectUrl} />;
|
|
188
|
-
}
|
|
189
|
-
if (preview.renderMode === 'pdfjs') {
|
|
190
|
-
return (
|
|
191
|
-
<iframe
|
|
192
|
-
className="oxa-preview-frame"
|
|
193
|
-
src={objectUrl}
|
|
194
|
-
title={`${preview.file.name} PDF 预览`}
|
|
195
|
-
/>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
if (preview.renderMode === 'docx-html') return <DocxDocument blob={blob} />;
|
|
199
|
-
if (
|
|
200
|
-
preview.renderMode === 'excel-client' ||
|
|
201
|
-
preview.renderMode === 'excel-basic'
|
|
202
|
-
) {
|
|
203
|
-
return <SpreadsheetDocument blob={blob} />;
|
|
204
|
-
}
|
|
205
|
-
return (
|
|
206
|
-
<Empty description={preview.unsupportedReason || '当前文件没有可用的预览器'} />
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function previewIcon(preview?: DataFilePreview) {
|
|
211
|
-
if (preview?.previewType === 'image') return <FileImageOutlined />;
|
|
212
|
-
if (preview?.previewType === 'pdf') return <FilePdfOutlined />;
|
|
213
|
-
if (preview?.previewType === 'spreadsheet') return <FileExcelOutlined />;
|
|
214
|
-
if (preview?.previewType === 'office') return <FileWordOutlined />;
|
|
215
|
-
return <PaperClipOutlined />;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function FilePreviewPage() {
|
|
219
|
-
const { resourceCode = '', fileId = '' } = useParams();
|
|
220
|
-
const [preview, setPreview] = useState<DataFilePreview>();
|
|
221
|
-
const [blob, setBlob] = useState<Blob>();
|
|
222
|
-
const [error, setError] = useState('');
|
|
223
|
-
const [downloading, setDownloading] = useState(false);
|
|
224
|
-
useEffect(() => {
|
|
225
|
-
let active = true;
|
|
226
|
-
setPreview(undefined);
|
|
227
|
-
setBlob(undefined);
|
|
228
|
-
setError('');
|
|
229
|
-
if (!resourceCode || !fileId) {
|
|
230
|
-
setError('OPENXIANGDA_NATIVE_DATA_FILE_PREVIEW_PATH_INVALID');
|
|
231
|
-
return () => {
|
|
232
|
-
active = false;
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
void loadDataFilePreview(resourceCode, fileId)
|
|
236
|
-
.then(async metadata => {
|
|
237
|
-
if (!active) return;
|
|
238
|
-
setPreview(metadata);
|
|
239
|
-
document.title = `${metadata.file.name} - 文件预览`;
|
|
240
|
-
if (metadata.canPreview && metadata.renderMode !== 'download') {
|
|
241
|
-
const content = await fetchDataFileBlob(resourceCode, fileId);
|
|
242
|
-
if (active) setBlob(content);
|
|
243
|
-
}
|
|
244
|
-
})
|
|
245
|
-
.catch(reason => {
|
|
246
|
-
if (active) setError(reason instanceof Error ? reason.message : String(reason));
|
|
247
|
-
});
|
|
248
|
-
return () => {
|
|
249
|
-
active = false;
|
|
250
|
-
};
|
|
251
|
-
}, [fileId, resourceCode]);
|
|
252
|
-
|
|
253
|
-
const download = async () => {
|
|
254
|
-
if (!resourceCode || !fileId) return;
|
|
255
|
-
setDownloading(true);
|
|
256
|
-
try {
|
|
257
|
-
await downloadDataFile(
|
|
258
|
-
resourceCode,
|
|
259
|
-
fileId,
|
|
260
|
-
preview?.file.name || 'attachment'
|
|
261
|
-
);
|
|
262
|
-
} catch (reason) {
|
|
263
|
-
setError(reason instanceof Error ? reason.message : String(reason));
|
|
264
|
-
} finally {
|
|
265
|
-
setDownloading(false);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
return (
|
|
269
|
-
<div className="oxa-file-preview-page">
|
|
270
|
-
<header className="oxa-file-preview-toolbar">
|
|
271
|
-
<div className="oxa-file-preview-title">
|
|
272
|
-
{previewIcon(preview)}
|
|
273
|
-
<div>
|
|
274
|
-
<strong>{preview?.file.name || '文件预览'}</strong>
|
|
275
|
-
<span>
|
|
276
|
-
{preview
|
|
277
|
-
? `${preview.extension.toUpperCase() || '文件'} · ${formatManagedFileSize(preview.file.size)} · 只读预览`
|
|
278
|
-
: '正在加载预览信息'}
|
|
279
|
-
</span>
|
|
280
|
-
</div>
|
|
281
|
-
</div>
|
|
282
|
-
<div>
|
|
283
|
-
{resourceCode && fileId && (
|
|
284
|
-
<Button
|
|
285
|
-
icon={<DownloadOutlined />}
|
|
286
|
-
loading={downloading}
|
|
287
|
-
onClick={() => void download()}
|
|
288
|
-
>
|
|
289
|
-
下载文件
|
|
290
|
-
</Button>
|
|
291
|
-
)}
|
|
292
|
-
<Button icon={<CloseOutlined />} onClick={() => window.close()}>
|
|
293
|
-
关闭
|
|
294
|
-
</Button>
|
|
295
|
-
</div>
|
|
296
|
-
</header>
|
|
297
|
-
<main className="oxa-file-preview-body">
|
|
298
|
-
{error ? (
|
|
299
|
-
<Result
|
|
300
|
-
status="error"
|
|
301
|
-
title="文件预览加载失败"
|
|
302
|
-
subTitle={error}
|
|
303
|
-
extra={
|
|
304
|
-
resourceCode && fileId ? (
|
|
305
|
-
<Button
|
|
306
|
-
icon={<DownloadOutlined />}
|
|
307
|
-
loading={downloading}
|
|
308
|
-
onClick={() => void download()}
|
|
309
|
-
type="primary"
|
|
310
|
-
>
|
|
311
|
-
下载文件
|
|
312
|
-
</Button>
|
|
313
|
-
) : undefined
|
|
314
|
-
}
|
|
315
|
-
/>
|
|
316
|
-
) : preview ? (
|
|
317
|
-
<PreviewBody blob={blob} preview={preview} />
|
|
318
|
-
) : (
|
|
319
|
-
<Spin description="正在确认文件权限与预览能力" />
|
|
320
|
-
)}
|
|
321
|
-
</main>
|
|
322
|
-
</div>
|
|
323
|
-
);
|
|
324
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { Select, Typography } from 'antd';
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
import type {
|
|
4
|
-
RoleSubjectChoice,
|
|
5
|
-
RoleSubjectChoicePage,
|
|
6
|
-
} from 'openxiangda-contracts/browser';
|
|
7
|
-
import { loadRoleSubjects } from './platform-client';
|
|
8
|
-
|
|
9
|
-
function mergeSubjects(
|
|
10
|
-
current: RoleSubjectChoice[],
|
|
11
|
-
incoming: RoleSubjectChoice[]
|
|
12
|
-
) {
|
|
13
|
-
const merged = new Map(current.map(item => [item.subjectKey, item]));
|
|
14
|
-
for (const item of incoming) merged.set(item.subjectKey, item);
|
|
15
|
-
return [...merged.values()];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function RoleSubjectSelect({
|
|
19
|
-
disabled,
|
|
20
|
-
initialPage,
|
|
21
|
-
onChange,
|
|
22
|
-
value,
|
|
23
|
-
}: {
|
|
24
|
-
disabled?: boolean;
|
|
25
|
-
initialPage: RoleSubjectChoicePage;
|
|
26
|
-
onChange: (value: string) => void;
|
|
27
|
-
value?: string;
|
|
28
|
-
}) {
|
|
29
|
-
const [keyword, setKeyword] = useState('');
|
|
30
|
-
const [options, setOptions] = useState(initialPage.items);
|
|
31
|
-
const [cursor, setCursor] = useState(initialPage.nextCursor);
|
|
32
|
-
const [loading, setLoading] = useState(false);
|
|
33
|
-
const [loadError, setLoadError] = useState('');
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
let active = true;
|
|
37
|
-
const timer = window.setTimeout(() => {
|
|
38
|
-
setLoading(true);
|
|
39
|
-
setLoadError('');
|
|
40
|
-
void loadRoleSubjects({ keyword, limit: 20 })
|
|
41
|
-
.then(page => {
|
|
42
|
-
if (!active) return;
|
|
43
|
-
setOptions(current =>
|
|
44
|
-
mergeSubjects(
|
|
45
|
-
current.filter(item => item.subjectKey === value),
|
|
46
|
-
page.items
|
|
47
|
-
)
|
|
48
|
-
);
|
|
49
|
-
setCursor(page.nextCursor);
|
|
50
|
-
})
|
|
51
|
-
.catch(reason => {
|
|
52
|
-
if (!active) return;
|
|
53
|
-
setLoadError(
|
|
54
|
-
reason instanceof Error ? reason.message : '应用角色读取失败'
|
|
55
|
-
);
|
|
56
|
-
})
|
|
57
|
-
.finally(() => {
|
|
58
|
-
if (active) setLoading(false);
|
|
59
|
-
});
|
|
60
|
-
}, keyword ? 250 : 0);
|
|
61
|
-
return () => {
|
|
62
|
-
active = false;
|
|
63
|
-
window.clearTimeout(timer);
|
|
64
|
-
};
|
|
65
|
-
}, [keyword, value]);
|
|
66
|
-
|
|
67
|
-
const loadMore = async () => {
|
|
68
|
-
if (!cursor || loading) return;
|
|
69
|
-
setLoading(true);
|
|
70
|
-
setLoadError('');
|
|
71
|
-
try {
|
|
72
|
-
const page = await loadRoleSubjects({ keyword, cursor, limit: 20 });
|
|
73
|
-
setOptions(current => mergeSubjects(current, page.items));
|
|
74
|
-
setCursor(page.nextCursor);
|
|
75
|
-
} catch (reason) {
|
|
76
|
-
setLoadError(
|
|
77
|
-
reason instanceof Error ? reason.message : '更多应用角色读取失败'
|
|
78
|
-
);
|
|
79
|
-
} finally {
|
|
80
|
-
setLoading(false);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<Select
|
|
86
|
-
aria-label="选择应用角色"
|
|
87
|
-
className="oxa-role-switch-select"
|
|
88
|
-
disabled={disabled}
|
|
89
|
-
filterOption={false}
|
|
90
|
-
loading={loading}
|
|
91
|
-
notFoundContent={
|
|
92
|
-
loadError ? (
|
|
93
|
-
<Typography.Text type="danger">应用角色读取失败,请重试</Typography.Text>
|
|
94
|
-
) : undefined
|
|
95
|
-
}
|
|
96
|
-
onChange={onChange}
|
|
97
|
-
onPopupScroll={event => {
|
|
98
|
-
const target = event.currentTarget;
|
|
99
|
-
if (
|
|
100
|
-
target.scrollTop + target.clientHeight >=
|
|
101
|
-
target.scrollHeight - 12
|
|
102
|
-
) {
|
|
103
|
-
void loadMore();
|
|
104
|
-
}
|
|
105
|
-
}}
|
|
106
|
-
onSearch={setKeyword}
|
|
107
|
-
optionRender={option => {
|
|
108
|
-
const subject = options.find(item => item.subjectKey === option.value);
|
|
109
|
-
return (
|
|
110
|
-
<div className="oxa-role-switch-option">
|
|
111
|
-
<strong>{subject?.role.name || option.label}</strong>
|
|
112
|
-
{subject?.role.description && (
|
|
113
|
-
<span>{subject.role.description}</span>
|
|
114
|
-
)}
|
|
115
|
-
</div>
|
|
116
|
-
);
|
|
117
|
-
}}
|
|
118
|
-
options={options.map(subject => ({
|
|
119
|
-
label: subject.role.name,
|
|
120
|
-
value: subject.subjectKey,
|
|
121
|
-
}))}
|
|
122
|
-
placeholder="选择当前应用角色"
|
|
123
|
-
showSearch
|
|
124
|
-
status={loadError ? 'error' : undefined}
|
|
125
|
-
value={value}
|
|
126
|
-
/>
|
|
127
|
-
);
|
|
128
|
-
}
|