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.
- package/package.json +4 -4
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/instruments.spec.ts +285 -0
- package/template/apps/web/package.json +4 -2
- package/template/apps/web/scripts/check.mjs +1 -1
- package/template/apps/web/scripts/verify-build.mjs +14 -2
- package/template/apps/web/src/FilePreviewPage.tsx +303 -0
- package/template/apps/web/src/InstrumentForm.tsx +21 -130
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +226 -0
- package/template/apps/web/src/main.tsx +5 -0
- package/template/apps/web/src/platform-client.ts +38 -3
- package/template/apps/web/src/runtime-meta.ts +15 -0
- package/template/apps/web/src/styles.css +163 -24
- package/template/apps/web/test/contracts.test.ts +104 -0
- package/template/package.json +3 -3
- package/template/scripts/verify-template-budget.mjs +1 -1
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DownloadOutlined,
|
|
3
|
-
EyeOutlined,
|
|
4
|
-
PaperClipOutlined,
|
|
5
2
|
PictureOutlined,
|
|
6
|
-
PlusOutlined,
|
|
7
3
|
ReloadOutlined,
|
|
8
4
|
UploadOutlined,
|
|
9
5
|
} from '@ant-design/icons';
|
|
@@ -17,28 +13,24 @@ import {
|
|
|
17
13
|
Input,
|
|
18
14
|
InputNumber,
|
|
19
15
|
Select,
|
|
20
|
-
Space,
|
|
21
16
|
Switch,
|
|
22
17
|
Tag,
|
|
23
18
|
Timeline,
|
|
24
19
|
Typography,
|
|
25
20
|
Upload,
|
|
26
|
-
type UploadFile,
|
|
27
21
|
} from 'antd';
|
|
28
22
|
import dayjs from 'dayjs';
|
|
29
|
-
import { useEffect, useState } from 'react';
|
|
23
|
+
import { useEffect, useRef, useState } from 'react';
|
|
30
24
|
import type { DataAuditEntry, DataFileRef } from 'openxiangda-contracts/browser';
|
|
31
25
|
import {
|
|
32
26
|
AuthoritativeSelector,
|
|
33
27
|
ResolvedValueText,
|
|
34
28
|
} from './AuthoritativeSelector';
|
|
29
|
+
import { AttachmentFileList } from './components/platform-fields/AttachmentFileList';
|
|
35
30
|
import { PlatformDirectoryPicker } from './components/platform-fields/PlatformDirectoryPicker';
|
|
36
31
|
import { instrumentFields, sectionTitles, type InstrumentField } from './fields';
|
|
37
32
|
import type { InstrumentRecord } from './instrument';
|
|
38
|
-
import {
|
|
39
|
-
instrumentDataApi,
|
|
40
|
-
instrumentFileContentUrl,
|
|
41
|
-
} from './platform-client';
|
|
33
|
+
import { instrumentDataApi } from './platform-client';
|
|
42
34
|
import { useRuntime } from './runtime';
|
|
43
35
|
|
|
44
36
|
type Values = Record<string, unknown>;
|
|
@@ -335,18 +327,13 @@ function FileValue({
|
|
|
335
327
|
const [uploading, setUploading] = useState(false);
|
|
336
328
|
const [uploadError, setUploadError] = useState('');
|
|
337
329
|
const refs = Array.isArray(value) ? value : value ? [value] : [];
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
type: file.contentType,
|
|
343
|
-
status: 'done',
|
|
344
|
-
url: instrumentFileContentUrl(file.id),
|
|
345
|
-
}));
|
|
330
|
+
const refsRef = useRef(refs);
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
refsRef.current = refs;
|
|
333
|
+
}, [refs]);
|
|
346
334
|
const uploadProps = {
|
|
347
335
|
accept,
|
|
348
336
|
disabled,
|
|
349
|
-
fileList: files,
|
|
350
337
|
maxCount,
|
|
351
338
|
multiple,
|
|
352
339
|
customRequest: async (options: {
|
|
@@ -365,7 +352,9 @@ function FileValue({
|
|
|
365
352
|
recordId
|
|
366
353
|
);
|
|
367
354
|
options.onProgress?.({ percent: 100 });
|
|
368
|
-
|
|
355
|
+
const next = multiple ? [...refsRef.current, uploaded] : uploaded;
|
|
356
|
+
refsRef.current = Array.isArray(next) ? next : [next];
|
|
357
|
+
onChange?.(next);
|
|
369
358
|
options.onSuccess?.(uploaded);
|
|
370
359
|
} catch (error) {
|
|
371
360
|
const failure =
|
|
@@ -376,26 +365,6 @@ function FileValue({
|
|
|
376
365
|
setUploading(false);
|
|
377
366
|
}
|
|
378
367
|
},
|
|
379
|
-
onDownload: (file: UploadFile) => {
|
|
380
|
-
window.open(
|
|
381
|
-
instrumentFileContentUrl(file.uid),
|
|
382
|
-
'_blank',
|
|
383
|
-
'noopener,noreferrer'
|
|
384
|
-
);
|
|
385
|
-
},
|
|
386
|
-
onPreview: (file: UploadFile) => {
|
|
387
|
-
window.open(
|
|
388
|
-
instrumentFileContentUrl(file.uid),
|
|
389
|
-
'_blank',
|
|
390
|
-
'noopener,noreferrer'
|
|
391
|
-
);
|
|
392
|
-
},
|
|
393
|
-
onRemove: (file: UploadFile) => {
|
|
394
|
-
if (disabled) return false;
|
|
395
|
-
const next = refs.filter(item => item.id !== file.uid);
|
|
396
|
-
onChange?.(multiple ? next : undefined);
|
|
397
|
-
return true;
|
|
398
|
-
},
|
|
399
368
|
showUploadList: false,
|
|
400
369
|
};
|
|
401
370
|
return (
|
|
@@ -437,46 +406,16 @@ function FileValue({
|
|
|
437
406
|
</Button>
|
|
438
407
|
</div>
|
|
439
408
|
)}
|
|
440
|
-
{
|
|
441
|
-
<
|
|
442
|
-
{
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
</span>
|
|
451
|
-
<Space size={2}>
|
|
452
|
-
<Button
|
|
453
|
-
aria-label={`预览${file.name}`}
|
|
454
|
-
icon={<EyeOutlined />}
|
|
455
|
-
onClick={() => uploadProps.onPreview(file)}
|
|
456
|
-
size="small"
|
|
457
|
-
type="text"
|
|
458
|
-
/>
|
|
459
|
-
<Button
|
|
460
|
-
aria-label={`下载${file.name}`}
|
|
461
|
-
icon={<DownloadOutlined />}
|
|
462
|
-
onClick={() => uploadProps.onDownload(file)}
|
|
463
|
-
size="small"
|
|
464
|
-
type="text"
|
|
465
|
-
/>
|
|
466
|
-
{!disabled && (
|
|
467
|
-
<Button
|
|
468
|
-
aria-label={`移除${file.name}`}
|
|
469
|
-
danger
|
|
470
|
-
icon={<PlusOutlined rotate={45} />}
|
|
471
|
-
onClick={() => uploadProps.onRemove(file)}
|
|
472
|
-
size="small"
|
|
473
|
-
type="text"
|
|
474
|
-
/>
|
|
475
|
-
)}
|
|
476
|
-
</Space>
|
|
477
|
-
</div>
|
|
478
|
-
))}
|
|
479
|
-
</div>
|
|
409
|
+
{refs.length ? (
|
|
410
|
+
<AttachmentFileList
|
|
411
|
+
files={refs}
|
|
412
|
+
onRemove={file => {
|
|
413
|
+
const next = refs.filter(item => item.id !== file.id);
|
|
414
|
+
refsRef.current = next;
|
|
415
|
+
onChange?.(multiple ? next : undefined);
|
|
416
|
+
}}
|
|
417
|
+
removable={!disabled}
|
|
418
|
+
/>
|
|
480
419
|
) : (
|
|
481
420
|
<div className="oxa-file-empty">尚未上传文件</div>
|
|
482
421
|
)}
|
|
@@ -575,49 +514,7 @@ function Detail({
|
|
|
575
514
|
|
|
576
515
|
function AttachmentReadOnly({ values }: { values: DataFileRef[] }) {
|
|
577
516
|
if (!values.length) return <>-</>;
|
|
578
|
-
return
|
|
579
|
-
<div className="oxa-attachment-readonly">
|
|
580
|
-
{values.map(file => (
|
|
581
|
-
<div className="oxa-attachment-row" key={file.id}>
|
|
582
|
-
<PaperClipOutlined />
|
|
583
|
-
<span className="oxa-attachment-name">{file.name}</span>
|
|
584
|
-
<span className="oxa-muted">{formatFileSize(file.size)}</span>
|
|
585
|
-
<Space size={4}>
|
|
586
|
-
{file.contentType.startsWith('image/') && (
|
|
587
|
-
<Button
|
|
588
|
-
aria-label={`预览${file.name}`}
|
|
589
|
-
icon={<EyeOutlined />}
|
|
590
|
-
onClick={() =>
|
|
591
|
-
window.open(
|
|
592
|
-
instrumentFileContentUrl(file.id),
|
|
593
|
-
'_blank',
|
|
594
|
-
'noopener,noreferrer'
|
|
595
|
-
)
|
|
596
|
-
}
|
|
597
|
-
size="small"
|
|
598
|
-
type="text"
|
|
599
|
-
/>
|
|
600
|
-
)}
|
|
601
|
-
<Button
|
|
602
|
-
aria-label={`下载${file.name}`}
|
|
603
|
-
icon={<DownloadOutlined />}
|
|
604
|
-
onClick={() =>
|
|
605
|
-
window.open(
|
|
606
|
-
instrumentFileContentUrl(file.id),
|
|
607
|
-
'_blank',
|
|
608
|
-
'noopener,noreferrer'
|
|
609
|
-
)
|
|
610
|
-
}
|
|
611
|
-
size="small"
|
|
612
|
-
type="text"
|
|
613
|
-
>
|
|
614
|
-
下载
|
|
615
|
-
</Button>
|
|
616
|
-
</Space>
|
|
617
|
-
</div>
|
|
618
|
-
))}
|
|
619
|
-
</div>
|
|
620
|
-
);
|
|
517
|
+
return <AttachmentFileList files={values} />;
|
|
621
518
|
}
|
|
622
519
|
|
|
623
520
|
function displayValue(field: InstrumentField, value: unknown) {
|
|
@@ -676,9 +573,3 @@ function auditLabel(operation: DataAuditEntry['operation']) {
|
|
|
676
573
|
function formatDateTime(value?: string) {
|
|
677
574
|
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-';
|
|
678
575
|
}
|
|
679
|
-
|
|
680
|
-
function formatFileSize(value: number) {
|
|
681
|
-
if (value < 1024) return `${value} B`;
|
|
682
|
-
if (value < 1024 * 1024) return `${Math.ceil(value / 1024)} KB`;
|
|
683
|
-
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
|
684
|
-
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CloseOutlined,
|
|
3
|
+
DownloadOutlined,
|
|
4
|
+
EyeOutlined,
|
|
5
|
+
FileExcelOutlined,
|
|
6
|
+
FileImageOutlined,
|
|
7
|
+
FilePdfOutlined,
|
|
8
|
+
FileTextOutlined,
|
|
9
|
+
FileWordOutlined,
|
|
10
|
+
PaperClipOutlined,
|
|
11
|
+
} from '@ant-design/icons';
|
|
12
|
+
import { App, Button, Image, Space } from 'antd';
|
|
13
|
+
import type { DataFileRef } from 'openxiangda-contracts/browser';
|
|
14
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
dataFileContentUrl,
|
|
17
|
+
fetchDataFileBlob,
|
|
18
|
+
loadDataFilePreview,
|
|
19
|
+
} from '../../platform-client';
|
|
20
|
+
import { attachmentPreviewPath } from '../../runtime-meta';
|
|
21
|
+
|
|
22
|
+
interface PreviewImage {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
src: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
29
|
+
'avif',
|
|
30
|
+
'bmp',
|
|
31
|
+
'gif',
|
|
32
|
+
'ico',
|
|
33
|
+
'jpeg',
|
|
34
|
+
'jpg',
|
|
35
|
+
'png',
|
|
36
|
+
'svg',
|
|
37
|
+
'webp',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function extension(name: string) {
|
|
41
|
+
return name.toLowerCase().match(/\.([a-z0-9]+)$/)?.[1] || '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isImage(file: DataFileRef) {
|
|
45
|
+
return (
|
|
46
|
+
file.contentType.toLowerCase().startsWith('image/') ||
|
|
47
|
+
IMAGE_EXTENSIONS.has(extension(file.name))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fileIcon(file: DataFileRef) {
|
|
52
|
+
const suffix = extension(file.name);
|
|
53
|
+
if (isImage(file)) return <FileImageOutlined />;
|
|
54
|
+
if (suffix === 'pdf') return <FilePdfOutlined />;
|
|
55
|
+
if (suffix === 'doc' || suffix === 'docx') return <FileWordOutlined />;
|
|
56
|
+
if (suffix === 'xls' || suffix === 'xlsx') return <FileExcelOutlined />;
|
|
57
|
+
if (suffix === 'txt' || suffix === 'csv') return <FileTextOutlined />;
|
|
58
|
+
return <PaperClipOutlined />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatManagedFileSize(value: number) {
|
|
62
|
+
if (value < 1024) return `${value} B`;
|
|
63
|
+
if (value < 1024 * 1024) return `${Math.ceil(value / 1024)} KB`;
|
|
64
|
+
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function revokeImages(images: PreviewImage[]) {
|
|
68
|
+
images.forEach(image => URL.revokeObjectURL(image.src));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function openIsolatedWindow(url: string) {
|
|
72
|
+
const target = window.open('about:blank', '_blank');
|
|
73
|
+
if (!target) return false;
|
|
74
|
+
target.opener = null;
|
|
75
|
+
target.location.replace(url);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function AttachmentFileList({
|
|
80
|
+
files,
|
|
81
|
+
resourceCode = 'instruments',
|
|
82
|
+
removable = false,
|
|
83
|
+
onRemove,
|
|
84
|
+
}: {
|
|
85
|
+
files: DataFileRef[];
|
|
86
|
+
resourceCode?: string;
|
|
87
|
+
removable?: boolean;
|
|
88
|
+
onRemove?: (file: DataFileRef) => void;
|
|
89
|
+
}) {
|
|
90
|
+
const { message } = App.useApp();
|
|
91
|
+
const [openingId, setOpeningId] = useState('');
|
|
92
|
+
const [images, setImages] = useState<PreviewImage[]>([]);
|
|
93
|
+
const [imageOpen, setImageOpen] = useState(false);
|
|
94
|
+
const [currentImage, setCurrentImage] = useState(0);
|
|
95
|
+
const imageFiles = useMemo(() => files.filter(isImage), [files]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => () => revokeImages(images), [images]);
|
|
98
|
+
|
|
99
|
+
const closeImages = () => {
|
|
100
|
+
setImageOpen(false);
|
|
101
|
+
setImages(current => {
|
|
102
|
+
revokeImages(current);
|
|
103
|
+
return [];
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const previewImage = async (file: DataFileRef) => {
|
|
108
|
+
setOpeningId(file.id);
|
|
109
|
+
try {
|
|
110
|
+
const settled = await Promise.allSettled(
|
|
111
|
+
imageFiles.map(async candidate => {
|
|
112
|
+
const preview = await loadDataFilePreview(resourceCode, candidate.id);
|
|
113
|
+
if (!preview.canPreview || preview.previewType !== 'image') {
|
|
114
|
+
throw new Error(
|
|
115
|
+
preview.unsupportedReason || `${candidate.name} 暂不支持图片预览`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const blob = await fetchDataFileBlob(resourceCode, candidate.id);
|
|
119
|
+
return {
|
|
120
|
+
id: candidate.id,
|
|
121
|
+
name: candidate.name,
|
|
122
|
+
src: URL.createObjectURL(blob),
|
|
123
|
+
};
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
const resolved = settled.flatMap(result =>
|
|
127
|
+
result.status === 'fulfilled' ? [result.value] : []
|
|
128
|
+
);
|
|
129
|
+
const current = resolved.findIndex(image => image.id === file.id);
|
|
130
|
+
if (current < 0) {
|
|
131
|
+
const failure = settled.find(
|
|
132
|
+
result => result.status === 'rejected'
|
|
133
|
+
) as PromiseRejectedResult | undefined;
|
|
134
|
+
throw failure?.reason || new Error('图片预览加载失败');
|
|
135
|
+
}
|
|
136
|
+
setImages(previous => {
|
|
137
|
+
revokeImages(previous);
|
|
138
|
+
return resolved;
|
|
139
|
+
});
|
|
140
|
+
setCurrentImage(current);
|
|
141
|
+
setImageOpen(true);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
message.error(error instanceof Error ? error.message : String(error));
|
|
144
|
+
} finally {
|
|
145
|
+
setOpeningId('');
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const openPreview = (file: DataFileRef) => {
|
|
150
|
+
if (isImage(file)) {
|
|
151
|
+
void previewImage(file);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!openIsolatedWindow(attachmentPreviewPath(resourceCode, file.id))) {
|
|
155
|
+
void message.warning('浏览器阻止了预览窗口,请允许弹出窗口后重试');
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const download = (file: DataFileRef) => {
|
|
160
|
+
if (!openIsolatedWindow(dataFileContentUrl(resourceCode, file.id))) {
|
|
161
|
+
void message.warning('浏览器阻止了下载窗口,请允许弹出窗口后重试');
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<>
|
|
167
|
+
<div className="oxa-file-list">
|
|
168
|
+
{files.map(file => (
|
|
169
|
+
<div className="oxa-file-item" key={file.id}>
|
|
170
|
+
<span className="oxa-file-kind">{fileIcon(file)}</span>
|
|
171
|
+
<button
|
|
172
|
+
className="oxa-file-meta oxa-file-name-button"
|
|
173
|
+
disabled={openingId === file.id}
|
|
174
|
+
onClick={() => openPreview(file)}
|
|
175
|
+
type="button"
|
|
176
|
+
>
|
|
177
|
+
<strong>{file.name}</strong>
|
|
178
|
+
<small>{formatManagedFileSize(file.size)} · 已上传</small>
|
|
179
|
+
</button>
|
|
180
|
+
<Space size={2}>
|
|
181
|
+
<Button
|
|
182
|
+
aria-label={`预览${file.name}`}
|
|
183
|
+
icon={<EyeOutlined />}
|
|
184
|
+
loading={openingId === file.id}
|
|
185
|
+
onClick={() => openPreview(file)}
|
|
186
|
+
size="small"
|
|
187
|
+
type="text"
|
|
188
|
+
/>
|
|
189
|
+
<Button
|
|
190
|
+
aria-label={`下载${file.name}`}
|
|
191
|
+
icon={<DownloadOutlined />}
|
|
192
|
+
onClick={() => download(file)}
|
|
193
|
+
size="small"
|
|
194
|
+
type="text"
|
|
195
|
+
/>
|
|
196
|
+
{removable && (
|
|
197
|
+
<Button
|
|
198
|
+
aria-label={`移除${file.name}`}
|
|
199
|
+
danger
|
|
200
|
+
icon={<CloseOutlined />}
|
|
201
|
+
onClick={() => onRemove?.(file)}
|
|
202
|
+
size="small"
|
|
203
|
+
type="text"
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
</Space>
|
|
207
|
+
</div>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
<Image.PreviewGroup
|
|
211
|
+
items={images.map(image => ({ src: image.src, alt: image.name }))}
|
|
212
|
+
preview={{
|
|
213
|
+
open: imageOpen,
|
|
214
|
+
current: currentImage,
|
|
215
|
+
countRender: (current, total) => `${current}/${total}`,
|
|
216
|
+
onChange: setCurrentImage,
|
|
217
|
+
onOpenChange: open => {
|
|
218
|
+
if (!open) closeImages();
|
|
219
|
+
},
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<span aria-hidden="true" style={{ display: 'none' }} />
|
|
223
|
+
</Image.PreviewGroup>
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -6,6 +6,7 @@ import ReactDOM from 'react-dom/client';
|
|
|
6
6
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
|
7
7
|
import { instrumentProvider } from './data-provider';
|
|
8
8
|
import { CollegePage } from './CollegePage';
|
|
9
|
+
import { FilePreviewPage } from './FilePreviewPage';
|
|
9
10
|
import { InstrumentDetailPage } from './InstrumentDetailPage';
|
|
10
11
|
import { InstrumentFormPage } from './InstrumentFormPage';
|
|
11
12
|
import { InstrumentListPage } from './InstrumentListPage';
|
|
@@ -51,6 +52,10 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
51
52
|
<Route path="/" element={<Navigate replace to="/instruments" />} />
|
|
52
53
|
<Route path="/instruments" element={<InstrumentListPage />} />
|
|
53
54
|
<Route path="/colleges" element={<CollegePage />} />
|
|
55
|
+
<Route
|
|
56
|
+
path="/files/:resourceCode/:fileId/preview"
|
|
57
|
+
element={<FilePreviewPage />}
|
|
58
|
+
/>
|
|
54
59
|
<Route
|
|
55
60
|
path="/instruments/new"
|
|
56
61
|
element={<InstrumentFormPage mode="create" />}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SCHEMA_VERSIONS,
|
|
3
|
+
type DataFilePreview,
|
|
3
4
|
type DataFileRef,
|
|
4
5
|
type DataFileUploadPlan,
|
|
5
6
|
type DataAuditPage,
|
|
@@ -377,10 +378,44 @@ export const instrumentDataApi: InstrumentDataApiAdapter = {
|
|
|
377
378
|
},
|
|
378
379
|
};
|
|
379
380
|
|
|
380
|
-
export function
|
|
381
|
-
|
|
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(
|
|
382
391
|
fileId
|
|
383
|
-
)}/content
|
|
392
|
+
)}/content?${query.toString()}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
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
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
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();
|
|
384
419
|
}
|
|
385
420
|
|
|
386
421
|
export interface CollegeRecord extends Record<string, unknown> {
|
|
@@ -45,6 +45,21 @@ export function applicationBasename() {
|
|
|
45
45
|
return resolveApplicationBasename(runtimeMount());
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function attachmentPreviewPath(resourceCode: string, fileId: string) {
|
|
49
|
+
return resolveAttachmentPreviewPath(runtimeMount(), resourceCode, fileId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveAttachmentPreviewPath(
|
|
53
|
+
mount: RuntimeMount | null,
|
|
54
|
+
resourceCode: string,
|
|
55
|
+
fileId: string
|
|
56
|
+
) {
|
|
57
|
+
const base = resolveApplicationBasename(mount)?.replace(/\/+$/, '') || '';
|
|
58
|
+
return `${base}/files/${encodeURIComponent(resourceCode)}/${encodeURIComponent(
|
|
59
|
+
fileId
|
|
60
|
+
)}/preview`;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
export function resolveApplicationApiPath(
|
|
49
64
|
mount: RuntimeMount | null,
|
|
50
65
|
path: string
|