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,147 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XCascadeSelect —— 级联选择器(antd Cascader 的 RN 自实现版)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 数据结构(antd Cascader 同款):
|
|
8
|
+
* options = [{label, value, children: [{label, value, children: ...}]}]
|
|
9
|
+
* value = ['hunan', 'changsha', 'yuelu'](各级 value 组成的路径数组)
|
|
10
|
+
* 展示 = 湖南 / 长沙 / 岳麓(路径上各级 label 用 " / " 连接)
|
|
11
|
+
*
|
|
12
|
+
* 交互形态:点击触发区弹出底部面板(XPullView),多列滚轮(XWheel)联动——
|
|
13
|
+
* 改动第 i 列会截断更深的路径,后续列选项自动跟着变。
|
|
14
|
+
* 路径必须走到叶子(没有 children 的选项)才能点确定。
|
|
15
|
+
*
|
|
16
|
+
* 用法(Form.Item 注入 value/onChange 即可作表单控件):
|
|
17
|
+
* <XCascadeSelect options={AREA_DATA} placeholder='请选择省市区' />
|
|
18
|
+
*/
|
|
19
|
+
import { useMemo, useRef, useState } from 'react';
|
|
20
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
21
|
+
import { XPullView } from '../XPullView';
|
|
22
|
+
import { XWheel } from '../XWheel';
|
|
23
|
+
import { useXTheme } from '../theme';
|
|
24
|
+
import { useXLocale } from '../XLocale';
|
|
25
|
+
import AntDesign from '@react-native-vector-icons/ant-design';
|
|
26
|
+
/** 沿路径取各级选项(找不到即截断) */
|
|
27
|
+
export function getCascadePathOptions(options, path) {
|
|
28
|
+
const result = [];
|
|
29
|
+
let cur = options;
|
|
30
|
+
for (const v of path) {
|
|
31
|
+
const hit = cur?.find(o => o.value === v);
|
|
32
|
+
if (!hit)
|
|
33
|
+
break;
|
|
34
|
+
result.push(hit);
|
|
35
|
+
cur = hit.children ?? [];
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
/** 路径是否走到叶子(末级选项没有 children) */
|
|
40
|
+
function isPathComplete(options, path) {
|
|
41
|
+
const nodes = getCascadePathOptions(options, path);
|
|
42
|
+
if (nodes.length !== path.length || nodes.length === 0)
|
|
43
|
+
return false;
|
|
44
|
+
return !nodes[nodes.length - 1].children?.length;
|
|
45
|
+
}
|
|
46
|
+
export function XCascadeSelect({ value, onChange, options, placeholder, title, children }) {
|
|
47
|
+
const theme = useXTheme();
|
|
48
|
+
const { t } = useXLocale();
|
|
49
|
+
const resolvedPlaceholder = placeholder ?? t('pleaseSelect');
|
|
50
|
+
const resolvedTitle = title ?? t('pleaseSelect');
|
|
51
|
+
const [visible, setVisible] = useState(false);
|
|
52
|
+
/** 面板内的临时路径,确定时才回传 */
|
|
53
|
+
const [draft, setDraft] = useState([]);
|
|
54
|
+
/** 上一次的 visible,识别"刚被打开"同步草稿 */
|
|
55
|
+
const prevVisibleRef = useRef(visible);
|
|
56
|
+
if (visible && !prevVisibleRef.current) {
|
|
57
|
+
setDraft(value ?? []);
|
|
58
|
+
}
|
|
59
|
+
prevVisibleRef.current = visible;
|
|
60
|
+
/**
|
|
61
|
+
* 各列选项:第 0 列 = options,第 i 列 = draft[i-1] 的 children。
|
|
62
|
+
* draft 改动第 i 项后,更深的列会整体消失/重建(联动)。
|
|
63
|
+
*/
|
|
64
|
+
const columns = useMemo(() => {
|
|
65
|
+
const cols = [options];
|
|
66
|
+
let cur = options;
|
|
67
|
+
for (const v of draft) {
|
|
68
|
+
const next = cur?.find(o => o.value === v)?.children;
|
|
69
|
+
if (!next?.length)
|
|
70
|
+
break;
|
|
71
|
+
cols.push(next);
|
|
72
|
+
cur = next;
|
|
73
|
+
}
|
|
74
|
+
return cols;
|
|
75
|
+
}, [options, draft]);
|
|
76
|
+
const complete = isPathComplete(options, draft);
|
|
77
|
+
const display = (value?.length ? getCascadePathOptions(options, value).map(o => o.label) : []).join(' / ');
|
|
78
|
+
/** 每列一个 wheel 句柄,确定时按视觉位置结算(拖拽/spring 进行中也正确) */
|
|
79
|
+
const wheelRefs = useRef([]);
|
|
80
|
+
wheelRefs.current = [];
|
|
81
|
+
const handleColumnSelect = (colIndex, v) => {
|
|
82
|
+
// 改第 i 列:截断更深路径后落下当前值,后续列自动联动
|
|
83
|
+
setDraft(prev => [...prev.slice(0, colIndex), v]);
|
|
84
|
+
};
|
|
85
|
+
const handleConfirm = () => {
|
|
86
|
+
if (!complete)
|
|
87
|
+
return;
|
|
88
|
+
// 结算:以每列滚轮当前视觉位置为准(与 XPicker 同款 flush 策略)
|
|
89
|
+
const flushed = wheelRefs.current.slice(0, columns.length).map(w => w?.flush()?.value);
|
|
90
|
+
const path = flushed.filter(v => v !== undefined);
|
|
91
|
+
if (!isPathComplete(options, path))
|
|
92
|
+
return;
|
|
93
|
+
onChange?.(path);
|
|
94
|
+
setVisible(false);
|
|
95
|
+
};
|
|
96
|
+
return (_jsxs(_Fragment, { children: [children ? (_jsx(Pressable, { onPress: () => setVisible(true), children: children })) : (_jsxs(Pressable, { style: [styles.trigger, { borderColor: theme.colorBorder, backgroundColor: theme.colorBgLayout }], onPress: () => setVisible(true), children: [_jsx(Text, { style: [styles.triggerText, { color: display ? theme.colorText : theme.colorTextQuaternary }], allowFontScaling: false, children: display || resolvedPlaceholder }), _jsx(AntDesign, { name: 'right', size: 16, color: theme.colorTextTertiary })] })), _jsx(XPullView, { visible: visible, onClose: () => setVisible(false), side: 'bottom', duration: 200, children: _jsxs(View, { style: [styles.panel, { backgroundColor: theme.colorBgContainer }], children: [_jsxs(View, { style: [styles.header, { borderBottomColor: theme.colorSplit }], children: [_jsx(Pressable, { onPress: () => setVisible(false), hitSlop: 8, children: _jsx(Text, { style: [styles.headerBtn, { color: theme.colorTextSecondary }], children: t('cancel') }) }), _jsx(Text, { style: [styles.headerTitle, { color: theme.colorText }], children: resolvedTitle }), _jsx(Pressable, { onPress: handleConfirm, hitSlop: 8, disabled: !complete, children: _jsx(Text, { style: [styles.headerBtn, styles.confirmBtn, { color: !complete ? theme.colorTextQuaternary : theme.colorPrimary }], children: t('confirm') }) })] }), _jsx(View, { style: styles.wheelRow, children: columns.map((col, i) => (_jsx(View, { style: styles.wheelCol, children: _jsx(XWheel, { ref: (r) => {
|
|
97
|
+
wheelRefs.current[i] = r;
|
|
98
|
+
}, items: col, value: draft[i], onSelect: v => handleColumnSelect(i, v) }) }, i))) })] }) })] }));
|
|
99
|
+
}
|
|
100
|
+
const styles = StyleSheet.create({
|
|
101
|
+
trigger: {
|
|
102
|
+
flexDirection: 'row',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
justifyContent: 'space-between',
|
|
105
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
106
|
+
borderRadius: 8,
|
|
107
|
+
paddingHorizontal: 12,
|
|
108
|
+
paddingVertical: 10,
|
|
109
|
+
minHeight: 40,
|
|
110
|
+
},
|
|
111
|
+
triggerText: {
|
|
112
|
+
flex: 1,
|
|
113
|
+
fontSize: 15,
|
|
114
|
+
marginRight: 8,
|
|
115
|
+
},
|
|
116
|
+
panel: {
|
|
117
|
+
borderTopLeftRadius: 12,
|
|
118
|
+
borderTopRightRadius: 12,
|
|
119
|
+
paddingBottom: 20,
|
|
120
|
+
},
|
|
121
|
+
header: {
|
|
122
|
+
flexDirection: 'row',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
justifyContent: 'space-between',
|
|
125
|
+
paddingHorizontal: 16,
|
|
126
|
+
height: 48,
|
|
127
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
128
|
+
},
|
|
129
|
+
headerTitle: {
|
|
130
|
+
fontSize: 16,
|
|
131
|
+
fontWeight: '600',
|
|
132
|
+
},
|
|
133
|
+
headerBtn: {
|
|
134
|
+
fontSize: 15,
|
|
135
|
+
paddingHorizontal: 4,
|
|
136
|
+
},
|
|
137
|
+
confirmBtn: {
|
|
138
|
+
fontWeight: '600',
|
|
139
|
+
},
|
|
140
|
+
wheelRow: {
|
|
141
|
+
flexDirection: 'row',
|
|
142
|
+
},
|
|
143
|
+
wheelCol: {
|
|
144
|
+
flex: 1,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
export default XCascadeSelect;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XChart —— 轻量图表三件套(react-native-svg 自研)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 【方案定位】用户选定方案:基于项目已有 react-native-svg 自研,
|
|
7
|
+
* 不引第三方图表库 —— 体积小、无原生依赖、token 与 XTheme 完全一致。
|
|
8
|
+
* 覆盖最常用三类:
|
|
9
|
+
* - XLineChart 折线/面积图(平滑曲线、网格线、数据点)
|
|
10
|
+
* - XBarChart 柱状图(圆角柱、逐柱配色、顶部数值)
|
|
11
|
+
* - XPieChart 饼图/环形图(donut、中心文案、图例)
|
|
12
|
+
*
|
|
13
|
+
* 【实现约定】
|
|
14
|
+
* - 宽度 onLayout 实测(不写死,适配容器),高度 props 指定;
|
|
15
|
+
* - 颜色默认取 useXTheme() token,可逐项覆盖;
|
|
16
|
+
* - 纯声明式渲染,数据变化即重绘(SVG 无动画库依赖,入场动画后续版本可加)。
|
|
17
|
+
* ============================================================================
|
|
18
|
+
*/
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import { ViewStyle, StyleProp } from 'react-native';
|
|
21
|
+
export interface XLineChartProps {
|
|
22
|
+
/** 数值序列 */
|
|
23
|
+
data: number[];
|
|
24
|
+
/** x 轴标签(可与 data 等长,超出抽样显示) */
|
|
25
|
+
labels?: string[];
|
|
26
|
+
height?: number;
|
|
27
|
+
/** 线色,默认 colorPrimary */
|
|
28
|
+
color?: string;
|
|
29
|
+
/** 面积填充(渐变到透明),默认 false */
|
|
30
|
+
area?: boolean;
|
|
31
|
+
/** 平滑曲线,默认 true */
|
|
32
|
+
smooth?: boolean;
|
|
33
|
+
/** 水平网格线条数,默认 4(0 关闭) */
|
|
34
|
+
grid?: number;
|
|
35
|
+
/** 数据点上方显示数值,默认 false */
|
|
36
|
+
showValues?: boolean;
|
|
37
|
+
style?: StyleProp<ViewStyle>;
|
|
38
|
+
}
|
|
39
|
+
export declare function XLineChart({ data, labels, height, color, area, smooth, grid, showValues, style, }: XLineChartProps): React.JSX.Element;
|
|
40
|
+
export interface XBarChartItem {
|
|
41
|
+
label: string;
|
|
42
|
+
value: number;
|
|
43
|
+
color?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface XBarChartProps {
|
|
46
|
+
data: XBarChartItem[];
|
|
47
|
+
height?: number;
|
|
48
|
+
/** 柱宽占比(0~1),默认 0.5 */
|
|
49
|
+
barWidthRatio?: number;
|
|
50
|
+
/** 顶部显示数值,默认 true */
|
|
51
|
+
showValues?: boolean;
|
|
52
|
+
grid?: number;
|
|
53
|
+
style?: StyleProp<ViewStyle>;
|
|
54
|
+
}
|
|
55
|
+
export declare function XBarChart({ data, height, barWidthRatio, showValues, grid, style }: XBarChartProps): React.JSX.Element;
|
|
56
|
+
export interface XPieChartItem {
|
|
57
|
+
label: string;
|
|
58
|
+
value: number;
|
|
59
|
+
color?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface XPieChartProps {
|
|
62
|
+
data: XPieChartItem[];
|
|
63
|
+
height?: number;
|
|
64
|
+
/** 环形(donut),默认 true */
|
|
65
|
+
donut?: boolean;
|
|
66
|
+
/** 环心文案 */
|
|
67
|
+
centerLabel?: string;
|
|
68
|
+
/** 图例,默认显示 */
|
|
69
|
+
legend?: boolean;
|
|
70
|
+
style?: StyleProp<ViewStyle>;
|
|
71
|
+
}
|
|
72
|
+
export declare function XPieChart({ data, height, donut, centerLabel, legend, style }: XPieChartProps): React.JSX.Element;
|
|
73
|
+
declare const _default: {
|
|
74
|
+
XLineChart: typeof XLineChart;
|
|
75
|
+
XBarChart: typeof XBarChart;
|
|
76
|
+
XPieChart: typeof XPieChart;
|
|
77
|
+
};
|
|
78
|
+
export default _default;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XChart —— 轻量图表三件套(react-native-svg 自研)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 【方案定位】用户选定方案:基于项目已有 react-native-svg 自研,
|
|
8
|
+
* 不引第三方图表库 —— 体积小、无原生依赖、token 与 XTheme 完全一致。
|
|
9
|
+
* 覆盖最常用三类:
|
|
10
|
+
* - XLineChart 折线/面积图(平滑曲线、网格线、数据点)
|
|
11
|
+
* - XBarChart 柱状图(圆角柱、逐柱配色、顶部数值)
|
|
12
|
+
* - XPieChart 饼图/环形图(donut、中心文案、图例)
|
|
13
|
+
*
|
|
14
|
+
* 【实现约定】
|
|
15
|
+
* - 宽度 onLayout 实测(不写死,适配容器),高度 props 指定;
|
|
16
|
+
* - 颜色默认取 useXTheme() token,可逐项覆盖;
|
|
17
|
+
* - 纯声明式渲染,数据变化即重绘(SVG 无动画库依赖,入场动画后续版本可加)。
|
|
18
|
+
* ============================================================================
|
|
19
|
+
*/
|
|
20
|
+
import React, { useCallback, useState } from 'react';
|
|
21
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
22
|
+
import Svg, { Circle, Defs, Line, LinearGradient, Path, Rect, Stop, Text as SvgText } from 'react-native-svg';
|
|
23
|
+
import { useXTheme } from '../theme';
|
|
24
|
+
/** 默认分类色板(主色打头,暗黑下同样可读) */
|
|
25
|
+
function palette(t) {
|
|
26
|
+
return [t.colorPrimary, t.colorSuccess, t.colorWarning, t.colorError, '#8C8CFF', '#36CFC9', '#FF9C6E', '#5CDBD3'];
|
|
27
|
+
}
|
|
28
|
+
/** 宽度自测量 Hook */
|
|
29
|
+
function useChartWidth() {
|
|
30
|
+
const [width, setWidth] = useState(0);
|
|
31
|
+
const onLayout = useCallback((e) => {
|
|
32
|
+
const w = e.nativeEvent.layout.width;
|
|
33
|
+
setWidth(prev => (prev === w ? prev : w));
|
|
34
|
+
}, []);
|
|
35
|
+
return { width, onLayout };
|
|
36
|
+
}
|
|
37
|
+
export function XLineChart({ data, labels, height = 220, color, area = false, smooth = true, grid = 4, showValues = false, style, }) {
|
|
38
|
+
const t = useXTheme();
|
|
39
|
+
const { width, onLayout } = useChartWidth();
|
|
40
|
+
const lineColor = color ?? t.colorPrimary;
|
|
41
|
+
const PAD = { l: 34, r: 12, tt: 14, b: 26 };
|
|
42
|
+
const innerW = Math.max(0, width - PAD.l - PAD.r);
|
|
43
|
+
const innerH = Math.max(0, height - PAD.tt - PAD.b);
|
|
44
|
+
const maxV = Math.max(...data, 1);
|
|
45
|
+
const n = data.length;
|
|
46
|
+
/** 数值 → 坐标 */
|
|
47
|
+
const pt = (i, v) => ({
|
|
48
|
+
x: n <= 1 ? PAD.l + innerW / 2 : PAD.l + (innerW * i) / (n - 1),
|
|
49
|
+
y: PAD.tt + innerH - (v / (maxV * 1.1)) * innerH,
|
|
50
|
+
});
|
|
51
|
+
const points = data.map((v, i) => pt(i, v));
|
|
52
|
+
/** 平滑路径:中点三次贝塞尔(视觉平滑且不过冲) */
|
|
53
|
+
const linePath = (() => {
|
|
54
|
+
if (points.length === 0)
|
|
55
|
+
return '';
|
|
56
|
+
const d = [`M ${points[0].x} ${points[0].y}`];
|
|
57
|
+
for (let i = 1; i < points.length; i++) {
|
|
58
|
+
const p0 = points[i - 1];
|
|
59
|
+
const p1 = points[i];
|
|
60
|
+
if (smooth) {
|
|
61
|
+
const mx = (p0.x + p1.x) / 2;
|
|
62
|
+
d.push(`C ${mx} ${p0.y}, ${mx} ${p1.y}, ${p1.x} ${p1.y}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
d.push(`L ${p1.x} ${p1.y}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return d.join(' ');
|
|
69
|
+
})();
|
|
70
|
+
const baselineY = PAD.tt + innerH;
|
|
71
|
+
const areaPath = points.length
|
|
72
|
+
? `${linePath} L ${points[points.length - 1].x} ${baselineY} L ${points[0].x} ${baselineY} Z`
|
|
73
|
+
: '';
|
|
74
|
+
const labelStep = Math.ceil((labels?.length ?? 0) / 6);
|
|
75
|
+
return (_jsx(View, { style: style, onLayout: onLayout, children: width > 0 && (_jsxs(Svg, { width: width, height: height, children: [area && (_jsx(Defs, { children: _jsxs(LinearGradient, { id: 'xLineArea', x1: '0', y1: '0', x2: '0', y2: '1', children: [_jsx(Stop, { offset: '0', stopColor: lineColor, stopOpacity: 0.25 }), _jsx(Stop, { offset: '1', stopColor: lineColor, stopOpacity: 0 })] }) })), Array.from({ length: grid + 1 }, (_, i) => {
|
|
76
|
+
const ratio = i / grid;
|
|
77
|
+
const y = PAD.tt + innerH * (1 - ratio);
|
|
78
|
+
const value = Math.round(maxV * 1.1 * ratio);
|
|
79
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Line, { x1: PAD.l, y1: y, x2: width - PAD.r, y2: y, stroke: t.colorSplit, strokeWidth: 1 }), _jsx(SvgText, { x: PAD.l - 6, y: y + 4, fontSize: 10, fill: t.colorTextTertiary, textAnchor: 'end', children: value })] }, i));
|
|
80
|
+
}), area && areaPath ? _jsx(Path, { d: areaPath, fill: 'url(#xLineArea)' }) : null, _jsx(Path, { d: linePath, fill: 'none', stroke: lineColor, strokeWidth: 2.5, strokeLinecap: 'round' }), points.map((p, i) => (_jsxs(React.Fragment, { children: [_jsx(Circle, { cx: p.x, cy: p.y, r: 3.5, fill: t.colorBgContainer, stroke: lineColor, strokeWidth: 2 }), showValues && (_jsx(SvgText, { x: p.x, y: p.y - 8, fontSize: 10, fill: t.colorTextSecondary, textAnchor: 'middle', children: data[i] }))] }, i))), labels?.map((label, i) => i % labelStep === 0 || i === labels.length - 1 ? (_jsx(SvgText, { x: points[i]?.x ?? 0, y: height - 8, fontSize: 10, fill: t.colorTextTertiary, textAnchor: 'middle', children: label }, i)) : null)] })) }));
|
|
81
|
+
}
|
|
82
|
+
export function XBarChart({ data, height = 220, barWidthRatio = 0.5, showValues = true, grid = 4, style }) {
|
|
83
|
+
const t = useXTheme();
|
|
84
|
+
const { width, onLayout } = useChartWidth();
|
|
85
|
+
const colors = palette(t);
|
|
86
|
+
const PAD = { l: 34, r: 12, tt: 20, b: 26 };
|
|
87
|
+
const innerW = Math.max(0, width - PAD.l - PAD.r);
|
|
88
|
+
const innerH = Math.max(0, height - PAD.tt - PAD.b);
|
|
89
|
+
const maxV = Math.max(...data.map(d => d.value), 1);
|
|
90
|
+
const band = data.length ? innerW / data.length : 0;
|
|
91
|
+
const barW = Math.min(band * barWidthRatio, 40);
|
|
92
|
+
return (_jsx(View, { style: style, onLayout: onLayout, children: width > 0 && (_jsxs(Svg, { width: width, height: height, children: [Array.from({ length: grid + 1 }, (_, i) => {
|
|
93
|
+
const ratio = i / grid;
|
|
94
|
+
const y = PAD.tt + innerH * (1 - ratio);
|
|
95
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Line, { x1: PAD.l, y1: y, x2: width - PAD.r, y2: y, stroke: t.colorSplit, strokeWidth: 1 }), _jsx(SvgText, { x: PAD.l - 6, y: y + 4, fontSize: 10, fill: t.colorTextTertiary, textAnchor: 'end', children: Math.round(maxV * 1.1 * ratio) })] }, i));
|
|
96
|
+
}), data.map((item, i) => {
|
|
97
|
+
const h = (item.value / (maxV * 1.1)) * innerH;
|
|
98
|
+
const x = PAD.l + band * i + (band - barW) / 2;
|
|
99
|
+
const y = PAD.tt + innerH - h;
|
|
100
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Rect, { x: x, y: y, width: barW, height: Math.max(h, 2), rx: 3, fill: item.color ?? t.colorPrimary }), showValues && (_jsx(SvgText, { x: x + barW / 2, y: y - 6, fontSize: 10, fill: t.colorTextSecondary, textAnchor: 'middle', children: item.value })), _jsx(SvgText, { x: x + barW / 2, y: height - 8, fontSize: 10, fill: t.colorTextTertiary, textAnchor: 'middle', children: item.label })] }, i));
|
|
101
|
+
})] })) }));
|
|
102
|
+
}
|
|
103
|
+
/** 极坐标 → 直角坐标 */
|
|
104
|
+
const polar = (cx, cy, r, deg) => {
|
|
105
|
+
const rad = ((deg - 90) * Math.PI) / 180;
|
|
106
|
+
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
|
|
107
|
+
};
|
|
108
|
+
/** 扇形路径(含 donut 内弧) */
|
|
109
|
+
function slicePath(cx, cy, rOuter, rInner, startDeg, endDeg) {
|
|
110
|
+
const large = endDeg - startDeg > 180 ? 1 : 0;
|
|
111
|
+
const p1 = polar(cx, cy, rOuter, startDeg);
|
|
112
|
+
const p2 = polar(cx, cy, rOuter, endDeg);
|
|
113
|
+
if (rInner <= 0) {
|
|
114
|
+
return `M ${cx} ${cy} L ${p1.x} ${p1.y} A ${rOuter} ${rOuter} 0 ${large} 1 ${p2.x} ${p2.y} Z`;
|
|
115
|
+
}
|
|
116
|
+
const p3 = polar(cx, cy, rInner, endDeg);
|
|
117
|
+
const p4 = polar(cx, cy, rInner, startDeg);
|
|
118
|
+
return `M ${p1.x} ${p1.y} A ${rOuter} ${rOuter} 0 ${large} 1 ${p2.x} ${p2.y} L ${p3.x} ${p3.y} A ${rInner} ${rInner} 0 ${large} 0 ${p4.x} ${p4.y} Z`;
|
|
119
|
+
}
|
|
120
|
+
export function XPieChart({ data, height = 220, donut = true, centerLabel, legend = true, style }) {
|
|
121
|
+
const t = useXTheme();
|
|
122
|
+
const { width, onLayout } = useChartWidth();
|
|
123
|
+
const colors = palette(t);
|
|
124
|
+
const total = data.reduce((s, d) => s + d.value, 0) || 1;
|
|
125
|
+
const size = Math.min(width || height, height) - 8;
|
|
126
|
+
const cx = (width || size) / 2;
|
|
127
|
+
const cy = height / 2;
|
|
128
|
+
const rOuter = size / 2;
|
|
129
|
+
const rInner = donut ? rOuter * 0.62 : 0;
|
|
130
|
+
/** 求各扇区起止角 */
|
|
131
|
+
let acc = 0;
|
|
132
|
+
const slices = data.map((item, i) => {
|
|
133
|
+
const startDeg = (acc / total) * 360;
|
|
134
|
+
acc += item.value;
|
|
135
|
+
const endDeg = (acc / total) * 360;
|
|
136
|
+
return { ...item, color: item.color ?? colors[i % colors.length], startDeg, endDeg };
|
|
137
|
+
});
|
|
138
|
+
return (_jsx(View, { style: style, onLayout: onLayout, children: width > 0 && (_jsxs(_Fragment, { children: [_jsxs(Svg, { width: width, height: height, children: [slices.map((s, i) => (_jsx(Path, { d: slicePath(cx, cy, rOuter, rInner, s.startDeg, s.endDeg), fill: s.color }, i))), donut && centerLabel ? (_jsx(SvgText, { x: cx, y: cy + 5, fontSize: 13, fontWeight: '600', fill: t.colorText, textAnchor: 'middle', children: centerLabel })) : null] }), legend && (_jsx(View, { style: styles.legend, children: slices.map((s, i) => (_jsxs(View, { style: styles.legendItem, children: [_jsx(View, { style: [styles.legendDot, { backgroundColor: s.color }] }), _jsxs(Text, { style: [styles.legendText, { color: t.colorTextSecondary }], children: [s.label, " ", Math.round((s.value / total) * 100), "%"] })] }, i))) }))] })) }));
|
|
139
|
+
}
|
|
140
|
+
const styles = StyleSheet.create({
|
|
141
|
+
legend: {
|
|
142
|
+
flexDirection: 'row',
|
|
143
|
+
flexWrap: 'wrap',
|
|
144
|
+
gap: 10,
|
|
145
|
+
justifyContent: 'center',
|
|
146
|
+
marginTop: 10,
|
|
147
|
+
},
|
|
148
|
+
legendItem: {
|
|
149
|
+
flexDirection: 'row',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
gap: 4,
|
|
152
|
+
},
|
|
153
|
+
legendDot: {
|
|
154
|
+
width: 8,
|
|
155
|
+
height: 8,
|
|
156
|
+
borderRadius: 4,
|
|
157
|
+
},
|
|
158
|
+
legendText: {
|
|
159
|
+
fontSize: 12,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
export default { XLineChart, XBarChart, XPieChart };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XCheckbox —— 对标 antd Checkbox(RN 自实现版)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 支持的 antd API:
|
|
7
|
+
* - <XCheckbox checked defaultChecked indeterminate disabled onChange value>
|
|
8
|
+
* - <XCheckbox.Group value defaultValue options onChange disabled>
|
|
9
|
+
* - value: 选中值数组;onChange(checkedValues: any[])
|
|
10
|
+
* - options: {label, value, disabled?}[]
|
|
11
|
+
*
|
|
12
|
+
* 与 antd 的差异:
|
|
13
|
+
* - onChange 的参数是轻量合成事件 {checked, value, target}(antd 是 DOM 事件包装)
|
|
14
|
+
* - 勾选用 View 边框旋转画出来的(L 形旋转 45°),不依赖图标字体
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
18
|
+
/** antd Checkbox onChange 事件对象的 RN 等价物 */
|
|
19
|
+
export interface XCheckboxChangeEvent {
|
|
20
|
+
checked: boolean;
|
|
21
|
+
value?: any;
|
|
22
|
+
target: {
|
|
23
|
+
checked: boolean;
|
|
24
|
+
value?: any;
|
|
25
|
+
};
|
|
26
|
+
stopPropagation: () => void;
|
|
27
|
+
}
|
|
28
|
+
export interface XCheckboxProps {
|
|
29
|
+
checked?: boolean;
|
|
30
|
+
defaultChecked?: boolean;
|
|
31
|
+
/** 半选态:显示横杠而不是勾(父级全选框常用) */
|
|
32
|
+
indeterminate?: boolean;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
/** Group 内用于标识自己的值;独立使用时只是透传给 onChange */
|
|
35
|
+
value?: any;
|
|
36
|
+
onChange?: (e: XCheckboxChangeEvent) => void;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
style?: StyleProp<ViewStyle>;
|
|
39
|
+
textStyle?: StyleProp<TextStyle>;
|
|
40
|
+
testID?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function XCheckbox({ checked, defaultChecked, indeterminate, disabled, value, onChange, children, style, textStyle, testID, }: XCheckboxProps): React.JSX.Element;
|
|
43
|
+
export declare namespace XCheckbox {
|
|
44
|
+
var Group: typeof XCheckboxGroup;
|
|
45
|
+
}
|
|
46
|
+
export interface XCheckboxGroupProps {
|
|
47
|
+
value?: any[];
|
|
48
|
+
defaultValue?: any[];
|
|
49
|
+
options?: Array<{
|
|
50
|
+
label: React.ReactNode;
|
|
51
|
+
value: any;
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
onChange?: (checkedValues: any[]) => void;
|
|
55
|
+
disabled?: boolean;
|
|
56
|
+
children?: React.ReactNode;
|
|
57
|
+
style?: StyleProp<ViewStyle>;
|
|
58
|
+
testID?: string;
|
|
59
|
+
}
|
|
60
|
+
declare function XCheckboxGroup(props: XCheckboxGroupProps): React.JSX.Element;
|
|
61
|
+
export default XCheckbox;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XCheckbox —— 对标 antd Checkbox(RN 自实现版)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 支持的 antd API:
|
|
8
|
+
* - <XCheckbox checked defaultChecked indeterminate disabled onChange value>
|
|
9
|
+
* - <XCheckbox.Group value defaultValue options onChange disabled>
|
|
10
|
+
* - value: 选中值数组;onChange(checkedValues: any[])
|
|
11
|
+
* - options: {label, value, disabled?}[]
|
|
12
|
+
*
|
|
13
|
+
* 与 antd 的差异:
|
|
14
|
+
* - onChange 的参数是轻量合成事件 {checked, value, target}(antd 是 DOM 事件包装)
|
|
15
|
+
* - 勾选用 View 边框旋转画出来的(L 形旋转 45°),不依赖图标字体
|
|
16
|
+
*/
|
|
17
|
+
import { createContext, useContext, useMemo, useState } from 'react';
|
|
18
|
+
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
|
19
|
+
import Svg, { Path } from 'react-native-svg';
|
|
20
|
+
import { useXTheme, xTheme } from '../theme';
|
|
21
|
+
const CheckboxGroupContext = createContext(null);
|
|
22
|
+
export function XCheckbox({ checked, defaultChecked, indeterminate = false, disabled, value, onChange, children, style, textStyle, testID, }) {
|
|
23
|
+
const t = useXTheme();
|
|
24
|
+
const group = useContext(CheckboxGroupContext);
|
|
25
|
+
const [innerChecked, setInnerChecked] = useState(!!defaultChecked);
|
|
26
|
+
const inGroupWithValue = group && value !== undefined;
|
|
27
|
+
const isChecked = inGroupWithValue ? group.isChecked(value) : checked ?? innerChecked;
|
|
28
|
+
const isDisabled = disabled || (group?.disabled ?? false);
|
|
29
|
+
const handlePress = () => {
|
|
30
|
+
if (isDisabled)
|
|
31
|
+
return;
|
|
32
|
+
const next = !isChecked;
|
|
33
|
+
if (inGroupWithValue) {
|
|
34
|
+
group.toggle(value);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
setInnerChecked(next);
|
|
38
|
+
}
|
|
39
|
+
onChange?.({
|
|
40
|
+
checked: next,
|
|
41
|
+
value,
|
|
42
|
+
target: { checked: next, value },
|
|
43
|
+
stopPropagation: () => { },
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs(Pressable, { testID: testID, onPress: handlePress, disabled: isDisabled, style: ({ pressed }) => [styles.row, pressed && !isDisabled && styles.pressed, style], children: [_jsx(XCheckboxIndicator, { checked: isChecked, indeterminate: indeterminate, disabled: isDisabled }), children != null && (_jsx(Text, { style: [styles.label, { color: isDisabled ? t.colorTextQuaternary : t.colorText }, textStyle], allowFontScaling: false, children: children }))] }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 方框指示器:20x20 圆角方框;选中画 SVG 勾(几何精确、居中且不裁切),
|
|
50
|
+
* 半选画主色横杠。
|
|
51
|
+
* 勾用 react-native-svg 的 Path 画,不用 View 旋转拼装的近似画法——
|
|
52
|
+
* 那种画法旋转后的视觉包围盒超出布局盒,会出现勾显示不全的问题。
|
|
53
|
+
*/
|
|
54
|
+
function XCheckboxIndicator({ checked, indeterminate, disabled }) {
|
|
55
|
+
const t = useXTheme();
|
|
56
|
+
const active = checked || indeterminate;
|
|
57
|
+
// 底色:选中/半选用主色(禁用时用禁用灰),否则透明白底
|
|
58
|
+
const bgColor = disabled
|
|
59
|
+
? active
|
|
60
|
+
? t.colorBgContainerDisabled
|
|
61
|
+
: t.colorBgContainer
|
|
62
|
+
: active
|
|
63
|
+
? t.colorPrimary
|
|
64
|
+
: t.colorBgContainer;
|
|
65
|
+
const borderColor = disabled ? t.colorBorder : active ? t.colorPrimary : t.colorBorder;
|
|
66
|
+
// 禁用时勾用灰色(白勾在禁用浅灰底上看不见)
|
|
67
|
+
const checkColor = disabled ? t.colorTextQuaternary : t.colorTextLightSolid;
|
|
68
|
+
return (_jsxs(View, { style: [styles.box, { backgroundColor: bgColor, borderColor }], children: [checked && !indeterminate && (_jsx(Svg, { width: 12, height: 12, viewBox: '0 0 12 12', children: _jsx(Path, { d: 'M2 6.5 L4.8 9.2 L10 3', stroke: checkColor, strokeWidth: 2, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round' }) })), indeterminate && !checked && _jsx(View, { style: [styles.dash, { backgroundColor: disabled ? t.colorTextQuaternary : t.colorPrimary }] })] }));
|
|
69
|
+
}
|
|
70
|
+
function XCheckboxGroup(props) {
|
|
71
|
+
const { value, defaultValue, options, onChange, disabled, children, style, testID } = props;
|
|
72
|
+
const [innerValue, setInnerValue] = useState(defaultValue ?? []);
|
|
73
|
+
const currentValue = value !== undefined ? value : innerValue;
|
|
74
|
+
const toggle = (v) => {
|
|
75
|
+
const next = currentValue.includes(v) ? currentValue.filter(item => item !== v) : [...currentValue, v];
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
setInnerValue(next);
|
|
78
|
+
}
|
|
79
|
+
onChange?.(next);
|
|
80
|
+
};
|
|
81
|
+
const ctx = useMemo(() => ({
|
|
82
|
+
value: currentValue,
|
|
83
|
+
disabled,
|
|
84
|
+
toggle,
|
|
85
|
+
isChecked: (v) => currentValue.includes(v),
|
|
86
|
+
}), [currentValue, disabled, value, onChange]);
|
|
87
|
+
return (_jsx(CheckboxGroupContext.Provider, { value: ctx, children: _jsx(View, { testID: testID, style: [styles.group, style], children: options
|
|
88
|
+
? options.map(opt => (_jsx(XCheckbox, { value: opt.value, disabled: opt.disabled, children: opt.label }, String(opt.value))))
|
|
89
|
+
: children }) }));
|
|
90
|
+
}
|
|
91
|
+
XCheckbox.Group = XCheckboxGroup;
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
row: {
|
|
94
|
+
flexDirection: 'row',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
paddingVertical: 8, // 扩大点击热区
|
|
97
|
+
alignSelf: 'flex-start',
|
|
98
|
+
},
|
|
99
|
+
pressed: {
|
|
100
|
+
opacity: 0.7,
|
|
101
|
+
},
|
|
102
|
+
box: {
|
|
103
|
+
width: 20,
|
|
104
|
+
height: 20,
|
|
105
|
+
borderRadius: 4,
|
|
106
|
+
borderWidth: 2,
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
justifyContent: 'center',
|
|
109
|
+
},
|
|
110
|
+
/** 半选横杠(antd 半选是主色短横线) */
|
|
111
|
+
dash: {
|
|
112
|
+
width: 10,
|
|
113
|
+
height: 2,
|
|
114
|
+
borderRadius: 1,
|
|
115
|
+
},
|
|
116
|
+
label: {
|
|
117
|
+
fontSize: xTheme.fontSize,
|
|
118
|
+
marginLeft: 8,
|
|
119
|
+
},
|
|
120
|
+
group: {
|
|
121
|
+
flexDirection: 'row',
|
|
122
|
+
flexWrap: 'wrap',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
rowGap: 4,
|
|
125
|
+
columnGap: 16,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
export default XCheckbox;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XConfirmForm —— 命令式确认框(duxui XConfirmForm 移植)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 【声明式 vs 命令式】
|
|
7
|
+
* XActionSheet/XPicker 是"声明式":页面里写 <xxx visible={...} />。
|
|
8
|
+
* XConfirmForm 是"命令式":不用管 visible,直接调
|
|
9
|
+
* const ok = await XConfirmForm.show({...})
|
|
10
|
+
* const ok = await confirm({...}) // 别名,等价
|
|
11
|
+
* 返回 Promise<boolean>:true=点了确定,false=取消/点遮罩。
|
|
12
|
+
*
|
|
13
|
+
* 【如何做到"不用挂载就能调"?】
|
|
14
|
+
* 1. XPopupProvider 在应用根部常驻渲染 <XConfirmForm />(一个实例);
|
|
15
|
+
* 2. 这个实例挂载时通过 XConfirmFormService._setHost({show, hide})
|
|
16
|
+
* 把自己"注册"到模块级变量 host;
|
|
17
|
+
* 3. XConfirmForm.show 内部实际调用 host.show —— 无论从哪个页面调用,
|
|
18
|
+
* 最终都落到根部那个常驻实例上。
|
|
19
|
+
* (LoadingModalProvider 用的就是同款套路,RN 没有原生 Portal,
|
|
20
|
+
* 这就是最常用的"伪 Portal"方案。)
|
|
21
|
+
*
|
|
22
|
+
* 【Promise 如何落地?】
|
|
23
|
+
* resolver 保存 Promise 的 resolve 函数:
|
|
24
|
+
* - show() 里 new Promise(resolve => resolver.current = resolve);
|
|
25
|
+
* - 用户点确定 → hide(true) → resolver(true) → Promise 兑现为 true;
|
|
26
|
+
* - 点取消/遮罩 → hide(false) → Promise 兑现为 false。
|
|
27
|
+
* 注意:同一时间只允许一个确认框(后开的会覆盖前一个)。
|
|
28
|
+
* ============================================================================
|
|
29
|
+
*/
|
|
30
|
+
import React from 'react';
|
|
31
|
+
/** show 时的配置 */
|
|
32
|
+
export interface XConfirmFormOptions {
|
|
33
|
+
title?: string;
|
|
34
|
+
/** 提示内容:字符串或任意 ReactNode(可以放表单组件) */
|
|
35
|
+
content?: React.ReactNode;
|
|
36
|
+
confirmText?: string;
|
|
37
|
+
cancelText?: string;
|
|
38
|
+
/** 危险操作:内容下方显示红色警示文案 */
|
|
39
|
+
danger?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** 宿主实例暴露给 Service 的接口 */
|
|
42
|
+
interface ConfirmFormHandle {
|
|
43
|
+
show: (options: XConfirmFormOptions) => Promise<boolean>;
|
|
44
|
+
hide: (result?: boolean) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* XConfirmFormComponent —— 常驻的确认框实例
|
|
48
|
+
* 本身也是一个"受控的 XModalForm":visible 由内部状态控制。
|
|
49
|
+
*/
|
|
50
|
+
export declare const XConfirmFormComponent: () => React.JSX.Element;
|
|
51
|
+
export declare const XConfirmFormService: {
|
|
52
|
+
/** 由 XConfirmFormComponent 挂载/卸载时调用 */
|
|
53
|
+
_setHost: (h: ConfirmFormHandle | null) => void;
|
|
54
|
+
show: (options: XConfirmFormOptions) => Promise<boolean>;
|
|
55
|
+
hide: () => void | undefined;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* 对外导出的"组件 + 静态方法":
|
|
59
|
+
* - <XConfirmForm />(XPopupProvider 里用的就是它)
|
|
60
|
+
* - XConfirmForm.show({...}) / XConfirmForm.hide()
|
|
61
|
+
* Object.assign 把静态方法挂到函数组件上。
|
|
62
|
+
*/
|
|
63
|
+
export declare const XConfirmForm: (() => React.JSX.Element) & {
|
|
64
|
+
show: (options: XConfirmFormOptions) => Promise<boolean>;
|
|
65
|
+
hide: () => void | undefined;
|
|
66
|
+
};
|
|
67
|
+
/** 保留 duxui 的 confirm 命名风格:confirm({...}) 等价 XConfirmForm.show({...}) */
|
|
68
|
+
export declare const confirm: (options: XConfirmFormOptions) => Promise<boolean>;
|
|
69
|
+
export {};
|