expo-gaode-map 1.1.3 → 1.1.5

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,504 @@
1
+ /*
2
+ * @Author : 尚博信_王强 wangqiang03@sunboxsoft.com
3
+ * @Date : 2025-03-11 09:29:07
4
+ * @LastEditors : 尚博信_王强 wangqiang03@sunboxsoft.com
5
+ * @LastEditTime : 2025-11-23 17:41:21
6
+ * @FilePath : /expo-gaode-map/test/ClockMapView.tsx
7
+ * @Description :
8
+ *
9
+ * Copyright (c) 2025 by 尚博信_王强, All Rights Reserved.
10
+ */
11
+ import { Text, View } from "@/components/Themed";
12
+ import useMap from "@/hook/useMap";
13
+ import React, { useEffect, useMemo, useRef, useState } from "react";
14
+
15
+ import { useStyles, createStyleSheet } from "react-native-unistyles";
16
+ // import {
17
+ // MapType,
18
+ // MapView,
19
+ // Circle,
20
+ // Marker,
21
+ // } from "@/lib/amap3d";
22
+ import { Marker, Circle, MapView, MapType } from 'expo-gaode-map'
23
+ import { haversineDistance, isNotEmpty } from "@/utils/Fun";
24
+ import { DebouncePressable } from "@/components/Debounce.Button";
25
+ import { Image, InteractionManager, ActivityIndicator, StyleSheet } from "react-native";
26
+ import ImageAsset from "@/assets";
27
+ import usePermissions from "@/hook/usePermissions";
28
+ import { FontAwesome } from "@expo/vector-icons";
29
+ import useCameraStore, { Photo } from "@/store/useCameraStore";
30
+ import CommonConstants from "@/hook/constants/CommonConstants";
31
+ import useTeamsStore from "@/store/useTeamsStore";
32
+ import Animated, { FadeIn } from "react-native-reanimated";
33
+ import Button from "@/components/Button";
34
+
35
+ const iconUri = Image.resolveAssetSource(ImageAsset.positio_icon).uri;
36
+
37
+ const ClockMapView = () => {
38
+
39
+ const { styles, theme } = useStyles(styleSheet);
40
+ const { isLocationPermissions } = usePermissions();
41
+ const { photo, setPhoto } = useCameraStore();
42
+ const { clockType } = useTeamsStore();
43
+ const [isInitializing, setIsInitializing] = useState(true);
44
+
45
+ const {
46
+ mapViewRef,
47
+ data,
48
+ takePhoto,
49
+ ClockIn,
50
+ moveToCurrentLocation,
51
+ clockInCoord,
52
+ chooseClockRule,
53
+ clockRuleIndex,
54
+ clockRule,
55
+ chooseShift,
56
+ shifts,
57
+ shiftIndex,
58
+ clockRange,
59
+ setIsWithinRange,
60
+ isWithinRange,
61
+ mapState,
62
+ setMapState,
63
+ getClockInRule,
64
+ isSupportOutWork,
65
+ initialPosition
66
+
67
+ } = useMap();
68
+
69
+
70
+ let timers = useRef<NodeJS.Timeout | number>(0);
71
+
72
+ // 所有的 useMemo 和其他 Hooks 必须在组件顶层调用
73
+ const mapView = useMemo(() => {
74
+ // ✅ 必须等待 initialPosition 准备好才渲染 MapView
75
+ if (!initialPosition) {
76
+ return null;
77
+ }
78
+
79
+ return (
80
+ <Animated.View style={{ flex: 1 }}>
81
+ <MapView
82
+ style={{ flex: 1 }}
83
+ ref={mapViewRef}
84
+ onLoad={(nativeEvent) => {
85
+ console.log('onLoad', nativeEvent)
86
+ setMapState(false);
87
+ }}
88
+ indoorViewEnabled={true}
89
+ zoomGesturesEnabled={true}
90
+ buildingsEnabled={true}
91
+ userLocationRepresentation={{
92
+ showsAccuracyRing: false,
93
+ image: iconUri,
94
+ imageWidth: 40,
95
+ imageHeight: 40,
96
+ }}
97
+ trafficEnabled={true}
98
+ zoomControlsEnabled={false}
99
+ scaleControlsEnabled={false}
100
+ tiltGesturesEnabled={false}
101
+ compassEnabled={false}
102
+ followUserLocation={false}
103
+ // onCameraIdle={({ nativeEvent }) => {}}
104
+ onLocation={({ nativeEvent }) => {
105
+ const { latitude, longitude } = nativeEvent;
106
+ if (!latitude || !longitude) return;
107
+ let index = clockInCoord.some((item) => {
108
+ const distance = haversineDistance(
109
+ {
110
+ latitude: item.lat || 0,
111
+ longitude: item.lon || 0,
112
+ },
113
+ {
114
+ latitude: latitude || 0,
115
+ longitude: longitude || 0,
116
+ }
117
+ );
118
+ if (distance <= clockRange) {
119
+ return true;
120
+ } else {
121
+ return false;
122
+ }
123
+ });
124
+ setIsWithinRange(index);
125
+ }}
126
+ distanceFilter={1}
127
+ // myLocationIcon={true}
128
+ myLocationEnabled={true}
129
+ initialCameraPosition={initialPosition || {
130
+ target: { latitude: 39.9, longitude: 116.4 },
131
+ zoom: 15
132
+ }}>
133
+
134
+ {clockInCoord &&
135
+ clockInCoord?.length > 0 &&
136
+ clockInCoord.map((item, index) => {
137
+ if (!item?.lat || !item?.lon) return null;
138
+
139
+ // ✅ 使用基于坐标的稳定 key,而不是 index
140
+ const stableKey = `${item.lat.toFixed(6)}-${item.lon.toFixed(6)}`;
141
+
142
+ return (
143
+ <React.Fragment key={stableKey}>
144
+ <Circle
145
+ center={{
146
+ latitude: item?.lat || 39.908692,
147
+ longitude: item?.lon || 116.397477,
148
+ }}
149
+ radius={clockRange}
150
+ zIndex={1000}
151
+ strokeWidth={2}
152
+ strokeColor={theme.colors.primary}
153
+ fillColor="rgba(43, 133, 255, 0.3)"
154
+ />
155
+ <Marker
156
+ position={{
157
+ latitude: item?.lat || 39.908692,
158
+ longitude: item?.lon || 116.397477,
159
+ }}
160
+ draggable={true}
161
+ zIndex={1000}
162
+ >
163
+ <View style={styles.markerContainer}>
164
+ <Text style={styles.markerText}>
165
+ {clockRule[clockRuleIndex]?.positionName}
166
+ </Text>
167
+ </View>
168
+ </Marker>
169
+ </React.Fragment>
170
+ );
171
+ })}
172
+
173
+ </MapView>
174
+ <DebouncePressable
175
+ style={styles.orientationView}
176
+ onPress={() => {
177
+ moveToCurrentLocation();
178
+ }}
179
+ >
180
+ <Image source={ImageAsset.orientation} style={styles.orientation} />
181
+ </DebouncePressable>
182
+ </Animated.View>
183
+ );
184
+ }, [
185
+ data.location?.address,
186
+ mapViewRef,
187
+ mapState,
188
+ clockInCoord,
189
+ clockRange,
190
+ isLocationPermissions,
191
+ initialPosition,
192
+ clockRuleIndex
193
+ ]);
194
+
195
+
196
+
197
+ //底部打卡按钮
198
+ const clockButton = useMemo(() => {
199
+ // 添加初始加载状态检查
200
+ if (!data.location?.address || mapState || clockInCoord.length === 0) {
201
+ return (
202
+ <Animated.View style={styles.buttonView} exiting={FadeIn.duration(300)}>
203
+ <View style={styles.buttonDisabled}>
204
+ <Text style={styles.buttonDisabledText}>
205
+ 正在获取位置信息...
206
+ </Text>
207
+ </View>
208
+ </Animated.View>
209
+ );
210
+ }
211
+
212
+ if (clockType === 1000) {
213
+ return (
214
+ <Animated.View style={styles.buttonView} exiting={FadeIn.duration(300)}>
215
+ <View style={styles.buttonDisabled}>
216
+ <Text style={styles.buttonDisabledText}>
217
+ 不在考勤规则内,无法打卡
218
+ </Text>
219
+ </View>
220
+ </Animated.View>
221
+ );
222
+ } else {
223
+ return (
224
+ <Animated.View style={styles.buttonView} exiting={FadeIn.duration(300)}>
225
+ <Button
226
+ label={
227
+ !isWithinRange
228
+ ? isSupportOutWork ? clockType === 1001 ? '上班打卡(外勤)' : '下班打卡(外勤)' : "当前位置不在考勤范围无法打卡"
229
+ : clockType === 1001
230
+ ? CommonConstants.clock_in
231
+ : CommonConstants.clock_out
232
+ }
233
+ onPress={ClockIn}
234
+ style={[styles.button]}
235
+ labelStyle={[styles.buttonLabel]}
236
+ />
237
+ </Animated.View>
238
+ );
239
+ }
240
+ }, [data.location?.address, mapState, clockType, isWithinRange, clockInCoord, photo, isSupportOutWork]);
241
+
242
+
243
+ // 使用 useEffect 替代 useFocusEffect 来避免潜在的 Hook 顺序问题
244
+ useEffect(() => {
245
+ // 设置初始化状态
246
+ setIsInitializing(true);
247
+ //@ts-ignore
248
+ // setPhoto(undefined);
249
+
250
+ // 使用 InteractionManager 确保页面转场动画完成后再执行耗时操作
251
+ InteractionManager.runAfterInteractions(() => {
252
+ // 延迟获取考勤规则,让页面先渲染出来
253
+ timers.current = setTimeout(() => {
254
+ try {
255
+ getClockInRule().catch(err => {
256
+ console.error('获取考勤规则失败:', err);
257
+ }).finally(() => {
258
+ setIsInitializing(false);
259
+ });
260
+ } catch (error) {
261
+ console.error('获取考勤规则出错:', error);
262
+ setIsInitializing(false);
263
+ }
264
+ }, 1000);
265
+ });
266
+
267
+ // 添加安全机制:无论如何,2秒后强制结束加载状态
268
+ const timer = setTimeout(() => {
269
+ setIsInitializing(false);
270
+ }, 2000);
271
+
272
+ return () => {
273
+ clearTimeout(timer);
274
+ clearTimeout(timers.current);
275
+ setPhoto({} as Photo)
276
+ };
277
+ }, []);
278
+
279
+
280
+
281
+
282
+ return (
283
+ <Animated.View style={styles.container} exiting={FadeIn.duration(300)}>
284
+ {/* ✅ 加载状态:等待 initialPosition */}
285
+ {!initialPosition ? (
286
+ <View style={[StyleSheet.absoluteFill, styles.mapLoad]}>
287
+ <ActivityIndicator size="large" color={theme.colors.primary} />
288
+ <Text style={styles.loadingText}>正在初始化地图...</Text>
289
+ </View>
290
+ ) : (
291
+ <>
292
+ {mapView}
293
+ </>
294
+ )}
295
+ <View style={styles.mapView}>
296
+ <View style={styles.topView}>
297
+ <DebouncePressable
298
+ style={[styles.topLeftItem]}
299
+ onPress={() => {
300
+ chooseClockRule();
301
+ }}
302
+ >
303
+ <Text style={styles.topLeftText} numberOfLines={1}>
304
+ {clockRule[clockRuleIndex]?.ruleName ?? "请选择"}
305
+ </Text>
306
+ <FontAwesome
307
+ name="angle-down"
308
+ size={18}
309
+ color={theme.colors.text_placeholder}
310
+ />
311
+ </DebouncePressable>
312
+ <DebouncePressable
313
+ style={[styles.topLeftItem]}
314
+ onPress={() => {
315
+ chooseShift();
316
+ }}
317
+ >
318
+ <Text style={styles.topLeftText}>
319
+ {shifts[shiftIndex] ?? "请选择"}
320
+ </Text>
321
+ <FontAwesome
322
+ name="angle-down"
323
+ size={18}
324
+ color={theme.colors.text_placeholder}
325
+ />
326
+ </DebouncePressable>
327
+ </View>
328
+ <View>
329
+ <View style={styles.cameraView}>
330
+ <DebouncePressable onPress={takePhoto} //disabled={!isWithinRange}
331
+ >
332
+ <Image
333
+ source={
334
+ photo && isNotEmpty(photo?.content)
335
+ ? { uri: photo.content }
336
+ : ImageAsset.camera
337
+ }
338
+ style={styles.cameraIcon}
339
+ />
340
+ <Text style={styles.cameraText}>
341
+ {CommonConstants.take_photo_upload}
342
+ </Text>
343
+ </DebouncePressable>
344
+ </View>
345
+ <View style={styles.locationView}>
346
+ <Image source={ImageAsset.coordinate} style={styles.locationIcon} />
347
+ <Text style={styles.address}>{data.location?.address}</Text>
348
+ </View>
349
+ </View>
350
+ </View>
351
+ {initialPosition && clockButton}
352
+ </Animated.View>
353
+ )
354
+ };
355
+
356
+ export default React.memo(ClockMapView)
357
+
358
+ const styleSheet = createStyleSheet((theme, tr) => ({
359
+ container: {
360
+ flex: 1,
361
+ backgroundColor: theme.colors.background,
362
+ },
363
+ mapView: {
364
+ flex: 1,
365
+ width: "100%",
366
+ height: "50%",
367
+ backgroundColor: theme.colors.card,
368
+ },
369
+ mapLoad: {
370
+ position: "absolute",
371
+ width: "100%",
372
+ height: "100%",
373
+ justifyContent: "center",
374
+ alignItems: "center",
375
+ zIndex: 99,
376
+ backgroundColor: theme.colors.background,
377
+ },
378
+ address: {
379
+ fontSize: 14,
380
+ color: theme.colors.timer,
381
+ },
382
+ orientationView: {
383
+ position: "absolute",
384
+ right: 5,
385
+ bottom: 0,
386
+ zIndex: 10,
387
+ width: 50,
388
+ height: 50,
389
+ },
390
+ orientation: {
391
+ width: 50,
392
+ height: 50,
393
+ },
394
+ locationView: {
395
+ flexDirection: "row",
396
+ alignItems: "center",
397
+ paddingVertical: 12,
398
+ gap: 5,
399
+ paddingHorizontal: 12,
400
+ },
401
+ locationIcon: {
402
+ width: 10,
403
+ height: 12,
404
+ },
405
+ cameraIcon: {
406
+ width: 90,
407
+ height: 90,
408
+ marginBottom: 5,
409
+ borderRadius: 5,
410
+ },
411
+ cameraText: {
412
+ fontSize: 14,
413
+ color: theme.colors.text,
414
+ fontWeight: "600",
415
+ alignSelf: "center",
416
+ },
417
+ cameraView: {
418
+ justifyContent: "center",
419
+ alignItems: "center",
420
+ },
421
+ buttonView: {
422
+ flexDirection: "row",
423
+ justifyContent: "center",
424
+ alignItems: "center",
425
+ position: "absolute",
426
+ bottom: 20,
427
+ left: 0,
428
+ right: 0,
429
+ paddingHorizontal: 12,
430
+ backgroundColor: theme.colors.card,
431
+ },
432
+ button: {
433
+ height: 44,
434
+ width: "88%",
435
+ },
436
+ buttonLabel: {
437
+ fontSize: 14,
438
+ color: theme.colors.white,
439
+ },
440
+ buttonDisabled: {
441
+ backgroundColor: "#EFEFEF",
442
+ height: 44,
443
+ width: "88%",
444
+ justifyContent: "center",
445
+ alignItems: "center",
446
+ borderRadius: 44,
447
+ gap: 10,
448
+ flexDirection: "row",
449
+ },
450
+ buttonDisabledText: {
451
+ fontSize: 14,
452
+ color: theme.colors.text_placeholder,
453
+ },
454
+ topView: {
455
+ flexDirection: "row",
456
+ padding: 5,
457
+ gap: 5,
458
+ marginBottom: 20,
459
+ },
460
+ topLeftItem: {
461
+ flexDirection: "row",
462
+ alignItems: "center",
463
+ justifyContent: "space-between",
464
+ borderWidth: 1,
465
+ borderColor: theme.colors.primary,
466
+ borderRadius: 5,
467
+ paddingVertical: 5,
468
+ backgroundColor: theme.colors.primary_light,
469
+ flex: 1,
470
+ paddingHorizontal: 10,
471
+ },
472
+ topLeftText: {
473
+ fontSize: 12,
474
+ color: theme.colors.primary,
475
+ fontWeight: "600",
476
+ marginRight: 30,
477
+ },
478
+ markerText: {
479
+ fontSize: 12,
480
+ color: theme.colors.text,
481
+ fontWeight: "600",
482
+ },
483
+ markerContainer: {
484
+ backgroundColor: theme.colors.white,
485
+ borderRadius: 5,
486
+ padding: 5,
487
+ alignItems: "center",
488
+ borderWidth: 1,
489
+ borderColor: "#ddd",
490
+ },
491
+ loadingContainer: {
492
+ flex: 1,
493
+ justifyContent: 'center',
494
+ alignItems: 'center',
495
+ backgroundColor: theme.colors.background,
496
+ flexDirection: 'row',
497
+ gap: 10,
498
+ },
499
+ loadingText: {
500
+ marginTop: 10,
501
+ fontSize: 14,
502
+ color: theme.colors.text_placeholder,
503
+ },
504
+ }));