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,227 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XAnimatedView —— 通用动画视图(duxapp Animated.View 的 RN 移植版)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 【它是干什么的?】
|
|
8
|
+
* XPullView 内部直接用了 useSharedValue + withTiming 写动画;而这里提供的
|
|
9
|
+
* XAnimatedView + anim() 是更通用的"动画引擎",可以给任意自定义组件用,
|
|
10
|
+
* 用法和 duxapp 的 Animated.create() 完全一致(链式声明多段动画)。
|
|
11
|
+
*
|
|
12
|
+
* 【动画声明格式】
|
|
13
|
+
* anim(200, 'ease-out') // 默认时长 200ms、缓动 ease-out
|
|
14
|
+
* .translateY(300) // 第 1 步:Y 移到 300(屏外)
|
|
15
|
+
* .opacity(0) // 同时透明
|
|
16
|
+
* .step() // 把上面这两个"动作"打包成一段
|
|
17
|
+
* .translateY(0) // 第 2 步:Y 移回 0
|
|
18
|
+
* .opacity(1) // 同时不透明
|
|
19
|
+
* .step() // 打包成第二段
|
|
20
|
+
* .export() // 输出 XAnimStep[] 交给 XAnimatedView
|
|
21
|
+
*
|
|
22
|
+
* 【多段动画如何播放?】
|
|
23
|
+
* 用一个 0→N 的 progress(withSequence 顺序播放 N 段 withTiming),
|
|
24
|
+
* 每段把 progress 推向 1、2、3……N。useAnimatedStyle 里用 interpolate
|
|
25
|
+
* 把 progress 映射成每个属性的具体值(inputRange: [0,1,2..N])。
|
|
26
|
+
*
|
|
27
|
+
* 【为什么动画在 UI 线程?】
|
|
28
|
+
* useAnimatedStyle 的函数会被 Reanimated 编译成 worklet,运行在 UI 线程。
|
|
29
|
+
* JS 线程只需要发一次"开始动画"的指令,之后每一帧的插值计算
|
|
30
|
+
* 都在 UI 线程完成 → 即使 JS 线程忙(网络请求、列表渲染),动画也不掉帧。
|
|
31
|
+
* ============================================================================
|
|
32
|
+
*/
|
|
33
|
+
import { useEffect, useMemo } from 'react';
|
|
34
|
+
import { StyleSheet } from 'react-native';
|
|
35
|
+
import Animated, { Easing, cancelAnimation, interpolate, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming, } from 'react-native-reanimated';
|
|
36
|
+
/** 缓动函数表:字符串名 → Reanimated Easing 函数 */
|
|
37
|
+
const EASINGS = {
|
|
38
|
+
linear: Easing.linear, // 匀速
|
|
39
|
+
ease: Easing.ease, // 默认缓动
|
|
40
|
+
'ease-in': Easing.in(Easing.ease), // 慢→快
|
|
41
|
+
'ease-out': Easing.out(Easing.ease), // 快→慢(入场常用,起步跟手)
|
|
42
|
+
'ease-in-out': Easing.inOut(Easing.ease), // 慢→快→慢(过渡常用)
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* 动画构建器(链式调用)
|
|
46
|
+
*
|
|
47
|
+
* 实现思路:
|
|
48
|
+
* - `current` 暂存当前累积的动作({translateY: 300, opacity: 0});
|
|
49
|
+
* - `.step()` 把 current 打包成 {option, action} 推入 steps,然后清空 current;
|
|
50
|
+
* - `.export()` 把最后一段也打包并返回 steps 数组。
|
|
51
|
+
*/
|
|
52
|
+
export function anim(duration = 200, timingFunction = 'ease-in-out', delay = 0) {
|
|
53
|
+
const steps = [];
|
|
54
|
+
let current = {};
|
|
55
|
+
const api = {
|
|
56
|
+
step(option) {
|
|
57
|
+
// 只有 current 里真的有动作时才打包(避免空 step)
|
|
58
|
+
if (Object.keys(current).length) {
|
|
59
|
+
steps.push({
|
|
60
|
+
option: { duration, timingFunction, delay, ...option },
|
|
61
|
+
action: current,
|
|
62
|
+
});
|
|
63
|
+
current = {};
|
|
64
|
+
}
|
|
65
|
+
return api;
|
|
66
|
+
},
|
|
67
|
+
set(name, value) {
|
|
68
|
+
current[name] = value;
|
|
69
|
+
return api;
|
|
70
|
+
},
|
|
71
|
+
translateY(v) {
|
|
72
|
+
return api.set('translateY', v);
|
|
73
|
+
},
|
|
74
|
+
translateX(v) {
|
|
75
|
+
return api.set('translateX', v);
|
|
76
|
+
},
|
|
77
|
+
scale(v) {
|
|
78
|
+
return api.set('scale', v);
|
|
79
|
+
},
|
|
80
|
+
opacity(v) {
|
|
81
|
+
return api.set('opacity', v);
|
|
82
|
+
},
|
|
83
|
+
rotate(v) {
|
|
84
|
+
return api.set('rotate', v);
|
|
85
|
+
},
|
|
86
|
+
export() {
|
|
87
|
+
api.step();
|
|
88
|
+
return steps;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return api;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 推导动画起点:
|
|
95
|
+
* 组件 style 里写的是什么,动画就从哪开始。
|
|
96
|
+
* 比如 style={{transform: [{translateY: 300}], opacity: 0}},
|
|
97
|
+
* 那第一帧就停在这个隐藏态,动画再插值到目标值 —— 这就是"不闪现"的秘诀。
|
|
98
|
+
* 如果没有显式初始值:opacity/scale 默认 1,其余默认 0。
|
|
99
|
+
*/
|
|
100
|
+
const getStartValue = (type, style) => {
|
|
101
|
+
const flat = StyleSheet.flatten(style);
|
|
102
|
+
// 1. 直接写在 style 上的属性(如 opacity)
|
|
103
|
+
if (typeof flat?.[type] === 'number') {
|
|
104
|
+
return flat[type];
|
|
105
|
+
}
|
|
106
|
+
// 2. 写在 transform 数组里的属性(如 translateY)
|
|
107
|
+
if (Array.isArray(flat?.transform)) {
|
|
108
|
+
const item = flat.transform.find((v) => v && typeof v === 'object' && type in v);
|
|
109
|
+
if (item && typeof item[type] === 'number') {
|
|
110
|
+
return item[type];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// 3. 兜底默认值
|
|
114
|
+
if (type === 'opacity' || type.startsWith('scale')) {
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
};
|
|
119
|
+
export const XAnimatedView = ({ animation = [], style, children, ...props }) => {
|
|
120
|
+
/** progress:0 → 1 → 2 … → N,驱动所有属性的插值 */
|
|
121
|
+
const progress = useSharedValue(0);
|
|
122
|
+
/**
|
|
123
|
+
* 把 animation 序列化成字符串作为依赖:
|
|
124
|
+
* 数组每次渲染都是新引用,直接拿它当依赖会导致动画重复播放;
|
|
125
|
+
* 序列化后只有内容真的变化才重播。
|
|
126
|
+
*/
|
|
127
|
+
const animationKey = useMemo(() => JSON.stringify(animation), [animation]);
|
|
128
|
+
/**
|
|
129
|
+
* 播放动画:把 progress 从 0 按顺序推进到 1..N
|
|
130
|
+
*/
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
// 先取消上一次动画、归零,保证重复播放时从起点开始
|
|
133
|
+
cancelAnimation(progress);
|
|
134
|
+
progress.value = 0;
|
|
135
|
+
if (!animation.length) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// 每段动画 = 一个 withTiming:progress 推进到 index+1
|
|
139
|
+
const steps = animation.map((step, index) => {
|
|
140
|
+
const opt = step.option ?? {};
|
|
141
|
+
const timing = withTiming(index + 1, {
|
|
142
|
+
duration: opt.duration ?? 200,
|
|
143
|
+
easing: EASINGS[opt.timingFunction ?? 'linear'] ?? Easing.linear,
|
|
144
|
+
});
|
|
145
|
+
// 有 delay 就包一层 withDelay
|
|
146
|
+
return opt.delay ? withDelay(opt.delay, timing) : timing;
|
|
147
|
+
});
|
|
148
|
+
// withSequence:按顺序播放所有段(前一段完再播下一段)
|
|
149
|
+
progress.value = withSequence(...steps);
|
|
150
|
+
// 卸载时取消动画
|
|
151
|
+
return () => cancelAnimation(progress);
|
|
152
|
+
}, [animationKey, progress]);
|
|
153
|
+
/**
|
|
154
|
+
* 【关键】在 JS 线程预先计算好全部插值数据:
|
|
155
|
+
* worklet(useAnimatedStyle)运行在 UI 线程,里面不能调用任何
|
|
156
|
+
* 非 worklet 的 JS 函数,否则会抛
|
|
157
|
+
* "[Worklets] Tried to synchronously call a RemoteFunction"。
|
|
158
|
+
* 所以 getStartValue / 属性收集 / outputRange 推导全部在这里做完,
|
|
159
|
+
* worklet 里只留纯 interpolate 数值计算。
|
|
160
|
+
*/
|
|
161
|
+
const animData = useMemo(() => {
|
|
162
|
+
if (!animation.length) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
// 收集所有段里出现过的属性类型(去重)
|
|
166
|
+
const typeSet = [];
|
|
167
|
+
animation.forEach(step => {
|
|
168
|
+
Object.keys(step.action).forEach(type => {
|
|
169
|
+
if (!typeSet.includes(type)) {
|
|
170
|
+
typeSet.push(type);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
if (!typeSet.length) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
// inputRange:progress 的取值点 [0, 1, 2, ..., N]
|
|
178
|
+
const inputRange = [0, ...animation.map((_, index) => index + 1)];
|
|
179
|
+
const entries = typeSet.map(type => {
|
|
180
|
+
// outputRange:起点(style 里的初始值)→ 每段的目标值
|
|
181
|
+
const outputRange = [getStartValue(type, style)];
|
|
182
|
+
animation.forEach(step => {
|
|
183
|
+
// 某一段没写这个属性就用上一段的值(保持不动)
|
|
184
|
+
outputRange.push(step.action[type] ?? outputRange[outputRange.length - 1]);
|
|
185
|
+
});
|
|
186
|
+
return { type, outputRange };
|
|
187
|
+
});
|
|
188
|
+
return { inputRange, entries };
|
|
189
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
|
+
}, [animationKey]);
|
|
191
|
+
/**
|
|
192
|
+
* 把 progress 映射成样式(worklet,UI 线程执行):
|
|
193
|
+
* 只做纯数值插值,不调用任何外部 JS 函数。
|
|
194
|
+
*/
|
|
195
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
196
|
+
if (!animData) {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
const res = {};
|
|
200
|
+
const transform = [];
|
|
201
|
+
const { inputRange, entries } = animData;
|
|
202
|
+
for (let i = 0; i < entries.length; i++) {
|
|
203
|
+
const { type, outputRange } = entries[i];
|
|
204
|
+
// interpolate:把 progress 映射到 outputRange 区间
|
|
205
|
+
const value = interpolate(progress.value, inputRange, outputRange);
|
|
206
|
+
const isTransform = type.startsWith('translate') || type.startsWith('scale') || type.startsWith('rotate');
|
|
207
|
+
if (isTransform) {
|
|
208
|
+
// transform 属性:角度类要拼 'deg' 后缀
|
|
209
|
+
if (type.startsWith('rotate')) {
|
|
210
|
+
transform.push({ [type]: `${value}deg` });
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
transform.push({ [type]: value });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// 非 transform 属性(如 opacity)直接放根级
|
|
218
|
+
res[type] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (transform.length) {
|
|
222
|
+
res.transform = transform;
|
|
223
|
+
}
|
|
224
|
+
return res;
|
|
225
|
+
});
|
|
226
|
+
return (_jsx(Animated.View, { style: [style, animatedStyle], ...props, children: children }));
|
|
227
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XButton —— 对标 antd Button(RN 自实现版)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 支持的 antd API:
|
|
7
|
+
* - type: primary | default | ghost | dashed | link | text
|
|
8
|
+
* - danger: 语义色变为错误红
|
|
9
|
+
* - size: large | middle | small
|
|
10
|
+
* - block: 撑满父容器宽度
|
|
11
|
+
* - loading: 加载态(自动禁用 + 显示转圈)
|
|
12
|
+
* - icon: 前置图标节点
|
|
13
|
+
* - shape: default | round | circle(circle 一般配合 icon-only 使用)
|
|
14
|
+
* - onPress / disabled / style / textStyle
|
|
15
|
+
*
|
|
16
|
+
* 与 antd 的差异(RN 环境限制,有意为之):
|
|
17
|
+
* - dashed 在 Android 上圆角+虚线组合会退化为实线(RN 已知行为),视觉上仍保留虚线
|
|
18
|
+
* - 无 hover 态,按压反馈统一用透明度变化(对标 antd 的 active 态)
|
|
19
|
+
*/
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import { TextStyle, ViewStyle, StyleProp } from 'react-native';
|
|
22
|
+
export type XButtonType = 'primary' | 'default' | 'ghost' | 'dashed' | 'link' | 'text';
|
|
23
|
+
export type XButtonSize = 'large' | 'middle' | 'small';
|
|
24
|
+
export type XButtonShape = 'default' | 'round' | 'circle';
|
|
25
|
+
export interface XButtonProps {
|
|
26
|
+
type?: XButtonType;
|
|
27
|
+
size?: XButtonSize;
|
|
28
|
+
danger?: boolean;
|
|
29
|
+
block?: boolean;
|
|
30
|
+
loading?: boolean;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
icon?: React.ReactNode;
|
|
33
|
+
shape?: XButtonShape;
|
|
34
|
+
onPress?: () => void;
|
|
35
|
+
onPressIn?: () => void;
|
|
36
|
+
onPressOut?: () => void;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
style?: StyleProp<ViewStyle>;
|
|
39
|
+
/** 覆盖文字样式(一般只用来调字号/加粗,颜色跟随 type 自动算) */
|
|
40
|
+
textStyle?: StyleProp<TextStyle>;
|
|
41
|
+
testID?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function XButton({ type, size, danger, block, loading, disabled, icon, shape, onPress, onPressIn, onPressOut, children, style, textStyle, testID, }: XButtonProps): React.JSX.Element;
|
|
44
|
+
export default XButton;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useXTheme, xTheme } from '../theme';
|
|
4
|
+
/** 尺寸相关样式:高度、左右内边距、字号、圆角基准 */
|
|
5
|
+
function useSizeStyles(size, shape) {
|
|
6
|
+
switch (size) {
|
|
7
|
+
case 'large':
|
|
8
|
+
return {
|
|
9
|
+
height: xTheme.controlHeightLG,
|
|
10
|
+
paddingHorizontal: shape === 'circle' ? 0 : 18,
|
|
11
|
+
fontSize: xTheme.fontSizeLG,
|
|
12
|
+
radius: shape === 'round' || shape === 'circle' ? xTheme.controlHeightLG / 2 : xTheme.borderRadius,
|
|
13
|
+
};
|
|
14
|
+
case 'small':
|
|
15
|
+
return {
|
|
16
|
+
height: xTheme.controlHeightSM,
|
|
17
|
+
paddingHorizontal: shape === 'circle' ? 0 : 10,
|
|
18
|
+
fontSize: xTheme.fontSizeSM,
|
|
19
|
+
radius: shape === 'round' || shape === 'circle' ? xTheme.controlHeightSM / 2 : xTheme.borderRadius,
|
|
20
|
+
};
|
|
21
|
+
default:
|
|
22
|
+
return {
|
|
23
|
+
height: xTheme.controlHeight,
|
|
24
|
+
paddingHorizontal: shape === 'circle' ? 0 : 14,
|
|
25
|
+
fontSize: xTheme.fontSize,
|
|
26
|
+
radius: shape === 'round' || shape === 'circle' ? xTheme.controlHeight / 2 : xTheme.borderRadius,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function XButton({ type = 'default', size = 'middle', danger = false, block = false, loading = false, disabled = false, icon, shape = 'default', onPress, onPressIn, onPressOut, children, style, textStyle, testID, }) {
|
|
31
|
+
const t = useXTheme();
|
|
32
|
+
const s = useSizeStyles(size, shape);
|
|
33
|
+
// 语义基准色:danger 用错误红,否则用主色
|
|
34
|
+
const baseColor = danger ? t.colorError : t.colorPrimary;
|
|
35
|
+
const isFilled = type === 'primary'; // 只有 primary 是实底
|
|
36
|
+
const isTextLike = type === 'link' || type === 'text'; // 无边框无底
|
|
37
|
+
const isGhostOrDashed = type === 'ghost' || type === 'dashed'; // 透明底 + 描边
|
|
38
|
+
// ---- 按 type × disabled 组合算出底色 / 文字色 / 边框色 ----
|
|
39
|
+
let backgroundColor = t.colorBgContainer;
|
|
40
|
+
let textColor = t.colorText;
|
|
41
|
+
let borderColor = t.colorBorder;
|
|
42
|
+
if (isFilled) {
|
|
43
|
+
backgroundColor = baseColor;
|
|
44
|
+
textColor = t.colorTextLightSolid;
|
|
45
|
+
borderColor = undefined;
|
|
46
|
+
}
|
|
47
|
+
else if (isGhostOrDashed) {
|
|
48
|
+
backgroundColor = 'transparent';
|
|
49
|
+
textColor = baseColor;
|
|
50
|
+
borderColor = baseColor;
|
|
51
|
+
}
|
|
52
|
+
else if (isTextLike) {
|
|
53
|
+
backgroundColor = 'transparent';
|
|
54
|
+
textColor = baseColor;
|
|
55
|
+
borderColor = undefined;
|
|
56
|
+
}
|
|
57
|
+
// 禁用/加载态统一压成灰(antd 规则:禁用后不区分 danger/type,统一样式)
|
|
58
|
+
const inert = disabled || loading;
|
|
59
|
+
if (inert) {
|
|
60
|
+
if (isFilled) {
|
|
61
|
+
backgroundColor = t.colorPrimaryDisabled;
|
|
62
|
+
textColor = t.colorTextLightSolid;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
backgroundColor = isTextLike ? 'transparent' : t.colorBgContainerDisabled;
|
|
66
|
+
textColor = t.colorTextQuaternary;
|
|
67
|
+
borderColor = isTextLike ? undefined : t.colorBorder;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// link 类型下划线:用 Text 的 textDecorationLine 实现
|
|
71
|
+
const underline = type === 'link' && !inert;
|
|
72
|
+
/**
|
|
73
|
+
* 纯文本 children 判定:字符串 / 数字 / 纯字符串数组都算。
|
|
74
|
+
* JSX 里写 `确定{cond ? '(2)' : ''}` 会生成字符串数组——若不包 <Text>
|
|
75
|
+
* 直接丢给 Pressable(View),RN 会报
|
|
76
|
+
* "Text strings must be rendered within a <Text> component"。
|
|
77
|
+
*/
|
|
78
|
+
const isPlainChildren = typeof children === 'string' ||
|
|
79
|
+
typeof children === 'number' ||
|
|
80
|
+
(Array.isArray(children) && children.every(c => typeof c === 'string' || typeof c === 'number'));
|
|
81
|
+
const content = (_jsxs(_Fragment, { children: [loading && (_jsx(ActivityIndicator, { size: 'small', color: isFilled ? t.colorTextLightSolid : baseColor, style: styles.spinner })), !loading && icon != null && _jsx(View, { style: styles.iconWrap, children: icon }), isPlainChildren ? (_jsx(Text, { style: [
|
|
82
|
+
{ fontSize: s.fontSize, color: textColor, fontWeight: isFilled ? '600' : '400' },
|
|
83
|
+
underline && { textDecorationLine: 'underline' },
|
|
84
|
+
textStyle,
|
|
85
|
+
], allowFontScaling: false, children: children })) : (children)] }));
|
|
86
|
+
const borderRadius = shape === 'circle' ? s.height / 2 : s.radius;
|
|
87
|
+
return (_jsx(Pressable, { testID: testID, disabled: inert, onPress: onPress, onPressIn: onPressIn, onPressOut: onPressOut, style: ({ pressed }) => [
|
|
88
|
+
styles.base,
|
|
89
|
+
{
|
|
90
|
+
height: s.height,
|
|
91
|
+
paddingHorizontal: s.paddingHorizontal,
|
|
92
|
+
borderRadius,
|
|
93
|
+
backgroundColor,
|
|
94
|
+
borderColor,
|
|
95
|
+
borderWidth: isTextLike ? 0 : StyleSheet.hairlineWidth * 2,
|
|
96
|
+
},
|
|
97
|
+
type === 'dashed' && !inert && { borderStyle: 'dashed' },
|
|
98
|
+
block && styles.block,
|
|
99
|
+
pressed && !inert && styles.pressed,
|
|
100
|
+
style,
|
|
101
|
+
], children: content }));
|
|
102
|
+
}
|
|
103
|
+
const styles = StyleSheet.create({
|
|
104
|
+
base: {
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
alignSelf: 'flex-start',
|
|
109
|
+
overflow: 'hidden',
|
|
110
|
+
},
|
|
111
|
+
block: {
|
|
112
|
+
alignSelf: 'stretch',
|
|
113
|
+
},
|
|
114
|
+
pressed: {
|
|
115
|
+
opacity: 0.75,
|
|
116
|
+
},
|
|
117
|
+
spinner: {
|
|
118
|
+
marginRight: 6,
|
|
119
|
+
},
|
|
120
|
+
iconWrap: {
|
|
121
|
+
marginRight: 6,
|
|
122
|
+
justifyContent: 'center',
|
|
123
|
+
alignItems: 'center',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
export default XButton;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XCalendarPopup —— 弹窗日历(XCalendar + XPullView 组合)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 【定位】duxui 官方没有独立弹窗日历(把 Calendar 塞 PullView 用),
|
|
7
|
+
* 这里做成正式组件:
|
|
8
|
+
* - 底部弹出(XPullView,与项目弹窗族同款动画/遮罩);
|
|
9
|
+
* - 顶部工具栏「取消 | 标题 | 确定」与本项目 XPickerDate 完全同款布局,
|
|
10
|
+
* 颜色全部走 useXTheme(暗黑自适应);
|
|
11
|
+
* - 打开瞬间把外部 value 复制进草稿(XPickerDate 的 prevVisibleRef 套路),
|
|
12
|
+
* 点确定才 onConfirm,点取消/遮罩丢弃草稿,符合选择器交互惯例。
|
|
13
|
+
* ============================================================================
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { XCalendarMode, XCalendarValue } from './index';
|
|
17
|
+
export interface XCalendarPopupProps {
|
|
18
|
+
visible: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
/** 点确定:回传草稿值 */
|
|
21
|
+
onConfirm?: (value: XCalendarValue) => void;
|
|
22
|
+
/** 当前值(每次打开同步进草稿) */
|
|
23
|
+
value?: XCalendarValue;
|
|
24
|
+
/** 传给 XCalendar 的模式 */
|
|
25
|
+
mode?: XCalendarMode;
|
|
26
|
+
/** day 模式多选 */
|
|
27
|
+
multiple?: boolean;
|
|
28
|
+
/** 面板标题,默认"请选择日期" */
|
|
29
|
+
title?: string;
|
|
30
|
+
min?: string;
|
|
31
|
+
max?: string;
|
|
32
|
+
disabledDate?: (date: import('dayjs').Dayjs) => boolean;
|
|
33
|
+
weekStartsOn?: 0 | 1;
|
|
34
|
+
onMonthChange?: (year: number, month: number) => void;
|
|
35
|
+
duration?: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function XCalendarPopup({ visible, onClose, onConfirm, value, mode, multiple, title, min, max, disabledDate, weekStartsOn, onMonthChange, duration, }: XCalendarPopupProps): React.JSX.Element;
|
|
38
|
+
export default XCalendarPopup;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XCalendarPopup —— 弹窗日历(XCalendar + XPullView 组合)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* 【定位】duxui 官方没有独立弹窗日历(把 Calendar 塞 PullView 用),
|
|
8
|
+
* 这里做成正式组件:
|
|
9
|
+
* - 底部弹出(XPullView,与项目弹窗族同款动画/遮罩);
|
|
10
|
+
* - 顶部工具栏「取消 | 标题 | 确定」与本项目 XPickerDate 完全同款布局,
|
|
11
|
+
* 颜色全部走 useXTheme(暗黑自适应);
|
|
12
|
+
* - 打开瞬间把外部 value 复制进草稿(XPickerDate 的 prevVisibleRef 套路),
|
|
13
|
+
* 点确定才 onConfirm,点取消/遮罩丢弃草稿,符合选择器交互惯例。
|
|
14
|
+
* ============================================================================
|
|
15
|
+
*/
|
|
16
|
+
import { useCallback, useRef, useState } from 'react';
|
|
17
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
18
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
19
|
+
import { XPullView } from '../XPullView';
|
|
20
|
+
import { useXTheme } from '../theme';
|
|
21
|
+
import { useXLocale } from '../XLocale';
|
|
22
|
+
import { XCalendar } from './index';
|
|
23
|
+
export function XCalendarPopup({ visible, onClose, onConfirm, value, mode = 'day', multiple = false, title, min, max, disabledDate, weekStartsOn = 1, onMonthChange, duration = 200, }) {
|
|
24
|
+
const t = useXTheme();
|
|
25
|
+
const { t: i18n } = useXLocale();
|
|
26
|
+
const insets = useSafeAreaInsets();
|
|
27
|
+
/** 草稿:点确定前的一切选择都在这里,取消即丢弃 */
|
|
28
|
+
const [draft, setDraft] = useState(value);
|
|
29
|
+
const prevVisibleRef = useRef(visible);
|
|
30
|
+
/** 打开瞬间同步草稿(渲染期修正,与 XPickerDate 同套路) */
|
|
31
|
+
if (visible !== prevVisibleRef.current) {
|
|
32
|
+
prevVisibleRef.current = visible;
|
|
33
|
+
if (visible)
|
|
34
|
+
setDraft(value);
|
|
35
|
+
}
|
|
36
|
+
/** 确定:回传草稿并关闭 */
|
|
37
|
+
const handleConfirm = useCallback(() => {
|
|
38
|
+
onConfirm?.(draft ?? (multiple ? [] : mode === 'day' ? '' : ['', '']));
|
|
39
|
+
onClose();
|
|
40
|
+
}, [draft, multiple, mode, onConfirm, onClose]);
|
|
41
|
+
return (_jsx(XPullView, { visible: visible, onClose: onClose, side: 'bottom', duration: duration, overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.panel, { backgroundColor: t.colorBgContainer, paddingBottom: Math.max(insets.bottom, 10) }], children: [_jsxs(View, { style: [styles.toolbar, { borderBottomColor: t.colorSplit }], children: [_jsx(Pressable, { onPress: onClose, hitSlop: 8, children: _jsx(Text, { style: [styles.cancelBtn, { color: t.colorTextSecondary }], children: i18n('cancel') }) }), _jsx(Text, { style: [styles.title, { color: t.colorText }], numberOfLines: 1, children: title ?? i18n('selectDate') }), _jsx(Pressable, { onPress: handleConfirm, hitSlop: 8, children: _jsx(Text, { style: [styles.confirmBtn, { color: t.colorPrimary }], children: i18n('confirm') }) })] }), _jsx(View, { style: styles.calendarWrap, children: _jsx(XCalendar, { mode: mode, multiple: multiple, value: draft, onChange: setDraft, min: min, max: max, disabledDate: disabledDate, weekStartsOn: weekStartsOn, onMonthChange: onMonthChange }) })] }) }));
|
|
42
|
+
}
|
|
43
|
+
const styles = StyleSheet.create({
|
|
44
|
+
panel: {
|
|
45
|
+
borderTopLeftRadius: 12,
|
|
46
|
+
borderTopRightRadius: 12,
|
|
47
|
+
overflow: 'hidden',
|
|
48
|
+
},
|
|
49
|
+
toolbar: {
|
|
50
|
+
flexDirection: 'row',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
justifyContent: 'space-between',
|
|
53
|
+
paddingHorizontal: 16,
|
|
54
|
+
height: 48,
|
|
55
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
56
|
+
},
|
|
57
|
+
cancelBtn: {
|
|
58
|
+
fontSize: 15,
|
|
59
|
+
},
|
|
60
|
+
title: {
|
|
61
|
+
fontSize: 16,
|
|
62
|
+
fontWeight: '600',
|
|
63
|
+
flex: 1,
|
|
64
|
+
textAlign: 'center',
|
|
65
|
+
marginHorizontal: 8,
|
|
66
|
+
},
|
|
67
|
+
confirmBtn: {
|
|
68
|
+
fontSize: 15,
|
|
69
|
+
fontWeight: '600',
|
|
70
|
+
},
|
|
71
|
+
calendarWrap: {
|
|
72
|
+
paddingHorizontal: 12,
|
|
73
|
+
paddingTop: 4,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
export default XCalendarPopup;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XCalendar —— 日历(duxui Calendar 的 TS + XTheme 重写版)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 【与 duxui Calendar 的关系】
|
|
7
|
+
* 逻辑照抄 duxui:纯 View 网格自绘(不依赖任何日历库)、单选/周选/范围
|
|
8
|
+
* 三种模式统一按"范围"处理、min/max + disabledDate 拦截。
|
|
9
|
+
* 重写点:JSX → TS、静态色值 → useXTheme() token、文案 → i18n(中英)、
|
|
10
|
+
* 周起始默认周一(duxui 同款),多选支持 antd 式 multiple。
|
|
11
|
+
*
|
|
12
|
+
* 【模式与取值】
|
|
13
|
+
* mode='day' value: 'YYYY-MM-DD' (multiple 时: string[])
|
|
14
|
+
* mode='week' value: ['YYYY-MM-DD','YYYY-MM-DD'] 所在周一起始
|
|
15
|
+
* mode='scope' value: ['YYYY-MM-DD','YYYY-MM-DD'] 任意范围
|
|
16
|
+
*
|
|
17
|
+
* 【弹窗用法】配合 XCalendarPopup(底部 XPullView + 取消/确定工具栏,
|
|
18
|
+
* 样式对齐本项目 XPickerDate)。
|
|
19
|
+
* ============================================================================
|
|
20
|
+
*/
|
|
21
|
+
import React from 'react';
|
|
22
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
23
|
+
import { Dayjs } from 'dayjs';
|
|
24
|
+
export type XCalendarMode = 'day' | 'week' | 'scope';
|
|
25
|
+
/** day 模式单选 = string;multiple/scope/week = string[](scope/week 恒为 [start,end]) */
|
|
26
|
+
export type XCalendarValue = string | string[];
|
|
27
|
+
export interface XCalendarProps {
|
|
28
|
+
/** day 单选 / week 整周 / scope 任意范围,默认 day */
|
|
29
|
+
mode?: XCalendarMode;
|
|
30
|
+
/** 受控值(建议传组件输出的同格式值) */
|
|
31
|
+
value?: XCalendarValue;
|
|
32
|
+
/** 默认值(非受控) */
|
|
33
|
+
defaultValue?: XCalendarValue;
|
|
34
|
+
/** day 模式多选 */
|
|
35
|
+
multiple?: boolean;
|
|
36
|
+
onChange?: (value: XCalendarValue) => void;
|
|
37
|
+
/** 最小日期 'YYYY-MM-DD' */
|
|
38
|
+
min?: string;
|
|
39
|
+
/** 最大日期 'YYYY-MM-DD' */
|
|
40
|
+
max?: string;
|
|
41
|
+
/** 禁用判断(返回 true 禁点) */
|
|
42
|
+
disabledDate?: (date: Dayjs) => boolean;
|
|
43
|
+
/** 周起始:0 周日 / 1 周一,默认 1 */
|
|
44
|
+
weekStartsOn?: 0 | 1;
|
|
45
|
+
/** 月份切换回调(year, month 1~12) */
|
|
46
|
+
onMonthChange?: (year: number, month: number) => void;
|
|
47
|
+
style?: StyleProp<ViewStyle>;
|
|
48
|
+
}
|
|
49
|
+
export declare function XCalendar({ mode, value, defaultValue, multiple, onChange, min, max, disabledDate, weekStartsOn, onMonthChange, style, }: XCalendarProps): React.JSX.Element;
|
|
50
|
+
export default XCalendar;
|