openxiangda-cli 2.0.0-alpha.58 → 2.0.0-alpha.59

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.
@@ -692,6 +692,25 @@ body {
692
692
  gap: 2px;
693
693
  }
694
694
 
695
+ .oxa-file-name-button {
696
+ border: 0;
697
+ background: transparent;
698
+ padding: 0;
699
+ color: inherit;
700
+ font: inherit;
701
+ text-align: left;
702
+ cursor: pointer;
703
+ }
704
+
705
+ .oxa-file-name-button:hover strong {
706
+ color: #1677ff;
707
+ }
708
+
709
+ .oxa-file-name-button:disabled {
710
+ cursor: wait;
711
+ opacity: 0.65;
712
+ }
713
+
695
714
  .oxa-file-meta strong {
696
715
  overflow: hidden;
697
716
  text-overflow: ellipsis;
@@ -725,30 +744,6 @@ body {
725
744
  padding: 22px;
726
745
  }
727
746
 
728
- .oxa-attachment-readonly {
729
- display: grid;
730
- width: 100%;
731
- gap: 8px;
732
- }
733
-
734
- .oxa-attachment-row {
735
- display: grid;
736
- grid-template-columns: auto minmax(0, 1fr) auto auto;
737
- align-items: center;
738
- gap: 10px;
739
- min-height: 42px;
740
- border: 1px solid #e2e8f0;
741
- border-radius: 7px;
742
- padding: 6px 10px;
743
- }
744
-
745
- .oxa-attachment-name {
746
- overflow: hidden;
747
- color: #27364d;
748
- text-overflow: ellipsis;
749
- white-space: nowrap;
750
- }
751
-
752
747
  .oxa-detail-hero,
753
748
  .oxa-edit-hero {
754
749
  margin-bottom: 16px;
@@ -768,6 +763,150 @@ body {
768
763
  gap: 16px;
769
764
  }
770
765
 
766
+ .oxa-file-preview-page {
767
+ min-height: 100vh;
768
+ background: #eef2f7;
769
+ }
770
+
771
+ .oxa-file-preview-toolbar {
772
+ position: sticky;
773
+ z-index: 10;
774
+ top: 0;
775
+ display: flex;
776
+ min-height: 66px;
777
+ align-items: center;
778
+ justify-content: space-between;
779
+ gap: 20px;
780
+ border-bottom: 1px solid #dfe5ee;
781
+ background: rgb(255 255 255 / 96%);
782
+ padding: 10px 20px;
783
+ box-shadow: 0 2px 12px rgb(26 45 75 / 6%);
784
+ backdrop-filter: blur(12px);
785
+ }
786
+
787
+ .oxa-file-preview-toolbar > div:last-child {
788
+ display: flex;
789
+ gap: 8px;
790
+ }
791
+
792
+ .oxa-file-preview-title {
793
+ display: flex;
794
+ min-width: 0;
795
+ align-items: center;
796
+ gap: 12px;
797
+ }
798
+
799
+ .oxa-file-preview-title > .anticon {
800
+ color: #1677ff;
801
+ font-size: 26px;
802
+ }
803
+
804
+ .oxa-file-preview-title > div {
805
+ display: grid;
806
+ min-width: 0;
807
+ gap: 2px;
808
+ }
809
+
810
+ .oxa-file-preview-title strong,
811
+ .oxa-file-preview-title span {
812
+ overflow: hidden;
813
+ text-overflow: ellipsis;
814
+ white-space: nowrap;
815
+ }
816
+
817
+ .oxa-file-preview-title span {
818
+ color: #7b879b;
819
+ font-size: 12px;
820
+ }
821
+
822
+ .oxa-file-preview-body {
823
+ display: grid;
824
+ min-height: calc(100vh - 66px);
825
+ place-items: center;
826
+ padding: 20px;
827
+ }
828
+
829
+ .oxa-preview-frame {
830
+ width: 100%;
831
+ height: calc(100vh - 106px);
832
+ border: 0;
833
+ border-radius: 8px;
834
+ background: #fff;
835
+ box-shadow: 0 8px 32px rgb(24 42 72 / 10%);
836
+ }
837
+
838
+ .oxa-preview-image {
839
+ max-width: 100%;
840
+ max-height: calc(100vh - 106px);
841
+ border-radius: 8px;
842
+ object-fit: contain;
843
+ box-shadow: 0 8px 32px rgb(24 42 72 / 12%);
844
+ }
845
+
846
+ .oxa-docx-preview,
847
+ .oxa-spreadsheet-preview {
848
+ width: 100%;
849
+ height: calc(100vh - 106px);
850
+ overflow: auto;
851
+ border-radius: 8px;
852
+ background: #fff;
853
+ box-shadow: 0 8px 32px rgb(24 42 72 / 10%);
854
+ }
855
+
856
+ .oxa-docx-preview .docx-wrapper {
857
+ min-height: 100%;
858
+ background: #dfe4eb;
859
+ padding: 24px;
860
+ }
861
+
862
+ .oxa-spreadsheet-preview {
863
+ display: flex;
864
+ flex-direction: column;
865
+ }
866
+
867
+ .oxa-spreadsheet-preview > .ant-segmented {
868
+ flex: 0 0 auto;
869
+ margin: 12px;
870
+ }
871
+
872
+ .oxa-spreadsheet-scroll {
873
+ min-height: 0;
874
+ flex: 1;
875
+ overflow: auto;
876
+ border-top: 1px solid #e5eaf1;
877
+ }
878
+
879
+ .oxa-spreadsheet-scroll table {
880
+ min-width: 100%;
881
+ border-spacing: 0;
882
+ border-collapse: separate;
883
+ font-size: 13px;
884
+ }
885
+
886
+ .oxa-spreadsheet-scroll th,
887
+ .oxa-spreadsheet-scroll td {
888
+ min-width: 96px;
889
+ max-width: 360px;
890
+ border-right: 1px solid #e5eaf1;
891
+ border-bottom: 1px solid #e5eaf1;
892
+ padding: 7px 9px;
893
+ background: #fff;
894
+ text-align: left;
895
+ vertical-align: top;
896
+ white-space: pre-wrap;
897
+ word-break: break-word;
898
+ }
899
+
900
+ .oxa-spreadsheet-scroll th {
901
+ position: sticky;
902
+ z-index: 1;
903
+ left: 0;
904
+ min-width: 52px;
905
+ background: #f5f7fa;
906
+ color: #7b879b;
907
+ text-align: center;
908
+ }
909
+
771
910
  .oxa-detail-hero-top {
772
911
  align-items: flex-start;
773
912
  }
@@ -10,6 +10,8 @@ import {
10
10
  browseDepartmentTree,
11
11
  browseDepartmentUsers,
12
12
  instrumentDataApi,
13
+ fetchDataFileBlob,
14
+ loadDataFilePreview,
13
15
  loadCurrentIdentity,
14
16
  resolveCollegeScope,
15
17
  resolveDirectory,
@@ -19,6 +21,7 @@ import {
19
21
  import {
20
22
  applicationApiPath,
21
23
  readRuntimeMount,
24
+ resolveAttachmentPreviewPath,
22
25
  resolveApplicationApiPath,
23
26
  resolveApplicationBasename,
24
27
  } from '../src/runtime-meta';
@@ -65,6 +68,14 @@ test('application shell and instrument surfaces match the approved navigation mo
65
68
  new URL('../src/components/platform-fields/PlatformDirectoryPicker.tsx', import.meta.url),
66
69
  'utf8'
67
70
  );
71
+ const attachment = readFileSync(
72
+ new URL('../src/components/platform-fields/AttachmentFileList.tsx', import.meta.url),
73
+ 'utf8'
74
+ );
75
+ const preview = readFileSync(
76
+ new URL('../src/FilePreviewPage.tsx', import.meta.url),
77
+ 'utf8'
78
+ );
68
79
  for (const marker of [
69
80
  '仪器资源管理',
70
81
  '工作台',
@@ -92,6 +103,13 @@ test('application shell and instrument surfaces match the approved navigation mo
92
103
  assert.match(form, /PlatformDirectoryPicker/);
93
104
  assert.match(form, /oxa-file-dropzone/);
94
105
  assert.match(form, /oxa-detail-layout/);
106
+ assert.match(routes, /path="\/files\/:resourceCode\/:fileId\/preview"/);
107
+ assert.match(attachment, /Image\.PreviewGroup/);
108
+ assert.match(attachment, /attachmentPreviewPath/);
109
+ assert.match(preview, /import\('docx-preview'\)/);
110
+ assert.match(preview, /import\('xlsx'\)/);
111
+ assert.match(preview, /只读预览/);
112
+ assert.doesNotMatch(form, /instrumentFileContentUrl|window\.open/);
95
113
  assert.doesNotMatch(
96
114
  `${form}\n${directory}`,
97
115
  /FormContext|tools\/openxiangda|RoleSession/
@@ -253,6 +271,92 @@ test('deployed app/environment come only from platform-injected runtime meta', (
253
271
  resolveApplicationApiPath(mount, 'api/instruments/context'),
254
272
  '/service/openxiangda-app-api/v2/instrument-center/production/api/instruments/context'
255
273
  );
274
+ assert.equal(
275
+ resolveAttachmentPreviewPath(
276
+ mount,
277
+ 'instruments',
278
+ '77777777-7777-4777-8777-777777777777'
279
+ ),
280
+ '/view/instrument-center/files/instruments/77777777-7777-4777-8777-777777777777/preview'
281
+ );
282
+ });
283
+
284
+ test('managed file preview reads metadata and bytes without a RoleSession', async () => {
285
+ const calls: Array<{ path: string; init?: RequestInit }> = [];
286
+ const originalFetch = globalThis.fetch;
287
+ globalThis.fetch = async (input, init) => {
288
+ const path = String(input);
289
+ calls.push({ path, init });
290
+ if (path.endsWith('/dev-sessions/current')) {
291
+ return Response.json({
292
+ code: 200,
293
+ data: {
294
+ environment: { id: 'test-id', key: 'preproduction' },
295
+ principal: {
296
+ userId,
297
+ roleCodes: ['school_admin'],
298
+ capabilityCodes: [],
299
+ isAppSuperAdmin: false,
300
+ },
301
+ },
302
+ });
303
+ }
304
+ if (path.includes('/files/file-1/preview?')) {
305
+ return Response.json({
306
+ code: 200,
307
+ data: {
308
+ schemaVersion: 'openxiangda.data-file-preview/v2',
309
+ resourceCode: 'instruments',
310
+ file: {
311
+ schemaVersion: 'openxiangda.data-file-ref/v2',
312
+ id: 'file-1',
313
+ name: 'manual.docx',
314
+ size: 12,
315
+ contentType:
316
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
317
+ },
318
+ extension: 'docx',
319
+ previewType: 'office',
320
+ renderMode: 'docx-html',
321
+ previewSurface: 'document',
322
+ previewProvider: 'browser',
323
+ canPreview: true,
324
+ canDownload: true,
325
+ },
326
+ });
327
+ }
328
+ if (path.includes('/files/file-1/content?')) {
329
+ return new Response(new Uint8Array([80, 75, 3, 4]), {
330
+ status: 200,
331
+ headers: {
332
+ 'content-type':
333
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
334
+ },
335
+ });
336
+ }
337
+ return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
338
+ };
339
+ try {
340
+ await loadCurrentIdentity();
341
+ const preview = await loadDataFilePreview('instruments', 'file-1');
342
+ const blob = await fetchDataFileBlob('instruments', 'file-1');
343
+ assert.equal(preview.renderMode, 'docx-html');
344
+ assert.equal(blob.size, 4);
345
+ } finally {
346
+ globalThis.fetch = originalFetch;
347
+ }
348
+ const previewCall = calls.find(call => call.path.includes('/files/file-1/preview?'))!;
349
+ const contentCall = calls.find(call => call.path.includes('/files/file-1/content?'))!;
350
+ assert.match(previewCall.path, /environmentKey=preproduction/);
351
+ assert.match(contentCall.path, /disposition=inline/);
352
+ assert.equal(
353
+ calls.some(call =>
354
+ Object.keys(call.init?.headers || {}).some(header =>
355
+ header.toLowerCase().includes('role-session')
356
+ )
357
+ ),
358
+ false
359
+ );
256
360
  });
257
361
 
258
362
  test('list uses the native Data API with server paging, filters, and sorting', async () => {
@@ -21,9 +21,9 @@
21
21
  "build": "pnpm --recursive build"
22
22
  },
23
23
  "devDependencies": {
24
- "openxiangda-cli": "2.0.0-alpha.58",
25
- "openxiangda-contracts": "2.0.0-alpha.27",
26
- "openxiangda-devkit-core": "2.0.0-alpha.34",
24
+ "openxiangda-cli": "2.0.0-alpha.59",
25
+ "openxiangda-contracts": "2.0.0-alpha.28",
26
+ "openxiangda-devkit-core": "2.0.0-alpha.35",
27
27
  "typescript": "5.9.3"
28
28
  }
29
29
  }
@@ -16,7 +16,7 @@ function visit(directory) {
16
16
  }
17
17
  visit(root);
18
18
  const bytes = files.reduce((total, file) => total + statSync(file).size, 0);
19
- if (files.length > 50) throw new Error(`TEMPLATE_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>50`);
19
+ if (files.length > 52) throw new Error(`TEMPLATE_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>52`);
20
20
  if (bytes > 500_000) throw new Error(`TEMPLATE_SOURCE_BYTE_BUDGET_EXCEEDED:${bytes}>500000`);
21
21
  const productionFiles = files.filter(file => {
22
22
  const name = relative(root, file).replaceAll('\\', '/');