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

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.
@@ -12,30 +12,62 @@ import {
12
12
  type DirectoryResolveRequest,
13
13
  } from 'openxiangda-contracts/browser';
14
14
  import { applicationCode, runtimeMount } from './runtime-meta';
15
+ import { useSyncExternalStore } from 'react';
15
16
 
16
17
  interface PlatformEnvelope<T> {
17
18
  code: number;
18
19
  message?: string;
19
20
  errorCode?: string;
21
+ requestId?: string;
20
22
  data: T;
21
23
  }
22
24
 
23
- async function request<T>(path: string, init?: RequestInit): Promise<T> {
24
- const response = await fetch(path, {
25
- credentials: 'include',
26
- ...init,
27
- headers: {
28
- accept: 'application/json',
29
- ...(init?.body ? { 'content-type': 'application/json' } : {}),
30
- ...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);
31
43
  },
32
- });
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;
35
- if (!response.ok || (envelope && envelope.code !== 200)) {
36
- throw new Error(`${envelope?.errorCode || `HTTP_${response.status}`}: ${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();
37
70
  }
38
- return envelope ? envelope.data : payload as T;
39
71
  }
40
72
 
41
73
  export interface RuntimeIdentity {
@@ -169,7 +201,11 @@ export function createNativeResourceClient(code: string, surface: DataResourceSu
169
201
  }
170
202
  for (const [fieldCode, value] of Object.entries(query.filters || {})) {
171
203
  if (value === undefined || value === null || value === '') continue;
172
- const field = surface.fields[assertField(fieldCode)];
204
+ // A list state can outlive a resource route during client-side navigation.
205
+ // Ignore filters from the previous resource instead of constructing an
206
+ // invalid Native Data query for the current resource.
207
+ if (!declaredFields.has(fieldCode)) continue;
208
+ const field = surface.fields[fieldCode];
173
209
  if (Array.isArray(value) && value.length === 2 && ['date', 'datetime', 'number', 'money', 'percent'].includes(field?.widget || '')) {
174
210
  if (value[0] !== undefined && value[0] !== null && value[0] !== '') filters.push({ field: fieldCode, operator: 'gte', value: value[0] });
175
211
  if (value[1] !== undefined && value[1] !== null && value[1] !== '') filters.push({ field: fieldCode, operator: 'lte', value: value[1] });
@@ -183,8 +219,10 @@ export function createNativeResourceClient(code: string, surface: DataResourceSu
183
219
  };
184
220
  return {
185
221
  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 };
222
+ const fallbackSort = surface.list?.defaultSort || { field: Object.keys(surface.fields)[0] || 'id', order: 'asc' as const };
223
+ const requestedSort = query.sort || fallbackSort;
224
+ const sort = declaredFields.has(requestedSort.field) ? requestedSort : fallbackSort;
225
+ const body: DataQuery = { schemaVersion: SCHEMA_VERSIONS.dataQuery, filters: filtersFor(query), order: [{ field: sort.field, direction: sort.order || 'asc' }], limit: query.pageSize, offset: (query.page - 1) * query.pageSize };
188
226
  const page = await request<DataPage<Record<string, unknown>>>(`${base}/query`, { method: 'POST', body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }) });
189
227
  return { rows: page.items, total: page.total };
190
228
  },
@@ -24,6 +24,25 @@ body {
24
24
  place-items: center;
25
25
  }
26
26
 
27
+ .oxa-global-request-loading {
28
+ position: fixed;
29
+ z-index: 1200;
30
+ top: 14px;
31
+ left: 50%;
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: 8px;
35
+ padding: 7px 13px;
36
+ border: 1px solid #dbe7f7;
37
+ border-radius: 999px;
38
+ background: rgba(255, 255, 255, 0.96);
39
+ box-shadow: 0 8px 24px rgba(31, 59, 102, 0.15);
40
+ color: #355070;
41
+ font-size: 13px;
42
+ pointer-events: none;
43
+ transform: translateX(-50%);
44
+ }
45
+
27
46
  .oxa-app-layout {
28
47
  min-height: 100vh;
29
48
  background: #f3f7fd;
@@ -1075,6 +1094,61 @@ body {
1075
1094
  .oxa-audit-entry {
1076
1095
  display: grid;
1077
1096
  gap: 4px;
1097
+ padding: 10px 0;
1098
+ border-bottom: 1px solid #edf0f5;
1099
+ }
1100
+
1101
+ .oxa-audit-entry:last-child {
1102
+ border-bottom: 0;
1103
+ }
1104
+
1105
+ .oxa-audit-heading,
1106
+ .oxa-audit-meta,
1107
+ .oxa-audit-change-values {
1108
+ display: flex;
1109
+ align-items: center;
1110
+ gap: 8px;
1111
+ }
1112
+
1113
+ .oxa-audit-heading {
1114
+ justify-content: space-between;
1115
+ }
1116
+
1117
+ .oxa-audit-meta {
1118
+ flex-wrap: wrap;
1119
+ color: #7b879b;
1120
+ font-size: 12px;
1121
+ }
1122
+
1123
+ .oxa-audit-meta .ant-typography {
1124
+ font-size: 12px;
1125
+ }
1126
+
1127
+ .oxa-audit-changes {
1128
+ display: grid;
1129
+ gap: 7px;
1130
+ margin-top: 4px;
1131
+ }
1132
+
1133
+ .oxa-audit-change {
1134
+ display: grid;
1135
+ gap: 3px;
1136
+ padding: 7px 8px;
1137
+ border-radius: 6px;
1138
+ background: #f7f9fc;
1139
+ }
1140
+
1141
+ .oxa-audit-change-values {
1142
+ min-width: 0;
1143
+ color: #47566f;
1144
+ font-size: 12px;
1145
+ }
1146
+
1147
+ .oxa-audit-change-values > :not(svg) {
1148
+ min-width: 0;
1149
+ overflow: hidden;
1150
+ text-overflow: ellipsis;
1151
+ white-space: nowrap;
1078
1152
  }
1079
1153
 
1080
1154
  .oxa-audit-entry > span {
@@ -1082,6 +1156,214 @@ body {
1082
1156
  font-size: 12px;
1083
1157
  }
1084
1158
 
1159
+ .oxa-mobile-reference-trigger {
1160
+ display: flex;
1161
+ width: 100%;
1162
+ min-height: 42px;
1163
+ align-items: center;
1164
+ justify-content: space-between;
1165
+ gap: 8px;
1166
+ padding: 9px 12px;
1167
+ border: 1px solid #d9dfe9;
1168
+ border-radius: 8px;
1169
+ background: #fff;
1170
+ color: #1f2b3d;
1171
+ font: inherit;
1172
+ text-align: left;
1173
+ }
1174
+
1175
+ .oxa-mobile-reference-trigger:disabled {
1176
+ background: #f5f7fa;
1177
+ color: #9aa5b5;
1178
+ }
1179
+
1180
+ .oxa-mobile-reference-placeholder {
1181
+ color: #9aa5b5;
1182
+ }
1183
+
1184
+ .oxa-mobile-reference-backdrop {
1185
+ position: fixed;
1186
+ z-index: 1200;
1187
+ inset: 0;
1188
+ display: flex;
1189
+ align-items: flex-end;
1190
+ background: rgb(15 23 42 / 42%);
1191
+ }
1192
+
1193
+ .oxa-mobile-reference-sheet {
1194
+ display: grid;
1195
+ width: 100%;
1196
+ max-height: min(78vh, 640px);
1197
+ gap: 12px;
1198
+ padding: 16px 16px max(16px, env(safe-area-inset-bottom));
1199
+ overflow: auto;
1200
+ border-radius: 16px 16px 0 0;
1201
+ background: #fff;
1202
+ box-shadow: 0 -8px 32px rgb(15 23 42 / 18%);
1203
+ }
1204
+
1205
+ .oxa-mobile-reference-header {
1206
+ display: flex;
1207
+ align-items: center;
1208
+ justify-content: space-between;
1209
+ color: #1f2b3d;
1210
+ }
1211
+
1212
+ .oxa-mobile-reference-header button,
1213
+ .oxa-mobile-reference-more,
1214
+ .oxa-mobile-reference-confirm {
1215
+ border: 0;
1216
+ background: transparent;
1217
+ color: #2d6cdf;
1218
+ font: inherit;
1219
+ }
1220
+
1221
+ .oxa-mobile-reference-search {
1222
+ width: 100%;
1223
+ }
1224
+
1225
+ .oxa-mobile-reference-options {
1226
+ display: grid;
1227
+ gap: 4px;
1228
+ }
1229
+
1230
+ .oxa-mobile-reference-option {
1231
+ display: flex;
1232
+ min-height: 48px;
1233
+ align-items: center;
1234
+ gap: 10px;
1235
+ padding: 8px 4px;
1236
+ border: 0;
1237
+ border-radius: 8px;
1238
+ background: transparent;
1239
+ color: #1f2b3d;
1240
+ font: inherit;
1241
+ text-align: left;
1242
+ }
1243
+
1244
+ .oxa-mobile-reference-option.is-selected {
1245
+ background: #edf4ff;
1246
+ }
1247
+
1248
+ .oxa-mobile-reference-option:disabled {
1249
+ color: #aab3c0;
1250
+ opacity: 0.7;
1251
+ }
1252
+
1253
+ .oxa-mobile-reference-option-copy {
1254
+ display: grid;
1255
+ min-width: 0;
1256
+ flex: 1;
1257
+ gap: 2px;
1258
+ }
1259
+
1260
+ .oxa-mobile-reference-option-copy small {
1261
+ overflow: hidden;
1262
+ color: #7b879b;
1263
+ font-size: 12px;
1264
+ text-overflow: ellipsis;
1265
+ white-space: nowrap;
1266
+ }
1267
+
1268
+ .oxa-mobile-reference-loading,
1269
+ .oxa-mobile-reference-empty,
1270
+ .oxa-mobile-reference-error {
1271
+ padding: 12px 4px;
1272
+ color: #7b879b;
1273
+ font-size: 13px;
1274
+ text-align: center;
1275
+ }
1276
+
1277
+ .oxa-mobile-reference-error {
1278
+ color: #cf3c46;
1279
+ }
1280
+
1281
+ .oxa-mobile-reference-more,
1282
+ .oxa-mobile-reference-confirm {
1283
+ min-height: 42px;
1284
+ border-radius: 8px;
1285
+ background: #f0f5ff;
1286
+ font-weight: 600;
1287
+ text-align: center;
1288
+ }
1289
+
1290
+ .oxa-mobile-reference-confirm {
1291
+ background: #2d6cdf;
1292
+ color: #fff;
1293
+ }
1294
+
1295
+ .oxa-mobile-file-field {
1296
+ display: grid;
1297
+ gap: 8px;
1298
+ }
1299
+
1300
+ .oxa-mobile-file-button {
1301
+ display: inline-flex !important;
1302
+ width: 100%;
1303
+ min-height: 42px;
1304
+ align-items: center;
1305
+ justify-content: center;
1306
+ border: 1px solid #d9dfe9 !important;
1307
+ border-radius: 8px;
1308
+ background: #fff !important;
1309
+ color: #2d6cdf;
1310
+ font-weight: 600;
1311
+ }
1312
+
1313
+ .oxa-mobile-file-button.ant-btn:disabled {
1314
+ background: #f5f7fa;
1315
+ color: #9aa5b5;
1316
+ }
1317
+
1318
+ .oxa-mobile-pending-files {
1319
+ display: grid;
1320
+ gap: 8px;
1321
+ }
1322
+
1323
+ .oxa-mobile-pending-file {
1324
+ display: flex;
1325
+ min-height: 42px;
1326
+ align-items: center;
1327
+ gap: 8px;
1328
+ padding: 7px 9px;
1329
+ border: 1px solid #dbe3ee;
1330
+ border-radius: 8px;
1331
+ background: #fbfcfe;
1332
+ font-size: 13px;
1333
+ }
1334
+
1335
+ .oxa-mobile-pending-file-name {
1336
+ min-width: 0;
1337
+ overflow: hidden;
1338
+ text-overflow: ellipsis;
1339
+ white-space: nowrap;
1340
+ }
1341
+
1342
+ .oxa-mobile-pending-file-copy {
1343
+ display: grid;
1344
+ min-width: 0;
1345
+ flex: 1;
1346
+ gap: 2px;
1347
+ }
1348
+
1349
+ .oxa-mobile-pending-file-copy small {
1350
+ color: #7b879b;
1351
+ font-size: 12px;
1352
+ }
1353
+
1354
+ .oxa-mobile-pending-status {
1355
+ color: #2d6cdf;
1356
+ white-space: nowrap;
1357
+ }
1358
+
1359
+ .oxa-mobile-pending-error {
1360
+ max-width: 42%;
1361
+ overflow: hidden;
1362
+ color: #cf3c46;
1363
+ text-overflow: ellipsis;
1364
+ white-space: nowrap;
1365
+ }
1366
+
1085
1367
  @media (max-width: 1280px) {
1086
1368
  .oxa-grid,
1087
1369
  .oxa-filter-grid {
@@ -20,9 +20,26 @@ test('standard surfaces keep authoritative directory/resource selectors and mobi
20
20
  const crud = readFileSync(new URL('../src/components/resource/GeneratedResourceCrud.tsx', import.meta.url), 'utf8');
21
21
  assert.match(surface, /PlatformDirectoryPicker/);
22
22
  assert.match(surface, /source="resource"/);
23
+ assert.match(surface, /MobileAuthoritativeSelector/);
24
+ assert.match(surface, /oxa-mobile-file-field/);
25
+ assert.match(surface, /mobilePending/);
26
+ assert.match(surface, /上传中/);
27
+ assert.match(surface, /重试/);
28
+ assert.match(surface, /AttachmentFileList files=\{refs\}/);
23
29
  assert.match(selector, /searchResource|resolveResource/);
30
+ assert.match(selector, /role="dialog"/);
31
+ assert.match(selector, /oxa-mobile-reference-sheet/);
24
32
  assert.match(crud, /MobileResourceListPage/);
25
33
  assert.match(crud, /fieldWritable/);
34
+ assert.match(crud, /exportResourceRows/);
35
+ assert.match(crud, /text\/csv;charset=utf-8/);
36
+ assert.match(crud, /数据读取失败/);
37
+ assert.match(crud, /state\.list\.query\.refetch/);
38
+ assert.match(crud, /key=\{pageKey\}/);
39
+ assert.match(crud, /setQuery\(\{\}\)/);
40
+ const client = readFileSync(new URL('../src/platform-client.ts', import.meta.url), 'utf8');
41
+ assert.match(client, /declaredFields\.has\(requestedSort\.field\)/);
42
+ assert.match(client, /if \(!declaredFields\.has\(fieldCode\)\) continue/);
26
43
  });
27
44
 
28
45
  test('runtime metadata stays mount-scoped', () => {
@@ -21,9 +21,9 @@
21
21
  "build": "pnpm --recursive build"
22
22
  },
23
23
  "devDependencies": {
24
- "openxiangda-cli": "2.0.0-alpha.62",
25
- "openxiangda-contracts": "2.0.0-alpha.30",
26
- "openxiangda-devkit-core": "2.0.0-alpha.38",
24
+ "openxiangda-cli": "2.0.0-alpha.67",
25
+ "openxiangda-contracts": "2.0.0-alpha.31",
26
+ "openxiangda-devkit-core": "2.0.0-alpha.41",
27
27
  "typescript": "5.9.3"
28
28
  }
29
29
  }