react-native-x-components 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/XActionSheet/global.d.ts +15 -0
- package/dist/XActionSheet/global.js +32 -0
- package/dist/XActionSheet/index.js +3 -2
- package/dist/XCalendar/index.js +10 -6
- package/dist/XDropdownMenu/index.js +37 -22
- package/dist/XInput/index.d.ts +11 -0
- package/dist/XInput/index.js +84 -5
- package/dist/XLicensePlate/index.js +3 -1
- package/dist/XPopupProvider/index.js +2 -1
- package/dist/XSignature/XSignatureSkia.js +13 -5
- package/dist/XSignature/index.js +1 -2
- package/dist/XTheme/BrandColor.d.ts +20 -0
- package/dist/XTheme/BrandColor.js +33 -0
- package/dist/XTheme/ThemeControls.d.ts +22 -0
- package/dist/XTheme/ThemeControls.js +88 -0
- package/dist/XUpload/XUploadImage.js +57 -11
- package/dist/XUpload/XUploadVideo.js +29 -8
- package/dist/XUpload/helpers.d.ts +8 -0
- package/dist/XUpload/helpers.js +23 -0
- package/dist/XUpload/index.d.ts +1 -1
- package/dist/XUpload/index.js +1 -1
- package/dist/XVideoPreview/index.d.ts +17 -0
- package/dist/XVideoPreview/index.js +78 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -1
- package/dist/theme.d.ts +0 -8
- package/dist/theme.js +22 -6
- package/package.json +3 -5
- package/dist/XElevator/index.d.ts +0 -52
- package/dist/XElevator/index.js +0 -138
package/README.md
CHANGED
|
@@ -17,12 +17,22 @@ Ant Design 风格的 React Native (Expo) 组件库。对齐 antd v5 设计 token
|
|
|
17
17
|
npm install react-native-x-components dayjs zustand
|
|
18
18
|
# 必需 peer 依赖(Expo 项目推荐用 expo install 自动匹配版本)
|
|
19
19
|
npx expo install react-native-reanimated react-native-safe-area-context react-native-svg
|
|
20
|
+
# 推荐安装(XImagePreview/XInput 焦点等需要手势处理):
|
|
21
|
+
npx expo install react-native-gesture-handler
|
|
20
22
|
# 可选(按需装):
|
|
21
23
|
# 签名: npx expo install @shopify/react-native-skia
|
|
22
24
|
# 录音: npx expo install expo-audio
|
|
23
25
|
# 上传: npx expo install expo-image-picker expo-image-manipulator expo-file-system
|
|
24
26
|
```
|
|
25
27
|
|
|
28
|
+
### Android 权限说明(首次录音 / 拍照 / 选媒体时系统会自动弹)
|
|
29
|
+
|
|
30
|
+
- **录音**:`expo-audio` 在 `npx expo prebuild --clean` 后会自动往 AndroidManifest.xml 注入 `RECORD_AUDIO`、iOS Info.plist 注入 `NSMicrophoneUsageDescription`
|
|
31
|
+
- **拍照**:自动注入 `CAMERA`、iOS NSCameraUsageDescription
|
|
32
|
+
- **选媒体**:自动注入 `READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO` (Android 13+)、`READ_EXTERNAL_STORAGE` (Android 12-) 及 NSPhotoLibraryUsageDescription (iOS)
|
|
33
|
+
|
|
34
|
+
如果预编译后系统仍不弹权限框,先执行 `npx expo prebuild --clean && npx expo run:android` 让插件重新注入原生配置。
|
|
35
|
+
|
|
26
36
|
## 快速上手
|
|
27
37
|
|
|
28
38
|
```tsx
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XActionSheet 全局命令式 API:
|
|
3
|
+
* XActionSheet.show({title, options}) → Promise<value|null>
|
|
4
|
+
*
|
|
5
|
+
* 在 App 根部的 XPopupProvider 下挂载一次 XActionSheetGlobalOverlay 即可。
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { XActionSheetOption } from './index';
|
|
9
|
+
/** 命令式调用入口 */
|
|
10
|
+
export declare function showXActionSheet(opts: {
|
|
11
|
+
title?: string;
|
|
12
|
+
options: XActionSheetOption[];
|
|
13
|
+
}): Promise<any | null>;
|
|
14
|
+
/** 全局覆盖层:挂一次到 XPopupProvider 内即可 */
|
|
15
|
+
export declare function XActionSheetGlobalOverlay(): React.JSX.Element | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { XActionSheet } from './index';
|
|
4
|
+
const useStore = create(set => ({
|
|
5
|
+
visible: false,
|
|
6
|
+
options: [],
|
|
7
|
+
show: (opts) => {
|
|
8
|
+
return new Promise(resolve => {
|
|
9
|
+
set({ visible: true, title: opts.title, options: opts.options, resolve });
|
|
10
|
+
});
|
|
11
|
+
},
|
|
12
|
+
close: (value) => {
|
|
13
|
+
set(state => {
|
|
14
|
+
state.resolve?.(value);
|
|
15
|
+
return { visible: false, options: [], resolve: undefined };
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
/** 命令式调用入口 */
|
|
20
|
+
export async function showXActionSheet(opts) {
|
|
21
|
+
return useStore.getState().show(opts);
|
|
22
|
+
}
|
|
23
|
+
/** 全局覆盖层:挂一次到 XPopupProvider 内即可 */
|
|
24
|
+
export function XActionSheetGlobalOverlay() {
|
|
25
|
+
const visible = useStore(s => s.visible);
|
|
26
|
+
const title = useStore(s => s.title);
|
|
27
|
+
const options = useStore(s => s.options);
|
|
28
|
+
const close = useStore(s => s.close);
|
|
29
|
+
if (!visible)
|
|
30
|
+
return null;
|
|
31
|
+
return (_jsx(XActionSheet, { visible: visible, onClose: () => close(null), options: options, title: title, onSelect: opt => close(opt.value) }));
|
|
32
|
+
}
|
|
@@ -89,10 +89,11 @@ export function XActionSheet({ visible, onClose, options, title, cancelText, dur
|
|
|
89
89
|
color: t.colorTextSecondary,
|
|
90
90
|
},
|
|
91
91
|
}), [t]);
|
|
92
|
-
/**
|
|
92
|
+
/** 点选项:先通知业务方选中了谁,再关弹层(顺序关键:命令式 show() 的
|
|
93
|
+
* Promise 依赖 onSelect 先 resolve,onClose 后触发时 resolve 已消费) */
|
|
93
94
|
const handleSelect = useCallback((option) => {
|
|
94
|
-
onClose();
|
|
95
95
|
onSelect?.(option);
|
|
96
|
+
onClose();
|
|
96
97
|
}, [onClose, onSelect]);
|
|
97
98
|
return (_jsx(XPullView, { visible: visible, onClose: onClose, side: 'bottom', duration: duration, overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.panel, style, { paddingBottom: Math.max(insets.bottom, 10) }], children: [!!title && (_jsx(View, { style: styles.header, children: _jsx(Text, { style: styles.title, children: title }) })), _jsx(View, { style: styles.optionList, children: options.map(option => (_jsx(Pressable, { disabled: option.disabled, onPress: () => handleSelect(option),
|
|
98
99
|
// style 可以是函数:pressed 状态时换个底色,模拟按压反馈
|
package/dist/XCalendar/index.js
CHANGED
|
@@ -147,24 +147,28 @@ export function XCalendar({ mode = 'day', value, defaultValue, multiple = false,
|
|
|
147
147
|
setInnerValue(next);
|
|
148
148
|
onChange?.(next);
|
|
149
149
|
}, [mode, multiple, currentValue, onChange, weekStartsOn, pendingStart, isDisabled]);
|
|
150
|
-
/** 单元格样式计算
|
|
150
|
+
/** 单元格样式计算
|
|
151
|
+
* 【scope 重新选择规则】已选完一个范围后再点击:pendingStart 存在时
|
|
152
|
+
* 优先渲染"新一轮起点",忽略旧范围(否则样式纹丝不动,像卡死)。
|
|
153
|
+
* 渲染优先级:pendingStart(scope 新起点)> 旧范围 > day 选中 */
|
|
151
154
|
const cellState = useCallback((d) => {
|
|
152
155
|
const key = d.format('YYYY-MM-DD');
|
|
153
156
|
const disabled = isDisabled(d);
|
|
154
157
|
const isToday = d.isSame(dayjs(), 'day');
|
|
155
158
|
let selected = false;
|
|
156
159
|
let inRange = false;
|
|
157
|
-
if (mode === '
|
|
160
|
+
if (mode === 'scope' && pendingStart) {
|
|
161
|
+
// 新一轮选择中:只高亮起点,旧范围不再显示
|
|
162
|
+
selected = key === pendingStart.format('YYYY-MM-DD');
|
|
163
|
+
}
|
|
164
|
+
else if (mode === 'day') {
|
|
158
165
|
selected = multiple ? selectedSet.has(key) : key === selStart?.format('YYYY-MM-DD');
|
|
159
166
|
}
|
|
160
167
|
else if (selStart && selEnd) {
|
|
161
|
-
// week/scope
|
|
168
|
+
// week/scope:起止实心、中间浅底
|
|
162
169
|
selected = key === selStart.format('YYYY-MM-DD') || key === selEnd.format('YYYY-MM-DD');
|
|
163
170
|
inRange = d.isAfter(selStart, 'day') && d.isBefore(selEnd, 'day');
|
|
164
171
|
}
|
|
165
|
-
else if (mode === 'scope' && pendingStart) {
|
|
166
|
-
selected = key === pendingStart.format('YYYY-MM-DD');
|
|
167
|
-
}
|
|
168
172
|
return { selected, inRange, disabled, isToday };
|
|
169
173
|
}, [mode, multiple, selectedSet, selStart, selEnd, pendingStart, isDisabled]);
|
|
170
174
|
const renderCell = (d, index) => {
|
|
@@ -46,42 +46,56 @@ function TriggerTitle({ itemProps, active, onPress, height, }) {
|
|
|
46
46
|
? itemProps.options.find(o => o.value === itemProps.value)?.name
|
|
47
47
|
: undefined;
|
|
48
48
|
const label = selectedName ?? itemProps.title;
|
|
49
|
-
return (_jsxs(Pressable, { onPress: onPress, style: [styles.triggerItem, { height }], disabled: itemProps.disabled || !onPress, children: [_jsx(Text, { style: [styles.triggerText, { color }, active && styles.triggerTextActive], numberOfLines: 1, allowFontScaling: false, children: label }), _jsx(Text, { style: [styles.arrow, { color }, active && styles.arrowOpen], allowFontScaling: false, children: "\
|
|
49
|
+
return (_jsxs(Pressable, { onPress: onPress, style: [styles.triggerItem, { height }], disabled: itemProps.disabled || !onPress, children: [_jsx(Text, { style: [styles.triggerText, { color }, active && styles.triggerTextActive], numberOfLines: 1, allowFontScaling: false, children: label }), _jsx(Text, { style: [styles.arrow, { color }, active && styles.arrowOpen], allowFontScaling: false, children: "\u25BC" })] }));
|
|
50
50
|
}
|
|
51
51
|
const MenuOverlay = ({ visible, trigger, round, onClose, onExitEnd, snapshot, panel }) => {
|
|
52
52
|
const t = useXTheme();
|
|
53
|
-
/** 面板内容高度(onLayout
|
|
53
|
+
/** 面板内容高度(onLayout 实测,决定遮罩起点 & 完全收起判断) */
|
|
54
54
|
const [panelHeight, setPanelHeight] = useState(0);
|
|
55
|
-
|
|
55
|
+
/** scaleY 展开动画(transformOrigin 顶部 = 触发条下沿),像"从触发条下面抽出/收回" */
|
|
56
|
+
const scaleY = useSharedValue(0);
|
|
56
57
|
const maskOpacity = useSharedValue(0);
|
|
57
|
-
/**
|
|
58
|
-
* 面板位移:隐藏态 = -(panelHeight + 20)(藏在触发条上方),visible 后
|
|
59
|
-
* 下一帧滑到 0(duxui 的 translateY(-100%) 下拉)。panelHeight 未测得前
|
|
60
|
-
* 不动画,避免从 0 位置闪现。
|
|
61
|
-
*/
|
|
58
|
+
/** 展开 scaleY 0→1(顶部锚点),遮罩淡入 */
|
|
62
59
|
useEffect(() => {
|
|
63
|
-
if (visible
|
|
60
|
+
if (visible) {
|
|
64
61
|
const raf = requestAnimationFrame(() => {
|
|
65
|
-
|
|
66
|
-
maskOpacity.value = withTiming(
|
|
62
|
+
scaleY.value = withTiming(1, { duration: 200, easing: Easing.out(Easing.cubic) });
|
|
63
|
+
maskOpacity.value = withTiming(0.4, { duration: 200 });
|
|
67
64
|
});
|
|
68
65
|
return () => cancelAnimationFrame(raf);
|
|
69
66
|
}
|
|
70
|
-
}, [visible,
|
|
71
|
-
/**
|
|
67
|
+
}, [visible, scaleY, maskOpacity]);
|
|
68
|
+
/** 收起 scaleY →0(同锚点收回,不再向上平移),动画后通知宿主移除 */
|
|
72
69
|
useEffect(() => {
|
|
73
70
|
if (!visible) {
|
|
74
|
-
|
|
75
|
-
maskOpacity.value = withTiming(0, { duration:
|
|
76
|
-
const timer = setTimeout(onExitEnd,
|
|
71
|
+
scaleY.value = withTiming(0, { duration: 180, easing: Easing.in(Easing.cubic) });
|
|
72
|
+
maskOpacity.value = withTiming(0, { duration: 180 });
|
|
73
|
+
const timer = setTimeout(onExitEnd, 220);
|
|
77
74
|
return () => clearTimeout(timer);
|
|
78
75
|
}
|
|
79
|
-
}, [visible,
|
|
76
|
+
}, [visible, scaleY, maskOpacity, onExitEnd]);
|
|
80
77
|
const panelStyle = useAnimatedStyle(() => ({
|
|
81
|
-
transform: [{
|
|
78
|
+
transform: [{ scaleY: scaleY.value }],
|
|
82
79
|
}));
|
|
83
80
|
const maskStyle = useAnimatedStyle(() => ({ opacity: maskOpacity.value }));
|
|
84
|
-
return (_jsxs(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none', children: [_jsx(AnimatedPressable, { style: [
|
|
81
|
+
return (_jsxs(View, { style: StyleSheet.absoluteFill, pointerEvents: 'box-none', children: [_jsx(AnimatedPressable, { style: [
|
|
82
|
+
{
|
|
83
|
+
position: 'absolute',
|
|
84
|
+
left: 0,
|
|
85
|
+
right: 0,
|
|
86
|
+
top: trigger.y + trigger.height,
|
|
87
|
+
bottom: 0,
|
|
88
|
+
backgroundColor: '#000',
|
|
89
|
+
},
|
|
90
|
+
maskStyle,
|
|
91
|
+
], onPress: onClose }), _jsx(View, { pointerEvents: 'none', style: {
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
left: trigger.x,
|
|
94
|
+
top: trigger.y,
|
|
95
|
+
width: trigger.width,
|
|
96
|
+
height: trigger.height,
|
|
97
|
+
backgroundColor: t.colorBgContainer,
|
|
98
|
+
}, children: snapshot }), _jsx(Animated.View, { style: [
|
|
85
99
|
styles.panel,
|
|
86
100
|
{
|
|
87
101
|
left: trigger.x,
|
|
@@ -90,6 +104,7 @@ const MenuOverlay = ({ visible, trigger, round, onClose, onExitEnd, snapshot, pa
|
|
|
90
104
|
backgroundColor: t.colorBgContainer,
|
|
91
105
|
borderBottomLeftRadius: round ? t.borderRadiusXL : 0,
|
|
92
106
|
borderBottomRightRadius: round ? t.borderRadiusXL : 0,
|
|
107
|
+
transformOrigin: '50% 0%',
|
|
93
108
|
},
|
|
94
109
|
panelStyle,
|
|
95
110
|
], pointerEvents: 'auto', children: _jsx(View, { onLayout: e => {
|
|
@@ -192,7 +207,7 @@ export function XDropdownMenu({ round = true, onOpenChange, triggerHeight = 44,
|
|
|
192
207
|
}, [remove]);
|
|
193
208
|
/** context value:open/close/activeIndex 分发给每个 Item */
|
|
194
209
|
const ctx = useMemo(() => ({ index: -1, activeIndex: activeIndex ?? -1, open, close, triggerHeight }), [activeIndex, open, close, triggerHeight]);
|
|
195
|
-
return (_jsx(View, { ref: triggerRef, style: [styles.triggerRow, { height: triggerHeight }, style], children: items.map((child, i) => (_jsx(MenuContext.Provider, { value: { ...ctx, index: i }, children: child }, i))) }));
|
|
210
|
+
return (_jsx(View, { ref: triggerRef, style: [styles.triggerRow, { height: triggerHeight, backgroundColor: t.colorBgContainer }, style], children: items.map((child, i) => (_jsx(MenuContext.Provider, { value: { ...ctx, index: i }, children: child }, i))) }));
|
|
196
211
|
}
|
|
197
212
|
/**
|
|
198
213
|
* 触发条单项(真实渲染):从 MenuContext 拿到自己的序号与开合动作。
|
|
@@ -202,7 +217,7 @@ export function XDropdownMenuItem(props) {
|
|
|
202
217
|
const ctx = useContext(MenuContext);
|
|
203
218
|
if (!ctx)
|
|
204
219
|
return null;
|
|
205
|
-
return (_jsx(TriggerTitle, { itemProps: props, active: ctx.activeIndex === ctx.index, onPress: () => !props.disabled && ctx.open(ctx.index), height:
|
|
220
|
+
return (_jsx(TriggerTitle, { itemProps: props, active: ctx.activeIndex === ctx.index, onPress: () => !props.disabled && ctx.open(ctx.index), height: ctx.triggerHeight }));
|
|
206
221
|
}
|
|
207
222
|
const styles = StyleSheet.create({
|
|
208
223
|
triggerRow: {
|
|
@@ -224,7 +239,7 @@ const styles = StyleSheet.create({
|
|
|
224
239
|
fontWeight: '600',
|
|
225
240
|
},
|
|
226
241
|
arrow: {
|
|
227
|
-
fontSize:
|
|
242
|
+
fontSize: 9,
|
|
228
243
|
marginLeft: 4,
|
|
229
244
|
},
|
|
230
245
|
arrowOpen: {
|
package/dist/XInput/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* <XInput value={name} onChangeText={setName} placeholder='请输入姓名' />
|
|
12
12
|
* <XInput multiline placeholder='请输入备注' />
|
|
13
13
|
* <XInput disabled value='只读' />
|
|
14
|
+
* <XInput customKeyboard='numeric' value={amount} onChangeText={setAmount} /> // 自绘数字键盘
|
|
14
15
|
* ```
|
|
15
16
|
*
|
|
16
17
|
* 与 antd Input 的对应:
|
|
@@ -18,6 +19,8 @@
|
|
|
18
19
|
* - disabled:对应 antd disabled(不可编辑 + 禁用样式)
|
|
19
20
|
* - multiline:对应 antd Input.TextArea
|
|
20
21
|
* - allowClear:对应 antd allowClear(非多行时显示清空按钮)
|
|
22
|
+
* - customKeyboard='numeric':盖住系统键盘,弹起自绘数字键盘(带"完成"按钮)
|
|
23
|
+
* 适合金额/数量等场景,外观风格统一;与 XNumberKeyboard 同源渲染
|
|
21
24
|
* - 其余 props(maxLength/keyboardType/secureTextEntry 等)原样透传给 TextInput
|
|
22
25
|
*
|
|
23
26
|
* 注意:value 是完全受控透传,不做任何 trim/transform —— 改写会打断
|
|
@@ -33,6 +36,14 @@ export interface XInputProps extends TextInputProps {
|
|
|
33
36
|
disabled?: boolean;
|
|
34
37
|
/** 有内容时显示清空按钮(antd allowClear;multiline 下不生效) */
|
|
35
38
|
allowClear?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 自定义键盘:'numeric' 时盖住系统键盘,弹起自绘数字键盘
|
|
41
|
+
* (带"完成"工具栏,按键直接回写 value;与 XNumberKeyboard 同源)。
|
|
42
|
+
* 仅单行模式生效;与 multiline 互斥。
|
|
43
|
+
*/
|
|
44
|
+
customKeyboard?: 'numeric';
|
|
45
|
+
/** 自定义键盘的小数点(false 隐藏;true 显示,默认 true) */
|
|
46
|
+
customKeyboardPoint?: boolean;
|
|
36
47
|
style?: StyleProp<TextStyle>;
|
|
37
48
|
/** 外层容器样式(有 allowClear 按钮时想要撑满/贴边用) */
|
|
38
49
|
containerStyle?: StyleProp<ViewStyle>;
|
package/dist/XInput/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* <XInput value={name} onChangeText={setName} placeholder='请输入姓名' />
|
|
13
13
|
* <XInput multiline placeholder='请输入备注' />
|
|
14
14
|
* <XInput disabled value='只读' />
|
|
15
|
+
* <XInput customKeyboard='numeric' value={amount} onChangeText={setAmount} /> // 自绘数字键盘
|
|
15
16
|
* ```
|
|
16
17
|
*
|
|
17
18
|
* 与 antd Input 的对应:
|
|
@@ -19,17 +20,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
19
20
|
* - disabled:对应 antd disabled(不可编辑 + 禁用样式)
|
|
20
21
|
* - multiline:对应 antd Input.TextArea
|
|
21
22
|
* - allowClear:对应 antd allowClear(非多行时显示清空按钮)
|
|
23
|
+
* - customKeyboard='numeric':盖住系统键盘,弹起自绘数字键盘(带"完成"按钮)
|
|
24
|
+
* 适合金额/数量等场景,外观风格统一;与 XNumberKeyboard 同源渲染
|
|
22
25
|
* - 其余 props(maxLength/keyboardType/secureTextEntry 等)原样透传给 TextInput
|
|
23
26
|
*
|
|
24
27
|
* 注意:value 是完全受控透传,不做任何 trim/transform —— 改写会打断
|
|
25
28
|
* Android 中文输入法的合成过程(XForm 的同值守卫也依赖原样透传)。
|
|
26
29
|
*/
|
|
27
30
|
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
28
|
-
import { Pressable, StyleSheet, TextInput, View } from 'react-native';
|
|
31
|
+
import { Pressable, StyleSheet, TextInput, View, Text } from 'react-native';
|
|
32
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
29
33
|
import { useXTheme } from '../theme';
|
|
30
|
-
|
|
34
|
+
import { useXLocale } from '../XLocale';
|
|
35
|
+
import { XPullView } from '../XPullView';
|
|
36
|
+
import { XNumberKeyboard } from '../XNumberKeyboard';
|
|
37
|
+
export const XInput = forwardRef(function XInput({ value, onChangeText, multiline, disabled, allowClear, customKeyboard, customKeyboardPoint = true, style, containerStyle, editable, ...rest }, ref) {
|
|
31
38
|
const [focused, setFocused] = useState(false);
|
|
32
39
|
const isDisabled = disabled || editable === false;
|
|
40
|
+
const useCustomKb = customKeyboard === 'numeric' && !multiline;
|
|
41
|
+
/** 自定义键盘弹层是否展开 */
|
|
42
|
+
const [kbVisible, setKbVisible] = useState(false);
|
|
33
43
|
/**
|
|
34
44
|
* Android IME 防回抛镜像(关键):
|
|
35
45
|
* 受控模式下若把父级 store 的 value 每键原样回写给 TextInput,IME 合成会被
|
|
@@ -56,15 +66,58 @@ export const XInput = forwardRef(function XInput({ value, onChangeText, multilin
|
|
|
56
66
|
// 受控用法显示镜像;非受控(没传 value)不干预,TextInput 自己管
|
|
57
67
|
const displayValue = value !== undefined ? mirror : undefined;
|
|
58
68
|
const t = useXTheme();
|
|
69
|
+
const { t: i18n } = useXLocale();
|
|
70
|
+
const insets = useSafeAreaInsets();
|
|
59
71
|
const showClear = !!allowClear && !multiline && !isDisabled && !!displayValue;
|
|
60
72
|
/** 清空后把光标送回输入框,继续输入不用再点一次 */
|
|
61
73
|
const handleClear = () => {
|
|
62
74
|
reportedRef.current = '';
|
|
63
75
|
setMirror('');
|
|
64
76
|
onChangeText?.('');
|
|
65
|
-
|
|
77
|
+
if (!useCustomKb) {
|
|
78
|
+
ref && typeof ref === 'object' && ref.current?.focus();
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
setKbVisible(true);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
/** 自定义键盘:按键 → 回写 value(带 maxLength 校验) */
|
|
85
|
+
const handleKeyPress = (key) => {
|
|
86
|
+
const prev = displayValue ?? '';
|
|
87
|
+
const maxLength = rest.maxLength;
|
|
88
|
+
if (typeof maxLength === 'number' && prev.length >= maxLength)
|
|
89
|
+
return;
|
|
90
|
+
// 小数点只允许一次、不允许开头
|
|
91
|
+
if (key === '.' && (prev.includes('.') || prev.length === 0))
|
|
92
|
+
return;
|
|
93
|
+
handleChange(prev + key);
|
|
94
|
+
};
|
|
95
|
+
/** 自定义键盘:退格 */
|
|
96
|
+
const handleBackspace = () => {
|
|
97
|
+
const prev = displayValue ?? '';
|
|
98
|
+
handleChange(prev.slice(0, -1));
|
|
66
99
|
};
|
|
67
|
-
|
|
100
|
+
// 自定义键盘模式下,"聚焦状态"由弹层是否展开驱动
|
|
101
|
+
const isFocused = useCustomKb ? kbVisible : focused;
|
|
102
|
+
return (_jsxs(View, { style: [styles.container, containerStyle], children: [useCustomKb ? (_jsx(Pressable, { onPress: () => !isDisabled && setKbVisible(true), disabled: isDisabled, children: ({ pressed }) => (_jsx(View, { style: [
|
|
103
|
+
styles.input,
|
|
104
|
+
{
|
|
105
|
+
borderColor: isFocused && !isDisabled ? t.colorPrimary : t.colorBorder,
|
|
106
|
+
backgroundColor: isDisabled
|
|
107
|
+
? t.colorBgContainerDisabled
|
|
108
|
+
: pressed
|
|
109
|
+
? t.colorBgLayout
|
|
110
|
+
: t.colorBgContainer,
|
|
111
|
+
},
|
|
112
|
+
], pointerEvents: 'none', children: _jsx(Text, { style: [
|
|
113
|
+
styles.kbText,
|
|
114
|
+
{
|
|
115
|
+
color: displayValue ? t.colorText : t.colorTextQuaternary,
|
|
116
|
+
},
|
|
117
|
+
typeof style === 'object' && !Array.isArray(style)
|
|
118
|
+
? style
|
|
119
|
+
: undefined,
|
|
120
|
+
], numberOfLines: 1, allowFontScaling: false, children: displayValue || rest.placeholder }) })) })) : (_jsx(TextInput, { ...rest, ref: ref, value: displayValue, onChangeText: handleChange, multiline: multiline, editable: !isDisabled, pointerEvents: isDisabled ? 'none' : undefined, style: [
|
|
68
121
|
styles.input,
|
|
69
122
|
{
|
|
70
123
|
borderColor: focused && !isDisabled ? t.colorPrimary : t.colorBorder,
|
|
@@ -83,7 +136,7 @@ export const XInput = forwardRef(function XInput({ value, onChangeText, multilin
|
|
|
83
136
|
}, onBlur: e => {
|
|
84
137
|
setFocused(false);
|
|
85
138
|
rest.onBlur?.(e);
|
|
86
|
-
} }), showClear && (_jsx(Pressable, { onPress: handleClear, style: styles.clearBtn, hitSlop: 8, children: _jsxs(View, { style: [styles.clearCircle, { backgroundColor: t.colorTextQuaternary }], children: [_jsx(View, { style: [styles.clearX1, { backgroundColor: t.colorTextLightSolid }] }), _jsx(View, { style: [styles.clearX2, { backgroundColor: t.colorTextLightSolid }] })] }) }))] }));
|
|
139
|
+
} })), showClear && (_jsx(Pressable, { onPress: handleClear, style: styles.clearBtn, hitSlop: 8, children: _jsxs(View, { style: [styles.clearCircle, { backgroundColor: t.colorTextQuaternary }], children: [_jsx(View, { style: [styles.clearX1, { backgroundColor: t.colorTextLightSolid }] }), _jsx(View, { style: [styles.clearX2, { backgroundColor: t.colorTextLightSolid }] })] }) })), useCustomKb && (_jsx(XPullView, { visible: kbVisible, onClose: () => setKbVisible(false), side: 'bottom', overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.kbPanel, { backgroundColor: t.colorBgContainer, paddingBottom: Math.max(insets.bottom, 10) }], children: [_jsxs(View, { style: [styles.kbToolbar, { borderBottomColor: t.colorSplit }], children: [_jsx(Text, { style: [styles.kbToolbarTitle, { color: t.colorTextSecondary }], numberOfLines: 1, children: rest.placeholder ?? i18n('pleaseInput') }), _jsx(Pressable, { onPress: () => setKbVisible(false), hitSlop: 8, children: _jsx(Text, { style: [styles.kbDone, { color: t.colorPrimary }], children: i18n('done') }) })] }), _jsx(XNumberKeyboard, { onKeyPress: handleKeyPress, onBackspace: handleBackspace, extraKey: customKeyboardPoint ? '.' : null })] }) }))] }));
|
|
87
140
|
});
|
|
88
141
|
/** antd Input.TextArea 的对应形态 */
|
|
89
142
|
export const XTextArea = (props) => _jsx(XInput, { multiline: true, ...props });
|
|
@@ -107,6 +160,32 @@ const styles = StyleSheet.create({
|
|
|
107
160
|
textAlignVertical: 'top',
|
|
108
161
|
paddingVertical: 10,
|
|
109
162
|
},
|
|
163
|
+
kbText: {
|
|
164
|
+
fontSize: 15,
|
|
165
|
+
paddingVertical: 2,
|
|
166
|
+
},
|
|
167
|
+
kbPanel: {
|
|
168
|
+
borderTopLeftRadius: 12,
|
|
169
|
+
borderTopRightRadius: 12,
|
|
170
|
+
overflow: 'hidden',
|
|
171
|
+
},
|
|
172
|
+
kbToolbar: {
|
|
173
|
+
flexDirection: 'row',
|
|
174
|
+
alignItems: 'center',
|
|
175
|
+
justifyContent: 'space-between',
|
|
176
|
+
paddingHorizontal: 16,
|
|
177
|
+
height: 44,
|
|
178
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
179
|
+
},
|
|
180
|
+
kbToolbarTitle: {
|
|
181
|
+
fontSize: 13,
|
|
182
|
+
flex: 1,
|
|
183
|
+
marginRight: 8,
|
|
184
|
+
},
|
|
185
|
+
kbDone: {
|
|
186
|
+
fontSize: 15,
|
|
187
|
+
fontWeight: '600',
|
|
188
|
+
},
|
|
110
189
|
clearBtn: {
|
|
111
190
|
position: 'absolute',
|
|
112
191
|
right: 8,
|
|
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
*/
|
|
16
16
|
import { useCallback, useMemo, useState } from 'react';
|
|
17
17
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
18
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
18
19
|
import { XPullView } from '../XPullView';
|
|
19
20
|
import { useXTheme } from '../theme';
|
|
20
21
|
import { useXLocale } from '../XLocale';
|
|
@@ -81,6 +82,7 @@ export function XLicensePlateKeyboard({ phase, onInput, onBackspace, allowedKeys
|
|
|
81
82
|
export function XLicensePlate({ value = '', onChange, length = 7, disabled = false, placeholder, style }) {
|
|
82
83
|
const t = useXTheme();
|
|
83
84
|
const { t: i18n } = useXLocale();
|
|
85
|
+
const insets = useSafeAreaInsets();
|
|
84
86
|
const [visible, setVisible] = useState(false);
|
|
85
87
|
/** 当前键盘阶段 */
|
|
86
88
|
const phase = value.length === 0 ? 'province' : value.length === 1 ? 'city' : 'alnum';
|
|
@@ -112,7 +114,7 @@ export function XLicensePlate({ value = '', onChange, length = 7, disabled = fal
|
|
|
112
114
|
isNewEnergyTail && { borderColor: t.colorSuccess, borderWidth: 1.5 },
|
|
113
115
|
], children: filled && (_jsx(Text, { style: [styles.cellText, { color: t.colorText }], allowFontScaling: false, children: value[i] })) }, i));
|
|
114
116
|
});
|
|
115
|
-
return (_jsxs(_Fragment, { children: [_jsx(Pressable, { onPress: () => !disabled && setVisible(true), style: [styles.cellRow, style], disabled: disabled, children: cells }), _jsx(XPullView, { visible: visible, onClose: () => setVisible(false), side: 'bottom', overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.panel, { backgroundColor: t.colorBgContainer }], children: [_jsxs(View, { style: [styles.header, { borderBottomColor: t.colorSplit }], children: [_jsx(Text, { style: [styles.headerTitle, { color: t.colorTextSecondary }], allowFontScaling: false, children: length === 8 ? i18n('newEnergyTip') : i18n('platePlaceholder') }), _jsx(Pressable, { onPress: () => setVisible(false), hitSlop: 8, children: _jsx(Text, { style: [styles.doneBtn, { color: t.colorPrimary }], children: i18n('done') }) })] }), _jsx(XLicensePlateKeyboard, { phase: phase, onInput: handleInput, onBackspace: handleBackspace, allowedKeys: allowedKeys, isLast: isLast && length === 7 })] }) })] }));
|
|
117
|
+
return (_jsxs(_Fragment, { children: [_jsx(Pressable, { onPress: () => !disabled && setVisible(true), style: [styles.cellRow, style], disabled: disabled, children: cells }), _jsx(XPullView, { visible: visible, onClose: () => setVisible(false), side: 'bottom', overlayOpacity: 0.4, children: _jsxs(View, { style: [styles.panel, { backgroundColor: t.colorBgContainer, paddingBottom: Math.max(insets.bottom, 10) }], children: [_jsxs(View, { style: [styles.header, { borderBottomColor: t.colorSplit }], children: [_jsx(Text, { style: [styles.headerTitle, { color: t.colorTextSecondary }], allowFontScaling: false, children: length === 8 ? i18n('newEnergyTip') : i18n('platePlaceholder') }), _jsx(Pressable, { onPress: () => setVisible(false), hitSlop: 8, children: _jsx(Text, { style: [styles.doneBtn, { color: t.colorPrimary }], children: i18n('done') }) })] }), _jsx(XLicensePlateKeyboard, { phase: phase, onInput: handleInput, onBackspace: handleBackspace, allowedKeys: allowedKeys, isLast: isLast && length === 7 })] }) })] }));
|
|
116
118
|
}
|
|
117
119
|
const styles = StyleSheet.create({
|
|
118
120
|
keyboard: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { XConfirmForm } from '../XConfirmForm';
|
|
3
3
|
import { XTopView } from '../XTopView';
|
|
4
|
+
import { XActionSheetGlobalOverlay } from '../XActionSheet/global';
|
|
4
5
|
export const XPopupProvider = ({ children }) => {
|
|
5
|
-
return (_jsxs(_Fragment, { children: [children, _jsx(XConfirmForm, {}), _jsx(XTopView, {})] }));
|
|
6
|
+
return (_jsxs(_Fragment, { children: [children, _jsx(XConfirmForm, {}), _jsx(XActionSheetGlobalOverlay, {}), _jsx(XTopView, {})] }));
|
|
6
7
|
};
|
|
@@ -32,7 +32,9 @@ export function XSignatureSkia({ value, onChange, buttonText = '签名', strokeC
|
|
|
32
32
|
const t = useXTheme();
|
|
33
33
|
const { t: i18n } = useXLocale();
|
|
34
34
|
const insets = useSafeAreaInsets();
|
|
35
|
-
|
|
35
|
+
/** 非受控兜底:未传 value/onChange 时内部自持状态(示例/轻量场景开箱即用) */
|
|
36
|
+
const [innerValue, setInnerValue] = useState(null);
|
|
37
|
+
const signatureValue = value !== undefined ? (value?.characters ? value : emptyValue()) : (innerValue ?? emptyValue());
|
|
36
38
|
/** 预览画布 ref(完成时截图用) */
|
|
37
39
|
const previewCanvasRef = useCanvasRef();
|
|
38
40
|
const [visible, setVisible] = useState(false);
|
|
@@ -43,8 +45,14 @@ export function XSignatureSkia({ value, onChange, buttonText = '签名', strokeC
|
|
|
43
45
|
/** 预览:字符集拼到预览画布 */
|
|
44
46
|
const previewStrokes = useMemo(() => transformCharacters(signatureValue.characters, previewSize), [previewSize, signatureValue.characters]);
|
|
45
47
|
const emitCharacters = useCallback((characters) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
const next = characters.length ? { version: 1, characters } : null;
|
|
49
|
+
if (value !== undefined) {
|
|
50
|
+
onChange?.(next);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
setInnerValue(next);
|
|
54
|
+
}
|
|
55
|
+
}, [value, onChange]);
|
|
48
56
|
/** 一笔画完 */
|
|
49
57
|
const handleStrokeComplete = useCallback((points, size) => {
|
|
50
58
|
setCurrentStrokes(strokes => [
|
|
@@ -105,12 +113,12 @@ export function XSignatureSkia({ value, onChange, buttonText = '签名', strokeC
|
|
|
105
113
|
setVisible(false);
|
|
106
114
|
}, [signatureValue.characters, currentStrokes, emitCharacters, previewCanvasRef, onExport]);
|
|
107
115
|
/** 签名板内容(XPullView 内) */
|
|
108
|
-
const board = (_jsxs(View, { style: [styles.board, { backgroundColor: t.colorBgContainer, paddingBottom: Math.max(insets.bottom, 8) }], children: [_jsxs(ScrollView, { horizontal: true, style: [styles.characterStrip, { backgroundColor: t.colorBgLayout }], contentContainerStyle: styles.characterStripContent, children: [_jsx(Text, { style: [styles.savedCount, { color: t.colorTextTertiary }], allowFontScaling: false, children: i18n('savedChars', { n: signatureValue.characters.length }) }), signatureValue.characters.map(character => (_jsx(Pressable, { onLongPress: () => handleRemoveCharacter(character.id), style: [styles.characterChip, { borderColor: t.colorBorder, backgroundColor: t.colorBgContainer }], children: _jsx(CharacterChip, { character: character }) }, character.id)))] }), _jsxs(View, { style: styles.signArea, children: [_jsx(SignatureCanvas, { strokes: currentStrokes, touchEnabled: true, strokeColor: strokeColor, strokeWidth: strokeWidth, onStrokeComplete: handleStrokeComplete, onLayoutSize: setCanvasSize }), canvasSize.width > 0 && canvasSize.height > 0 && (_jsxs(Svg, { pointerEvents: 'none', width: canvasSize.width, height: canvasSize.height, style: styles.grid, children: [_jsx(Line, { x1: '0%', y1: '50%', x2: '100%', y2: '50%', stroke:
|
|
116
|
+
const board = (_jsxs(View, { style: [styles.board, { backgroundColor: t.colorBgContainer, paddingBottom: Math.max(insets.bottom, 8) }], children: [_jsxs(ScrollView, { horizontal: true, style: [styles.characterStrip, { backgroundColor: t.colorBgLayout }], contentContainerStyle: styles.characterStripContent, children: [_jsx(Text, { style: [styles.savedCount, { color: t.colorTextTertiary }], allowFontScaling: false, children: i18n('savedChars', { n: signatureValue.characters.length }) }), signatureValue.characters.map(character => (_jsx(Pressable, { onLongPress: () => handleRemoveCharacter(character.id), style: [styles.characterChip, { borderColor: t.colorBorder, backgroundColor: t.colorBgContainer }], children: _jsx(CharacterChip, { character: character }) }, character.id)))] }), _jsxs(View, { style: [styles.signArea, { height: 320 }], children: [_jsx(SignatureCanvas, { strokes: currentStrokes, touchEnabled: true, strokeColor: strokeColor, strokeWidth: strokeWidth, onStrokeComplete: handleStrokeComplete, onLayoutSize: setCanvasSize }), canvasSize.width > 0 && canvasSize.height > 0 && (_jsxs(Svg, { pointerEvents: 'none', width: canvasSize.width, height: canvasSize.height, style: styles.grid, children: [_jsx(Line, { x1: '0%', y1: '50%', x2: '100%', y2: '50%', stroke: '#DDDDDD', strokeWidth: '1', strokeDasharray: [4, 4] }), _jsx(Line, { x1: '50%', y1: '0%', x2: '50%', y2: '100%', stroke: '#DDDDDD', strokeWidth: '1', strokeDasharray: [4, 4] }), _jsx(Line, { x1: '0%', y1: '0%', x2: '100%', y2: '100%', stroke: '#DDDDDD', strokeWidth: '1', strokeDasharray: [4, 4] }), _jsx(Line, { x1: '100%', y1: '0%', x2: '0%', y2: '100%', stroke: '#DDDDDD', strokeWidth: '1', strokeDasharray: [4, 4] })] }))] }), _jsxs(View, { style: styles.footer, children: [_jsx(XButton, { type: 'text', onPress: () => setVisible(false), children: i18n('close') }), _jsx(XButton, { type: 'text', onPress: handleClearCurrent, children: i18n('rewrite') }), _jsx(XButton, { type: 'default', size: 'small', onPress: handleSaveCharacter, children: i18n('saveChar') }), _jsx(XButton, { type: 'text', danger: true, onPress: handleClearAll, children: i18n('clear') }), _jsx(XButton, { type: 'primary', size: 'small', onPress: handleDone, children: i18n('done') })] })] }));
|
|
109
117
|
// inline 模式:外部自己控制 XPullView(board 由 renderBoard 暴露)——当前版本仅内部用
|
|
110
118
|
if (inline) {
|
|
111
119
|
return board;
|
|
112
120
|
}
|
|
113
|
-
return (_jsxs(View, { style: styles.container, children: [_jsx(XButton, { type: 'primary', style: styles.openButton, onPress: () => setVisible(true), children: buttonText }), _jsxs(View, { style: [styles.preview, { borderColor: t.colorBorder }], children: [_jsx(SignatureCanvas, { strokes: previewStrokes, strokeColor: strokeColor, strokeWidth: strokeWidth, onLayoutSize: setPreviewSize, canvasRef: previewCanvasRef }), previewStrokes.length === 0 && (_jsx(Text, { style: [styles.placeholder, { color: t.colorTextTertiary }], children: i18n('signHint') }))] }), _jsx(XPullView, { visible: visible, onClose: () => setVisible(false), side: 'bottom', overlayOpacity: 0.5, children: _jsx(View, { style: { height:
|
|
121
|
+
return (_jsxs(View, { style: styles.container, children: [_jsx(XButton, { type: 'primary', style: styles.openButton, onPress: () => setVisible(true), children: buttonText }), _jsxs(View, { style: [styles.preview, { borderColor: t.colorBorder }], children: [_jsx(SignatureCanvas, { strokes: previewStrokes, strokeColor: strokeColor, strokeWidth: strokeWidth, onLayoutSize: setPreviewSize, canvasRef: previewCanvasRef }), previewStrokes.length === 0 && (_jsx(Text, { style: [styles.placeholder, { color: t.colorTextTertiary }], children: i18n('signHint') }))] }), _jsx(XPullView, { visible: visible, onClose: () => setVisible(false), side: 'bottom', overlayOpacity: 0.5, children: _jsx(View, { style: { height: 520, paddingBottom: 10 }, children: board }) })] }));
|
|
114
122
|
}
|
|
115
123
|
/** 单个已存字的 chip 缩略渲染 */
|
|
116
124
|
function CharacterChip({ character }) {
|
package/dist/XSignature/index.js
CHANGED
|
@@ -17,7 +17,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
17
17
|
import { useCallback, useState } from 'react';
|
|
18
18
|
import { ImageFormat, useCanvasRef } from '@shopify/react-native-skia';
|
|
19
19
|
import { StyleSheet, Text, View } from 'react-native';
|
|
20
|
-
import Svg, { Line } from 'react-native-svg';
|
|
21
20
|
import { XButton } from '../XButton';
|
|
22
21
|
import { useXTheme } from '../theme';
|
|
23
22
|
import { useXLocale } from '../XLocale';
|
|
@@ -59,7 +58,7 @@ export function XSignature({ height = 200, strokeColor = '#111111', strokeWidth
|
|
|
59
58
|
if (image)
|
|
60
59
|
onExport?.(image.encodeToBase64(ImageFormat.PNG));
|
|
61
60
|
}, [strokes.length, canvasRef, onExport]);
|
|
62
|
-
return (_jsxs(View, { style: [styles.container, style], children: [_jsxs(View, { style: [styles.paper, { height, borderColor: t.colorBorder, borderRadius: t.borderRadiusXL }], children: [_jsx(SignatureCanvas, { strokes: strokes, touchEnabled: !disabled, strokeColor: strokeColor, strokeWidth: strokeWidth, onStrokeComplete: handleStrokeComplete, onLayoutSize: setSize, canvasRef: canvasRef }),
|
|
61
|
+
return (_jsxs(View, { style: [styles.container, style], children: [_jsxs(View, { style: [styles.paper, { height, borderColor: t.colorBorder, borderRadius: t.borderRadiusXL }], children: [_jsx(SignatureCanvas, { strokes: strokes, touchEnabled: !disabled, strokeColor: strokeColor, strokeWidth: strokeWidth, onStrokeComplete: handleStrokeComplete, onLayoutSize: setSize, canvasRef: canvasRef }), strokes.length === 0 && (_jsx(Text, { pointerEvents: 'none', style: [styles.placeholder, { color: t.colorTextTertiary }], children: i18n('signHint') }))] }), _jsxs(View, { style: styles.actions, children: [_jsx(XButton, { size: 'small', onPress: handleUndo, disabled: disabled || !strokes.length, children: i18n('undo') }), _jsx(XButton, { size: 'small', danger: true, type: 'text', onPress: handleClear, disabled: disabled || !strokes.length, children: i18n('clear') }), _jsx(XButton, { size: 'small', type: 'primary', onPress: handleDone, disabled: disabled || !strokes.length, children: i18n('done') })] })] }));
|
|
63
62
|
}
|
|
64
63
|
const styles = StyleSheet.create({
|
|
65
64
|
container: {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface XBrandPreset {
|
|
2
|
+
name: string;
|
|
3
|
+
/** 浅色主色 */
|
|
4
|
+
primary: string;
|
|
5
|
+
/** 暗黑主色 */
|
|
6
|
+
primaryDark: string;
|
|
7
|
+
/** 弱背景色(antd colorPrimaryBg) */
|
|
8
|
+
primaryBg: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const X_BRAND_PRESETS: XBrandPreset[];
|
|
11
|
+
interface XBrandState {
|
|
12
|
+
brand: XBrandPreset;
|
|
13
|
+
setXBrand: (brand: XBrandPreset) => void;
|
|
14
|
+
setXBrandByName: (name: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare const useXBrandStore: import("zustand").UseBoundStore<import("zustand").StoreApi<XBrandState>>;
|
|
17
|
+
/** 命令式:切换品牌色 */
|
|
18
|
+
export declare function setXBrand(brand: XBrandPreset): void;
|
|
19
|
+
export declare function setXBrandByName(name: string): void;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XBrand —— 运行时主题色切换
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* 一组预设品牌色 + zustand 全局 store。setXBrand(name) 立即生效:
|
|
6
|
+
* 浅/深两套 token 里的 colorPrimary / colorPrimaryActive / colorPrimaryBg
|
|
7
|
+
* 被覆盖。组件统一 useXTheme() 取色,不需要额外订阅。
|
|
8
|
+
* ============================================================================
|
|
9
|
+
*/
|
|
10
|
+
import { create } from 'zustand';
|
|
11
|
+
export const X_BRAND_PRESETS = [
|
|
12
|
+
{ name: '海洋蓝', primary: '#2080F0', primaryDark: '#3C93F5', primaryBg: 'rgba(32,128,240,0.18)' },
|
|
13
|
+
{ name: '薰衣紫', primary: '#7C3AED', primaryDark: '#A78BFA', primaryBg: 'rgba(124,58,237,0.18)' },
|
|
14
|
+
{ name: '翡翠绿', primary: '#16A34A', primaryDark: '#4ADE80', primaryBg: 'rgba(22,163,74,0.18)' },
|
|
15
|
+
{ name: '暖阳橙', primary: '#F97316', primaryDark: '#FB923C', primaryBg: 'rgba(249,115,22,0.18)' },
|
|
16
|
+
];
|
|
17
|
+
const DEFAULT_BRAND = X_BRAND_PRESETS[0];
|
|
18
|
+
export const useXBrandStore = create(set => ({
|
|
19
|
+
brand: DEFAULT_BRAND,
|
|
20
|
+
setXBrand: brand => set({ brand }),
|
|
21
|
+
setXBrandByName: name => {
|
|
22
|
+
const found = X_BRAND_PRESETS.find(p => p.name === name);
|
|
23
|
+
if (found)
|
|
24
|
+
set({ brand: found });
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
/** 命令式:切换品牌色 */
|
|
28
|
+
export function setXBrand(brand) {
|
|
29
|
+
useXBrandStore.getState().setXBrand(brand);
|
|
30
|
+
}
|
|
31
|
+
export function setXBrandByName(name) {
|
|
32
|
+
useXBrandStore.getState().setXBrandByName(name);
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* ThemeControls —— 暗黑/主题色/语言 三个切换按钮组
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* 在 3 个位置使用:
|
|
6
|
+
* 1. 介绍页(app/index.tsx)顶部
|
|
7
|
+
* 2. 组件页(app/components/index.tsx)Stack headerRight
|
|
8
|
+
* 3. 我的/关于页(app/mine.tsx)顶部
|
|
9
|
+
*
|
|
10
|
+
* props:
|
|
11
|
+
* compact?: boolean true 时紧凑(用于 headerRight 单行)
|
|
12
|
+
* inline?: boolean true 时背景透明、无边框(用于页面顶部)
|
|
13
|
+
* ============================================================================
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
export interface ThemeControlsProps {
|
|
17
|
+
compact?: boolean;
|
|
18
|
+
style?: any;
|
|
19
|
+
}
|
|
20
|
+
/** 三个按钮组合 */
|
|
21
|
+
export declare function ThemeControls({ compact, style }: ThemeControlsProps): React.JSX.Element;
|
|
22
|
+
export default ThemeControls;
|