cabloy 5.1.98 → 5.1.99
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/.cabloy-version +1 -1
- package/.claude/hooks/contract-loop-gate.ts +151 -44
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +2 -0
- package/cabloy-docs/.vitepress/config.mjs +4 -0
- package/cabloy-docs/backend/controller-aop-guide.md +0 -1
- package/cabloy-docs/frontend/a-openapi-under-the-hood.md +1 -1
- package/cabloy-docs/frontend/generated-contract-consumption-entry-branch.md +1 -1
- package/cabloy-docs/frontend/generated-contract-consumption-list-branch.md +1 -1
- package/cabloy-docs/frontend/generated-contract-consumption-verify-playbook.md +1 -1
- package/cabloy-docs/frontend/model-resource-owner-pattern.md +1 -1
- package/cabloy-docs/frontend/model-state-guide.md +3 -1
- package/cabloy-docs/frontend/resource-entry-page-deep-dive.md +2 -2
- package/cabloy-docs/frontend/resource-list-page-deep-dive.md +1 -1
- package/cabloy-docs/frontend/rest-resource-source-reading-map.md +2 -2
- package/cabloy-docs/frontend/rest-resource-under-the-hood.md +4 -4
- package/cabloy-docs/frontend/use-state-data-best-practices.md +384 -0
- package/package.json +1 -1
- package/scripts/upgrade.ts +2 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/beanBase.ts +9 -9
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +4 -4
- package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +22 -0
- package/vona/src/suite/a-training/modules/training-record/src/bean/meta.version.ts +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts +47 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +13 -0
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +13 -0
- package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +16 -5
- package/vona/src/suite/a-training/modules/training-record/src/index.ts +2 -0
- package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +1 -0
- package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +34 -0
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +209 -123
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +18 -1
- package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -2
- package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +1 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/package.json +5 -3
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +28 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +8 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyRequest.ts +15 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +30 -0
- package/vona/src/suite-vendor/a-file/package.json +2 -2
- package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +27 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +8 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyRequest.ts +15 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +27 -0
- package/vona/src/suite-vendor/a-image/modules/image-native/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +1 -1
- package/vona/src/suite-vendor/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/.metadata/index.ts +24 -9
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +87 -73
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionActionByRoles.ts +27 -0
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionUser.ts +20 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +4 -3
- package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +5 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/.metadata/index.ts +0 -13
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/passport.ts +0 -7
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/bean/meta.version.ts +2 -24
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +1 -1
- package/zova/packages-zova/zova-core/src/bean/beanBase.ts +9 -13
- package/zova/pnpm-lock.yaml +29 -14
- package/zova/src/suite/a-demo/modules/demo-basic/src/page/toolOne/controller.tsx +2 -2
- package/zova/src/suite/a-demo/modules/demo-todo/src/page/todo/controller.tsx +2 -2
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +2 -2
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +2 -2
- package/zova/src/suite/a-home/modules/home-login/src/page/login/controller.tsx +2 -2
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockDetails/controller.tsx +5 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockTable/controller.tsx +20 -15
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/service/detail.tsx +6 -1
- package/zova/src/suite/cabloy-basic/modules/basic-file/cli/openapi.config.ts +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/package.json +58 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/component/formFieldFile.ts +31 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +222 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/locales.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/this.ts +2 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +121 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/baseURL.ts +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/index.ts +3 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +306 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +5718 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +40 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +187 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +534 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/en-us.ts +15 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/zh-cn.ts +15 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/index.ts +4 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +74 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/model/file.ts +23 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/file.ts +12 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.build.json +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.json +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +61 -0
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +38 -1
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +31 -0
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +52 -0
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +614 -1
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +10 -0
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +126 -53
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/model/image.ts +23 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +44 -4
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +17 -12
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +2 -2
- package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +2 -2
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/controller.tsx +7 -2
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +34 -33
- package/zova/src/suite/cabloy-basic/package.json +1 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +5 -3
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +2 -2
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resource/controller.tsx +2 -2
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +17 -17
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +8 -3
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permission.ts +0 -17
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.userName.ts +0 -35
package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
import type { IComponentOptions, TypeControllerInnerProps } from 'zova';
|
|
3
|
+
import type { IJsxRenderContextFormField } from 'zova-module-a-form';
|
|
4
|
+
import type { ControllerFormField, IFormFieldComponentOptions } from 'zova-module-a-form';
|
|
5
|
+
import type { IResourceFormFieldOptionsBase } from 'zova-module-a-openapi';
|
|
6
|
+
import type { ScopeModuleBasicFile } from 'zova-module-basic-file';
|
|
7
|
+
|
|
8
|
+
import { classes } from 'typestyle';
|
|
9
|
+
import { BeanControllerBase, ClientOnly, Use } from 'zova';
|
|
10
|
+
import { Controller } from 'zova-module-a-bean';
|
|
11
|
+
import { ZFormField } from 'zova-module-a-form';
|
|
12
|
+
import { $QueryEnsureLoaded } from 'zova-module-a-model';
|
|
13
|
+
|
|
14
|
+
import type { IFilePreviewItem } from '../../types/file.js';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
formatFileDate,
|
|
18
|
+
formatFileSize,
|
|
19
|
+
inferFileRelationName,
|
|
20
|
+
resolveFileDownloadUrl,
|
|
21
|
+
} from '../../lib/index.js';
|
|
22
|
+
import { ModelFile } from '../../model/file.js';
|
|
23
|
+
|
|
24
|
+
declare module 'zova-module-a-openapi' {
|
|
25
|
+
export interface IResourceFormFieldRecord {
|
|
26
|
+
'basic-file:formFieldFile'?: IResourceFormFieldFileOptions;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IResourceFormFieldFileOptions extends IResourceFormFieldOptionsBase {
|
|
31
|
+
fileScene?: string;
|
|
32
|
+
relationName?: string;
|
|
33
|
+
multiple?: boolean;
|
|
34
|
+
maxCount?: number;
|
|
35
|
+
accept?: string | string[];
|
|
36
|
+
mimeTypes?: string[];
|
|
37
|
+
extensions?: string[];
|
|
38
|
+
maxSize?: number;
|
|
39
|
+
minSize?: number;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ControllerFormFieldFileProps extends IFormFieldComponentOptions {
|
|
44
|
+
options?: IResourceFormFieldFileOptions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface IUploadPolicyState {
|
|
48
|
+
acceptAttr?: string;
|
|
49
|
+
multiple: boolean;
|
|
50
|
+
pending: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Controller()
|
|
54
|
+
export class ControllerFormFieldFile extends BeanControllerBase {
|
|
55
|
+
static $propsDefault = {
|
|
56
|
+
options: {
|
|
57
|
+
maxCount: 1,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
static $componentOptions: IComponentOptions = { inheritAttrs: false, deepExtendDefault: true };
|
|
62
|
+
|
|
63
|
+
fileInputRef?: HTMLInputElement;
|
|
64
|
+
errorMessage?: string;
|
|
65
|
+
isUploading = false;
|
|
66
|
+
currentValue?: TableIdentity | TableIdentity[] | string;
|
|
67
|
+
currentOptions: IResourceFormFieldFileOptions = {};
|
|
68
|
+
$$formField?: ControllerFormField;
|
|
69
|
+
uploadedPreviewMap: Record<string, IFilePreviewItem> = {};
|
|
70
|
+
|
|
71
|
+
@Use()
|
|
72
|
+
$$modelFile: ModelFile;
|
|
73
|
+
|
|
74
|
+
@Use({ injectionScope: 'host' })
|
|
75
|
+
$$renderContext: IJsxRenderContextFormField;
|
|
76
|
+
|
|
77
|
+
protected async __init__() {}
|
|
78
|
+
|
|
79
|
+
protected render() {
|
|
80
|
+
if (this._props.readonly) {
|
|
81
|
+
return this._renderReadonlyPreset();
|
|
82
|
+
}
|
|
83
|
+
return (
|
|
84
|
+
<ZFormField
|
|
85
|
+
{...this._props}
|
|
86
|
+
slotDefault={({ propsBucket, props }, $$formField) => {
|
|
87
|
+
this.$$formField = $$formField;
|
|
88
|
+
const fieldOptions = (propsBucket.options ?? {}) as IResourceFormFieldFileOptions;
|
|
89
|
+
const disableNotifyChanged = propsBucket.disableNotifyChanged as boolean | undefined;
|
|
90
|
+
const fieldValue = propsBucket.value as unknown;
|
|
91
|
+
const propsClass = (props as { class?: string }).class;
|
|
92
|
+
this.currentValue = fieldValue as any;
|
|
93
|
+
this.currentOptions = fieldOptions;
|
|
94
|
+
const uploadPolicyState = this._getUploadPolicyState(fieldOptions);
|
|
95
|
+
const items = this._getPreviewItems(fieldValue, uploadPolicyState.multiple);
|
|
96
|
+
const maxCount = this._getMaxCount(fieldOptions);
|
|
97
|
+
const hasValidationError = !$$formField.field.state.meta.isValid;
|
|
98
|
+
const cardClass = classes(
|
|
99
|
+
'rounded-box border border-base-300 bg-base-100 p-4',
|
|
100
|
+
(hasValidationError || !!this.errorMessage) && 'border-error',
|
|
101
|
+
propsClass,
|
|
102
|
+
);
|
|
103
|
+
return (
|
|
104
|
+
<div class={cardClass}>
|
|
105
|
+
<ClientOnly>
|
|
106
|
+
<input
|
|
107
|
+
ref={ref => {
|
|
108
|
+
this.fileInputRef = ref as HTMLInputElement;
|
|
109
|
+
}}
|
|
110
|
+
class="hidden"
|
|
111
|
+
type="file"
|
|
112
|
+
accept={uploadPolicyState.acceptAttr}
|
|
113
|
+
multiple={uploadPolicyState.multiple}
|
|
114
|
+
onChange={event => {
|
|
115
|
+
void this._handleFileChange(event, disableNotifyChanged);
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
</ClientOnly>
|
|
119
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
120
|
+
{items.length < maxCount && (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
disabled={this.isUploading || uploadPolicyState.pending}
|
|
124
|
+
class={classes(
|
|
125
|
+
'btn btn-primary',
|
|
126
|
+
(this.isUploading || uploadPolicyState.pending) && 'btn-disabled',
|
|
127
|
+
)}
|
|
128
|
+
onClick={() => {
|
|
129
|
+
if (this.isUploading || uploadPolicyState.pending) return;
|
|
130
|
+
this._applyInputPolicy(uploadPolicyState);
|
|
131
|
+
this.fileInputRef?.click();
|
|
132
|
+
}}
|
|
133
|
+
>
|
|
134
|
+
{this._getUploadButtonText(items.length, uploadPolicyState.multiple)}
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
{this.isUploading && (
|
|
138
|
+
<span class="inline-flex items-center gap-2 text-sm text-base-content/70">
|
|
139
|
+
<span class="loading loading-spinner loading-sm text-primary"></span>
|
|
140
|
+
{this._scope.locale.Uploading()}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
{!items.length && !this.isUploading && (
|
|
144
|
+
<span class="text-sm text-base-content/60">
|
|
145
|
+
{fieldOptions.placeholder ?? this._scope.locale.NoFileSelected()}
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
{!!this.errorMessage && <p class="mt-3 text-sm text-error">{this.errorMessage}</p>}
|
|
150
|
+
{!!items.length && (
|
|
151
|
+
<div class="mt-4 space-y-3">
|
|
152
|
+
{items.map((item, index) => {
|
|
153
|
+
return this._renderPreviewCard(item, index, false, disableNotifyChanged);
|
|
154
|
+
})}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}}
|
|
160
|
+
></ZFormField>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private get _props() {
|
|
165
|
+
return this.$props as TypeControllerInnerProps<
|
|
166
|
+
ControllerFormFieldFileProps,
|
|
167
|
+
keyof typeof ControllerFormFieldFile.$propsDefault
|
|
168
|
+
>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private get _scope() {
|
|
172
|
+
return this.scope as unknown as ScopeModuleBasicFile;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private _renderReadonlyPreset() {
|
|
176
|
+
return (
|
|
177
|
+
<ZFormField
|
|
178
|
+
{...this._props}
|
|
179
|
+
slotDefault={({ propsBucket, props }) => {
|
|
180
|
+
const fieldOptions = (propsBucket.options ?? {}) as IResourceFormFieldFileOptions;
|
|
181
|
+
const fieldValue = propsBucket.value as unknown;
|
|
182
|
+
const propsClass = (props as { class?: string }).class;
|
|
183
|
+
this.currentValue = fieldValue as any;
|
|
184
|
+
this.currentOptions = fieldOptions;
|
|
185
|
+
const items = this._getPreviewItems(fieldValue);
|
|
186
|
+
return <div class={propsClass}>{this._renderReadonlyItems(items)}</div>;
|
|
187
|
+
}}
|
|
188
|
+
></ZFormField>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private _renderReadonlyItems(items: IFilePreviewItem[]) {
|
|
193
|
+
if (!items.length) {
|
|
194
|
+
return (
|
|
195
|
+
<span class="text-sm text-base-content/60">{this._scope.locale.NoFileSelected()}</span>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return (
|
|
199
|
+
<div class="space-y-3">
|
|
200
|
+
{items.map((item, index) => this._renderPreviewCard(item, index, true, false))}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private _renderPreviewCard(
|
|
206
|
+
item: IFilePreviewItem,
|
|
207
|
+
index: number,
|
|
208
|
+
readonly: boolean,
|
|
209
|
+
disableNotifyChanged?: boolean,
|
|
210
|
+
) {
|
|
211
|
+
const downloadUrl = item.downloadUrl ? this._resolveDownloadUrl(item.downloadUrl) : undefined;
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
key={`${item.id}-${index}`}
|
|
215
|
+
class="rounded-box border border-base-300 bg-base-100 p-4 shadow-sm"
|
|
216
|
+
>
|
|
217
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
218
|
+
<div class="min-w-0 space-y-1">
|
|
219
|
+
<div class="truncate font-medium text-base-content">
|
|
220
|
+
{item.filename ?? `#${String(item.id)}`}
|
|
221
|
+
</div>
|
|
222
|
+
<div class="text-sm text-base-content/70">
|
|
223
|
+
{item.contentType ?? '-'} · {formatFileSize(item.size)}
|
|
224
|
+
</div>
|
|
225
|
+
<div class="text-xs text-base-content/60">
|
|
226
|
+
{this._scope.locale.UploadedAt()}: {formatFileDate(item.uploadedAt)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="flex flex-wrap gap-2">
|
|
230
|
+
{downloadUrl && (
|
|
231
|
+
<a class="btn btn-sm btn-outline" href={downloadUrl} target="_blank" rel="noreferrer">
|
|
232
|
+
{this._scope.locale.DownloadFile()}
|
|
233
|
+
</a>
|
|
234
|
+
)}
|
|
235
|
+
{!readonly && (
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
class="btn btn-sm btn-outline btn-error"
|
|
239
|
+
onClick={() => {
|
|
240
|
+
this._removeItem(item.id, disableNotifyChanged);
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{this._scope.locale.RemoveFile()}
|
|
244
|
+
</button>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private _getUploadButtonText(
|
|
253
|
+
itemCount: number,
|
|
254
|
+
multiple = this._getEffectiveMultiple(this.currentOptions),
|
|
255
|
+
) {
|
|
256
|
+
if (multiple) {
|
|
257
|
+
return itemCount > 0 ? this._scope.locale.AddFile() : this._scope.locale.SelectFile();
|
|
258
|
+
}
|
|
259
|
+
return itemCount > 0 ? this._scope.locale.ReplaceFile() : this._scope.locale.SelectFile();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private _getUploadPolicyQuery(options?: IResourceFormFieldFileOptions) {
|
|
263
|
+
return this.$$modelFile.getUploadPolicy(options?.fileScene);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private _getUploadPolicyState(options?: IResourceFormFieldFileOptions): IUploadPolicyState {
|
|
267
|
+
const query = this._getUploadPolicyQuery(options);
|
|
268
|
+
const policy = query?.data;
|
|
269
|
+
return {
|
|
270
|
+
acceptAttr: this._getAcceptAttr(options, policy),
|
|
271
|
+
multiple: this._getEffectiveMultiple(options, policy),
|
|
272
|
+
pending: query ? query.data === undefined && !!(query.isPending || query.isFetching) : false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private _getCachedUploadPolicy(options?: IResourceFormFieldFileOptions) {
|
|
277
|
+
return this._getUploadPolicyQuery(options)?.data;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async _waitForUploadPolicy(options?: IResourceFormFieldFileOptions) {
|
|
281
|
+
await $QueryEnsureLoaded(() => this._getUploadPolicyQuery(options));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private _getEffectiveMultiple(
|
|
285
|
+
options?: IResourceFormFieldFileOptions,
|
|
286
|
+
policy = this._getCachedUploadPolicy(options),
|
|
287
|
+
) {
|
|
288
|
+
return !!(options?.multiple ?? policy?.multiple);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private _applyInputPolicy(uploadPolicyState: IUploadPolicyState) {
|
|
292
|
+
if (!this.fileInputRef) return;
|
|
293
|
+
this.fileInputRef.accept = uploadPolicyState.acceptAttr ?? '';
|
|
294
|
+
this.fileInputRef.multiple = uploadPolicyState.multiple;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private async _handleFileChange(event: Event, disableNotifyChanged?: boolean) {
|
|
298
|
+
const input = event.target as HTMLInputElement;
|
|
299
|
+
const files = Array.from(input.files ?? []);
|
|
300
|
+
input.value = '';
|
|
301
|
+
if (files.length === 0) return;
|
|
302
|
+
this.errorMessage = undefined;
|
|
303
|
+
const options = this.currentOptions ?? {};
|
|
304
|
+
const uploadTarget = this._resolveUploadTarget();
|
|
305
|
+
this.isUploading = true;
|
|
306
|
+
try {
|
|
307
|
+
await this._waitForUploadPolicy(options);
|
|
308
|
+
const multiple = this._getEffectiveMultiple(options);
|
|
309
|
+
const currentIds = this._normalizeValueToFileIds(this.currentValue, multiple);
|
|
310
|
+
const filesToHandle = multiple ? files : files.slice(0, 1);
|
|
311
|
+
const maxCount = this._getMaxCount(options);
|
|
312
|
+
const nextCountCandidate = multiple
|
|
313
|
+
? currentIds.length + filesToHandle.length
|
|
314
|
+
: filesToHandle.length;
|
|
315
|
+
if (nextCountCandidate > maxCount) {
|
|
316
|
+
this.errorMessage = this._scope.locale.TooManyFiles(maxCount);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const uploadedItems: IFilePreviewItem[] = [];
|
|
320
|
+
for (const file of filesToHandle) {
|
|
321
|
+
const validationMessage = this._validateFile(file, options);
|
|
322
|
+
if (validationMessage) {
|
|
323
|
+
this.errorMessage = validationMessage;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const tokenRes = await this._scope.api.file.createUploadToken({
|
|
327
|
+
...uploadTarget,
|
|
328
|
+
size: file.size,
|
|
329
|
+
mimeType: file.type || 'application/octet-stream',
|
|
330
|
+
});
|
|
331
|
+
const uploaded = await this._scope.api.file.upload({
|
|
332
|
+
token: tokenRes.token,
|
|
333
|
+
file,
|
|
334
|
+
});
|
|
335
|
+
const item: IFilePreviewItem = {
|
|
336
|
+
id: uploaded.id as TableIdentity,
|
|
337
|
+
filename: uploaded.filename,
|
|
338
|
+
contentType: uploaded.contentType,
|
|
339
|
+
size: uploaded.size,
|
|
340
|
+
uploadedAt: uploaded.uploadedAt,
|
|
341
|
+
downloadUrl: uploaded.url,
|
|
342
|
+
public: uploaded.public,
|
|
343
|
+
signed: uploaded.signed,
|
|
344
|
+
};
|
|
345
|
+
this.uploadedPreviewMap[String(item.id)] = item;
|
|
346
|
+
uploadedItems.push(item);
|
|
347
|
+
}
|
|
348
|
+
if (uploadedItems.length === 0) return;
|
|
349
|
+
const nextIds = multiple
|
|
350
|
+
? [...currentIds, ...uploadedItems.map(item => item.id)]
|
|
351
|
+
: [uploadedItems[uploadedItems.length - 1].id];
|
|
352
|
+
this._setFieldValue(nextIds, disableNotifyChanged, multiple);
|
|
353
|
+
this.errorMessage = undefined;
|
|
354
|
+
} catch (err: any) {
|
|
355
|
+
this.errorMessage = err?.message ?? this._scope.locale.FileUploadFailed();
|
|
356
|
+
} finally {
|
|
357
|
+
this.isUploading = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private _resolveUploadTarget() {
|
|
362
|
+
const fileScene = this.currentOptions.fileScene;
|
|
363
|
+
if (!fileScene) {
|
|
364
|
+
throw new Error('should specify file upload scene');
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
fileScene,
|
|
368
|
+
} as const;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private _removeItem(fileId: TableIdentity, disableNotifyChanged?: boolean) {
|
|
372
|
+
const multiple = this._getEffectiveMultiple(this.currentOptions);
|
|
373
|
+
const currentIds = this._normalizeValueToFileIds(this.currentValue, multiple);
|
|
374
|
+
const nextIds = currentIds.filter(item => String(item) !== String(fileId));
|
|
375
|
+
delete this.uploadedPreviewMap[String(fileId)];
|
|
376
|
+
this._setFieldValue(nextIds, disableNotifyChanged, multiple);
|
|
377
|
+
this.errorMessage = undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private _setFieldValue(
|
|
381
|
+
fileIds: TableIdentity[],
|
|
382
|
+
disableNotifyChanged: boolean | undefined,
|
|
383
|
+
multiple: boolean,
|
|
384
|
+
) {
|
|
385
|
+
const nextValue = multiple ? fileIds : (fileIds[0] ?? '');
|
|
386
|
+
this.currentValue = nextValue as any;
|
|
387
|
+
this._syncRelationField(fileIds, multiple);
|
|
388
|
+
this.$$formField?.setValue(nextValue, disableNotifyChanged);
|
|
389
|
+
this.$$formField?.handleBlur();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private _syncRelationField(fileIds: TableIdentity[], multiple: boolean) {
|
|
393
|
+
const relationName = this._getRelationName();
|
|
394
|
+
if (!relationName) return;
|
|
395
|
+
const relationMap = this._getRelationPreviewMap();
|
|
396
|
+
const relationItems = fileIds.map(fileId => {
|
|
397
|
+
const key = String(fileId);
|
|
398
|
+
return this.uploadedPreviewMap[key] ?? relationMap[key] ?? { id: fileId };
|
|
399
|
+
});
|
|
400
|
+
const relationValue = multiple ? relationItems : (relationItems[0] ?? undefined);
|
|
401
|
+
this.$$renderContext.$$form.setFieldValue(relationName as never, relationValue, true);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private _getPreviewItems(
|
|
405
|
+
value: unknown,
|
|
406
|
+
multiple = this._getEffectiveMultiple(this.currentOptions),
|
|
407
|
+
) {
|
|
408
|
+
const fileIds = this._normalizeValueToFileIds(value, multiple);
|
|
409
|
+
const relationMap = this._getRelationPreviewMap();
|
|
410
|
+
if (fileIds.length === 0) {
|
|
411
|
+
return Object.values(relationMap);
|
|
412
|
+
}
|
|
413
|
+
return fileIds.map(fileId => {
|
|
414
|
+
const key = String(fileId);
|
|
415
|
+
return this.uploadedPreviewMap[key] ?? relationMap[key] ?? { id: fileId };
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private _getRelationPreviewMap() {
|
|
420
|
+
const relationName = this._getRelationName();
|
|
421
|
+
if (!relationName) return {} as Record<string, IFilePreviewItem>;
|
|
422
|
+
const relationValue = this.$$renderContext.$$form.getFieldValue(relationName as never);
|
|
423
|
+
const relationItems = Array.isArray(relationValue)
|
|
424
|
+
? relationValue
|
|
425
|
+
: relationValue
|
|
426
|
+
? [relationValue]
|
|
427
|
+
: [];
|
|
428
|
+
const map: Record<string, IFilePreviewItem> = {};
|
|
429
|
+
for (const relationItem of relationItems) {
|
|
430
|
+
const item = relationItem as Partial<IFilePreviewItem> | undefined;
|
|
431
|
+
if (!item?.id) continue;
|
|
432
|
+
map[String(item.id)] = {
|
|
433
|
+
id: item.id,
|
|
434
|
+
filename: item.filename,
|
|
435
|
+
contentType: item.contentType,
|
|
436
|
+
size: item.size,
|
|
437
|
+
uploadedAt: item.uploadedAt,
|
|
438
|
+
downloadUrl: item.downloadUrl,
|
|
439
|
+
public: item.public,
|
|
440
|
+
signed: item.signed,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return map;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private _getRelationName() {
|
|
447
|
+
return inferFileRelationName(this._props.name, this.currentOptions.relationName);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private _normalizeValueToFileIds(value: unknown, multiple: boolean): TableIdentity[] {
|
|
451
|
+
if (Array.isArray(value)) {
|
|
452
|
+
return value
|
|
453
|
+
.filter(item => item !== undefined && item !== null && item !== '')
|
|
454
|
+
.map(item => item as TableIdentity);
|
|
455
|
+
}
|
|
456
|
+
if (!multiple && value !== undefined && value !== null && value !== '') {
|
|
457
|
+
return [value as TableIdentity];
|
|
458
|
+
}
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private _getMaxCount(options: IResourceFormFieldFileOptions) {
|
|
463
|
+
if (!this._getEffectiveMultiple(options)) return 1;
|
|
464
|
+
return Math.max(options.maxCount ?? 1, 1);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private _getAcceptAttr(
|
|
468
|
+
options?: IResourceFormFieldFileOptions,
|
|
469
|
+
policy = this._getCachedUploadPolicy(options),
|
|
470
|
+
) {
|
|
471
|
+
if (!options) return undefined;
|
|
472
|
+
if (typeof options.accept === 'string') return options.accept;
|
|
473
|
+
if (options.accept?.length) return options.accept.join(',');
|
|
474
|
+
const parts = [
|
|
475
|
+
...(options.mimeTypes ?? policy?.mimeTypes ?? []),
|
|
476
|
+
...(options.extensions ?? policy?.extensions ?? []),
|
|
477
|
+
];
|
|
478
|
+
return parts.length > 0 ? parts.join(',') : undefined;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private _validateFile(file: File, options: IResourceFormFieldFileOptions) {
|
|
482
|
+
const acceptTokens = this._collectAcceptTokens(options);
|
|
483
|
+
if (acceptTokens.length > 0 && !this._matchesAccept(file, acceptTokens)) {
|
|
484
|
+
return this._scope.locale.InvalidFileType();
|
|
485
|
+
}
|
|
486
|
+
const maxSize = options.maxSize ?? this._getCachedUploadPolicy(options)?.maxSize;
|
|
487
|
+
if (typeof maxSize === 'number' && file.size > maxSize) {
|
|
488
|
+
return this._scope.locale.FileTooLarge(formatFileSize(maxSize));
|
|
489
|
+
}
|
|
490
|
+
if (typeof options.minSize === 'number' && file.size < options.minSize) {
|
|
491
|
+
return this._scope.locale.FileTooSmall(formatFileSize(options.minSize));
|
|
492
|
+
}
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private _collectAcceptTokens(options: IResourceFormFieldFileOptions) {
|
|
497
|
+
const accept = options.accept
|
|
498
|
+
? Array.isArray(options.accept)
|
|
499
|
+
? options.accept
|
|
500
|
+
: options.accept.split(',')
|
|
501
|
+
: [];
|
|
502
|
+
const policy = this._getCachedUploadPolicy(options);
|
|
503
|
+
return [
|
|
504
|
+
...accept,
|
|
505
|
+
...(options.mimeTypes ?? policy?.mimeTypes ?? []),
|
|
506
|
+
...(options.extensions ?? policy?.extensions ?? []),
|
|
507
|
+
]
|
|
508
|
+
.map(item => item.trim().toLowerCase())
|
|
509
|
+
.filter(item => !!item);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private _matchesAccept(file: File, acceptTokens: string[]) {
|
|
513
|
+
const mimeType = file.type.toLowerCase();
|
|
514
|
+
const extension = this._getFileExtension(file.name);
|
|
515
|
+
return acceptTokens.some(token => {
|
|
516
|
+
if (token.startsWith('.')) return extension === token;
|
|
517
|
+
if (token.endsWith('/*')) {
|
|
518
|
+
const prefix = token.slice(0, token.length - 1);
|
|
519
|
+
return mimeType.startsWith(prefix);
|
|
520
|
+
}
|
|
521
|
+
return mimeType === token;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private _getFileExtension(filename: string) {
|
|
526
|
+
const index = filename.lastIndexOf('.');
|
|
527
|
+
if (index === -1) return '';
|
|
528
|
+
return filename.slice(index).toLowerCase();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private _resolveDownloadUrl(url: string) {
|
|
532
|
+
return resolveFileDownloadUrl(url, this.sys.config.api.baseURL);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
AddFile: 'Add File',
|
|
3
|
+
DownloadFile: 'Download',
|
|
4
|
+
FileTooLarge: 'File size exceeds the limit: {0}',
|
|
5
|
+
FileTooSmall: 'File size is smaller than the limit: {0}',
|
|
6
|
+
FileUploadFailed: 'File upload failed',
|
|
7
|
+
InvalidFileType: 'Unsupported file type',
|
|
8
|
+
NoFileSelected: 'No file selected',
|
|
9
|
+
RemoveFile: 'Remove',
|
|
10
|
+
ReplaceFile: 'Replace File',
|
|
11
|
+
SelectFile: 'Select File',
|
|
12
|
+
TooManyFiles: 'You can upload up to {0} files',
|
|
13
|
+
UploadedAt: 'Uploaded At',
|
|
14
|
+
Uploading: 'Uploading...',
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
AddFile: '继续添加文件',
|
|
3
|
+
DownloadFile: '下载',
|
|
4
|
+
FileTooLarge: '文件大小超过限制:{0}',
|
|
5
|
+
FileTooSmall: '文件大小小于限制:{0}',
|
|
6
|
+
FileUploadFailed: '文件上传失败',
|
|
7
|
+
InvalidFileType: '文件类型不受支持',
|
|
8
|
+
NoFileSelected: '尚未选择文件',
|
|
9
|
+
RemoveFile: '移除',
|
|
10
|
+
ReplaceFile: '替换文件',
|
|
11
|
+
SelectFile: '选择文件',
|
|
12
|
+
TooManyFiles: '最多允许上传 {0} 个文件',
|
|
13
|
+
UploadedAt: '上传时间',
|
|
14
|
+
Uploading: '正在上传...',
|
|
15
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { IFilePreviewItem } from '../types/file.js';
|
|
2
|
+
|
|
3
|
+
export function inferFileRelationName(fieldName?: string, relationName?: string) {
|
|
4
|
+
if (relationName) return relationName;
|
|
5
|
+
if (!fieldName) return undefined;
|
|
6
|
+
if (fieldName.endsWith('Ids')) {
|
|
7
|
+
return `${fieldName.slice(0, -3)}s`;
|
|
8
|
+
}
|
|
9
|
+
if (fieldName.endsWith('Id')) {
|
|
10
|
+
return fieldName.slice(0, -2);
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveFileDownloadUrl(url: string | undefined, baseURL?: string) {
|
|
16
|
+
if (!url || !url.startsWith('/api/')) return url;
|
|
17
|
+
if (!baseURL) return url;
|
|
18
|
+
return `${baseURL.replace(/\/$/, '')}${url}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function summarizeFileRelationPreviewValue(value: unknown): {
|
|
22
|
+
count: number;
|
|
23
|
+
item?: IFilePreviewItem;
|
|
24
|
+
} {
|
|
25
|
+
if (!Array.isArray(value)) {
|
|
26
|
+
if (!isFilePreviewItem(value)) return { count: 0, item: undefined };
|
|
27
|
+
return { count: 1, item: value };
|
|
28
|
+
}
|
|
29
|
+
let count = 0;
|
|
30
|
+
let item: IFilePreviewItem | undefined;
|
|
31
|
+
for (const valueItem of value) {
|
|
32
|
+
if (!isFilePreviewItem(valueItem)) continue;
|
|
33
|
+
count += 1;
|
|
34
|
+
if (!item) item = valueItem;
|
|
35
|
+
}
|
|
36
|
+
return { count, item };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function collectFileRelationPreviewItems(value: unknown): IFilePreviewItem[] {
|
|
40
|
+
if (!Array.isArray(value)) {
|
|
41
|
+
if (!isFilePreviewItem(value)) return [];
|
|
42
|
+
return [value];
|
|
43
|
+
}
|
|
44
|
+
const items: IFilePreviewItem[] = [];
|
|
45
|
+
for (const valueItem of value) {
|
|
46
|
+
if (!isFilePreviewItem(valueItem)) continue;
|
|
47
|
+
items.push(valueItem);
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isFilePreviewItem(value: unknown): value is IFilePreviewItem {
|
|
53
|
+
if (!value || typeof value !== 'object') return false;
|
|
54
|
+
if (!('id' in value)) return false;
|
|
55
|
+
return value.id !== undefined && value.id !== null && value.id !== '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatFileSize(size?: number) {
|
|
59
|
+
if (size === undefined || size === null || Number.isNaN(size)) return '-';
|
|
60
|
+
if (size >= 1024 * 1024) {
|
|
61
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
|
62
|
+
}
|
|
63
|
+
if (size >= 1024) {
|
|
64
|
+
return `${Math.round(size / 1024)} KB`;
|
|
65
|
+
}
|
|
66
|
+
return `${size} B`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatFileDate(value?: string | Date) {
|
|
70
|
+
if (!value) return '-';
|
|
71
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
72
|
+
if (Number.isNaN(date.getTime())) return '-';
|
|
73
|
+
return date.toLocaleString();
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './file.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IDecoratorModelOptions } from 'zova-module-a-model';
|
|
2
|
+
|
|
3
|
+
import { BeanModelBase, Model } from 'zova-module-a-model';
|
|
4
|
+
|
|
5
|
+
import type { ApiApiFilegetUploadPolicyResponseBody } from '../api/file.js';
|
|
6
|
+
|
|
7
|
+
export interface IModelOptionsFile extends IDecoratorModelOptions {}
|
|
8
|
+
|
|
9
|
+
@Model<IModelOptionsFile>()
|
|
10
|
+
export class ModelFile extends BeanModelBase {
|
|
11
|
+
getUploadPolicy(fileScene?: string) {
|
|
12
|
+
if (!fileScene) return undefined;
|
|
13
|
+
return this.$useStateData<ApiApiFilegetUploadPolicyResponseBody>({
|
|
14
|
+
queryKey: ['uploadPolicy', 'file', fileScene],
|
|
15
|
+
queryFn: async () => {
|
|
16
|
+
return this.scope.api.file.getUploadPolicy({ fileScene });
|
|
17
|
+
},
|
|
18
|
+
meta: {
|
|
19
|
+
disableSuspenseOnInit: true,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
|
|
3
|
+
export interface IFilePreviewItem {
|
|
4
|
+
id: TableIdentity;
|
|
5
|
+
filename?: string;
|
|
6
|
+
contentType?: string;
|
|
7
|
+
size?: number;
|
|
8
|
+
uploadedAt?: string | Date;
|
|
9
|
+
downloadUrl?: string;
|
|
10
|
+
public?: boolean;
|
|
11
|
+
signed?: boolean;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './file.js';
|