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

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 (38) 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/package.json +1 -1
  8. package/template/apps/server/src/app.module.ts +1 -3
  9. package/template/apps/server/test/smoke.test.ts +6 -23
  10. package/template/apps/web/e2e/resources.spec.ts +20 -0
  11. package/template/apps/web/index.html +1 -1
  12. package/template/apps/web/package.json +1 -1
  13. package/template/apps/web/src/AuthoritativeSelector.tsx +38 -29
  14. package/template/apps/web/src/Shell.tsx +39 -211
  15. package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
  16. package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +26 -5
  17. package/template/apps/web/src/components/resource/SurfaceFields.tsx +32 -1
  18. package/template/apps/web/src/data-provider.ts +23 -102
  19. package/template/apps/web/src/main.tsx +22 -82
  20. package/template/apps/web/src/platform-client.ts +71 -415
  21. package/template/apps/web/src/runtime-meta.ts +1 -1
  22. package/template/apps/web/src/styles.css +3 -3
  23. package/template/apps/web/test/contracts.test.ts +31 -769
  24. package/template/openxiangda.config.ts +14 -81
  25. package/template/package.json +3 -3
  26. package/template/packages/contracts/src/generated.ts +7 -804
  27. package/template/apps/server/src/instrument-context.controller.ts +0 -25
  28. package/template/apps/web/e2e/instruments.spec.ts +0 -1338
  29. package/template/apps/web/src/CollegePage.tsx +0 -181
  30. package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
  31. package/template/apps/web/src/InstrumentForm.tsx +0 -380
  32. package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
  33. package/template/apps/web/src/InstrumentListPage.tsx +0 -457
  34. package/template/apps/web/src/fields.ts +0 -18
  35. package/template/apps/web/src/instrument.ts +0 -56
  36. package/template/platform/data/colleges.ts +0 -21
  37. package/template/platform/data/instrument-surface.ts +0 -125
  38. package/template/platform/data/instruments.ts +0 -83
@@ -1,27 +1,18 @@
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
15
 
20
- const resourceCode = 'instruments';
21
- const nativeBase = () =>
22
- `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
23
- const dataBase = (code = resourceCode) => `${nativeBase()}/data/${code}`;
24
-
25
16
  interface PlatformEnvelope<T> {
26
17
  code: number;
27
18
  message?: string;
@@ -39,19 +30,12 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
39
30
  ...init?.headers,
40
31
  },
41
32
  });
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;
33
+ const payload = (await response.json().catch(() => null)) as PlatformEnvelope<T> | T | null;
34
+ const envelope = payload && typeof payload === 'object' && 'code' in payload ? payload as PlatformEnvelope<T> : null;
50
35
  if (!response.ok || (envelope && envelope.code !== 200)) {
51
- const code = envelope?.errorCode || `HTTP_${response.status}`;
52
- throw new Error(`${code}: ${envelope?.message || '平台请求失败'}`);
36
+ throw new Error(`${envelope?.errorCode || `HTTP_${response.status}`}: ${envelope?.message || '平台请求失败'}`);
53
37
  }
54
- return envelope ? envelope.data : (payload as T);
38
+ return envelope ? envelope.data : payload as T;
55
39
  }
56
40
 
57
41
  export interface RuntimeIdentity {
@@ -72,33 +56,14 @@ export interface RuntimeIdentity {
72
56
 
73
57
  interface ConnectedCurrent {
74
58
  environment: RuntimeIdentity['environment'];
75
- principal: {
76
- userId: string;
77
- roleCodes: string[];
78
- capabilityCodes: string[];
79
- isAppSuperAdmin: boolean;
80
- };
59
+ principal: Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
81
60
  }
82
61
 
83
62
  interface DeployedCurrent {
84
63
  schemaVersion: 'openxiangda.current-user/v2';
85
64
  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
- };
65
+ environment: RuntimeIdentity['environment'];
66
+ principal: { type: 'user' } & Pick<RuntimeIdentity, 'userId' | 'roleCodes' | 'capabilityCodes' | 'isAppSuperAdmin'>;
102
67
  }
103
68
 
104
69
  let activeIdentity: RuntimeIdentity | undefined;
@@ -106,35 +71,16 @@ let activeIdentity: RuntimeIdentity | undefined;
106
71
  export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
107
72
  const mount = runtimeMount();
108
73
  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
- ) {
74
+ const current = await request<DeployedCurrent>(`${nativeBase()}/current-user?environmentKey=${encodeURIComponent(mount.environmentKey)}`);
75
+ if (current.schemaVersion !== 'openxiangda.current-user/v2' || current.appCode !== mount.appCode || current.environment.key !== mount.environmentKey || current.principal.type !== 'user') {
122
76
  throw new Error('OPENXIANGDA_DEPLOYED_CURRENT_USER_INVALID');
123
77
  }
124
78
  activeIdentity = { ...current.principal, environment: current.environment };
125
79
  return activeIdentity;
126
80
  }
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
- }
81
+ const current = await request<ConnectedCurrent>(`/service/openxiangda-api/v2/applications/${applicationCode()}/dev-sessions/current`);
82
+ activeIdentity = { ...current.principal, environment: current.environment };
83
+ return activeIdentity;
138
84
  }
139
85
 
140
86
  function currentEnvironmentKey() {
@@ -145,244 +91,59 @@ function currentEnvironmentKey() {
145
91
 
146
92
  export type DirectoryKind = 'user' | 'department';
147
93
 
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
- });
94
+ export async function searchDirectory(kind: DirectoryKind, options: { keyword: string; cursor?: string }) {
95
+ const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), keyword: options.keyword.trim(), limit: '20' });
157
96
  if (options.cursor) params.set('cursor', options.cursor);
158
- return request<DirectoryEntryPage>(
159
- `${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`
160
- );
97
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/${kind}s?${params}`);
161
98
  }
162
99
 
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
- });
100
+ export async function browseDepartmentTree(options: { parentId?: string; cursor?: string }) {
101
+ const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), limit: '100' });
171
102
  if (options.parentId) params.set('parentId', options.parentId);
172
103
  if (options.cursor) params.set('offset', options.cursor);
173
- return request<DirectoryEntryPage>(
174
- `${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`
175
- );
104
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/tree?${params}`);
176
105
  }
177
106
 
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
- );
107
+ export async function browseDepartmentUsers(departmentId: string, page = 1) {
108
+ const params = new URLSearchParams({ environmentKey: currentEnvironmentKey(), page: String(page), limit: '20' });
109
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/departments/${encodeURIComponent(departmentId)}/users?${params}`);
192
110
  }
193
111
 
194
- export async function resolveDirectory(
195
- kind: DirectoryKind,
196
- ids: string[]
197
- ) {
112
+ export async function resolveDirectory(kind: DirectoryKind, ids: string[]) {
198
113
  const uniqueIds = [...new Set(ids)];
199
- const body: DirectoryResolveRequest = {
200
- schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest,
201
- kind,
202
- ids: uniqueIds,
114
+ if (uniqueIds.length === 0) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED');
115
+ if (uniqueIds.length > 50) throw new Error('OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED');
116
+ const body: DirectoryResolveRequest = { schemaVersion: SCHEMA_VERSIONS.directoryResolveRequest, kind, ids: uniqueIds };
117
+ return request<DirectoryEntryPage>(`${nativeBase().replace(/\/native$/, '')}/directory/resolve?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`, { method: 'POST', body: JSON.stringify(body) });
118
+ }
119
+
120
+ export async function searchResource(resourceCode: string, labelField: string, options: { keyword: string; cursor?: string }) {
121
+ const pageSize = 20;
122
+ const offset = options.cursor ? Number(options.cursor) : 0;
123
+ const query: DataQuery = {
124
+ schemaVersion: SCHEMA_VERSIONS.dataQuery,
125
+ filters: options.keyword.trim() ? [{ field: labelField, operator: 'ilike', value: `%${options.keyword.trim()}%` }] : [],
126
+ order: [{ field: labelField, direction: 'asc' }],
127
+ limit: pageSize,
128
+ offset: Number.isSafeInteger(offset) && offset >= 0 ? offset : 0,
203
129
  };
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
- );
216
- }
217
-
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,
130
+ const page = await request<DataPage<Record<string, unknown>>>(`${dataBase(resourceCode)}/query`, { method: 'POST', body: JSON.stringify({ ...query, environmentKey: currentEnvironmentKey() }) });
131
+ return {
132
+ items: page.items.map(item => ({ id: String(item.id), label: String(item[labelField] ?? item.id), selectable: true })),
133
+ nextCursor: page.items.length >= pageSize ? String(page.offset + page.items.length) : null,
246
134
  };
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
135
  }
260
136
 
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>;
137
+ export async function resolveResource(resourceCode: string, ids: string[], labelField: string) {
138
+ if (ids.length === 0) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_REQUIRED');
139
+ if (ids.length > 50) throw new Error('OPENXIANGDA_RESOURCE_RESOLVE_IDS_EXCEEDED');
140
+ const items = await Promise.all(ids.map(async id => {
141
+ const record = await request<DataRecord<Record<string, unknown>>>(`${dataBase(resourceCode)}/records/${encodeURIComponent(id)}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
142
+ return { id, label: String(record.data[labelField] ?? id), selectable: true };
143
+ }));
144
+ return { items, nextCursor: null };
269
145
  }
270
146
 
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
147
  export interface GenericResourceQuery {
387
148
  page: number;
388
149
  pageSize: number;
@@ -392,17 +153,11 @@ export interface GenericResourceQuery {
392
153
  sort?: { field: string; order: 'asc' | 'desc' };
393
154
  }
394
155
 
395
- /** Native CRUD adapter used by resources that opt into generated pages. */
396
- export function createNativeResourceClient(
397
- code: string,
398
- surface: DataResourceSurface
399
- ) {
156
+ export function createNativeResourceClient(code: string, surface: DataResourceSurface) {
400
157
  const base = dataBase(code);
401
158
  const declaredFields = new Set(Object.keys(surface.fields));
402
159
  const assertField = (field: string) => {
403
- if (!declaredFields.has(field)) {
404
- throw new Error(`OPENXIANGDA_RESOURCE_QUERY_FIELD_NOT_DECLARED:${code}:${field}`);
405
- }
160
+ if (!declaredFields.has(field)) throw new Error(`OPENXIANGDA_RESOURCE_QUERY_FIELD_NOT_DECLARED:${code}:${field}`);
406
161
  return field;
407
162
  };
408
163
  const filtersFor = (query: GenericResourceQuery) => {
@@ -418,7 +173,7 @@ export function createNativeResourceClient(
418
173
  if (Array.isArray(value) && value.length === 2 && ['date', 'datetime', 'number', 'money', 'percent'].includes(field?.widget || '')) {
419
174
  if (value[0] !== undefined && value[0] !== null && value[0] !== '') filters.push({ field: fieldCode, operator: 'gte', value: value[0] });
420
175
  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 || '')) {
176
+ } else if (Array.isArray(value) && ['multi', 'directory-user', 'directory-department', 'resource'].includes(field?.widget || '')) {
422
177
  filters.push({ field: fieldCode, operator: 'cs', value });
423
178
  } else {
424
179
  filters.push({ field: fieldCode, operator: 'eq', value });
@@ -429,17 +184,8 @@ export function createNativeResourceClient(
429
184
  return {
430
185
  async list(query: GenericResourceQuery) {
431
186
  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
- });
187
+ 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 };
188
+ const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, { method: 'POST', body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }) });
443
189
  return { rows: page.items, total: page.total };
444
190
  },
445
191
  async get(id: string) {
@@ -447,7 +193,7 @@ export function createNativeResourceClient(
447
193
  return record.data;
448
194
  },
449
195
  async audit(id: string) {
450
- return await request<DataAuditPage>(`${base}/records/${encodeURIComponent(id)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
196
+ return request<DataAuditPage>(`${base}/records/${encodeURIComponent(id)}/audit?limit=10&offset=0&environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
451
197
  },
452
198
  async create(data: Record<string, unknown>) {
453
199
  const record = await request<DataRecord<Record<string, unknown>>>(`${base}/records`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey(), data }) });
@@ -466,120 +212,30 @@ export function createNativeResourceClient(
466
212
  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
213
  const uploaded = await fetch(plan.uploadUrl, { method: plan.uploadMethod, headers: plan.headers, body: file });
468
214
  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() }) });
215
+ return request<DataFileRef>(`${base}/files/${encodeURIComponent(plan.file.id)}/complete`, { method: 'POST', body: JSON.stringify({ environmentKey: currentEnvironmentKey() }) });
470
216
  },
471
217
  };
472
218
  }
473
219
 
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()}`;
220
+ function nativeBase() {
221
+ return `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
486
222
  }
487
223
 
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
- );
224
+ function dataBase(code: string) {
225
+ return `${nativeBase()}/data/${encodeURIComponent(code)}`;
497
226
  }
498
227
 
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();
228
+ export function dataFileContentUrl(resource: string, fileId: string, disposition: 'attachment' | 'inline' = 'attachment') {
229
+ const query = new URLSearchParams({ environmentKey: currentEnvironmentKey(), disposition });
230
+ return `${dataBase(resource)}/files/${encodeURIComponent(fileId)}/content?${query}`;
512
231
  }
513
232
 
514
- export interface CollegeRecord extends Record<string, unknown> {
515
- id: string;
516
- revision: number;
517
- name: string;
518
- enabled: boolean;
233
+ export async function loadDataFilePreview(resource: string, fileId: string) {
234
+ return request<DataFilePreview>(`${dataBase(resource)}/files/${encodeURIComponent(fileId)}/preview?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`);
519
235
  }
520
236
 
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
- }
237
+ export async function fetchDataFileBlob(resource: string, fileId: string) {
238
+ const response = await fetch(dataFileContentUrl(resource, fileId, 'inline'), { credentials: 'include', headers: { accept: 'application/octet-stream,*/*' } });
239
+ if (!response.ok) throw new Error(`FILE_CONTENT_READ_FAILED: HTTP_${response.status}`);
240
+ return response.blob();
585
241
  }
@@ -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) {
@@ -391,7 +391,7 @@ body {
391
391
  gap: 16px;
392
392
  }
393
393
 
394
- .oxa-instrument-form {
394
+ .oxa-resource-form {
395
395
  display: grid;
396
396
  gap: 16px;
397
397
  }
@@ -522,7 +522,7 @@ body {
522
522
  gap: 1px;
523
523
  }
524
524
 
525
- .oxa-resolved-college .oxa-resolved-item {
525
+ .oxa-resolved-resource .oxa-resolved-item {
526
526
  border-color: #cfe1ff;
527
527
  background: #eff6ff;
528
528
  color: #155fc7;
@@ -1029,7 +1029,7 @@ body {
1029
1029
  font-size: 22px;
1030
1030
  }
1031
1031
 
1032
- .oxa-instrument-name.ant-typography {
1032
+ .oxa-resource-name.ant-typography {
1033
1033
  margin-bottom: 2px;
1034
1034
  font-size: 18px;
1035
1035
  }