openxiangda-cli 2.0.0-alpha.56 → 2.0.0-alpha.58
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/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +13 -5
- package/dist/commands/create.js.map +1 -1
- package/dist/create-workspace.d.ts +6 -1
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +12 -9
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/.dockerignore +11 -0
- package/template/README.md +3 -1
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/instruments.spec.ts +912 -9
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/scripts/check.mjs +2 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +371 -0
- package/template/apps/web/src/CollegePage.tsx +181 -0
- package/template/apps/web/src/InstrumentDetailPage.tsx +94 -20
- package/template/apps/web/src/InstrumentForm.tsx +450 -88
- package/template/apps/web/src/InstrumentFormPage.tsx +49 -9
- package/template/apps/web/src/InstrumentListPage.tsx +30 -35
- package/template/apps/web/src/Shell.tsx +229 -41
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +698 -0
- package/template/apps/web/src/data-provider.ts +1 -1
- package/template/apps/web/src/fields.ts +17 -6
- package/template/apps/web/src/instrument.ts +1 -1
- package/template/apps/web/src/main.tsx +16 -1
- package/template/apps/web/src/platform-client.ts +213 -2
- package/template/apps/web/src/runtime.tsx +4 -2
- package/template/apps/web/src/styles.css +869 -29
- package/template/apps/web/test/contracts.test.ts +369 -10
- package/template/openxiangda.config.ts +24 -5
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +22 -0
- package/template/platform/data/colleges.ts +21 -0
- package/template/platform/data/instruments.ts +1 -1
- package/template/scripts/verify-template-budget.mjs +1 -1
|
@@ -8,7 +8,10 @@ export type FieldKind =
|
|
|
8
8
|
| 'multi'
|
|
9
9
|
| 'email'
|
|
10
10
|
| 'phone'
|
|
11
|
-
| 'file'
|
|
11
|
+
| 'file'
|
|
12
|
+
| 'scope'
|
|
13
|
+
| 'directory-user'
|
|
14
|
+
| 'directory-department';
|
|
12
15
|
|
|
13
16
|
export interface InstrumentField {
|
|
14
17
|
key: string;
|
|
@@ -58,11 +61,11 @@ export const instrumentFields: InstrumentField[] = [
|
|
|
58
61
|
{ key: 'openToOutside', label: '是否对外开放', kind: 'boolean', section: 'usage', required: true },
|
|
59
62
|
{ key: 'subjectAreas', label: '学科领域', kind: 'multi', section: 'usage', options: options([['材料', 'material'], ['生命科学', 'life'], ['化学', 'chemistry'], ['物理', 'physics']]) },
|
|
60
63
|
{ key: 'platformProperty', label: '平台属性', kind: 'select', section: 'usage', options: options([['共享平台设备', 'shared'], ['课题组设备', 'research']]) },
|
|
61
|
-
{ key: 'collegeId', label: '所属学院', kind: '
|
|
62
|
-
{ key: 'manageDepartmentId', label: '管理部门', kind: '
|
|
63
|
-
{ key: 'useDepartmentId', label: '使用部门', kind: '
|
|
64
|
-
{ key: 'instrumentAdminIds', label: '仪器管理员', kind: '
|
|
65
|
-
{ key: 'contactUserIds', label: '联系人', kind: '
|
|
64
|
+
{ key: 'collegeId', label: '所属学院', kind: 'scope', section: 'usage', required: true, createCapability: 'app:instrument-center:instrument:field:college-id:create', updateCapability: 'app:instrument-center:instrument:field:college-id:update' },
|
|
65
|
+
{ key: 'manageDepartmentId', label: '管理部门', kind: 'directory-department', section: 'management', required: true },
|
|
66
|
+
{ key: 'useDepartmentId', label: '使用部门', kind: 'directory-department', section: 'management', required: true },
|
|
67
|
+
{ key: 'instrumentAdminIds', label: '仪器管理员', kind: 'directory-user', section: 'management', required: true, multiple: true, createCapability: 'app:instrument-center:instrument:field:instrument-admin-ids:create', updateCapability: 'app:instrument-center:instrument:field:instrument-admin-ids:update' },
|
|
68
|
+
{ key: 'contactUserIds', label: '联系人', kind: 'directory-user', section: 'management', required: true, multiple: true },
|
|
66
69
|
{ key: 'contactPhone', label: '联系电话', kind: 'phone', section: 'management', required: true },
|
|
67
70
|
{ key: 'contactEmail', label: '联系邮箱', kind: 'email', section: 'management' },
|
|
68
71
|
{ key: 'locationId', label: '存放点', kind: 'select', section: 'materials', options: options([['A 区 101', 'A-101'], ['B 区 203', 'B-203']]) },
|
|
@@ -70,3 +73,11 @@ export const instrumentFields: InstrumentField[] = [
|
|
|
70
73
|
{ key: 'serviceContent', label: '服务内容与主要功能', kind: 'textarea', section: 'materials' },
|
|
71
74
|
{ key: 'attachments', label: '仪器资料', kind: 'file', section: 'materials', multiple: true, maxCount: 8 },
|
|
72
75
|
];
|
|
76
|
+
|
|
77
|
+
const instrumentResourceFieldCodes = new Set(
|
|
78
|
+
instrumentFields.filter(field => !field.system).map(field => field.key)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export function isInstrumentResourceField(field: string) {
|
|
82
|
+
return instrumentResourceFieldCodes.has(field);
|
|
83
|
+
}
|
|
@@ -5,6 +5,7 @@ import React from 'react';
|
|
|
5
5
|
import ReactDOM from 'react-dom/client';
|
|
6
6
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
|
7
7
|
import { instrumentProvider } from './data-provider';
|
|
8
|
+
import { CollegePage } from './CollegePage';
|
|
8
9
|
import { InstrumentDetailPage } from './InstrumentDetailPage';
|
|
9
10
|
import { InstrumentFormPage } from './InstrumentFormPage';
|
|
10
11
|
import { InstrumentListPage } from './InstrumentListPage';
|
|
@@ -16,7 +17,20 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
16
17
|
<React.StrictMode>
|
|
17
18
|
<ConfigProvider
|
|
18
19
|
locale={zhCN}
|
|
19
|
-
theme={{
|
|
20
|
+
theme={{
|
|
21
|
+
token: {
|
|
22
|
+
borderRadius: 8,
|
|
23
|
+
colorBgLayout: '#f3f7fd',
|
|
24
|
+
colorPrimary: '#1677ff',
|
|
25
|
+
colorText: '#172033',
|
|
26
|
+
fontSize: 14,
|
|
27
|
+
},
|
|
28
|
+
components: {
|
|
29
|
+
Card: { headerFontSize: 16 },
|
|
30
|
+
Layout: { bodyBg: '#f3f7fd', headerBg: '#ffffff', siderBg: '#ffffff' },
|
|
31
|
+
Menu: { itemBorderRadius: 8, itemSelectedBg: '#eaf3ff' },
|
|
32
|
+
},
|
|
33
|
+
}}
|
|
20
34
|
>
|
|
21
35
|
<AntdApp>
|
|
22
36
|
<RuntimeBoundary>
|
|
@@ -36,6 +50,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
36
50
|
<Routes>
|
|
37
51
|
<Route path="/" element={<Navigate replace to="/instruments" />} />
|
|
38
52
|
<Route path="/instruments" element={<InstrumentListPage />} />
|
|
53
|
+
<Route path="/colleges" element={<CollegePage />} />
|
|
39
54
|
<Route
|
|
40
55
|
path="/instruments/new"
|
|
41
56
|
element={<InstrumentFormPage mode="create" />}
|
|
@@ -2,17 +2,23 @@ import {
|
|
|
2
2
|
SCHEMA_VERSIONS,
|
|
3
3
|
type DataFileRef,
|
|
4
4
|
type DataFileUploadPlan,
|
|
5
|
+
type DataAuditPage,
|
|
5
6
|
type DataPage,
|
|
6
7
|
type DataQuery,
|
|
7
8
|
type DataRecord,
|
|
9
|
+
type DirectoryEntryPage,
|
|
10
|
+
type DirectoryResolveRequest,
|
|
11
|
+
type NativeScopeValuePage,
|
|
12
|
+
type NativeScopeValueResolveRequest,
|
|
8
13
|
} from 'openxiangda-contracts/browser';
|
|
9
14
|
import type { InstrumentListQuery, InstrumentRecord } from './instrument';
|
|
15
|
+
import { isInstrumentResourceField } from './fields';
|
|
10
16
|
import { applicationCode, runtimeMount } from './runtime-meta';
|
|
11
17
|
|
|
12
18
|
const resourceCode = 'instruments';
|
|
13
19
|
const nativeBase = () =>
|
|
14
20
|
`/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
|
|
15
|
-
const dataBase = () => `${nativeBase()}/data/${
|
|
21
|
+
const dataBase = (code = resourceCode) => `${nativeBase()}/data/${code}`;
|
|
16
22
|
|
|
17
23
|
interface PlatformEnvelope<T> {
|
|
18
24
|
code: number;
|
|
@@ -135,9 +141,125 @@ function currentEnvironmentKey() {
|
|
|
135
141
|
return key;
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
export type DirectoryKind = 'user' | 'department';
|
|
145
|
+
|
|
146
|
+
export async function searchDirectory(
|
|
147
|
+
kind: DirectoryKind,
|
|
148
|
+
options: { keyword: string; cursor?: string }
|
|
149
|
+
) {
|
|
150
|
+
const params = new URLSearchParams({
|
|
151
|
+
environmentKey: currentEnvironmentKey(),
|
|
152
|
+
keyword: options.keyword.trim(),
|
|
153
|
+
limit: '20',
|
|
154
|
+
});
|
|
155
|
+
if (options.cursor) params.set('cursor', options.cursor);
|
|
156
|
+
return request<DirectoryEntryPage>(
|
|
157
|
+
`${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function browseDepartmentTree(options: {
|
|
162
|
+
parentId?: string;
|
|
163
|
+
cursor?: string;
|
|
164
|
+
}) {
|
|
165
|
+
const params = new URLSearchParams({
|
|
166
|
+
environmentKey: currentEnvironmentKey(),
|
|
167
|
+
limit: '100',
|
|
168
|
+
});
|
|
169
|
+
if (options.parentId) params.set('parentId', options.parentId);
|
|
170
|
+
if (options.cursor) params.set('offset', options.cursor);
|
|
171
|
+
return request<DirectoryEntryPage>(
|
|
172
|
+
`${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function browseDepartmentUsers(
|
|
177
|
+
departmentId: string,
|
|
178
|
+
page = 1
|
|
179
|
+
) {
|
|
180
|
+
const params = new URLSearchParams({
|
|
181
|
+
environmentKey: currentEnvironmentKey(),
|
|
182
|
+
page: String(page),
|
|
183
|
+
limit: '20',
|
|
184
|
+
});
|
|
185
|
+
return request<DirectoryEntryPage>(
|
|
186
|
+
`${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(
|
|
187
|
+
departmentId
|
|
188
|
+
)}/users?${params}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function resolveDirectory(
|
|
193
|
+
kind: DirectoryKind,
|
|
194
|
+
ids: string[]
|
|
195
|
+
) {
|
|
196
|
+
const uniqueIds = [...new Set(ids)];
|
|
197
|
+
const body: DirectoryResolveRequest = {
|
|
198
|
+
schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest,
|
|
199
|
+
kind,
|
|
200
|
+
ids: uniqueIds,
|
|
201
|
+
};
|
|
202
|
+
if (body.ids.length === 0) {
|
|
203
|
+
throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
|
|
204
|
+
}
|
|
205
|
+
if (body.ids.length > 50) {
|
|
206
|
+
throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
|
|
207
|
+
}
|
|
208
|
+
return request<DirectoryEntryPage>(
|
|
209
|
+
`${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(
|
|
210
|
+
currentEnvironmentKey()
|
|
211
|
+
)}`,
|
|
212
|
+
{ method: 'POST', body: JSON.stringify(body) }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const scopeValuesBase = () =>
|
|
217
|
+
`${nativeBase()}/data-resources/${resourceCode}/fields/collegeId/scope-values`;
|
|
218
|
+
|
|
219
|
+
export async function searchCollegeScope(
|
|
220
|
+
operation: 'create' | 'update',
|
|
221
|
+
options: { keyword: string; cursor?: string }
|
|
222
|
+
) {
|
|
223
|
+
const params = new URLSearchParams({
|
|
224
|
+
environmentKey: currentEnvironmentKey(),
|
|
225
|
+
dimensionCode: 'college',
|
|
226
|
+
operation,
|
|
227
|
+
keyword: options.keyword.trim(),
|
|
228
|
+
limit: '20',
|
|
229
|
+
});
|
|
230
|
+
if (options.cursor) params.set('cursor', options.cursor);
|
|
231
|
+
return request<NativeScopeValuePage>(`${scopeValuesBase()}?${params}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function resolveCollegeScope(
|
|
235
|
+
operation: 'create' | 'update',
|
|
236
|
+
values: string[]
|
|
237
|
+
) {
|
|
238
|
+
const uniqueValues = [...new Set(values)];
|
|
239
|
+
const body: NativeScopeValueResolveRequest = {
|
|
240
|
+
schemaVersion: SCHEMA_VERSIONS.nativeScopeValueResolveRequest,
|
|
241
|
+
dimensionCode: 'college',
|
|
242
|
+
operation,
|
|
243
|
+
values: uniqueValues,
|
|
244
|
+
};
|
|
245
|
+
if (body.values.length === 0) {
|
|
246
|
+
throw new Error('OPENXIANGDA_SCOPE_RESOLVE_VALUES_REQUIRED');
|
|
247
|
+
}
|
|
248
|
+
if (body.values.length > 50) {
|
|
249
|
+
throw new Error('OPENXIANGDA_SCOPE_RESOLVE_VALUES_EXCEEDED');
|
|
250
|
+
}
|
|
251
|
+
return request<NativeScopeValuePage>(
|
|
252
|
+
`${scopeValuesBase()}/resolve?environmentKey=${encodeURIComponent(
|
|
253
|
+
currentEnvironmentKey()
|
|
254
|
+
)}`,
|
|
255
|
+
{ method: 'POST', body: JSON.stringify(body) }
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
138
259
|
export interface InstrumentDataApiAdapter {
|
|
139
260
|
list(query: InstrumentListQuery): Promise<{ rows: InstrumentRecord[]; total: number }>;
|
|
140
261
|
get(id: string): Promise<InstrumentRecord>;
|
|
262
|
+
audit(id: string): Promise<DataAuditPage>;
|
|
141
263
|
create(data: Partial<InstrumentRecord>): Promise<InstrumentRecord>;
|
|
142
264
|
update(id: string, expectedRevision: number, data: Partial<InstrumentRecord>): Promise<InstrumentRecord>;
|
|
143
265
|
remove(id: string, expectedRevision: number): Promise<InstrumentRecord>;
|
|
@@ -165,10 +287,11 @@ export const instrumentDataApi: InstrumentDataApiAdapter = {
|
|
|
165
287
|
const body: DataQuery = {
|
|
166
288
|
schemaVersion: SCHEMA_VERSIONS.dataQuery,
|
|
167
289
|
filters,
|
|
168
|
-
order: [{ field: String(query.sort?.field || '
|
|
290
|
+
order: [{ field: String(query.sort?.field || 'instrumentCode'), direction: query.sort?.order || 'asc' }],
|
|
169
291
|
limit: query.pageSize,
|
|
170
292
|
offset: (query.page - 1) * query.pageSize,
|
|
171
293
|
};
|
|
294
|
+
assertInstrumentQueryFields(body);
|
|
172
295
|
const page = await request<DataPage<InstrumentRecord>>(`${dataBase()}/query`, {
|
|
173
296
|
method: 'POST',
|
|
174
297
|
body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }),
|
|
@@ -183,6 +306,15 @@ export const instrumentDataApi: InstrumentDataApiAdapter = {
|
|
|
183
306
|
);
|
|
184
307
|
return record.data;
|
|
185
308
|
},
|
|
309
|
+
async audit(id) {
|
|
310
|
+
return await request<DataAuditPage>(
|
|
311
|
+
`${dataBase()}/records/${encodeURIComponent(
|
|
312
|
+
id
|
|
313
|
+
)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(
|
|
314
|
+
currentEnvironmentKey()
|
|
315
|
+
)}`
|
|
316
|
+
);
|
|
317
|
+
},
|
|
186
318
|
async create(data) {
|
|
187
319
|
const record = await request<DataRecord<InstrumentRecord>>(`${dataBase()}/records`, {
|
|
188
320
|
method: 'POST',
|
|
@@ -244,3 +376,82 @@ export const instrumentDataApi: InstrumentDataApiAdapter = {
|
|
|
244
376
|
);
|
|
245
377
|
},
|
|
246
378
|
};
|
|
379
|
+
|
|
380
|
+
export function instrumentFileContentUrl(fileId: string) {
|
|
381
|
+
return `${dataBase()}/files/${encodeURIComponent(
|
|
382
|
+
fileId
|
|
383
|
+
)}/content?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export interface CollegeRecord extends Record<string, unknown> {
|
|
387
|
+
id: string;
|
|
388
|
+
revision: number;
|
|
389
|
+
name: string;
|
|
390
|
+
enabled: boolean;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export const collegeDataApi = {
|
|
394
|
+
async list(page = 1, pageSize = 20) {
|
|
395
|
+
const body: DataQuery = {
|
|
396
|
+
schemaVersion: SCHEMA_VERSIONS.dataQuery,
|
|
397
|
+
order: [{ field: 'name', direction: 'asc' }],
|
|
398
|
+
limit: pageSize,
|
|
399
|
+
offset: (page - 1) * pageSize,
|
|
400
|
+
};
|
|
401
|
+
const result = await request<DataPage<CollegeRecord>>(
|
|
402
|
+
`${dataBase('colleges')}/query`,
|
|
403
|
+
{
|
|
404
|
+
method: 'POST',
|
|
405
|
+
body: JSON.stringify({
|
|
406
|
+
...body,
|
|
407
|
+
environmentKey: currentEnvironmentKey(),
|
|
408
|
+
}),
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
return { rows: result.items, total: result.total };
|
|
412
|
+
},
|
|
413
|
+
async create(data: Pick<CollegeRecord, 'name' | 'enabled'>) {
|
|
414
|
+
const record = await request<DataRecord<CollegeRecord>>(
|
|
415
|
+
`${dataBase('colleges')}/records`,
|
|
416
|
+
{
|
|
417
|
+
method: 'POST',
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
environmentKey: currentEnvironmentKey(),
|
|
420
|
+
data,
|
|
421
|
+
}),
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
return record.data;
|
|
425
|
+
},
|
|
426
|
+
async update(
|
|
427
|
+
id: string,
|
|
428
|
+
expectedRevision: number,
|
|
429
|
+
data: Pick<CollegeRecord, 'name' | 'enabled'>
|
|
430
|
+
) {
|
|
431
|
+
const record = await request<DataRecord<CollegeRecord>>(
|
|
432
|
+
`${dataBase('colleges')}/records/${encodeURIComponent(id)}/update`,
|
|
433
|
+
{
|
|
434
|
+
method: 'POST',
|
|
435
|
+
body: JSON.stringify({
|
|
436
|
+
environmentKey: currentEnvironmentKey(),
|
|
437
|
+
expectedRevision,
|
|
438
|
+
data,
|
|
439
|
+
}),
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
return record.data;
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
function assertInstrumentQueryFields(query: DataQuery) {
|
|
447
|
+
for (const field of [
|
|
448
|
+
...(query.filters || []).map(filter => filter.field),
|
|
449
|
+
...(query.order || []).map(order => order.field),
|
|
450
|
+
]) {
|
|
451
|
+
if (!isInstrumentResourceField(field)) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`OPENXIANGDA_INSTRUMENT_QUERY_FIELD_NOT_DECLARED:${field}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -47,7 +47,7 @@ export function RuntimeBoundary({ children }: { children: React.ReactNode }) {
|
|
|
47
47
|
showIcon
|
|
48
48
|
type="error"
|
|
49
49
|
data-testid="production-data-warning"
|
|
50
|
-
|
|
50
|
+
title="正在使用正式数据:所有新增、编辑和删除都会影响生产环境"
|
|
51
51
|
/>
|
|
52
52
|
)}
|
|
53
53
|
{error ? (
|
|
@@ -60,7 +60,9 @@ export function RuntimeBoundary({ children }: { children: React.ReactNode }) {
|
|
|
60
60
|
) : value ? (
|
|
61
61
|
<RuntimeContext.Provider value={value}>{children}</RuntimeContext.Provider>
|
|
62
62
|
) : (
|
|
63
|
-
<div className="oxa-loading"
|
|
63
|
+
<div className="oxa-loading">
|
|
64
|
+
<Spin description="正在读取平台身份与权限并集" />
|
|
65
|
+
</div>
|
|
64
66
|
)}
|
|
65
67
|
</>
|
|
66
68
|
);
|