@hzab/form-render-mobile 1.2.0 → 1.2.1-beta1

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.
@@ -1,287 +1,295 @@
1
- import { useState, useRef, useEffect, useMemo, useImperativeHandle, forwardRef } from "react";
2
- import { Button, Dialog, ActionSheet, Toast } from "antd-mobile";
3
- import { nanoid } from "nanoid";
4
-
5
- import ItemList from "./components/ItemList";
6
-
7
- import { getImage, getVideo, getAudio } from "./common/cordova-camera";
8
- import { handleMaxCount, handleInputFileList } from "./common/utils";
9
- import { handleOssUpload } from "./common/ossUpload";
10
- import uploadManager, { ChooserUploader, onGalleryUopload } from "../../common/location-utils";
11
-
12
- import "./uploader.less";
13
-
14
- const _ = require("lodash");
15
-
16
- function Uploader(props, ref) {
17
- const {
18
- name,
19
- multiple,
20
- // 模式字符串: all | select | image | video 或 数组 select | image | video 组合:如:['select', 'image']
21
- mode = "select",
22
- onChange,
23
- accept,
24
- disabled,
25
- readOnly,
26
- value,
27
- baseUrl = "",
28
- // 文件大小限制
29
- maxSize,
30
- maxCount = 1,
31
- // 是否使用 oss 上传文件
32
- isOssUpload,
33
- // 是否使用字符串结果
34
- isStrRes,
35
- // maxCount === 1 时,结果是否自动去除数组嵌套
36
- isResRemoveArr = true,
37
- ossUrl,
38
- ossOpt,
39
- onCountExceed,
40
- headers,
41
- uploadBtnNode,
42
- // cordova插件上传类型 cordova-plugin-chooser | cordova-plugin-camera
43
- cordovaPlugin = "cordova-plugin-camera",
44
- uploaderProps
45
- } = props;
46
- const [loading, setLoading] = useState(false);
47
- const [fileList, setFileList] = useState(handleInputFileList(value, maxCount));
48
- const [actionSheetVisible, setActionSheetVisible] = useState(false);
49
- const uploadManagerRef = useRef(new uploadManager(new ChooserUploader()));
50
-
51
- useEffect(() => {
52
- setFileList(handleInputFileList(value, maxCount));
53
- }, [value]);
54
-
55
- useImperativeHandle(ref, () => ({ onPicker, setActionSheetVisible }));
56
-
57
- async function onFileChange(e) {
58
- e.persist();
59
- const { files: rawFiles } = e.target;
60
- let files = [...fileList, ...[].slice.call(rawFiles)];
61
- // 超出个数限制只保留前面的 maxCount 个
62
- if (maxCount > 0 && files.length > maxCount) {
63
- onCountExceed && onCountExceed(files.length - maxCount);
64
- files.length = maxCount;
65
- Toast.show({
66
- icon: "fail",
67
- content: "文件个数超出限制",
68
- });
69
- }
70
- files = files.filter((it) => {
71
- if (!it.uid) {
72
- it.uid = nanoid();
73
- }
74
- if (maxSize && it.size > maxSize) {
75
- Toast.show({
76
- icon: "fail",
77
- content: "文件大小超出限制:" + it.size,
78
- });
79
- return false;
80
- }
81
- return it;
82
- });
83
- // 处理 oss 逻辑
84
- if (isOssUpload) {
85
- setLoading(true);
86
- files = await handleOssUpload(files, {
87
- axios: props.axios,
88
- axiosConf: props?.axiosConf,
89
- ...ossOpt,
90
- ossUrl,
91
- });
92
- setLoading(false);
93
- }
94
-
95
- e.target.value = ""; // HACK: fix the same file doesn't trigger onChange
96
- handleChange(files);
97
- }
98
-
99
- function handleChange(files, optType) {
100
- let _files = files;
101
- if (optType !== "del" && maxCount > 1) {
102
- _files = [...fileList, ..._files];
103
- }
104
- _files = handleMaxCount(files, maxCount);
105
- setFileList(_files);
106
- if (isOssUpload && isStrRes) {
107
- _files = _files?.map((file) => {
108
- return file.url || file.ossUrl;
109
- });
110
- }
111
- if (!_files || _files.length <= 0) {
112
- _files = undefined;
113
- } else if (isResRemoveArr && maxCount === 1) {
114
- // maxCount 为1的时候返回结果去除数组
115
- _files = _files[0];
116
- }
117
- onChange && onChange(_files);
118
- }
119
-
120
- const uploaderRef = useRef();
121
- const audioUploaderRef = useRef();
122
-
123
- const actions = useMemo(() => {
124
- const allEnum = {
125
- select: { text: "选择文件", key: "select" },
126
- image: { text: "拍照", key: "captureImage" },
127
- video: { text: "录像", key: "captureVideo" },
128
- audio: { text: "录音", key: "captureAudio" },
129
- };
130
- if (Array.isArray(mode)) {
131
- return mode.filter((it) => !!allEnum[it]).map((it) => allEnum[it]);
132
- }
133
- if (mode === "all") {
134
- return Object.keys(allEnum).map((key) => allEnum[key]);
135
- }
136
- return allEnum[mode] ? [allEnum[mode]] : [allEnum.select];
137
- }, [mode]);
138
-
139
- function onPicker({ key }) {
140
- switch (key) {
141
- case "select":
142
- if (navigator?.camera && cordovaPlugin === "cordova-plugin-camera") {
143
- onGalleryUopload?.({
144
- onUploadSuccess: (res) => {
145
- onFileChange({
146
- persist: () => { },
147
- target: {
148
- value: "",
149
- files: [res],
150
- },
151
- });
152
- },
153
- options: {
154
- cameraDirection: uploaderProps?.CameraConf?.frontCamera ? Camera.Direction.FRONT : null,
155
- ...uploaderProps?.CameraConf
156
- }
157
- });
158
- } else if (window?.chooser && cordovaPlugin === "cordova-plugin-chooser") {
159
- uploadManagerRef.current.uploadFile({
160
- errorCallback(type) {
161
- if (type === 2) {
162
- Toast.show("文件类型错误,请重新选择");
163
- }
164
- },
165
- onSuccess(values) {
166
- onFileChange({
167
- persist: () => { },
168
- target: {
169
- value: "",
170
- files: [values.blob],
171
- },
172
- });
173
- },
174
- onError(error) {
175
- console.log("文件选择失败:", error);
176
- },
177
- });
178
- } else {
179
- uploaderRef.current.click();
180
- }
181
- break;
182
-
183
- case "captureImage":
184
- getImage(props.imgPluginOpt, props.getImgOpt).then((res) => {
185
- onFileChange({
186
- persist: () => { },
187
- target: {
188
- value: "",
189
- files: [res],
190
- },
191
- });
192
- });
193
- break;
194
-
195
- case "captureVideo":
196
- getVideo().then((res) => {
197
- onFileChange({
198
- persist: () => { },
199
- target: {
200
- value: "",
201
- files: [res],
202
- },
203
- });
204
- });
205
- break;
206
-
207
- case "captureAudio":
208
- audioUploaderRef.current.click();
209
- // TODO: reader 报错
210
- // getAudio().then((res) => {
211
- // onFileChange({
212
- // persist: () => {},
213
- // target: {
214
- // value: "",
215
- // files: [res],
216
- // },
217
- // });
218
- // });
219
- break;
220
- }
221
- setActionSheetVisible(false);
222
- }
223
-
224
- function onItemDel(idx) {
225
- Dialog.confirm({
226
- content: "确认删除?",
227
- onConfirm: () => {
228
- const middleValue = _.cloneDeep(fileList);
229
- middleValue.splice(idx, 1);
230
- handleChange(middleValue, "del");
231
- setFileList(middleValue);
232
- },
233
- });
234
- }
235
-
236
- const inputProps = {
237
- className: "aria-hidden",
238
- style: { display: "none" },
239
- id: name,
240
- onChange: onFileChange,
241
- type: "file",
242
- name: name,
243
- multiple: multiple,
244
- accept: accept,
245
- capture: props.capture,
246
- };
247
-
248
- return (
249
- <div className="uploader">
250
- <input {...inputProps} ref={uploaderRef}></input>
251
- <input {...inputProps} ref={audioUploaderRef} id={name + "-audio"} accept="audio/*"></input>
252
- <ItemList
253
- fileList={fileList}
254
- baseUrl={isOssUpload ? baseUrl : ""}
255
- disabled={disabled}
256
- readOnly={readOnly}
257
- onItemDel={onItemDel}
258
- headers={headers}
259
- />
260
- {disabled || readOnly || (maxCount > 0 && fileList.length >= maxCount) ? null : (
261
- <>
262
- <div
263
- className={`uploader-add-btn ${loading ? "uploader-add-btn-loading" : ""}`}
264
- onClick={() => {
265
- if (loading) {
266
- return;
267
- }
268
- setActionSheetVisible(true);
269
- }}
270
- loading={loading}
271
- >
272
- {loading ? "上传中..." : uploadBtnNode ? uploadBtnNode : "+"}
273
- </div>
274
- <ActionSheet
275
- cancelText="取消"
276
- visible={actionSheetVisible}
277
- actions={actions}
278
- onAction={onPicker}
279
- onClose={() => setActionSheetVisible(false)}
280
- />
281
- </>
282
- )}
283
- </div>
284
- );
285
- }
286
-
287
- export default forwardRef(Uploader);
1
+ import { useState, useRef, useEffect, useMemo, useImperativeHandle, forwardRef } from "react";
2
+ import { Button, Dialog, ActionSheet, Toast } from "antd-mobile";
3
+ import { nanoid } from "nanoid";
4
+
5
+ import ItemList from "./components/ItemList";
6
+
7
+ import { getImage, getVideo, getAudio } from "./common/cordova-camera";
8
+ import { handleMaxCount, handleInputFileList } from "./common/utils";
9
+ import { handleOssUpload } from "./common/ossUpload";
10
+ import uploadManager, { ChooserUploader, onGalleryUopload } from "../../common/location-utils";
11
+
12
+ import "./uploader.less";
13
+
14
+ const _ = require("lodash");
15
+
16
+ function Uploader(props, ref) {
17
+ const {
18
+ name,
19
+ multiple,
20
+ // 模式字符串: all | select | image | video 或 数组 select | image | video 组合:如:['select', 'image']
21
+ mode = "select",
22
+ onChange,
23
+ accept,
24
+ disabled,
25
+ readOnly,
26
+ value,
27
+ baseUrl = "",
28
+ // 文件大小限制
29
+ maxSize,
30
+ maxCount = 1,
31
+ // 是否使用 oss 上传文件
32
+ isOssUpload,
33
+ // 是否使用字符串结果
34
+ isStrRes,
35
+ // maxCount === 1 时,结果是否自动去除数组嵌套
36
+ isResRemoveArr = true,
37
+ ossUrl,
38
+ ossOpt,
39
+ onCountExceed,
40
+ headers,
41
+ uploadBtnNode,
42
+ // cordova插件上传类型 cordova-plugin-chooser | cordova-plugin-camera
43
+ cordovaPlugin = "cordova-plugin-camera",
44
+ uploaderProps,
45
+ thumbnailParams = "x-oss-process=image/resize,w_100"
46
+ } = props;
47
+ const [loading, setLoading] = useState(false);
48
+ const [fileList, setFileList] = useState(handleInputFileList(value, maxCount, {
49
+ thumbnailParams,
50
+ ossOpt
51
+ }));
52
+ const [actionSheetVisible, setActionSheetVisible] = useState(false);
53
+ const uploadManagerRef = useRef(new uploadManager(new ChooserUploader()));
54
+
55
+ useEffect(() => {
56
+ setFileList(handleInputFileList(value, maxCount, {
57
+ thumbnailParams,
58
+ ossOpt
59
+ }));
60
+ }, [value, thumbnailParams, ossOpt]);
61
+
62
+ useImperativeHandle(ref, () => ({ onPicker, setActionSheetVisible }));
63
+
64
+ async function onFileChange(e) {
65
+ e.persist();
66
+ const { files: rawFiles } = e.target;
67
+ let files = [...fileList, ...[].slice.call(rawFiles)];
68
+ // 超出个数限制只保留前面的 maxCount 个
69
+ if (maxCount > 0 && files.length > maxCount) {
70
+ onCountExceed && onCountExceed(files.length - maxCount);
71
+ files.length = maxCount;
72
+ Toast.show({
73
+ icon: "fail",
74
+ content: "文件个数超出限制",
75
+ });
76
+ }
77
+ files = files.filter((it) => {
78
+ if (!it.uid) {
79
+ it.uid = nanoid();
80
+ }
81
+ if (maxSize && it.size > maxSize) {
82
+ Toast.show({
83
+ icon: "fail",
84
+ content: "文件大小超出限制:" + it.size,
85
+ });
86
+ return false;
87
+ }
88
+ return it;
89
+ });
90
+ // 处理 oss 逻辑
91
+ if (isOssUpload) {
92
+ setLoading(true);
93
+ files = await handleOssUpload(files, {
94
+ axios: props.axios,
95
+ axiosConf: props?.axiosConf,
96
+ ...ossOpt,
97
+ ossUrl,
98
+ thumbnailParams
99
+ });
100
+ setLoading(false);
101
+ }
102
+
103
+ e.target.value = ""; // HACK: fix the same file doesn't trigger onChange
104
+ handleChange(files);
105
+ }
106
+
107
+ function handleChange(files, optType) {
108
+ let _files = files;
109
+ if (optType !== "del" && maxCount > 1) {
110
+ _files = [...fileList, ..._files];
111
+ }
112
+ _files = handleMaxCount(files, maxCount);
113
+ setFileList(_files);
114
+ if (isOssUpload && isStrRes) {
115
+ _files = _files?.map((file) => {
116
+ return file.url || file.ossUrl;
117
+ });
118
+ }
119
+ if (!_files || _files.length <= 0) {
120
+ _files = undefined;
121
+ } else if (isResRemoveArr && maxCount === 1) {
122
+ // maxCount 为1的时候返回结果去除数组
123
+ _files = _files[0];
124
+ }
125
+ onChange && onChange(_files);
126
+ }
127
+
128
+ const uploaderRef = useRef();
129
+ const audioUploaderRef = useRef();
130
+
131
+ const actions = useMemo(() => {
132
+ const allEnum = {
133
+ select: { text: "选择文件", key: "select" },
134
+ image: { text: "拍照", key: "captureImage" },
135
+ video: { text: "录像", key: "captureVideo" },
136
+ audio: { text: "录音", key: "captureAudio" },
137
+ };
138
+ if (Array.isArray(mode)) {
139
+ return mode.filter((it) => !!allEnum[it]).map((it) => allEnum[it]);
140
+ }
141
+ if (mode === "all") {
142
+ return Object.keys(allEnum).map((key) => allEnum[key]);
143
+ }
144
+ return allEnum[mode] ? [allEnum[mode]] : [allEnum.select];
145
+ }, [mode]);
146
+
147
+ function onPicker({ key }) {
148
+ switch (key) {
149
+ case "select":
150
+ if (navigator?.camera && cordovaPlugin === "cordova-plugin-camera") {
151
+ onGalleryUopload?.({
152
+ onUploadSuccess: (res) => {
153
+ onFileChange({
154
+ persist: () => { },
155
+ target: {
156
+ value: "",
157
+ files: [res],
158
+ },
159
+ });
160
+ },
161
+ options: {
162
+ cameraDirection: uploaderProps?.CameraConf?.frontCamera ? Camera.Direction.FRONT : null,
163
+ ...uploaderProps?.CameraConf
164
+ }
165
+ });
166
+ } else if (window?.chooser && cordovaPlugin === "cordova-plugin-chooser") {
167
+ uploadManagerRef.current.uploadFile({
168
+ errorCallback(type) {
169
+ if (type === 2) {
170
+ Toast.show("文件类型错误,请重新选择");
171
+ }
172
+ },
173
+ onSuccess(values) {
174
+ onFileChange({
175
+ persist: () => { },
176
+ target: {
177
+ value: "",
178
+ files: [values.blob],
179
+ },
180
+ });
181
+ },
182
+ onError(error) {
183
+ console.log("文件选择失败:", error);
184
+ },
185
+ });
186
+ } else {
187
+ uploaderRef.current.click();
188
+ }
189
+ break;
190
+
191
+ case "captureImage":
192
+ getImage(props.imgPluginOpt, props.getImgOpt).then((res) => {
193
+ onFileChange({
194
+ persist: () => { },
195
+ target: {
196
+ value: "",
197
+ files: [res],
198
+ },
199
+ });
200
+ });
201
+ break;
202
+
203
+ case "captureVideo":
204
+ getVideo().then((res) => {
205
+ onFileChange({
206
+ persist: () => { },
207
+ target: {
208
+ value: "",
209
+ files: [res],
210
+ },
211
+ });
212
+ });
213
+ break;
214
+
215
+ case "captureAudio":
216
+ audioUploaderRef.current.click();
217
+ // TODO: reader 报错
218
+ // getAudio().then((res) => {
219
+ // onFileChange({
220
+ // persist: () => {},
221
+ // target: {
222
+ // value: "",
223
+ // files: [res],
224
+ // },
225
+ // });
226
+ // });
227
+ break;
228
+ }
229
+ setActionSheetVisible(false);
230
+ }
231
+
232
+ function onItemDel(idx) {
233
+ Dialog.confirm({
234
+ content: "确认删除?",
235
+ onConfirm: () => {
236
+ const middleValue = _.cloneDeep(fileList);
237
+ middleValue.splice(idx, 1);
238
+ handleChange(middleValue, "del");
239
+ setFileList(middleValue);
240
+ },
241
+ });
242
+ }
243
+
244
+ const inputProps = {
245
+ className: "aria-hidden",
246
+ style: { display: "none" },
247
+ id: name,
248
+ onChange: onFileChange,
249
+ type: "file",
250
+ name: name,
251
+ multiple: multiple,
252
+ accept: accept,
253
+ capture: props.capture,
254
+ };
255
+
256
+ return (
257
+ <div className="uploader">
258
+ <input {...inputProps} ref={uploaderRef}></input>
259
+ <input {...inputProps} ref={audioUploaderRef} id={name + "-audio"} accept="audio/*"></input>
260
+ <ItemList
261
+ fileList={fileList}
262
+ baseUrl={isOssUpload ? baseUrl : ""}
263
+ disabled={disabled}
264
+ readOnly={readOnly}
265
+ onItemDel={onItemDel}
266
+ headers={headers}
267
+ />
268
+ {disabled || readOnly || (maxCount > 0 && fileList.length >= maxCount) ? null : (
269
+ <>
270
+ <div
271
+ className={`uploader-add-btn ${loading ? "uploader-add-btn-loading" : ""}`}
272
+ onClick={() => {
273
+ if (loading) {
274
+ return;
275
+ }
276
+ setActionSheetVisible(true);
277
+ }}
278
+ loading={loading}
279
+ >
280
+ {loading ? "上传中..." : uploadBtnNode ? uploadBtnNode : "+"}
281
+ </div>
282
+ <ActionSheet
283
+ cancelText="取消"
284
+ visible={actionSheetVisible}
285
+ actions={actions}
286
+ onAction={onPicker}
287
+ onClose={() => setActionSheetVisible(false)}
288
+ />
289
+ </>
290
+ )}
291
+ </div>
292
+ );
293
+ }
294
+
295
+ export default forwardRef(Uploader);