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