openxiangda-cli 2.0.0-alpha.61 → 2.0.0-alpha.66

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 (39) hide show
  1. package/dist/create-workspace.d.ts.map +1 -1
  2. package/dist/create-workspace.js +1 -0
  3. package/dist/create-workspace.js.map +1 -1
  4. package/package.json +4 -4
  5. package/template/AGENTS.md +1 -1
  6. package/template/README.md +7 -7
  7. package/template/apps/server/Dockerfile +2 -1
  8. package/template/apps/server/package.json +1 -1
  9. package/template/apps/server/src/app.module.ts +1 -3
  10. package/template/apps/server/test/smoke.test.ts +6 -23
  11. package/template/apps/web/e2e/resources.spec.ts +20 -0
  12. package/template/apps/web/index.html +1 -1
  13. package/template/apps/web/package.json +1 -1
  14. package/template/apps/web/src/AuthoritativeSelector.tsx +298 -41
  15. package/template/apps/web/src/Shell.tsx +39 -211
  16. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  17. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +229 -16
  18. package/template/apps/web/src/components/resource/SurfaceFields.tsx +142 -2
  19. package/template/apps/web/src/data-provider.ts +23 -102
  20. package/template/apps/web/src/main.tsx +35 -83
  21. package/template/apps/web/src/platform-client.ts +112 -424
  22. package/template/apps/web/src/runtime-meta.ts +1 -1
  23. package/template/apps/web/src/styles.css +285 -3
  24. package/template/apps/web/test/contracts.test.ts +43 -769
  25. package/template/openxiangda.config.ts +14 -81
  26. package/template/package.json +3 -3
  27. package/template/packages/contracts/src/generated.ts +7 -804
  28. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  29. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  30. package/template/apps/web/src/CollegePage.tsx +0 -181
  31. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
  32. package/template/apps/web/src/InstrumentForm.tsx +0 -380
  33. package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
  34. package/template/apps/web/src/InstrumentListPage.tsx +0 -457
  35. package/template/apps/web/src/fields.ts +0 -18
  36. package/template/apps/web/src/instrument.ts +0 -56
  37. package/template/platform/data/colleges.ts +0 -21
  38. package/template/platform/data/instrument-surface.ts +0 -125
  39. package/template/platform/data/instruments.ts +0 -83
@@ -1,57 +1,73 @@
1
1
  import {
2
2
  SCHEMA_VERSIONS,
3
+ type DataAuditPage,
3
4
  type DataFilePreview,
4
5
  type DataFileRef,
5
6
  type DataFileUploadPlan,
6
- type DataAuditPage,
7
7
  type DataPage,
8
8
  type DataQuery,
9
9
  type DataRecord,
10
+ type DataResourceSurface,
10
11
  type DirectoryEntryPage,
11
12
  type DirectoryResolveRequest,
12
- type NativeScopeValuePage,
13
- type NativeScopeValueResolveRequest,
14
- type DataResourceSurface,
15
13
  } from 'openxiangda-contracts/browser';
16
- import type { InstrumentListQuery, InstrumentRecord } from './instrument';
17
- import { instrumentSurface, isInstrumentResourceField } from './fields';
18
14
  import { applicationCode, runtimeMount } from './runtime-meta';
19
-
20
- const resourceCode = 'instruments';
21
- const nativeBase = () =>
22
- `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
23
- const dataBase = (code = resourceCode) => `${nativeBase()}/data/${code}`;
15
+ import { useSyncExternalStore } from 'react';
24
16
 
25
17
  interface PlatformEnvelope<T> {
26
18
  code: number;
27
19
  message?: string;
28
20
  errorCode?: string;
21
+ requestId?: string;
29
22
  data: T;
30
23
  }
31
24
 
32
- async function request<T>(path: string, init?: RequestInit): Promise<T> {
33
- const response = await fetch(path, {
34
- credentials: 'include',
35
- ...init,
36
- headers: {
37
- accept: 'application/json',
38
- ...(init?.body ? { 'content-type': 'application/json' } : {}),
39
- ...init?.headers,
25
+ let globalRequestCount = 0;
26
+ const globalRequestListeners = new Set<() => void>();
27
+
28
+ function beginGlobalRequest() {
29
+ globalRequestCount += 1;
30
+ globalRequestListeners.forEach(listener => listener());
31
+ }
32
+
33
+ function endGlobalRequest() {
34
+ globalRequestCount = Math.max(0, globalRequestCount - 1);
35
+ globalRequestListeners.forEach(listener => listener());
36
+ }
37
+
38
+ export function useGlobalRequestLoading() {
39
+ return useSyncExternalStore(
40
+ listener => {
41
+ globalRequestListeners.add(listener);
42
+ return () => globalRequestListeners.delete(listener);
40
43
  },
41
- });
42
- const payload = (await response.json().catch(() => null)) as
43
- | PlatformEnvelope<T>
44
- | T
45
- | null;
46
- const envelope =
47
- payload && typeof payload === 'object' && 'code' in payload
48
- ? (payload as PlatformEnvelope<T>)
49
- : null;
50
- if (!response.ok || (envelope && envelope.code !== 200)) {
51
- const code = envelope?.errorCode || `HTTP_${response.status}`;
52
- throw new Error(`${code}: ${envelope?.message || '平台请求失败'}`);
44
+ () => globalRequestCount > 0,
45
+ () => false,
46
+ );
47
+ }
48
+
49
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
50
+ beginGlobalRequest();
51
+ try {
52
+ const response = await fetch(path, {
53
+ credentials: 'include',
54
+ ...init,
55
+ headers: {
56
+ accept: 'application/json',
57
+ ...(init?.body ? { 'content-type': 'application/json' } : {}),
58
+ ...init?.headers,
59
+ },
60
+ });
61
+ const payload = (await response.json().catch(() => null)) as PlatformEnvelope<T> | T | null;
62
+ const envelope = payload && typeof payload === 'object' && 'code' in payload ? payload as PlatformEnvelope<T> : null;
63
+ if (!response.ok || (envelope && envelope.code !== 200)) {
64
+ const requestId = envelope?.requestId ? ` (requestId: ${envelope.requestId})` : '';
65
+ throw new Error(`${envelope?.errorCode || `HTTP_${response.status}`}: ${envelope?.message || '平台请求失败'}${requestId}`);
66
+ }
67
+ return envelope ? envelope.data : payload as T;
68
+ } finally {
69
+ endGlobalRequest();
53
70
  }
54
- return envelope ? envelope.data : (payload as T);
55
71
  }
56
72
 
57
73
  export interface RuntimeIdentity {
@@ -72,33 +88,14 @@ export interface RuntimeIdentity {
72
88
 
73
89
  interface ConnectedCurrent {
74
90
  environment: RuntimeIdentity['environment'];
75
- principal: {
76
- userId: string;
77
- roleCodes: string[];
78
- capabilityCodes: string[];
79
- isAppSuperAdmin: boolean;
80
- };
91
+ principal: Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
81
92
  }
82
93
 
83
94
  interface DeployedCurrent {
84
95
  schemaVersion: 'openxiangda.current-user/v2';
85
96
  appCode: string;
86
- environment: {
87
- id: string;
88
- key: 'preproduction' | 'production';
89
- activeAppVersionId: string;
90
- headRevision: number;
91
- authzRevisionId: string;
92
- authzVersion: number;
93
- scopeDataVersion: string;
94
- };
95
- principal: {
96
- type: 'user';
97
- userId: string;
98
- roleCodes: string[];
99
- capabilityCodes: string[];
100
- isAppSuperAdmin: boolean;
101
- };
97
+ environment: RuntimeIdentity['environment'];
98
+ principal: { type: 'user' } & Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
102
99
  }
103
100
 
104
101
  let activeIdentity: RuntimeIdentity | undefined;
@@ -106,35 +103,16 @@ let activeIdentity: RuntimeIdentity | undefined;
106
103
  export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
107
104
  const mount = runtimeMount();
108
105
  if (mount) {
109
- const current = await request<DeployedCurrent>(
110
- `${nativeBase()}/current-user?environmentKey=${encodeURIComponent(
111
- mount.environmentKey
112
- )}`
113
- );
114
- if (
115
- current.schemaVersion !== 'openxiangda.current-user/v2' ||
116
- current.appCode !== mount.appCode ||
117
- current.environment.key !== mount.environmentKey ||
118
- current.principal.type !== 'user' ||
119
- !Array.isArray(current.principal.roleCodes) ||
120
- !Array.isArray(current.principal.capabilityCodes)
121
- ) {
106
+ const current = await request<DeployedCurrent>(`${nativeBase()}/current-user?environmentKey=${encodeURIComponent(mount.environmentKey)}`);
107
+ if (current.schemaVersion !== 'openxiangda.current-user/v2' || current.appCode !== mount.appCode || current.environment.key !== mount.environmentKey || current.principal.type !== 'user') {
122
108
  throw new Error('OPENXIANGDA_DEPLOYED_CURRENT_USER_INVALID');
123
109
  }
124
110
  activeIdentity = { ...current.principal, environment: current.environment };
125
111
  return activeIdentity;
126
112
  }
127
- const connectedPath =
128
- `/service/openxiangda-api/v2/applications/${applicationCode()}/dev-sessions/current`;
129
- try {
130
- const current = await request<ConnectedCurrent>(connectedPath);
131
- activeIdentity = { ...current.principal, environment: current.environment };
132
- return activeIdentity;
133
- } catch (error) {
134
- throw new Error(
135
- `OPENXIANGDA_CONNECTED_CURRENT_USER_FAILED: ${error instanceof Error ? error.message : String(error)}`
136
- );
137
- }
113
+ const current = await request<ConnectedCurrent>(`/service/openxiangda-api/v2/applications/${applicationCode()}/dev-sessions/current`);
114
+ activeIdentity = { ...current.principal, environment: current.environment };
115
+ return activeIdentity;
138
116
  }
139
117
 
140
118
  function currentEnvironmentKey() {
@@ -145,244 +123,59 @@ function currentEnvironmentKey() {
145
123
 
146
124
  export type DirectoryKind = 'user' | 'department';
147
125
 
148
- export async function searchDirectory(
149
- kind: DirectoryKind,
150
- options: { keyword: string; cursor?: string }
151
- ) {
152
- const params = new URLSearchParams({
153
- environmentKey: currentEnvironmentKey(),
154
- keyword: options.keyword.trim(),
155
- limit: '20',
156
- });
126
+ export async function searchDirectory(kind: DirectoryKind, options: { keyword: string; cursor?: string }) {
127
+ const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), keyword: options.keyword.trim(), limit: '20' });
157
128
  if (options.cursor) params.set('cursor', options.cursor);
158
- return request<DirectoryEntryPage>(
159
- `${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`
160
- );
129
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`);
161
130
  }
162
131
 
163
- export async function browseDepartmentTree(options: {
164
- parentId?: string;
165
- cursor?: string;
166
- }) {
167
- const params = new URLSearchParams({
168
- environmentKey: currentEnvironmentKey(),
169
- limit: '100',
170
- });
132
+ export async function browseDepartmentTree(options: { parentId?: string; cursor?: string }) {
133
+ const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), limit: '100' });
171
134
  if (options.parentId) params.set('parentId', options.parentId);
172
135
  if (options.cursor) params.set('offset', options.cursor);
173
- return request<DirectoryEntryPage>(
174
- `${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`
175
- );
136
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`);
176
137
  }
177
138
 
178
- export async function browseDepartmentUsers(
179
- departmentId: string,
180
- page = 1
181
- ) {
182
- const params = new URLSearchParams({
183
- environmentKey: currentEnvironmentKey(),
184
- page: String(page),
185
- limit: '20',
186
- });
187
- return request<DirectoryEntryPage>(
188
- `${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(
189
- departmentId
190
- )}/users?${params}`
191
- );
139
+ export async function browseDepartmentUsers(departmentId: string, page = 1) {
140
+ const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), page: String(page), limit: '20' });
141
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(departmentId)}/users?${params}`);
192
142
  }
193
143
 
194
- export async function resolveDirectory(
195
- kind: DirectoryKind,
196
- ids: string[]
197
- ) {
144
+ export async function resolveDirectory(kind: DirectoryKind, ids: string[]) {
198
145
  const uniqueIds = [...new Set(ids)];
199
- const body: DirectoryResolveRequest = {
200
- schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest,
201
- kind,
202
- ids: uniqueIds,
203
- };
204
- if (body.ids.length === 0) {
205
- throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
206
- }
207
- if (body.ids.length > 50) {
208
- throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
209
- }
210
- return request<DirectoryEntryPage>(
211
- `${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(
212
- currentEnvironmentKey()
213
- )}`,
214
- { method: 'POST', body: JSON.stringify(body) }
215
- );
146
+ if (uniqueIds.length === 0) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
147
+ if (uniqueIds.length > 50) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
148
+ const body: DirectoryResolveRequest = { schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest, kind, ids: uniqueIds };
149
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`, { method: 'POST', body: JSON.stringify(body) });
216
150
  }
217
151
 
218
- const scopeValuesBase = () =>
219
- `${nativeBase()}/data-resources/${resourceCode}/fields/collegeId/scope-values`;
220
-
221
- export async function searchCollegeScope(
222
- operation: 'create' | 'update',
223
- options: { keyword: string; cursor?: string }
224
- ) {
225
- const params = new URLSearchParams({
226
- environmentKey: currentEnvironmentKey(),
227
- dimensionCode: 'college',
228
- operation,
229
- keyword: options.keyword.trim(),
230
- limit: '20',
231
- });
232
- if (options.cursor) params.set('cursor', options.cursor);
233
- return request<NativeScopeValuePage>(`${scopeValuesBase()}?${params}`);
234
- }
235
-
236
- export async function resolveCollegeScope(
237
- operation: 'create' | 'update',
238
- values: string[]
239
- ) {
240
- const uniqueValues = [...new Set(values)];
241
- const body: NativeScopeValueResolveRequest = {
242
- schemaVersion: SCHEMA_VERSIONS.nativeScopeValueResolveRequest,
243
- dimensionCode: 'college',
244
- operation,
245
- values: uniqueValues,
152
+ export async function searchResource(resourceCode: string, labelField: string, options: { keyword: string; cursor?: string }) {
153
+ const pageSize = 20;
154
+ const offset = options.cursor ? Number(options.cursor) : 0;
155
+ const query: DataQuery = {
156
+ schemaVersion: SCHEMA_VERSIONS.dataQuery,
157
+ filters: options.keyword.trim() ? [{ field: labelField, operator: 'ilike', value: `%${options.keyword.trim()}%` }] : [],
158
+ order: [{ field: labelField, direction: 'asc' }],
159
+ limit: pageSize,
160
+ offset: Number.isSafeInteger(offset) && offset >= 0 ? offset : 0,
161
+ };
162
+ const page = await request<DataPage<Record<string, unknown>>>(`${dataBase(resourceCode)}/query`, { method: 'POST', body: JSON.stringify({ ...query, environmentKey: currentEnvironmentKey() }) });
163
+ return {
164
+ items: page.items.map(item => ({ id: String(item.id), label: String(item[labelField] ?? item.id), selectable: true })),
165
+ nextCursor: page.items.length >= pageSize ? String(page.offset + page.items.length) : null,
246
166
  };
247
- if (body.values.length === 0) {
248
- throw new Error('OPENXIANGDA_SCOPE_RESOLVE_VALUES_REQUIRED');
249
- }
250
- if (body.values.length > 50) {
251
- throw new Error('OPENXIANGDA_SCOPE_RESOLVE_VALUES_EXCEEDED');
252
- }
253
- return request<NativeScopeValuePage>(
254
- `${scopeValuesBase()}/resolve?environmentKey=${encodeURIComponent(
255
- currentEnvironmentKey()
256
- )}`,
257
- { method: 'POST', body: JSON.stringify(body) }
258
- );
259
167
  }
260
168
 
261
- export interface InstrumentDataApiAdapter {
262
- list(query: InstrumentListQuery): Promise<{ rows: InstrumentRecord[]; total: number }>;
263
- get(id: string): Promise<InstrumentRecord>;
264
- audit(id: string): Promise<DataAuditPage>;
265
- create(data: Partial<InstrumentRecord>): Promise<InstrumentRecord>;
266
- update(id: string, expectedRevision: number, data: Partial<InstrumentRecord>): Promise<InstrumentRecord>;
267
- remove(id: string, expectedRevision: number): Promise<InstrumentRecord>;
268
- upload(fieldCode: 'image' | 'attachments', file: File, recordId?: string): Promise<DataFileRef>;
169
+ export async function resolveResource(resourceCode: string, ids: string[], labelField: string) {
170
+ if (ids.length === 0) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_REQUIRED');
171
+ if (ids.length > 50) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_EXCEEDED');
172
+ const items = await Promise.all(ids.map(async id => {
173
+ const record = await request<DataRecord<Record<string, unknown>>>(`${dataBase(resourceCode)}/records/${encodeURIComponent(id)}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
174
+ return { id, label: String(record.data[labelField] ?? id), selectable: true };
175
+ }));
176
+ return { items, nextCursor: null };
269
177
  }
270
178
 
271
- export const instrumentDataApi: InstrumentDataApiAdapter = {
272
- async list(query) {
273
- const filters: NonNullable<DataQuery['filters']> = [];
274
- // The current DataQuery contract has AND filters but no grouped OR. Keep
275
- // this honest single-field search until the platform adds a native OR AST.
276
- if (query.keyword?.trim()) {
277
- const searchableFields = instrumentSurface.list?.searchableFields || [];
278
- const field = searchableFields.includes(query.searchField || '')
279
- ? query.searchField!
280
- : searchableFields[0] || 'chineseName';
281
- filters.push({ field, operator: 'ilike', value: `%${query.keyword.trim()}%` });
282
- }
283
- if (query.collegeId) filters.push({ field: 'collegeId', operator: 'eq', value: query.collegeId });
284
- if (query.instrumentAdminId) filters.push({ field: 'instrumentAdminIds', operator: 'cs', value: [query.instrumentAdminId] });
285
- if (query.instrumentStatus) filters.push({ field: 'instrumentStatus', operator: 'eq', value: query.instrumentStatus });
286
- if (query.openToOutside !== undefined) filters.push({ field: 'openToOutside', operator: 'eq', value: query.openToOutside });
287
- if (query.enabledDate) {
288
- filters.push({ field: 'enabledDate', operator: 'gte', value: query.enabledDate[0] });
289
- filters.push({ field: 'enabledDate', operator: 'lte', value: query.enabledDate[1] });
290
- }
291
- if (query.assetValue?.[0] !== undefined) filters.push({ field: 'assetValue', operator: 'gte', value: query.assetValue[0] });
292
- if (query.assetValue?.[1] !== undefined) filters.push({ field: 'assetValue', operator: 'lte', value: query.assetValue[1] });
293
- const body: DataQuery = {
294
- schemaVersion: SCHEMA_VERSIONS.dataQuery,
295
- filters,
296
- order: [{ field: String(query.sort?.field || 'instrumentCode'), direction: query.sort?.order || 'asc' }],
297
- limit: query.pageSize,
298
- offset: (query.page - 1) * query.pageSize,
299
- };
300
- assertInstrumentQueryFields(body);
301
- const page = await request<DataPage<InstrumentRecord>>(`${dataBase()}/query`, {
302
- method: 'POST',
303
- body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }),
304
- });
305
- return { rows: page.items, total: page.total };
306
- },
307
- async get(id) {
308
- const record = await request<DataRecord<InstrumentRecord>>(
309
- `${dataBase()}/records/${encodeURIComponent(
310
- id
311
- )}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
312
- );
313
- return record.data;
314
- },
315
- async audit(id) {
316
- return await request<DataAuditPage>(
317
- `${dataBase()}/records/${encodeURIComponent(
318
- id
319
- )}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(
320
- currentEnvironmentKey()
321
- )}`
322
- );
323
- },
324
- async create(data) {
325
- const record = await request<DataRecord<InstrumentRecord>>(`${dataBase()}/records`, {
326
- method: 'POST',
327
- body: JSON.stringify({ environmentKey: currentEnvironmentKey(), data }),
328
- });
329
- return record.data;
330
- },
331
- async update(id, expectedRevision, data) {
332
- const record = await request<DataRecord<InstrumentRecord>>(
333
- `${dataBase()}/records/${encodeURIComponent(id)}/update`,
334
- {
335
- method: 'POST',
336
- body: JSON.stringify({
337
- environmentKey: currentEnvironmentKey(),
338
- expectedRevision,
339
- data,
340
- }),
341
- }
342
- );
343
- return record.data;
344
- },
345
- async remove(id, expectedRevision) {
346
- const record = await request<DataRecord<InstrumentRecord>>(
347
- `${dataBase()}/records/${encodeURIComponent(id)}/delete`,
348
- {
349
- method: 'POST',
350
- body: JSON.stringify({
351
- environmentKey: currentEnvironmentKey(),
352
- expectedRevision,
353
- }),
354
- }
355
- );
356
- return record.data;
357
- },
358
- async upload(fieldCode, file, recordId) {
359
- const plan = await request<DataFileUploadPlan>(`${dataBase()}/files/uploads/initiate`, {
360
- method: 'POST',
361
- body: JSON.stringify({
362
- environmentKey: currentEnvironmentKey(),
363
- fieldCode,
364
- fileName: file.name,
365
- fileSize: file.size,
366
- contentType: file.type || 'application/octet-stream',
367
- ...(recordId ? { recordId } : {}),
368
- }),
369
- });
370
- const uploaded = await fetch(plan.uploadUrl, {
371
- method: plan.uploadMethod,
372
- headers: plan.headers,
373
- body: file,
374
- });
375
- if (!uploaded.ok) throw new Error(`FILE_UPLOAD_FAILED: HTTP_${uploaded.status}`);
376
- return await request<DataFileRef>(
377
- `${dataBase()}/files/${encodeURIComponent(plan.file.id)}/complete`,
378
- {
379
- method: 'POST',
380
- body: JSON.stringify({ environmentKey: currentEnvironmentKey() }),
381
- }
382
- );
383
- },
384
- };
385
-
386
179
  export interface GenericResourceQuery {
387
180
  page: number;
388
181
  pageSize: number;
@@ -392,17 +185,11 @@ export interface GenericResourceQuery {
392
185
  sort?: { field: string; order: 'asc' | 'desc' };
393
186
  }
394
187
 
395
- /** Native CRUD adapter used by resources that opt into generated pages. */
396
- export function createNativeResourceClient(
397
- code: string,
398
- surface: DataResourceSurface
399
- ) {
188
+ export function createNativeResourceClient(code: string, surface: DataResourceSurface) {
400
189
  const base = dataBase(code);
401
190
  const declaredFields = new Set(Object.keys(surface.fields));
402
191
  const assertField = (field: string) => {
403
- if (!declaredFields.has(field)) {
404
- throw new Error(`OPENXIANGDA_RESOURCE_QUERY_FIELD_NOT_DECLARED:${code}:${field}`);
405
- }
192
+ if (!declaredFields.has(field)) throw new Error(`OPENXIANGDA_RESOURCE_QUERY_FIELD_NOT_DECLARED:${code}:${field}`);
406
193
  return field;
407
194
  };
408
195
  const filtersFor = (query: GenericResourceQuery) => {
@@ -418,7 +205,7 @@ export function createNativeResourceClient(
418
205
  if (Array.isArray(value) && value.length === 2 && ['date', 'datetime', 'number', 'money', 'percent'].includes(field?.widget || '')) {
419
206
  if (value[0] !== undefined && value[0] !== null && value[0] !== '') filters.push({ field: fieldCode, operator: 'gte', value: value[0] });
420
207
  if (value[1] !== undefined && value[1] !== null && value[1] !== '') filters.push({ field: fieldCode, operator: 'lte', value: value[1] });
421
- } else if (Array.isArray(value) && ['multi', 'directory-user', 'directory-department'].includes(field?.widget || '')) {
208
+ } else if (Array.isArray(value) && ['multi', 'directory-user', 'directory-department', 'resource'].includes(field?.widget || '')) {
422
209
  filters.push({ field: fieldCode, operator: 'cs', value });
423
210
  } else {
424
211
  filters.push({ field: fieldCode, operator: 'eq', value });
@@ -429,17 +216,8 @@ export function createNativeResourceClient(
429
216
  return {
430
217
  async list(query: GenericResourceQuery) {
431
218
  const sort = query.sort || surface.list?.defaultSort || { field: Object.keys(surface.fields)[0] || 'id', order: 'asc' as const };
432
- const body: DataQuery = {
433
- schemaVersion: SCHEMA_VERSIONS.dataQuery,
434
- filters: filtersFor(query),
435
- order: [{ field: assertField(sort.field), direction: sort.order || 'asc' }],
436
- limit: query.pageSize,
437
- offset: (query.page - 1) * query.pageSize,
438
- };
439
- const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, {
440
- method: 'POST',
441
- body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }),
442
- });
219
+ const body: DataQuery = { schemaVersion: SCHEMA_VERSIONS.dataQuery, filters: filtersFor(query), order: [{ field: assertField(sort.field), direction: sort.order || 'asc' }], limit: query.pageSize, offset: (query.page - 1) * query.pageSize };
220
+ const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, { method: 'POST', body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }) });
443
221
  return { rows: page.items, total: page.total };
444
222
  },
445
223
  async get(id: string) {
@@ -447,7 +225,7 @@ export function createNativeResourceClient(
447
225
  return record.data;
448
226
  },
449
227
  async audit(id: string) {
450
- return await request<DataAuditPage>(`${base}/records/${encodeURIComponent(id)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
228
+ return request<DataAuditPage>(`${base}/records/${encodeURIComponent(id)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
451
229
  },
452
230
  async create(data: Record<string, unknown>) {
453
231
  const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey(), data }) });
@@ -466,120 +244,30 @@ export function createNativeResourceClient(
466
244
  const plan = await request<DataFileUploadPlan>(`${base}/files/uploads/initiate`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey(), fieldCode, fileName: file.name, fileSize: file.size, contentType: file.type || 'application/octet-stream', ...(recordId ? { recordId } : {}) }) });
467
245
  const uploaded = await fetch(plan.uploadUrl, { method: plan.uploadMethod, headers: plan.headers, body: file });
468
246
  if (!uploaded.ok) throw new Error(`FILE_UPLOAD_FAILED: HTTP_${uploaded.status}`);
469
- return await request<DataFileRef>(`${base}/files/${encodeURIComponent(plan.file.id)}/complete`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey() }) });
247
+ return request<DataFileRef>(`${base}/files/${encodeURIComponent(plan.file.id)}/complete`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey() }) });
470
248
  },
471
249
  };
472
250
  }
473
251
 
474
- export function dataFileContentUrl(
475
- resource: string,
476
- fileId: string,
477
- disposition: 'attachment' | 'inline' = 'attachment'
478
- ) {
479
- const query = new URLSearchParams({
480
- environmentKey: currentEnvironmentKey(),
481
- disposition,
482
- });
483
- return `${dataBase(resource)}/files/${encodeURIComponent(
484
- fileId
485
- )}/content?${query.toString()}`;
252
+ function nativeBase() {
253
+ return `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
486
254
  }
487
255
 
488
- export async function loadDataFilePreview(
489
- resource: string,
490
- fileId: string
491
- ) {
492
- return await request<DataFilePreview>(
493
- `${dataBase(resource)}/files/${encodeURIComponent(
494
- fileId
495
- )}/preview?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
496
- );
256
+ function dataBase(code: string) {
257
+ return `${nativeBase()}/data/${encodeURIComponent(code)}`;
497
258
  }
498
259
 
499
- export async function fetchDataFileBlob(resource: string, fileId: string) {
500
- const response = await fetch(dataFileContentUrl(resource, fileId, 'inline'), {
501
- credentials: 'include',
502
- headers: { accept: 'application/octet-stream,*/*' },
503
- });
504
- if (!response.ok) {
505
- const payload = (await response.json().catch(() => null)) as
506
- | PlatformEnvelope<null>
507
- | null;
508
- const code = payload?.errorCode || `HTTP_${response.status}`;
509
- throw new Error(`${code}: ${payload?.message || '文件内容读取失败'}`);
510
- }
511
- return await response.blob();
260
+ export function dataFileContentUrl(resource: string, fileId: string, disposition: 'attachment' | 'inline' = 'attachment') {
261
+ const query = new URLSearchParams({ environmentKey: currentEnvironmentKey(), disposition });
262
+ return `${dataBase(resource)}/files/${encodeURIComponent(fileId)}/content?${query}`;
512
263
  }
513
264
 
514
- export interface CollegeRecord extends Record<string, unknown> {
515
- id: string;
516
- revision: number;
517
- name: string;
518
- enabled: boolean;
265
+ export async function loadDataFilePreview(resource: string, fileId: string) {
266
+ return request<DataFilePreview>(`${dataBase(resource)}/files/${encodeURIComponent(fileId)}/preview?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
519
267
  }
520
268
 
521
- export const collegeDataApi = {
522
- async list(page = 1, pageSize = 20) {
523
- const body: DataQuery = {
524
- schemaVersion: SCHEMA_VERSIONS.dataQuery,
525
- order: [{ field: 'name', direction: 'asc' }],
526
- limit: pageSize,
527
- offset: (page - 1) * pageSize,
528
- };
529
- const result = await request<DataPage<CollegeRecord>>(
530
- `${dataBase('colleges')}/query`,
531
- {
532
- method: 'POST',
533
- body: JSON.stringify({
534
- ...body,
535
- environmentKey: currentEnvironmentKey(),
536
- }),
537
- }
538
- );
539
- return { rows: result.items, total: result.total };
540
- },
541
- async create(data: Pick<CollegeRecord, 'name' | 'enabled'>) {
542
- const record = await request<DataRecord<CollegeRecord>>(
543
- `${dataBase('colleges')}/records`,
544
- {
545
- method: 'POST',
546
- body: JSON.stringify({
547
- environmentKey: currentEnvironmentKey(),
548
- data,
549
- }),
550
- }
551
- );
552
- return record.data;
553
- },
554
- async update(
555
- id: string,
556
- expectedRevision: number,
557
- data: Pick<CollegeRecord, 'name' | 'enabled'>
558
- ) {
559
- const record = await request<DataRecord<CollegeRecord>>(
560
- `${dataBase('colleges')}/records/${encodeURIComponent(id)}/update`,
561
- {
562
- method: 'POST',
563
- body: JSON.stringify({
564
- environmentKey: currentEnvironmentKey(),
565
- expectedRevision,
566
- data,
567
- }),
568
- }
569
- );
570
- return record.data;
571
- },
572
- };
573
-
574
- function assertInstrumentQueryFields(query: DataQuery) {
575
- for (const field of [
576
- ...(query.filters || []).map(filter => filter.field),
577
- ...(query.order || []).map(order => order.field),
578
- ]) {
579
- if (!isInstrumentResourceField(field)) {
580
- throw new Error(
581
- `OPENXIANGDA_INSTRUMENT_QUERY_FIELD_NOT_DECLARED:${field}`
582
- );
583
- }
584
- }
269
+ export async function fetchDataFileBlob(resource: string, fileId: string) {
270
+ const response = await fetch(dataFileContentUrl(resource, fileId, 'inline'), { credentials: 'include', headers: { accept: 'application/octet-stream,*/*' } });
271
+ if (!response.ok) throw new Error(`FILE_CONTENT_READ_FAILED: HTTP_${response.status}`);
272
+ return response.blob();
585
273
  }
@@ -31,7 +31,7 @@ export function runtimeMount() {
31
31
  }
32
32
 
33
33
  export function applicationCode() {
34
- return runtimeMount()?.appCode || 'instrument-center';
34
+ return runtimeMount()?.appCode || 'openxiangda-application';
35
35
  }
36
36
 
37
37
  export function resolveApplicationBasename(mount: RuntimeMount | null) {