@hzab/form-render-mobile 0.0.1

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.
Files changed (39) hide show
  1. package/README.md +67 -0
  2. package/lib/index.js +46475 -0
  3. package/lib/static/imgs/approachPointIcon_f2eb69df51.png +0 -0
  4. package/lib/static/imgs/layers-2x_8f2c4d1147.png +0 -0
  5. package/lib/static/imgs/layers_416d91365b.png +0 -0
  6. package/lib/static/imgs/marker-icon_2b3e1faf89.png +0 -0
  7. package/package.json +59 -0
  8. package/src/common/schemaHandler.js +55 -0
  9. package/src/components/Cascader/index.jsx +75 -0
  10. package/src/components/Cascader/index.less +3 -0
  11. package/src/components/DatePicker/index.less +3 -0
  12. package/src/components/DatePicker/index.tsx +98 -0
  13. package/src/components/MapPicker/common/approachPointIcon.png +0 -0
  14. package/src/components/MapPicker/common/getCurrentPosition.js +27 -0
  15. package/src/components/MapPicker/common/gpsConvert.js +236 -0
  16. package/src/components/MapPicker/common/map-config.js +7 -0
  17. package/src/components/MapPicker/index.jsx +82 -0
  18. package/src/components/MapPicker/line-picker/car-route.json +426 -0
  19. package/src/components/MapPicker/line-picker/index.jsx +188 -0
  20. package/src/components/MapPicker/line-picker/index.less +41 -0
  21. package/src/components/MapPicker/line-picker/mockLonLat.js +1 -0
  22. package/src/components/MapPicker/location-picker/index.jsx +104 -0
  23. package/src/components/MapPicker/location-picker/index.less +36 -0
  24. package/src/components/NumberPicker/index.jsx +16 -0
  25. package/src/components/NumberPicker/index.less +3 -0
  26. package/src/components/Password/index.jsx +14 -0
  27. package/src/components/Select/index.less +3 -0
  28. package/src/components/Select/index.tsx +60 -0
  29. package/src/components/Text/index.tsx +23 -0
  30. package/src/components/Uploader/common/cordova-camera.js +188 -0
  31. package/src/components/Uploader/common/utils.js +87 -0
  32. package/src/components/Uploader/index.jsx +31 -0
  33. package/src/components/Uploader/uploader.jsx +246 -0
  34. package/src/components/Uploader/uploader.less +36 -0
  35. package/src/components/Uploader/video/index.jsx +37 -0
  36. package/src/components/Uploader/video/index.less +37 -0
  37. package/src/components/index.ts +10 -0
  38. package/src/index.tsx +101 -0
  39. package/src/type.d.ts +15 -0
@@ -0,0 +1,246 @@
1
+ import { useState, useRef, useEffect, useMemo } from "react";
2
+ import { Image, Button, Dialog, ActionSheet } from "antd-mobile";
3
+ import { CloseCircleFill } from "antd-mobile-icons";
4
+
5
+ import Video from "./video";
6
+
7
+ import { getImage, getVideo, getAudio } from "./common/cordova-camera";
8
+ import { getFileURL, checkImageUrl, checkVideoUrl, checkAudioUrl } from "./common/utils";
9
+
10
+ import "./uploader.less";
11
+
12
+ const TYPE_VIDEO = "video";
13
+ const TYPE_IMG = "img";
14
+ const TYPE_AUDIO = "audio";
15
+
16
+ const _ = require("lodash");
17
+
18
+ function Uploader(props) {
19
+ const {
20
+ name,
21
+ multiple,
22
+ // 模式字符串: all | select | image | video 或 数组 select | image | video 组合:如:['select', 'image']
23
+ mode = "select",
24
+ onChange,
25
+ accept,
26
+ disabled,
27
+ readOnly,
28
+ data,
29
+ } = props;
30
+ const [curAccept, setCurAccept] = useState(accept);
31
+ const [fileList, setFileList] = useState(data || []);
32
+ const [actionSheetVisible, setActionSheetVisible] = useState(false);
33
+
34
+ useEffect(() => {
35
+ setFileList(data || []);
36
+ }, [data]);
37
+
38
+ function onFileChange(e) {
39
+ e.persist();
40
+ const { files: rawFiles } = e.target;
41
+ let files = [].slice.call(rawFiles);
42
+ e.target.value = ""; // HACK: fix the same file doesn't trigger onChange
43
+ handleChange(files);
44
+ }
45
+
46
+ function handleChange(files, type) {
47
+ let _files = files;
48
+ if (type !== "del" && multiple) {
49
+ _files = [...fileList, ..._files];
50
+ }
51
+ setFileList(_files);
52
+ onChange && onChange(_files && _files.length > 0 ? _files : undefined);
53
+ }
54
+
55
+ const uploaderRef = useRef();
56
+
57
+ const actions = useMemo(() => {
58
+ const allEnum = {
59
+ select: { text: "选择文件", key: "select" },
60
+ image: { text: "拍照", key: "captureImage" },
61
+ video: { text: "录像", key: "captureVideo" },
62
+ audio: { text: "录音", key: "captureAudio" },
63
+ };
64
+ if (Array.isArray(mode)) {
65
+ return mode.filter((it) => !!allEnum[it]).map((it) => allEnum[it]);
66
+ }
67
+ if (mode === "all") {
68
+ return Object.keys(allEnum).map((key) => allEnum[key]);
69
+ }
70
+ return allEnum[mode] ? [allEnum[mode]] : [allEnum.select];
71
+ }, [mode]);
72
+
73
+ function onPicker({ key }) {
74
+ switch (key) {
75
+ case "select":
76
+ // uploaderRef.current.click();
77
+ setCurAccept(accept);
78
+ setTimeout(() => {
79
+ uploaderRef.current.click();
80
+ }, 1000);
81
+ break;
82
+
83
+ case "captureImage":
84
+ getImage().then((res) => {
85
+ handleChange([res]);
86
+ });
87
+ break;
88
+
89
+ case "captureVideo":
90
+ getVideo().then((res) => {
91
+ handleChange([res]);
92
+ });
93
+ break;
94
+
95
+ case "captureAudio":
96
+ setCurAccept("audio/*");
97
+ setTimeout(() => {
98
+ uploaderRef.current.click();
99
+ }, 1000);
100
+ // TODO: reader 报错
101
+ // getAudio().then((res) => {
102
+ // handleChange([res]);
103
+ // });
104
+ break;
105
+ }
106
+ setActionSheetVisible(false);
107
+ }
108
+
109
+ function onItemDel(idx) {
110
+ Dialog.confirm({
111
+ content: "确认删除?",
112
+ onConfirm: () => {
113
+ // setFileList((files) => {
114
+ // files.splice(idx, 1);
115
+ // handleChange(files, "del");
116
+ // return files;
117
+ // });
118
+ const middleValue = _.cloneDeep(fileList);
119
+ middleValue.splice(idx, 1);
120
+ handleChange(middleValue, "del");
121
+ setFileList(middleValue);
122
+ },
123
+ });
124
+ }
125
+
126
+ function ItemRender(props) {
127
+ const { index } = props;
128
+ return (
129
+ <div className="file-item">
130
+ {props.children}
131
+ {disabled || readOnly ? null : (
132
+ <div
133
+ className="file-item-del"
134
+ onClick={() => {
135
+ onItemDel(index);
136
+ }}
137
+ >
138
+ <CloseCircleFill fontSize={14} />
139
+ </div>
140
+ )}
141
+ </div>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <div className="uploader">
147
+ <input
148
+ className="aria-hidden"
149
+ style={{ display: "none" }}
150
+ ref={uploaderRef}
151
+ id={name}
152
+ onChange={onFileChange}
153
+ type="file"
154
+ name={name}
155
+ multiple={multiple}
156
+ accept={curAccept}
157
+ capture={props.capture}
158
+ ></input>
159
+ {fileList.map((it, idx) => {
160
+ const { type, name, url } = it;
161
+ let src = "";
162
+ let fileType = "";
163
+ let downLoadUrl = undefined;
164
+
165
+ // 判断文件类型,获取对应展示的数据
166
+ if (typeof it === "string" || url) {
167
+ src = url ? url : it;
168
+ it = url ? url : it;
169
+ // 图片
170
+ if (it.startsWith("data:image/") || (/[ ,]?image\//.test(accept) && checkImageUrl(it))) {
171
+ fileType = TYPE_IMG;
172
+ }
173
+ // 视频
174
+ if (/[ ,]?video\//.test(accept) && checkVideoUrl(it)) {
175
+ fileType = TYPE_VIDEO;
176
+ }
177
+ // 音频
178
+ if (/[ ,]?audio\//.test(accept) && checkAudioUrl(it)) {
179
+ fileType = TYPE_AUDIO;
180
+ }
181
+ } else {
182
+ // 图片
183
+ if (type?.startsWith("image/")) {
184
+ src = getFileURL(it);
185
+ fileType = TYPE_IMG;
186
+ }
187
+
188
+ // 视频
189
+ if (type?.startsWith("video/")) {
190
+ src = getFileURL(it);
191
+ fileType = TYPE_VIDEO;
192
+ // TODO: 确认下载逻辑
193
+ downLoadUrl = src;
194
+ }
195
+ if (type?.startsWith("audio/")) {
196
+ src = getFileURL(it);
197
+ fileType = TYPE_AUDIO;
198
+ // TODO: 确认下载逻辑
199
+ downLoadUrl = src;
200
+ }
201
+ }
202
+
203
+ if (fileType === TYPE_IMG) {
204
+ return (
205
+ <ItemRender index={idx} key={name + "_" + idx}>
206
+ <Image className="file-item-view" src={src} alt={name} />
207
+ </ItemRender>
208
+ );
209
+ }
210
+ if (fileType === TYPE_VIDEO) {
211
+ return (
212
+ <ItemRender index={idx} key={name + "_" + idx}>
213
+ <Video src={src} href={downLoadUrl} />
214
+ </ItemRender>
215
+ );
216
+ }
217
+ if (fileType === TYPE_AUDIO) {
218
+ return <audio src={src} controls></audio>;
219
+ }
220
+
221
+ return it.name;
222
+ })}
223
+ {disabled || readOnly || (!multiple && fileList.length > 0) ? null : (
224
+ <>
225
+ <Button
226
+ className="uploader-add-btn"
227
+ onClick={() => {
228
+ setActionSheetVisible(true);
229
+ }}
230
+ >
231
+ +
232
+ </Button>
233
+ <ActionSheet
234
+ cancelText="取消"
235
+ visible={actionSheetVisible}
236
+ actions={actions}
237
+ onAction={onPicker}
238
+ onClose={() => setActionSheetVisible(false)}
239
+ />
240
+ </>
241
+ )}
242
+ </div>
243
+ );
244
+ }
245
+
246
+ export default Uploader;
@@ -0,0 +1,36 @@
1
+ .uploader {
2
+ .uploader-add-btn,
3
+ .file-item {
4
+ position: relative;
5
+ display: inline-flex;
6
+ justify-content: center;
7
+ align-items: center;
8
+ width: 20vw;
9
+ height: 20vw;
10
+ margin-right: 2vw;
11
+ vertical-align: top;
12
+ border-radius: 4px;
13
+ .file-item-view {
14
+ max-width: 100%;
15
+ max-height: 100%;
16
+ }
17
+ .file-item-del {
18
+ position: absolute;
19
+ right: -1vw;
20
+ top: -1vw;
21
+ width: 3vw;
22
+ height: 3vw;
23
+ line-height: 3vw;
24
+ text-align: center;
25
+ border-radius: 50%;
26
+ user-select: none;
27
+ background-color: #fff;
28
+ }
29
+ }
30
+ .uploader-add-btn {
31
+ font-size: 6vw;
32
+ }
33
+ .aria-hidden {
34
+ display: none;
35
+ }
36
+ }
@@ -0,0 +1,37 @@
1
+ import { useState } from "react";
2
+ import { Mask } from "antd-mobile";
3
+ import { PlayOutline, CloseOutline } from "antd-mobile-icons";
4
+
5
+ import "./index.less";
6
+
7
+ function Video({ src, href }) {
8
+ const [maskVisible, setMaskVisible] = useState(false);
9
+
10
+ function onPreview() {
11
+ setMaskVisible(true);
12
+ }
13
+
14
+ return (
15
+ <div className="uploader-video-wrap">
16
+ <PlayOutline fontSize={30} onClick={onPreview} />
17
+ <Mask
18
+ className="uploader-video-mask"
19
+ visible={maskVisible}
20
+ getContainer={document.body}
21
+ destroyOnClose
22
+ onMaskClick={() => setMaskVisible(false)}
23
+ >
24
+ <video className="uploader-video-view" src={src} controls>
25
+ 抱歉,您的浏览器不支持内嵌视频,不过不用担心,你可以
26
+ <a href={href || src}>下载</a>
27
+ 并用你喜欢的播放器观看!
28
+ </video>
29
+ <div className="video-mask-close-btn" onClick={() => setMaskVisible(false)}>
30
+ X
31
+ </div>
32
+ </Mask>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ export default Video;
@@ -0,0 +1,37 @@
1
+ .uploader-video-wrap {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ width: 100%;
6
+ height: 100%;
7
+ border: 1px solid #eee;
8
+ color: #fff;
9
+ background-color: #000;
10
+ border-radius: 4px;
11
+ }
12
+ .uploader-video-mask {
13
+ display: flex;
14
+ justify-content: center;
15
+ align-items: center;
16
+ max-width: 100vw;
17
+ max-height: 100vh;
18
+ background-color: #000 !important;
19
+ .uploader-video-view {
20
+ max-width: 100vw;
21
+ max-height: 100vh;
22
+ width: 100vw;
23
+ }
24
+ .video-mask-close-btn {
25
+ position: absolute;
26
+ right: 2vw;
27
+ top: 1vh;
28
+ width: 5vw;
29
+ height: 5vw;
30
+ line-height: 5vw;
31
+ font-size: 3vw;
32
+ text-align: center;
33
+ color: #fff;
34
+ border-radius: 50%;
35
+ background-color: #333;
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ import Text from "./Text";
2
+ import Select from "./Select";
3
+ import Cascader from "./Cascader";
4
+ import NumberPicker from "./NumberPicker";
5
+ import DatePicker from "./DatePicker";
6
+ import Password from "./Password";
7
+ import MapPicker from "./MapPicker";
8
+ import Uploader from "./Uploader";
9
+
10
+ export { NumberPicker, Password, Select, Cascader, DatePicker, Text, MapPicker, Uploader };
package/src/index.tsx ADDED
@@ -0,0 +1,101 @@
1
+ import React, { useEffect, useMemo, useImperativeHandle, forwardRef } from "react";
2
+
3
+ import {
4
+ FormLayout,
5
+ FormItem,
6
+ CascadePicker,
7
+ CheckList,
8
+ Checkbox,
9
+ DatePicker,
10
+ FormGrid,
11
+ Input,
12
+ List,
13
+ Selector,
14
+ Stepper,
15
+ Switch,
16
+ FormButtonGroup,
17
+ Submit,
18
+ Reset,
19
+ } from "@formily/antd-mobile";
20
+ import { Dialog, Slider } from "antd-mobile";
21
+ import { createForm } from "@formily/core";
22
+ import { FormProvider, createSchemaField, Schema } from "@formily/react";
23
+
24
+ // 自定义组件
25
+ import * as customComponents from "./components/index";
26
+ import { schemaHandler } from "./common/schemaHandler";
27
+
28
+ import { formPropsI } from "./type.d";
29
+
30
+ const FormRender = forwardRef((props: formPropsI, parentRef) => {
31
+ const SchemaField = useMemo(
32
+ () =>
33
+ createSchemaField({
34
+ components: {
35
+ FormItem,
36
+ FormLayout,
37
+ CascadePicker,
38
+ CheckList,
39
+ Slider,
40
+ Checkbox,
41
+ DatePicker,
42
+ FormGrid,
43
+ Input,
44
+ List,
45
+ Selector,
46
+ Stepper,
47
+ Switch,
48
+ FormButtonGroup,
49
+ Submit,
50
+ Reset,
51
+ ...customComponents,
52
+ ...props.components,
53
+ },
54
+ scope: { ...props.schemaScope },
55
+ }),
56
+ [],
57
+ );
58
+
59
+ const formRender = useMemo(
60
+ () =>
61
+ createForm({
62
+ initialValues: props.initialValues,
63
+ // 禁用状态(注意,自定义组件组件自行获取对应的状态)
64
+ readOnly: props.readOnly,
65
+ disabled: props.disabled,
66
+ ...(props.formOptions || {}),
67
+ }),
68
+ [],
69
+ );
70
+
71
+ useImperativeHandle(parentRef, () => ({ formRender }));
72
+
73
+ useEffect(() => {
74
+ props.init && props.init(formRender);
75
+ }, []);
76
+
77
+ const schema = useMemo(() => {
78
+ return schemaHandler(props.schema).schema;
79
+ }, [props.schema]);
80
+
81
+ return (
82
+ <div className={`form-render ${props.className}`}>
83
+ <FormProvider form={formRender}>
84
+ {/* @ts-ignore */}
85
+ <SchemaField schema={schema} />
86
+ {/* <FormButtonGroup>
87
+ <Submit
88
+ onSubmit={(e) => {
89
+ console.log(e);
90
+ }}
91
+ >
92
+ 提交
93
+ </Submit>
94
+ </FormButtonGroup> */}
95
+ <div className="form-render-footer">{props.footerRender && props.footerRender()}</div>
96
+ </FormProvider>
97
+ </div>
98
+ );
99
+ });
100
+
101
+ export default FormRender;
package/src/type.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { SchemaReactComponents, Schema } from "@formily/react";
2
+ import { ReactElement } from "react";
3
+
4
+ export interface formPropsI {
5
+ className?: string;
6
+ initialValues?: Object;
7
+ schema: { schema: Object } | Object;
8
+ schemaScope?: Object;
9
+ components?: SchemaReactComponents;
10
+ readOnly?: boolean;
11
+ disabled?: boolean;
12
+ formOptions?: Object;
13
+ init?(form): void;
14
+ footerRender?(): ReactElement;
15
+ }