react-native-x-components 0.1.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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/XActionSheet/index.d.ts +47 -0
- package/dist/XActionSheet/index.js +100 -0
- package/dist/XAnimatedSearchPanel/index.d.ts +33 -0
- package/dist/XAnimatedSearchPanel/index.js +295 -0
- package/dist/XAnimatedView/index.d.ts +77 -0
- package/dist/XAnimatedView/index.js +227 -0
- package/dist/XButton/index.d.ts +44 -0
- package/dist/XButton/index.js +126 -0
- package/dist/XCalendar/XCalendarPopup.d.ts +38 -0
- package/dist/XCalendar/XCalendarPopup.js +76 -0
- package/dist/XCalendar/index.d.ts +50 -0
- package/dist/XCalendar/index.js +241 -0
- package/dist/XCarousel/index.d.ts +72 -0
- package/dist/XCarousel/index.js +183 -0
- package/dist/XCascadeSelect/index.d.ts +39 -0
- package/dist/XCascadeSelect/index.js +147 -0
- package/dist/XChart/index.d.ts +78 -0
- package/dist/XChart/index.js +162 -0
- package/dist/XCheckbox/index.d.ts +61 -0
- package/dist/XCheckbox/index.js +128 -0
- package/dist/XConfirmForm/index.d.ts +69 -0
- package/dist/XConfirmForm/index.js +113 -0
- package/dist/XDivider/index.d.ts +33 -0
- package/dist/XDivider/index.js +61 -0
- package/dist/XDropdownMenu/index.d.ts +66 -0
- package/dist/XDropdownMenu/index.js +258 -0
- package/dist/XElevator/index.d.ts +52 -0
- package/dist/XElevator/index.js +138 -0
- package/dist/XForm/FormStore.d.ts +82 -0
- package/dist/XForm/FormStore.js +279 -0
- package/dist/XForm/index.d.ts +116 -0
- package/dist/XForm/index.js +212 -0
- package/dist/XForm/types.d.ts +82 -0
- package/dist/XForm/types.js +1 -0
- package/dist/XForm/useForm.d.ts +2 -0
- package/dist/XForm/useForm.js +19 -0
- package/dist/XForm/utils.d.ts +42 -0
- package/dist/XForm/utils.js +131 -0
- package/dist/XForm/validate.d.ts +20 -0
- package/dist/XForm/validate.js +118 -0
- package/dist/XFormPro/components/FieldTrigger.d.ts +14 -0
- package/dist/XFormPro/components/FieldTrigger.js +38 -0
- package/dist/XFormPro/components/XDateField.d.ts +22 -0
- package/dist/XFormPro/components/XDateField.js +22 -0
- package/dist/XFormPro/components/XSelectField.d.ts +16 -0
- package/dist/XFormPro/components/XSelectField.js +19 -0
- package/dist/XFormPro/index.d.ts +44 -0
- package/dist/XFormPro/index.js +111 -0
- package/dist/XFormPro/renderers.d.ts +24 -0
- package/dist/XFormPro/renderers.js +53 -0
- package/dist/XFormPro/types.d.ts +142 -0
- package/dist/XFormPro/types.js +1 -0
- package/dist/XImage/index.d.ts +13 -0
- package/dist/XImage/index.js +32 -0
- package/dist/XInput/index.d.ts +43 -0
- package/dist/XInput/index.js +136 -0
- package/dist/XLicensePlate/index.d.ts +40 -0
- package/dist/XLicensePlate/index.js +172 -0
- package/dist/XLocale/index.d.ts +23 -0
- package/dist/XLocale/index.js +174 -0
- package/dist/XModalForm/index.d.ts +32 -0
- package/dist/XModalForm/index.js +85 -0
- package/dist/XMultiSelect/index.d.ts +31 -0
- package/dist/XMultiSelect/index.js +131 -0
- package/dist/XNumberKeyboard/index.d.ts +57 -0
- package/dist/XNumberKeyboard/index.js +142 -0
- package/dist/XPicker/index.d.ts +35 -0
- package/dist/XPicker/index.js +96 -0
- package/dist/XPickerDate/index.d.ts +58 -0
- package/dist/XPickerDate/index.js +234 -0
- package/dist/XPopupProvider/index.d.ts +23 -0
- package/dist/XPopupProvider/index.js +6 -0
- package/dist/XProgress/index.d.ts +49 -0
- package/dist/XProgress/index.js +196 -0
- package/dist/XProviders/XImagePreview/index.d.ts +44 -0
- package/dist/XProviders/XImagePreview/index.js +164 -0
- package/dist/XProviders/XLoadingModal/index.d.ts +46 -0
- package/dist/XProviders/XLoadingModal/index.js +119 -0
- package/dist/XProviders/XToast/index.d.ts +50 -0
- package/dist/XProviders/XToast/index.js +183 -0
- package/dist/XProviders/index.d.ts +31 -0
- package/dist/XProviders/index.js +28 -0
- package/dist/XProviders/usageExample.d.ts +19 -0
- package/dist/XProviders/usageExample.js +83 -0
- package/dist/XPullView/index.d.ts +76 -0
- package/dist/XPullView/index.js +220 -0
- package/dist/XRadio/index.d.ts +83 -0
- package/dist/XRadio/index.js +186 -0
- package/dist/XRecord/index.d.ts +37 -0
- package/dist/XRecord/index.js +160 -0
- package/dist/XSignature/XSignatureSkia.d.ts +39 -0
- package/dist/XSignature/XSignatureSkia.js +184 -0
- package/dist/XSignature/core.d.ts +69 -0
- package/dist/XSignature/core.js +108 -0
- package/dist/XSignature/index.d.ts +33 -0
- package/dist/XSignature/index.js +88 -0
- package/dist/XTabs/index.d.ts +65 -0
- package/dist/XTabs/index.js +210 -0
- package/dist/XTag/index.d.ts +61 -0
- package/dist/XTag/index.js +153 -0
- package/dist/XTopView/index.d.ts +25 -0
- package/dist/XTopView/index.js +8 -0
- package/dist/XTopViewStore/index.d.ts +45 -0
- package/dist/XTopViewStore/index.js +49 -0
- package/dist/XUpload/XUploadImage.d.ts +37 -0
- package/dist/XUpload/XUploadImage.js +142 -0
- package/dist/XUpload/XUploadVideo.d.ts +24 -0
- package/dist/XUpload/XUploadVideo.js +83 -0
- package/dist/XUpload/adapters.d.ts +50 -0
- package/dist/XUpload/adapters.js +110 -0
- package/dist/XUpload/helpers.d.ts +35 -0
- package/dist/XUpload/helpers.js +85 -0
- package/dist/XUpload/index.d.ts +12 -0
- package/dist/XUpload/index.js +5 -0
- package/dist/XUpload/provider.d.ts +26 -0
- package/dist/XUpload/provider.js +43 -0
- package/dist/XUpload/types.d.ts +31 -0
- package/dist/XUpload/types.js +4 -0
- package/dist/XWheel/index.d.ts +49 -0
- package/dist/XWheel/index.js +263 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.js +127 -0
- package/dist/theme.d.ts +164 -0
- package/dist/theme.js +161 -0
- package/package.json +78 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XImagePreview —— 命令式图片预览(X 系列,复刻自 components/ImagePreview)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 由四部分组成(单文件聚合,避免目录过碎):
|
|
8
|
+
* - XGalleryImage 单图渲染(fitContainer 适配屏幕)
|
|
9
|
+
* - XImagePreview 预览 UI(Gallery 翻页 + 页码 + 关闭按钮)
|
|
10
|
+
* - XImagePreviewProvider 状态宿主,App 根部挂载一次
|
|
11
|
+
* - XImagePreviewService 命令式入口,任意地方调用
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* XImagePreviewService.show({images: urls, initialIndex: 0});
|
|
15
|
+
* XImagePreviewService.hide();
|
|
16
|
+
*
|
|
17
|
+
* 实现要点:
|
|
18
|
+
* - 全局句柄 useEffect 注册/清理(无 render 副作用);
|
|
19
|
+
* - 关闭时清空 images,及时释放大图列表引用;
|
|
20
|
+
* - DEV 环境未挂载时给出 console.warn;
|
|
21
|
+
* - Modal 使用 transparent + fade,浮在原页面上(系统相册同款观感);
|
|
22
|
+
* - GestureHandlerRootView 是 react-native-zoom-toolkit 手势正常工作的必要包裹。
|
|
23
|
+
*/
|
|
24
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
25
|
+
import { Image, Modal, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
|
26
|
+
import { stackTransition, Gallery, fitContainer } from 'react-native-zoom-toolkit';
|
|
27
|
+
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
28
|
+
import AntDesign from '@react-native-vector-icons/ant-design';
|
|
29
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
30
|
+
const XGalleryImage = ({ uri }) => {
|
|
31
|
+
const { width, height } = useWindowDimensions();
|
|
32
|
+
const [resolution, setResolution] = useState({ width: 1, height: 1 });
|
|
33
|
+
const size = fitContainer(resolution.width / resolution.height, { width, height });
|
|
34
|
+
return (_jsx(Image, { source: { uri }, style: size, resizeMethod: 'scale', resizeMode: 'cover', onLoad: e => {
|
|
35
|
+
setResolution({
|
|
36
|
+
width: e.nativeEvent.source.width,
|
|
37
|
+
height: e.nativeEvent.source.height,
|
|
38
|
+
});
|
|
39
|
+
} }));
|
|
40
|
+
};
|
|
41
|
+
export const XImagePreview = ({ initialIndex = 0, imageList, onClose }) => {
|
|
42
|
+
const ref = useRef(null);
|
|
43
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
44
|
+
const insets = useSafeAreaInsets();
|
|
45
|
+
const renderItem = useCallback((item, index) => {
|
|
46
|
+
return _jsx(XGalleryImage, { uri: item, index: index });
|
|
47
|
+
}, []);
|
|
48
|
+
const keyExtractor = useCallback((item, index) => {
|
|
49
|
+
return `${item}-${index}`;
|
|
50
|
+
}, []);
|
|
51
|
+
const onIndexChange = useCallback((index) => {
|
|
52
|
+
setCurrentIndex(index);
|
|
53
|
+
}, []);
|
|
54
|
+
const transition = useCallback(stackTransition, []);
|
|
55
|
+
// 关闭按钮的手势:必须用 RNGH 的 Gesture.Tap,因为 Gallery 内部有一个全屏
|
|
56
|
+
// + zIndex 最大 + 带 Tap 的手势探测器,RN 的 TouchableOpacity 会被它抢走 touch。
|
|
57
|
+
// 关键:必须用 useMemo 真正稳定为一个 Gesture 对象,传函数 / useCallback(返回新对象) 会让 GestureDetector 反复重新注册识别器,导致手势永远识别失败。
|
|
58
|
+
const closeTap = useMemo(() => Gesture.Tap()
|
|
59
|
+
.runOnJS(true)
|
|
60
|
+
.onEnd((_e, success) => {
|
|
61
|
+
if (success)
|
|
62
|
+
onClose?.();
|
|
63
|
+
}), [onClose]);
|
|
64
|
+
// 下拉关闭手势(iOS 相册标配):垂直下拉超过 80px 触发关闭。
|
|
65
|
+
const verticalPullClose = useMemo(() => Gesture.Pan()
|
|
66
|
+
.activeOffsetY([10, 10])
|
|
67
|
+
.onEnd((e, success) => {
|
|
68
|
+
if (success && e.translationY > 80)
|
|
69
|
+
onClose?.();
|
|
70
|
+
}), [onClose]);
|
|
71
|
+
return (_jsxs(View, { style: styles.root, children: [_jsx(Gallery, { ref: ref, data: imageList, keyExtractor: keyExtractor, renderItem: renderItem, initialIndex: initialIndex, onIndexChange: onIndexChange, customTransition: transition, tapOnEdgeToItem: false }), _jsx(View, { pointerEvents: 'box-none', style: [styles.topOverlay, { paddingTop: insets.top + 20, paddingRight: 10 }], children: _jsx(GestureDetector, { gesture: closeTap, children: _jsx(View, { style: styles.closeButton, children: _jsx(AntDesign, { name: 'close', size: 22, color: 'rgba(255,255,255,0.92)' }) }) }) }), _jsx(View, { pointerEvents: 'box-none', style: [styles.bottomOverlay, { paddingBottom: insets.bottom + 24 }], children: _jsxs(Text, { style: styles.counter, children: [currentIndex + 1, " / ", imageList.length] }) })] }));
|
|
72
|
+
};
|
|
73
|
+
const styles = StyleSheet.create({
|
|
74
|
+
root: {
|
|
75
|
+
flex: 1,
|
|
76
|
+
backgroundColor: 'rgba(0,0,0,0.92)',
|
|
77
|
+
},
|
|
78
|
+
modalRoot: {
|
|
79
|
+
...{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
|
|
80
|
+
backgroundColor: 'rgba(0,0,0,0.92)',
|
|
81
|
+
},
|
|
82
|
+
topOverlay: {
|
|
83
|
+
position: 'absolute',
|
|
84
|
+
top: 0,
|
|
85
|
+
left: 0,
|
|
86
|
+
right: 0,
|
|
87
|
+
flexDirection: 'row',
|
|
88
|
+
justifyContent: 'flex-end',
|
|
89
|
+
alignItems: 'flex-start',
|
|
90
|
+
paddingHorizontal: 16,
|
|
91
|
+
// paddingTop 在组件渲染时动态设置(insets.top + 8)
|
|
92
|
+
},
|
|
93
|
+
closeButton: {
|
|
94
|
+
width: 34,
|
|
95
|
+
height: 34,
|
|
96
|
+
borderRadius: 17,
|
|
97
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
98
|
+
justifyContent: 'center',
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
},
|
|
101
|
+
bottomOverlay: {
|
|
102
|
+
position: 'absolute',
|
|
103
|
+
bottom: 0,
|
|
104
|
+
left: 0,
|
|
105
|
+
right: 0,
|
|
106
|
+
flexDirection: 'row',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
zIndex: 10,
|
|
110
|
+
},
|
|
111
|
+
counter: {
|
|
112
|
+
color: '#fff',
|
|
113
|
+
fontSize: 15,
|
|
114
|
+
fontWeight: '600',
|
|
115
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
116
|
+
paddingHorizontal: 14,
|
|
117
|
+
paddingVertical: 5,
|
|
118
|
+
borderRadius: 999,
|
|
119
|
+
overflow: 'hidden',
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
// ================= Provider + Service =================
|
|
123
|
+
/** 全局句柄:Provider 挂载时写入,卸载时清空 */
|
|
124
|
+
let handle = null;
|
|
125
|
+
export const XImagePreviewProvider = ({ children }) => {
|
|
126
|
+
const [visible, setVisible] = useState(false);
|
|
127
|
+
const [images, setImages] = useState([]);
|
|
128
|
+
const [initialIndex, setInitialIndex] = useState(0);
|
|
129
|
+
const show = useCallback(({ images: list, initialIndex: index = 0 }) => {
|
|
130
|
+
setImages(list);
|
|
131
|
+
setInitialIndex(index);
|
|
132
|
+
setVisible(true);
|
|
133
|
+
}, []);
|
|
134
|
+
const hide = useCallback(() => {
|
|
135
|
+
setVisible(false);
|
|
136
|
+
setImages([]); // 释放图片列表引用
|
|
137
|
+
}, []);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
handle = { show, hide };
|
|
140
|
+
return () => {
|
|
141
|
+
if (handle?.show === show) {
|
|
142
|
+
handle = null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}, [show, hide]);
|
|
146
|
+
return (_jsxs(_Fragment, { children: [children, _jsx(Modal, { visible: visible, transparent: true, animationType: 'fade', statusBarTranslucent: true, hardwareAccelerated: true, onRequestClose: hide, children: _jsx(View, { style: styles.modalRoot, children: _jsx(GestureHandlerRootView, { style: styles.modalRoot, children: _jsx(XImagePreview, { imageList: images, initialIndex: initialIndex, onClose: hide }) }) }) })] }));
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* XImagePreviewService —— 命令式调用入口(可在组件外使用)
|
|
150
|
+
*/
|
|
151
|
+
export const XImagePreviewService = {
|
|
152
|
+
show: (params) => {
|
|
153
|
+
if (__DEV__ && !handle) {
|
|
154
|
+
console.warn('[XImagePreviewService] XImagePreviewProvider 未挂载,本次 show 调用被忽略');
|
|
155
|
+
}
|
|
156
|
+
handle?.show(params);
|
|
157
|
+
},
|
|
158
|
+
hide: () => {
|
|
159
|
+
if (__DEV__ && !handle) {
|
|
160
|
+
console.warn('[XImagePreviewService] XImagePreviewProvider 未挂载,本次 hide 调用被忽略');
|
|
161
|
+
}
|
|
162
|
+
handle?.hide();
|
|
163
|
+
},
|
|
164
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XLoadingModal —— 命令式 Loading(X 系列,复刻自 components/LoadingModal)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 由三部分组成:
|
|
7
|
+
* - XLoadingModal 纯 UI 组件(Modal + 转圈 + 文案)
|
|
8
|
+
* - XLoadingModalProvider 状态宿主,App 根部挂载一次
|
|
9
|
+
* - XLoadingModalService 命令式入口,任意地方(含非组件环境)调用
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* App 根部挂载一次 XLoadingModalProvider,然后任意地方:
|
|
13
|
+
* XLoadingModalService.show({title: '上传中...'});
|
|
14
|
+
* XLoadingModalService.hide();
|
|
15
|
+
*
|
|
16
|
+
* 实现要点:
|
|
17
|
+
* - 全局句柄 useEffect 注册/清理,Provider 卸载后调用自动失效(无 render 副作用);
|
|
18
|
+
* - show/hide 引用计数,并发多次 show 时全部 hide 后才关闭;
|
|
19
|
+
* - show 传参为 Partial 合并,只改传入的字段;
|
|
20
|
+
* - DEV 环境未挂载时给出 console.warn,不再静默吞掉调用。
|
|
21
|
+
*/
|
|
22
|
+
import React, { type ReactNode } from 'react';
|
|
23
|
+
export interface XLoadingModalProps {
|
|
24
|
+
title?: string;
|
|
25
|
+
visible: boolean;
|
|
26
|
+
color?: string;
|
|
27
|
+
size?: number | 'small' | 'large';
|
|
28
|
+
transparent?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface XLoadingModalConfig {
|
|
31
|
+
title?: string;
|
|
32
|
+
color?: string;
|
|
33
|
+
size?: number | 'small' | 'large';
|
|
34
|
+
transparent?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare const XLoadingModal: React.FC<XLoadingModalProps>;
|
|
37
|
+
export declare const XLoadingModalProvider: ({ children }: {
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}) => React.JSX.Element;
|
|
40
|
+
/**
|
|
41
|
+
* XLoadingModalService —— 命令式调用入口(可在 axios 拦截器等非组件环境使用)
|
|
42
|
+
*/
|
|
43
|
+
export declare const XLoadingModalService: {
|
|
44
|
+
show: (params?: Partial<XLoadingModalConfig>) => void;
|
|
45
|
+
hide: () => void;
|
|
46
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XLoadingModal —— 命令式 Loading(X 系列,复刻自 components/LoadingModal)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 由三部分组成:
|
|
8
|
+
* - XLoadingModal 纯 UI 组件(Modal + 转圈 + 文案)
|
|
9
|
+
* - XLoadingModalProvider 状态宿主,App 根部挂载一次
|
|
10
|
+
* - XLoadingModalService 命令式入口,任意地方(含非组件环境)调用
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* App 根部挂载一次 XLoadingModalProvider,然后任意地方:
|
|
14
|
+
* XLoadingModalService.show({title: '上传中...'});
|
|
15
|
+
* XLoadingModalService.hide();
|
|
16
|
+
*
|
|
17
|
+
* 实现要点:
|
|
18
|
+
* - 全局句柄 useEffect 注册/清理,Provider 卸载后调用自动失效(无 render 副作用);
|
|
19
|
+
* - show/hide 引用计数,并发多次 show 时全部 hide 后才关闭;
|
|
20
|
+
* - show 传参为 Partial 合并,只改传入的字段;
|
|
21
|
+
* - DEV 环境未挂载时给出 console.warn,不再静默吞掉调用。
|
|
22
|
+
*/
|
|
23
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
24
|
+
import { ActivityIndicator, Modal, StyleSheet, Text, View } from 'react-native';
|
|
25
|
+
import { useXTheme } from '../../theme';
|
|
26
|
+
import { useXLocale } from '../../XLocale';
|
|
27
|
+
// ================= 纯 UI 组件 =================
|
|
28
|
+
export const XLoadingModal = ({ title, visible, color, size = 'large', transparent = false, }) => {
|
|
29
|
+
const theme = useXTheme();
|
|
30
|
+
const { t: i18n } = useXLocale();
|
|
31
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
32
|
+
mask: {
|
|
33
|
+
flex: 1,
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
backgroundColor: 'rgba(0,0,0,0.35)',
|
|
37
|
+
},
|
|
38
|
+
box: {
|
|
39
|
+
flexDirection: 'row',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: 10,
|
|
42
|
+
paddingHorizontal: 24,
|
|
43
|
+
paddingVertical: 18,
|
|
44
|
+
borderRadius: theme.borderRadiusLG,
|
|
45
|
+
// 深色半透明胶囊:明暗两种模式都压在暗色遮罩上,保持固定深色
|
|
46
|
+
backgroundColor: 'rgba(17,20,26,0.85)',
|
|
47
|
+
shadowColor: '#000',
|
|
48
|
+
shadowOpacity: 0.2,
|
|
49
|
+
shadowRadius: 16,
|
|
50
|
+
shadowOffset: { width: 0, height: 8 },
|
|
51
|
+
elevation: 10,
|
|
52
|
+
},
|
|
53
|
+
boxTransparent: {
|
|
54
|
+
backgroundColor: 'transparent',
|
|
55
|
+
shadowOpacity: 0,
|
|
56
|
+
elevation: 0,
|
|
57
|
+
},
|
|
58
|
+
title: {
|
|
59
|
+
// 浅色实底文字:深色胶囊上的固定对比色
|
|
60
|
+
color: theme.colorTextLightSolid,
|
|
61
|
+
fontSize: theme.fontSize,
|
|
62
|
+
lineHeight: 22,
|
|
63
|
+
},
|
|
64
|
+
}), [theme]);
|
|
65
|
+
const resolvedTitle = title ?? i18n('loading');
|
|
66
|
+
const resolvedColor = color ?? theme.colorTextLightSolid;
|
|
67
|
+
return (_jsx(Modal, { visible: visible, transparent: true, animationType: 'fade', statusBarTranslucent: true, presentationStyle: 'overFullScreen', hardwareAccelerated: true, children: _jsx(View, { style: styles.mask, children: _jsxs(View, { style: [styles.box, transparent && styles.boxTransparent], children: [_jsx(ActivityIndicator, { size: size, color: resolvedColor }), !!resolvedTitle && _jsx(Text, { style: styles.title, children: resolvedTitle })] }) }) }));
|
|
68
|
+
};
|
|
69
|
+
// ================= Provider + Service =================
|
|
70
|
+
/** 全局句柄:Provider 挂载时写入,卸载时清空 */
|
|
71
|
+
let handle = null;
|
|
72
|
+
const DEFAULT_CONFIG = {
|
|
73
|
+
// title / color 不在此写死:未传时由 XLoadingModal 解析为当前语言的 loading 文案与浅色实底色
|
|
74
|
+
size: 'large',
|
|
75
|
+
transparent: false,
|
|
76
|
+
};
|
|
77
|
+
export const XLoadingModalProvider = ({ children }) => {
|
|
78
|
+
const [visible, setVisible] = useState(false);
|
|
79
|
+
const [config, setConfig] = useState(DEFAULT_CONFIG);
|
|
80
|
+
/** 引用计数:并发 show 的次数,hide 到 0 才真正关闭 */
|
|
81
|
+
const countRef = useRef(0);
|
|
82
|
+
const show = useCallback((params = {}) => {
|
|
83
|
+
countRef.current += 1;
|
|
84
|
+
setConfig(prev => ({ ...prev, ...params }));
|
|
85
|
+
setVisible(true);
|
|
86
|
+
}, []);
|
|
87
|
+
const hide = useCallback(() => {
|
|
88
|
+
countRef.current = Math.max(0, countRef.current - 1);
|
|
89
|
+
if (countRef.current === 0) {
|
|
90
|
+
setVisible(false);
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
handle = { show, hide };
|
|
95
|
+
return () => {
|
|
96
|
+
if (handle?.show === show) {
|
|
97
|
+
handle = null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}, [show, hide]);
|
|
101
|
+
return (_jsxs(_Fragment, { children: [children, _jsx(XLoadingModal, { visible: visible, title: config.title, color: config.color, size: config.size, transparent: config.transparent })] }));
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* XLoadingModalService —— 命令式调用入口(可在 axios 拦截器等非组件环境使用)
|
|
105
|
+
*/
|
|
106
|
+
export const XLoadingModalService = {
|
|
107
|
+
show: (params = {}) => {
|
|
108
|
+
if (__DEV__ && !handle) {
|
|
109
|
+
console.warn('[XLoadingModalService] XLoadingModalProvider 未挂载,本次 show 调用被忽略');
|
|
110
|
+
}
|
|
111
|
+
handle?.show(params);
|
|
112
|
+
},
|
|
113
|
+
hide: () => {
|
|
114
|
+
if (__DEV__ && !handle) {
|
|
115
|
+
console.warn('[XLoadingModalService] XLoadingModalProvider 未挂载,本次 hide 调用被忽略');
|
|
116
|
+
}
|
|
117
|
+
handle?.hide();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XToast —— 全局轻提示(X 系列,复刻自 components/Toast 并现代化)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 由三部分组成:
|
|
7
|
+
* - XToast 单条 UI(深色胶囊:图标 + 文案)
|
|
8
|
+
* - XToastProvider 状态宿主,App 根部挂载一次(必须在 SafeAreaProvider 内)
|
|
9
|
+
* - XToastService 命令式入口,任意地方调用
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* XToastService.show('保存成功'); // 纯文本
|
|
13
|
+
* XToastService.show({message: '保存成功', type: 'success'}); // 带图标
|
|
14
|
+
* XToastService.show({message: '网络错误', type: 'error', position: 'top'});
|
|
15
|
+
*
|
|
16
|
+
* 视觉:iOS 风格深色胶囊,淡入 + 弹簧位移动画;top / center / bottom
|
|
17
|
+
* 三个位置独立堆叠,同屏最多 3 条(新的顶掉最旧的)。
|
|
18
|
+
*
|
|
19
|
+
* 实现要点:
|
|
20
|
+
* - 全局句柄 useEffect 注册/清理(无 render 副作用);
|
|
21
|
+
* - id 由自增计数器生成,避免同一毫秒多条 Toast 的 key 冲突;
|
|
22
|
+
* - 位移方向按位置自适应(top 从上滑入,其余从下滑入);
|
|
23
|
+
* - duration 传 0 表示不自动消失,适配手动控制场景。
|
|
24
|
+
*/
|
|
25
|
+
import React, { type ReactNode } from 'react';
|
|
26
|
+
export type XToastType = 'success' | 'error' | 'info' | 'warning' | 'loading' | 'text';
|
|
27
|
+
export type XToastPosition = 'top' | 'center' | 'bottom';
|
|
28
|
+
export interface XToastOptions {
|
|
29
|
+
message: string;
|
|
30
|
+
/** 显示时长(毫秒),默认 2000;0 表示不自动消失 */
|
|
31
|
+
duration?: number;
|
|
32
|
+
type?: XToastType;
|
|
33
|
+
position?: XToastPosition;
|
|
34
|
+
}
|
|
35
|
+
/** 组件内获取 addToast 的 Hook */
|
|
36
|
+
export declare const useXToast: () => {
|
|
37
|
+
addToast: (options: XToastOptions) => void;
|
|
38
|
+
};
|
|
39
|
+
export declare const XToastProvider: ({ children }: {
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}) => React.JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* XToastService —— 命令式调用入口(可在组件外使用)
|
|
44
|
+
* show 支持两种签名:
|
|
45
|
+
* show('保存成功') 纯文本
|
|
46
|
+
* show({message, type, duration, position}) 完整配置
|
|
47
|
+
*/
|
|
48
|
+
export declare const XToastService: {
|
|
49
|
+
show: (messageOrOptions: string | XToastOptions, duration?: number) => void;
|
|
50
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XToast —— 全局轻提示(X 系列,复刻自 components/Toast 并现代化)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 由三部分组成:
|
|
8
|
+
* - XToast 单条 UI(深色胶囊:图标 + 文案)
|
|
9
|
+
* - XToastProvider 状态宿主,App 根部挂载一次(必须在 SafeAreaProvider 内)
|
|
10
|
+
* - XToastService 命令式入口,任意地方调用
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* XToastService.show('保存成功'); // 纯文本
|
|
14
|
+
* XToastService.show({message: '保存成功', type: 'success'}); // 带图标
|
|
15
|
+
* XToastService.show({message: '网络错误', type: 'error', position: 'top'});
|
|
16
|
+
*
|
|
17
|
+
* 视觉:iOS 风格深色胶囊,淡入 + 弹簧位移动画;top / center / bottom
|
|
18
|
+
* 三个位置独立堆叠,同屏最多 3 条(新的顶掉最旧的)。
|
|
19
|
+
*
|
|
20
|
+
* 实现要点:
|
|
21
|
+
* - 全局句柄 useEffect 注册/清理(无 render 副作用);
|
|
22
|
+
* - id 由自增计数器生成,避免同一毫秒多条 Toast 的 key 冲突;
|
|
23
|
+
* - 位移方向按位置自适应(top 从上滑入,其余从下滑入);
|
|
24
|
+
* - duration 传 0 表示不自动消失,适配手动控制场景。
|
|
25
|
+
*/
|
|
26
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
27
|
+
import { ActivityIndicator, Animated, StyleSheet, Text, View } from 'react-native';
|
|
28
|
+
import AntDesign from '@react-native-vector-icons/ant-design';
|
|
29
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
30
|
+
import { useXTheme } from '../../theme';
|
|
31
|
+
/** 同屏最多保留的 Toast 条数 */
|
|
32
|
+
const MAX_VISIBLE = 3;
|
|
33
|
+
/** 深色底上的语义图标色(比 antd 默认色提亮一档,保证对比度);as const 保留图标名字面量类型 */
|
|
34
|
+
const TYPE_ICON = {
|
|
35
|
+
success: { name: 'check-circle', color: '#4ADE80' },
|
|
36
|
+
error: { name: 'close-circle', color: '#F87171' },
|
|
37
|
+
info: { name: 'info-circle', color: '#60A5FA' },
|
|
38
|
+
warning: { name: 'warning', color: '#FBBF24' },
|
|
39
|
+
};
|
|
40
|
+
// ================= 单条 Toast UI =================
|
|
41
|
+
const XToastItem = ({ toast, onClose }) => {
|
|
42
|
+
const theme = useXTheme();
|
|
43
|
+
const opacity = useRef(new Animated.Value(0)).current;
|
|
44
|
+
const fromTop = toast.position === 'top';
|
|
45
|
+
const translateY = useRef(new Animated.Value(fromTop ? -24 : 24)).current;
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
// 入场:淡入 + 弹簧位移
|
|
48
|
+
Animated.parallel([
|
|
49
|
+
Animated.timing(opacity, { toValue: 1, duration: 180, useNativeDriver: true }),
|
|
50
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true, friction: 9, tension: 80 }),
|
|
51
|
+
]).start();
|
|
52
|
+
// 定时退场(duration 为 0 时不自动消失,由调用方手动控制)
|
|
53
|
+
let timer;
|
|
54
|
+
if (toast.duration > 0) {
|
|
55
|
+
timer = setTimeout(() => {
|
|
56
|
+
Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true }).start(({ finished }) => {
|
|
57
|
+
if (finished)
|
|
58
|
+
onClose();
|
|
59
|
+
});
|
|
60
|
+
}, toast.duration);
|
|
61
|
+
}
|
|
62
|
+
return () => {
|
|
63
|
+
if (timer) {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
}, []);
|
|
69
|
+
const icon = TYPE_ICON[toast.type];
|
|
70
|
+
return (_jsxs(Animated.View, { style: [styles.toast, { opacity, transform: [{ translateY }] }], children: [toast.type === 'loading' ? (_jsx(ActivityIndicator, { size: 'small', color: theme.colorTextLightSolid })) : (icon && _jsx(AntDesign, { name: icon.name, size: 18, color: icon.color })), !!toast.message && (_jsx(Text, { style: [styles.text, { color: theme.colorTextLightSolid, fontSize: theme.fontSize }], children: toast.message }))] }));
|
|
71
|
+
};
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
toast: {
|
|
74
|
+
flexDirection: 'row',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
gap: 8,
|
|
77
|
+
maxWidth: '82%',
|
|
78
|
+
paddingHorizontal: 18,
|
|
79
|
+
paddingVertical: 12,
|
|
80
|
+
borderRadius: 999,
|
|
81
|
+
// 深色胶囊:明暗两种模式都保持固定深色底,保证语义图标对比度
|
|
82
|
+
backgroundColor: 'rgba(23,26,31,0.94)',
|
|
83
|
+
shadowColor: '#000',
|
|
84
|
+
shadowOpacity: 0.25,
|
|
85
|
+
shadowRadius: 16,
|
|
86
|
+
shadowOffset: { width: 0, height: 8 },
|
|
87
|
+
elevation: 8,
|
|
88
|
+
},
|
|
89
|
+
text: {
|
|
90
|
+
lineHeight: 20,
|
|
91
|
+
flexShrink: 1,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const styles_overlay = StyleSheet.create({
|
|
95
|
+
top: {
|
|
96
|
+
...{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
|
|
97
|
+
justifyContent: 'flex-start',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
gap: 8,
|
|
100
|
+
},
|
|
101
|
+
center: {
|
|
102
|
+
...{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
gap: 8,
|
|
106
|
+
},
|
|
107
|
+
bottom: {
|
|
108
|
+
...{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
|
|
109
|
+
justifyContent: 'flex-end',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
gap: 8,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
// ================= Context =================
|
|
115
|
+
const XToastContext = createContext({
|
|
116
|
+
addToast: () => { },
|
|
117
|
+
});
|
|
118
|
+
/** 组件内获取 addToast 的 Hook */
|
|
119
|
+
export const useXToast = () => useContext(XToastContext);
|
|
120
|
+
// ================= Provider + Service =================
|
|
121
|
+
/** 全局句柄:Provider 挂载时写入,卸载时清空 */
|
|
122
|
+
let addToastHandle = null;
|
|
123
|
+
export const XToastProvider = ({ children }) => {
|
|
124
|
+
const [toasts, setToasts] = useState([]);
|
|
125
|
+
const nextIdRef = useRef(0);
|
|
126
|
+
const insets = useSafeAreaInsets();
|
|
127
|
+
const addToast = useCallback((options) => {
|
|
128
|
+
const id = ++nextIdRef.current;
|
|
129
|
+
setToasts(prev => {
|
|
130
|
+
const next = [
|
|
131
|
+
...prev,
|
|
132
|
+
{
|
|
133
|
+
id,
|
|
134
|
+
message: options.message,
|
|
135
|
+
duration: options.duration ?? 2000,
|
|
136
|
+
type: options.type ?? 'text',
|
|
137
|
+
position: options.position ?? 'bottom',
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
// 超出上限时丢最旧的
|
|
141
|
+
return next.slice(-MAX_VISIBLE);
|
|
142
|
+
});
|
|
143
|
+
}, []);
|
|
144
|
+
const removeToast = useCallback((id) => {
|
|
145
|
+
setToasts(prev => prev.filter(toast => toast.id !== id));
|
|
146
|
+
}, []);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
addToastHandle = addToast;
|
|
149
|
+
return () => {
|
|
150
|
+
if (addToastHandle === addToast) {
|
|
151
|
+
addToastHandle = null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}, [addToast]);
|
|
155
|
+
const byPosition = (position) => toasts.filter(toast => toast.position === position);
|
|
156
|
+
const renderPosition = (position) => {
|
|
157
|
+
const list = byPosition(position);
|
|
158
|
+
if (list.length === 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
return (_jsx(View, { pointerEvents: 'box-none', style: [
|
|
162
|
+
styles_overlay[position],
|
|
163
|
+
position === 'top' ? { paddingTop: insets.top + 56 } : null,
|
|
164
|
+
position === 'bottom' ? { paddingBottom: insets.bottom + 80 } : null,
|
|
165
|
+
], children: list.map(toast => (_jsx(XToastItem, { toast: toast, onClose: () => removeToast(toast.id) }, toast.id))) }, position));
|
|
166
|
+
};
|
|
167
|
+
return (_jsxs(XToastContext.Provider, { value: { addToast }, children: [children, ['top', 'center', 'bottom'].map(renderPosition)] }));
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* XToastService —— 命令式调用入口(可在组件外使用)
|
|
171
|
+
* show 支持两种签名:
|
|
172
|
+
* show('保存成功') 纯文本
|
|
173
|
+
* show({message, type, duration, position}) 完整配置
|
|
174
|
+
*/
|
|
175
|
+
export const XToastService = {
|
|
176
|
+
show: (messageOrOptions, duration) => {
|
|
177
|
+
const options = typeof messageOrOptions === 'string' ? { message: messageOrOptions, duration } : messageOrOptions;
|
|
178
|
+
if (__DEV__ && !addToastHandle) {
|
|
179
|
+
console.warn('[XToastService] XToastProvider 未挂载,本次 show 调用被忽略');
|
|
180
|
+
}
|
|
181
|
+
addToastHandle?.(options);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XProviders —— X 系列命令式全局组件(Provider 类)统一出口
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 这个目录专门存放"Provider 类型"的 X 组件:需要在 App 根部挂载一次 Provider,
|
|
7
|
+
* 之后通过 Service 在任意位置(含非组件环境,如 axios 拦截器)命令式调用。
|
|
8
|
+
*
|
|
9
|
+
* 目前三件套:
|
|
10
|
+
* - XLoadingModal 全局 Loading(show/hide,引用计数,支持并发)
|
|
11
|
+
* - XImagePreview 全屏图片预览(缩放/翻页手势,微信相册同款交互)
|
|
12
|
+
* - XToast 全局轻提示(success/error/info/warning/loading,三位置)
|
|
13
|
+
*
|
|
14
|
+
* 统一挂载(App 根部,放 GestureHandlerRootView 内、SafeAreaProvider 内):
|
|
15
|
+
*
|
|
16
|
+
* <XLoadingModalProvider>
|
|
17
|
+
* <XImagePreviewProvider>
|
|
18
|
+
* <XToastProvider>
|
|
19
|
+
* {children}
|
|
20
|
+
* </XToastProvider>
|
|
21
|
+
* </XImagePreviewProvider>
|
|
22
|
+
* </XLoadingModalProvider>
|
|
23
|
+
*
|
|
24
|
+
* 完整可运行示例见同目录 usageExample.tsx。
|
|
25
|
+
*/
|
|
26
|
+
export { XLoadingModal, XLoadingModalProvider, XLoadingModalService } from './XLoadingModal';
|
|
27
|
+
export type { XLoadingModalProps, XLoadingModalConfig } from './XLoadingModal';
|
|
28
|
+
export { XImagePreview, XImagePreviewProvider, XImagePreviewService } from './XImagePreview';
|
|
29
|
+
export type { XImagePreviewParams } from './XImagePreview';
|
|
30
|
+
export { XToastProvider, XToastService, useXToast } from './XToast';
|
|
31
|
+
export type { XToastOptions, XToastType, XToastPosition } from './XToast';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XProviders —— X 系列命令式全局组件(Provider 类)统一出口
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 这个目录专门存放"Provider 类型"的 X 组件:需要在 App 根部挂载一次 Provider,
|
|
7
|
+
* 之后通过 Service 在任意位置(含非组件环境,如 axios 拦截器)命令式调用。
|
|
8
|
+
*
|
|
9
|
+
* 目前三件套:
|
|
10
|
+
* - XLoadingModal 全局 Loading(show/hide,引用计数,支持并发)
|
|
11
|
+
* - XImagePreview 全屏图片预览(缩放/翻页手势,微信相册同款交互)
|
|
12
|
+
* - XToast 全局轻提示(success/error/info/warning/loading,三位置)
|
|
13
|
+
*
|
|
14
|
+
* 统一挂载(App 根部,放 GestureHandlerRootView 内、SafeAreaProvider 内):
|
|
15
|
+
*
|
|
16
|
+
* <XLoadingModalProvider>
|
|
17
|
+
* <XImagePreviewProvider>
|
|
18
|
+
* <XToastProvider>
|
|
19
|
+
* {children}
|
|
20
|
+
* </XToastProvider>
|
|
21
|
+
* </XImagePreviewProvider>
|
|
22
|
+
* </XLoadingModalProvider>
|
|
23
|
+
*
|
|
24
|
+
* 完整可运行示例见同目录 usageExample.tsx。
|
|
25
|
+
*/
|
|
26
|
+
export { XLoadingModal, XLoadingModalProvider, XLoadingModalService } from './XLoadingModal';
|
|
27
|
+
export { XImagePreview, XImagePreviewProvider, XImagePreviewService } from './XImagePreview';
|
|
28
|
+
export { XToastProvider, XToastService, useXToast } from './XToast';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XProviders 使用示例(可运行演示页)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 演示 XLoadingModal / XImagePreview / XToast 三件套的完整用法。
|
|
7
|
+
* 页面自身演示了正确的挂载层级(生产环境应挂到 App 根部,这里为了独立可跑
|
|
8
|
+
* 直接包在本页内;SafeAreaProvider / GestureHandlerRootView 已由 App 根部
|
|
9
|
+
* 提供时无需重复包裹,本页包一层是为了单独调试也能正常运行)。
|
|
10
|
+
*
|
|
11
|
+
* 快速上手(别处使用):
|
|
12
|
+
* XLoadingModalService.show({title: '上传中...'});
|
|
13
|
+
* XLoadingModalService.hide();
|
|
14
|
+
* XImagePreviewService.show({images: urls, initialIndex: 0});
|
|
15
|
+
* XToastService.show({message: '保存成功', type: 'success'});
|
|
16
|
+
*/
|
|
17
|
+
import React from 'react';
|
|
18
|
+
declare const XProvidersDemo: () => React.JSX.Element;
|
|
19
|
+
export default XProvidersDemo;
|