@hzab/form-render-mobile 0.1.0 → 0.2.0

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.
@@ -0,0 +1,353 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { Popup, Button, SpinLoading, DotLoading, NoticeBar } from "antd-mobile";
3
+ import { connect, mapProps, useField, useFieldSchema } from "@formily/react";
4
+ import { LocationFill, ExclamationCircleOutline } from "antd-mobile-icons";
5
+ import { debounce } from "lodash";
6
+
7
+ import AMapCom from "./Map/AMap";
8
+ import { getCurrentPosition } from "../../common/location-utils";
9
+ import MapSearch from "./components/MapSearch";
10
+
11
+ import { getAddress } from "./servers/index";
12
+ import { getPropsValue, getParentValue } from "./common/utils";
13
+
14
+ import "./index.less";
15
+
16
+ const defaultLngLat = {
17
+ lon: 120.160217,
18
+ lat: 30.243861,
19
+ };
20
+
21
+ declare var AMap: any;
22
+
23
+ // 获取当前经纬度
24
+ getCurrentPosition()
25
+ .then((res) => {
26
+ const { coords = {} } = res;
27
+ defaultLngLat.lon = coords.longitude;
28
+ defaultLngLat.lat = coords.latitude;
29
+ })
30
+ .catch((e) => {
31
+ console.warn("Error getCurrentPosition e: ", e);
32
+ });
33
+
34
+ export function LocationPicker(props) {
35
+ const {
36
+ // 是否允许搜索
37
+ hasSearch = true,
38
+ /**
39
+ *
40
+ isObjectRes
41
+ true: 把所有数据存放在一个对象中(该对象和其他表单项平级),对象 key 为当前项的 name
42
+ false: 所有数据打平放到当前的 data 中,和其他表单项平级
43
+ */
44
+ isObjectRes = true,
45
+ /**
46
+ * 打开地图时是否根据经纬度自动修正已填的地址
47
+ */
48
+ isAutoFixAddr = false,
49
+ /**
50
+ * 改变经纬度等数据的触发模式
51
+ * 移动地图:move
52
+ * 点击地图:click
53
+ */
54
+ changeMode = "move",
55
+ lonKey = "longitude",
56
+ latKey = "latitude",
57
+ addrKey = "address",
58
+ value,
59
+ } = props;
60
+ const field: any = useField();
61
+ const fieldSchema = useFieldSchema();
62
+
63
+ const mapRef: any = useRef();
64
+ const markerRef: any = useRef();
65
+
66
+ const [loading, setLoading] = useState(false);
67
+ const [visible, setVisible] = useState(false);
68
+ // 数据格式转为内部格式,方便存取
69
+ const opt = {
70
+ isObjectRes,
71
+ lonKey,
72
+ latKey,
73
+ addrKey,
74
+ };
75
+
76
+ const formatVal = useMemo(
77
+ () => getPropsValue(value, field, opt),
78
+ [isObjectRes, lonKey, latKey, addrKey, value, field.data],
79
+ );
80
+
81
+ // 表单实际的值
82
+ const [resInfo, setResInfo] = useState(formatVal);
83
+ // 地图选点组件选中的值
84
+ const [pickInfo, setPickInfo] = useState(formatVal);
85
+ const [addrLoading, setAddrLoading] = useState(false);
86
+
87
+ useEffect(() => {
88
+ if (visible) {
89
+ let _lon = defaultLngLat.lon;
90
+ let _lat = defaultLngLat.lat;
91
+ if (resInfo.lon && resInfo.lat) {
92
+ _lon = resInfo.lon;
93
+ _lat = resInfo.lat;
94
+ }
95
+ // 解决关闭弹窗之后点位不居中的问题
96
+ if (window.AMap && window.AMap.LngLat) {
97
+ const position = new window.AMap.LngLat(_lon, _lat);
98
+ mapRef.current?.setCenter(position);
99
+ }
100
+ if (changeMode === "click") {
101
+ setMarker(_lon, _lat);
102
+ }
103
+ setPoint(_lon, _lat, false);
104
+ }
105
+ }, [visible, resInfo, changeMode]);
106
+
107
+ useEffect(() => {
108
+ setResInfo((_res) => {
109
+ const { lon: _lon, lat: _lat, addr: _addr } = formatVal;
110
+ if ((_lon && _lon != _res.lon) || (_lat && _lat != _res.lat)) {
111
+ setPoint(_lon, _lat, isAutoFixAddr);
112
+ }
113
+ return formatVal;
114
+ });
115
+ setPickInfo(formatVal);
116
+ }, [formatVal, isAutoFixAddr]);
117
+
118
+ function mapInit(_map, AMap) {
119
+ mapRef.current = _map;
120
+ setLoading(false);
121
+ let _lon = defaultLngLat.lon;
122
+ let _lat = defaultLngLat.lat;
123
+ if (pickInfo.lon && pickInfo.lat) {
124
+ _lon = pickInfo.lon;
125
+ _lat = pickInfo.lat;
126
+ }
127
+ setPoint(_lon, _lat, isAutoFixAddr);
128
+
129
+ if (changeMode === "click") {
130
+ setMarker(_lon, _lat);
131
+ // 点击选中点位的情况
132
+ mapRef.current.on("click", function (ev) {
133
+ const { lng: evLon, lat: evLat } = ev.lnglat || {};
134
+ if (AMap) {
135
+ const position = new AMap.LngLat(evLon, evLat);
136
+ mapRef.current.setCenter(position);
137
+ }
138
+ setMarker(evLon, evLat);
139
+ setPoint(evLon, evLat);
140
+ });
141
+ } else {
142
+ // 移动地图
143
+ mapRef.current.on(
144
+ "moveend",
145
+ debounce(function (ev) {
146
+ let currentCenter = mapRef.current.getCenter();
147
+ const { lng: centerLon, lat: centerLat } = currentCenter || {};
148
+ // 移动选中点位的情况
149
+ let _lon = centerLon;
150
+ let _lat = centerLat;
151
+ setPoint(_lon, _lat);
152
+ }, 500),
153
+ );
154
+ }
155
+ }
156
+
157
+ function setMarker(_lon, _lat) {
158
+ if (window.AMap && mapRef.current) {
159
+ const position = new AMap.LngLat(_lon, _lat);
160
+ // 创建 Marker 或修改位置
161
+ if (markerRef.current) {
162
+ markerRef.current.setPosition(position);
163
+ } else {
164
+ markerRef.current = new AMap.Marker({
165
+ position,
166
+ });
167
+ mapRef.current?.add(markerRef.current);
168
+ }
169
+ }
170
+ }
171
+
172
+ function setPoint(_lon, _lat, isAutoFixAddr = true) {
173
+ const lon = _lon || pickInfo.lon;
174
+ const lat = _lat || pickInfo.lat;
175
+ (isAutoFixAddr || !pickInfo.addr) && getAddr(lon, lat);
176
+ const res = { ...pickInfo, lon, lat };
177
+ setPickInfo(res);
178
+ }
179
+
180
+ function getAddr(_lng, _lat) {
181
+ if (!AMap) {
182
+ return "";
183
+ }
184
+ if (addrLoading) {
185
+ return;
186
+ }
187
+ setAddrLoading(true);
188
+ getAddress(_lng, _lat)
189
+ .then((addr) => {
190
+ setAddrLoading(false);
191
+
192
+ setPickInfo((_p) => ({ ..._p, addr }));
193
+ })
194
+ .catch((err) => {
195
+ setAddrLoading(false);
196
+ });
197
+ }
198
+
199
+ function onShow() {
200
+ !mapRef.current && setLoading(true);
201
+ setVisible(true);
202
+ }
203
+
204
+ function onClose() {
205
+ setVisible(false);
206
+ }
207
+
208
+ function onOk() {
209
+ const res = {
210
+ [lonKey]: pickInfo.lon,
211
+ [latKey]: pickInfo.lat,
212
+ [addrKey]: pickInfo.addr,
213
+ };
214
+ props.onChange && props.onChange(res);
215
+ if (isObjectRes === false) {
216
+ // 只有一层的情况
217
+ if (!field.parent) {
218
+ field.form.setValues(res);
219
+ } else if (field.parent && fieldSchema.parent) {
220
+ // 嵌套在 ArrayBase: ArrayTable ArrayCard 等 里面的情况
221
+ const parentVal = getParentValue(field);
222
+ Object.keys(res).forEach((key) => {
223
+ parentVal[key] = res[key];
224
+ });
225
+ }
226
+ }
227
+
228
+ onClose();
229
+ }
230
+
231
+ return (
232
+ <div className="location-picker">
233
+ <div className="location-value-box">
234
+ <div className="location-value-head-box">
235
+ <div className="location-lon-lat">
236
+ <div>经度:{resInfo.lon} </div>
237
+ <div>纬度:{resInfo.lat}</div>
238
+ </div>
239
+ <Button className="location-btn" color="primary" fill="none" onClick={onShow}>
240
+ <LocationFill />
241
+ 选点
242
+ </Button>
243
+ </div>
244
+ <div>地址:{resInfo.addr}</div>
245
+ </div>
246
+ <Popup
247
+ maskClassName="location-picker-popup-mask"
248
+ className="location-picker-popup"
249
+ onMaskClick={onClose}
250
+ onClose={onClose}
251
+ bodyStyle={{
252
+ minHeight: "80vh",
253
+ maxHeight: "100vh",
254
+ }}
255
+ visible={visible}
256
+ >
257
+ <div className="location-popup-header">
258
+ <Button fill="none" onClick={onClose}>
259
+ 取消
260
+ </Button>
261
+ <Button color="primary" fill="none" onClick={onOk} loading={addrLoading || loading}>
262
+ 确认
263
+ </Button>
264
+ </div>
265
+ <div className="picker-info">
266
+ <div className="picker-info-lon">经度:{pickInfo.lon} </div>
267
+ <div className="picker-info-lat">纬度:{pickInfo.lat}</div>
268
+ <div className="picker-info-addr">地址:{addrLoading ? <DotLoading /> : pickInfo.addr}</div>
269
+ </div>
270
+ {hasSearch ? <MapSearch setPoint={setPoint} /> : null}
271
+ <div className="location-picker-map">
272
+ <AMapCom
273
+ init={mapInit}
274
+ loading={loading}
275
+ center={pickInfo.center ? pickInfo.center : [defaultLngLat.lon, defaultLngLat.lat]}
276
+ style={{ height: "68vh" }}
277
+ />
278
+ <NoticeBar
279
+ className="location-picker-notice-bar"
280
+ content={changeMode === "click" ? "点击地图上的位置,选择目标地址" : "拖拽移动地图,中心标点为目标地址"}
281
+ color="default"
282
+ icon={<ExclamationCircleOutline />}
283
+ />
284
+ {!loading && changeMode !== "click" && <LocationFill className="location-picker-center-icon" />}
285
+ </div>
286
+ {loading && (
287
+ <div className="spin-loading">
288
+ <SpinLoading />
289
+ </div>
290
+ )}
291
+ </Popup>
292
+ </div>
293
+ );
294
+ }
295
+
296
+ // longitude latitude location
297
+ /*
298
+ isObject
299
+ true: 把所有数据存放在一个对象中(该对象和其他表单项平级),对象 key 为当前项的 name
300
+ false: 所有数据打平放到当前的 data 中,和其他表单项平级
301
+ */
302
+ export const LocationPickerT = connect(
303
+ LocationPicker,
304
+ mapProps((props, field, ...args) => {
305
+ const { lonKey = "longitude", latKey = "latitude", addrKey = "address", level } = props;
306
+ const _onChange = (val, ...args) => {
307
+ if (level === 1) {
308
+ // if (field.parent?.componentType === 'ArrayTable') {
309
+ const dataKey = field.parent?.props?.name;
310
+ const data = field?.form?.getValuesIn(dataKey);
311
+ const idx = field.indexes?.[0];
312
+ data[idx][addrKey] = val.addr;
313
+ field?.form?.setValues(dataKey, data);
314
+ } else if (level === 2) {
315
+ const parentKey = field.parent?.props?.name; //父级key值
316
+ // @ts-ignore
317
+ const dataKey = field.parent?.parent?.props?.basePath?.entire; //爷级key值
318
+ const data = field?.form?.getValuesIn(dataKey);
319
+ const grandpaidx = field.indexes?.[0]; //爷级下标
320
+ const parentidx = field.indexes?.[1]; //父级下标
321
+ // @ts-ignore
322
+ data[grandpaidx][parentKey][parentidx][addrKey] = val.addr;
323
+ field?.form?.setValues(dataKey[parentKey], data);
324
+ } else {
325
+ field?.form?.setValues({
326
+ // [lonKey]: val?.lng,
327
+ // [latKey]: val?.lat,
328
+ [addrKey]: val?.addr,
329
+ });
330
+ }
331
+ props.onChange && props.onChange(val, ...args);
332
+ // props.onChange && props.onChange(`${val?.lng}, ${val?.lat}`, ...args);
333
+ };
334
+ let lng;
335
+ let lat;
336
+ if (props.value) {
337
+ lng = props.value?.[lonKey];
338
+ lat = props.value?.[latKey];
339
+ }
340
+ // const lng = field?.form?.getValuesIn(lonKey);
341
+ // const lat = field?.form?.getValuesIn(latKey);
342
+ return {
343
+ ...props,
344
+ field,
345
+ lng,
346
+ lat,
347
+ addr: field?.form?.getValuesIn(addrKey),
348
+ _onChange,
349
+ };
350
+ }),
351
+ );
352
+
353
+ export default LocationPicker;
@@ -0,0 +1,109 @@
1
+ import { Toast } from "antd-mobile";
2
+
3
+ export function getAddress(lon, lat) {
4
+ return new Promise((resolve, reject) => {
5
+ if (!window.AMap) {
6
+ reject(Error("Error getAddress window.AMap is not defined."));
7
+ return;
8
+ }
9
+ const geocoder = new window.AMap.Geocoder({});
10
+ geocoder.getAddress([lon, lat], function (status, result) {
11
+ if (status === "complete" && result.info === "OK") {
12
+ const formattedAddress = result?.regeocode?.formattedAddress;
13
+ resolve(formattedAddress);
14
+ return formattedAddress;
15
+ } else if (result === "USER_DAILY_QUERY_OVER_LIMIT") {
16
+ // 超出限制,使用 webServer 进行请求
17
+ if (!window._AMapLoaderTemp.serverKey) {
18
+ Toast.show({ icon: "fail", content: "超出使用限制,请联系管理员" });
19
+ return;
20
+ }
21
+ fetch(
22
+ `https://restapi.amap.com/v3/geocode/regeo?location=${lon?.toFixed(6)},${lat?.toFixed(6)}&key=${
23
+ window._AMapLoaderTemp.serverKey
24
+ }`,
25
+ {
26
+ mode: "cors",
27
+ },
28
+ )
29
+ .then((res) => res.json())
30
+ .then((res) => {
31
+ if (res.status === "1") {
32
+ resolve(res.regeocode?.formatted_address);
33
+ } else {
34
+ Toast.show({ icon: "fail", content: res.info });
35
+ }
36
+ })
37
+ .catch(reject);
38
+ } else {
39
+ reject(result);
40
+ }
41
+ });
42
+ });
43
+ }
44
+
45
+ export function getSearchTips(search, cityCode = "0571") {
46
+ return new Promise((resolve, reject) => {
47
+ if (!window.AMap) {
48
+ reject(Error("Error getAddress window.AMap is not defined."));
49
+ return;
50
+ }
51
+ const autoComplete = new window.AMap.Autocomplete({
52
+ // city 限定城市,默认全国
53
+ city: cityCode,
54
+ });
55
+ autoComplete.search(search, function (status, result) {
56
+ if (status === "complete" && result.info === "OK") {
57
+ resolve(
58
+ result.tips?.map((it) => {
59
+ const { lng, lat } = it?.location || {};
60
+ return {
61
+ ...it,
62
+ label: it.name,
63
+ value: it.id,
64
+ lon: +lng,
65
+ lng: +lng,
66
+ lat: +lat,
67
+ };
68
+ }),
69
+ );
70
+ } else if (result === "USER_DAILY_QUERY_OVER_LIMIT") {
71
+ // 超出限制,使用 webServer 进行请求
72
+ if (!window._AMapLoaderTemp.serverKey) {
73
+ Toast.show({ icon: "fail", content: "超出使用限制,请联系管理员" });
74
+ return;
75
+ }
76
+ fetch(
77
+ `https://restapi.amap.com/v3/assistant/inputtips?key=${window._AMapLoaderTemp.serverKey}&keywords=${search}&city=${cityCode}`,
78
+ {
79
+ mode: "cors",
80
+ },
81
+ )
82
+ .then((res) => res.json())
83
+ .then((res) => {
84
+ if (res.status === "1") {
85
+ resolve(
86
+ res.tips?.map((it) => {
87
+ const [lng, lat] = it.location?.split(",");
88
+
89
+ return {
90
+ ...it,
91
+ label: it.name,
92
+ value: it.id,
93
+ lng: +lng,
94
+ lon: +lng,
95
+ lat: +lat,
96
+ };
97
+ }),
98
+ );
99
+ } else {
100
+ Toast.show({ icon: "fail", content: res.info });
101
+ }
102
+ })
103
+ .catch(reject);
104
+ } else {
105
+ reject(result);
106
+ }
107
+ });
108
+ });
109
+ }
@@ -1,7 +1,7 @@
1
1
  import "./index.less";
2
2
 
3
- export const Placeholder = (props) => {
4
- const { value, placeholder, className, isPicker } = props;
3
+ export const Placeholder = ({ className = "", isPicker = false, ...props }) => {
4
+ const { value, placeholder } = props;
5
5
  if (value !== null && value !== undefined && value !== "") {
6
6
  return value;
7
7
  }
@@ -17,6 +17,7 @@ type fieldT = {
17
17
  type SelectPropsT = {
18
18
  value: number | string;
19
19
  onChange: (val: any) => void;
20
+ onConfirm: (val: any) => void;
20
21
  } & formItemProps;
21
22
 
22
23
  export const Select = observer((props: SelectPropsT) => {
@@ -50,8 +51,10 @@ export const Select = observer((props: SelectPropsT) => {
50
51
  }
51
52
 
52
53
  function onConfirm(value) {
53
- // 去除数组?
54
- onChange && onChange(value.length === 1 ? value[0] : value);
54
+ // 去除数组
55
+ const resVal = value.length === 1 ? value[0] : value;
56
+ props.onConfirm && props.onConfirm(resVal);
57
+ onChange && onChange(resVal);
55
58
  }
56
59
 
57
60
  return (
@@ -8,6 +8,7 @@ import { formItemProps } from "../form-item";
8
8
 
9
9
  type TimePickerPropsT = {
10
10
  use12Hours: boolean;
11
+ onConfirm?: (date: string | number) => void;
11
12
  } & formItemProps;
12
13
 
13
14
  export const TimePicker = observer((props: TimePickerPropsT) => {
@@ -27,6 +28,7 @@ export const TimePicker = observer((props: TimePickerPropsT) => {
27
28
  resVal = [...val]?.slice(0, 3)?.join(":");
28
29
  resVal += ` ${val[3] || ""}`;
29
30
  }
31
+ props.onConfirm && props.onConfirm(resVal);
30
32
  onChange && onChange(resVal);
31
33
  }
32
34
 
@@ -35,4 +35,6 @@ export const Uploader = (props) => {
35
35
  return <UploaderCom {..._props} onChange={onUploadChange} />;
36
36
  };
37
37
 
38
+ export const Upload = Uploader;
39
+
38
40
  export default Uploader;
@@ -11,4 +11,5 @@ export * from "./TimePicker";
11
11
  export * from "./Uploader";
12
12
  export * from "./UserSelect";
13
13
  export * from "./Radio";
14
- export * from "./Checkbox";
14
+ export * from "./Checkbox";
15
+ export * from "./LocationPicker";