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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/XActionSheet/index.d.ts +47 -0
  4. package/dist/XActionSheet/index.js +100 -0
  5. package/dist/XAnimatedSearchPanel/index.d.ts +33 -0
  6. package/dist/XAnimatedSearchPanel/index.js +295 -0
  7. package/dist/XAnimatedView/index.d.ts +77 -0
  8. package/dist/XAnimatedView/index.js +227 -0
  9. package/dist/XButton/index.d.ts +44 -0
  10. package/dist/XButton/index.js +126 -0
  11. package/dist/XCalendar/XCalendarPopup.d.ts +38 -0
  12. package/dist/XCalendar/XCalendarPopup.js +76 -0
  13. package/dist/XCalendar/index.d.ts +50 -0
  14. package/dist/XCalendar/index.js +241 -0
  15. package/dist/XCarousel/index.d.ts +72 -0
  16. package/dist/XCarousel/index.js +183 -0
  17. package/dist/XCascadeSelect/index.d.ts +39 -0
  18. package/dist/XCascadeSelect/index.js +147 -0
  19. package/dist/XChart/index.d.ts +78 -0
  20. package/dist/XChart/index.js +162 -0
  21. package/dist/XCheckbox/index.d.ts +61 -0
  22. package/dist/XCheckbox/index.js +128 -0
  23. package/dist/XConfirmForm/index.d.ts +69 -0
  24. package/dist/XConfirmForm/index.js +113 -0
  25. package/dist/XDivider/index.d.ts +33 -0
  26. package/dist/XDivider/index.js +61 -0
  27. package/dist/XDropdownMenu/index.d.ts +66 -0
  28. package/dist/XDropdownMenu/index.js +258 -0
  29. package/dist/XElevator/index.d.ts +52 -0
  30. package/dist/XElevator/index.js +138 -0
  31. package/dist/XForm/FormStore.d.ts +82 -0
  32. package/dist/XForm/FormStore.js +279 -0
  33. package/dist/XForm/index.d.ts +116 -0
  34. package/dist/XForm/index.js +212 -0
  35. package/dist/XForm/types.d.ts +82 -0
  36. package/dist/XForm/types.js +1 -0
  37. package/dist/XForm/useForm.d.ts +2 -0
  38. package/dist/XForm/useForm.js +19 -0
  39. package/dist/XForm/utils.d.ts +42 -0
  40. package/dist/XForm/utils.js +131 -0
  41. package/dist/XForm/validate.d.ts +20 -0
  42. package/dist/XForm/validate.js +118 -0
  43. package/dist/XFormPro/components/FieldTrigger.d.ts +14 -0
  44. package/dist/XFormPro/components/FieldTrigger.js +38 -0
  45. package/dist/XFormPro/components/XDateField.d.ts +22 -0
  46. package/dist/XFormPro/components/XDateField.js +22 -0
  47. package/dist/XFormPro/components/XSelectField.d.ts +16 -0
  48. package/dist/XFormPro/components/XSelectField.js +19 -0
  49. package/dist/XFormPro/index.d.ts +44 -0
  50. package/dist/XFormPro/index.js +111 -0
  51. package/dist/XFormPro/renderers.d.ts +24 -0
  52. package/dist/XFormPro/renderers.js +53 -0
  53. package/dist/XFormPro/types.d.ts +142 -0
  54. package/dist/XFormPro/types.js +1 -0
  55. package/dist/XImage/index.d.ts +13 -0
  56. package/dist/XImage/index.js +32 -0
  57. package/dist/XInput/index.d.ts +43 -0
  58. package/dist/XInput/index.js +136 -0
  59. package/dist/XLicensePlate/index.d.ts +40 -0
  60. package/dist/XLicensePlate/index.js +172 -0
  61. package/dist/XLocale/index.d.ts +23 -0
  62. package/dist/XLocale/index.js +174 -0
  63. package/dist/XModalForm/index.d.ts +32 -0
  64. package/dist/XModalForm/index.js +85 -0
  65. package/dist/XMultiSelect/index.d.ts +31 -0
  66. package/dist/XMultiSelect/index.js +131 -0
  67. package/dist/XNumberKeyboard/index.d.ts +57 -0
  68. package/dist/XNumberKeyboard/index.js +142 -0
  69. package/dist/XPicker/index.d.ts +35 -0
  70. package/dist/XPicker/index.js +96 -0
  71. package/dist/XPickerDate/index.d.ts +58 -0
  72. package/dist/XPickerDate/index.js +234 -0
  73. package/dist/XPopupProvider/index.d.ts +23 -0
  74. package/dist/XPopupProvider/index.js +6 -0
  75. package/dist/XProgress/index.d.ts +49 -0
  76. package/dist/XProgress/index.js +196 -0
  77. package/dist/XProviders/XImagePreview/index.d.ts +44 -0
  78. package/dist/XProviders/XImagePreview/index.js +164 -0
  79. package/dist/XProviders/XLoadingModal/index.d.ts +46 -0
  80. package/dist/XProviders/XLoadingModal/index.js +119 -0
  81. package/dist/XProviders/XToast/index.d.ts +50 -0
  82. package/dist/XProviders/XToast/index.js +183 -0
  83. package/dist/XProviders/index.d.ts +31 -0
  84. package/dist/XProviders/index.js +28 -0
  85. package/dist/XProviders/usageExample.d.ts +19 -0
  86. package/dist/XProviders/usageExample.js +83 -0
  87. package/dist/XPullView/index.d.ts +76 -0
  88. package/dist/XPullView/index.js +220 -0
  89. package/dist/XRadio/index.d.ts +83 -0
  90. package/dist/XRadio/index.js +186 -0
  91. package/dist/XRecord/index.d.ts +37 -0
  92. package/dist/XRecord/index.js +160 -0
  93. package/dist/XSignature/XSignatureSkia.d.ts +39 -0
  94. package/dist/XSignature/XSignatureSkia.js +184 -0
  95. package/dist/XSignature/core.d.ts +69 -0
  96. package/dist/XSignature/core.js +108 -0
  97. package/dist/XSignature/index.d.ts +33 -0
  98. package/dist/XSignature/index.js +88 -0
  99. package/dist/XTabs/index.d.ts +65 -0
  100. package/dist/XTabs/index.js +210 -0
  101. package/dist/XTag/index.d.ts +61 -0
  102. package/dist/XTag/index.js +153 -0
  103. package/dist/XTopView/index.d.ts +25 -0
  104. package/dist/XTopView/index.js +8 -0
  105. package/dist/XTopViewStore/index.d.ts +45 -0
  106. package/dist/XTopViewStore/index.js +49 -0
  107. package/dist/XUpload/XUploadImage.d.ts +37 -0
  108. package/dist/XUpload/XUploadImage.js +142 -0
  109. package/dist/XUpload/XUploadVideo.d.ts +24 -0
  110. package/dist/XUpload/XUploadVideo.js +83 -0
  111. package/dist/XUpload/adapters.d.ts +50 -0
  112. package/dist/XUpload/adapters.js +110 -0
  113. package/dist/XUpload/helpers.d.ts +35 -0
  114. package/dist/XUpload/helpers.js +85 -0
  115. package/dist/XUpload/index.d.ts +12 -0
  116. package/dist/XUpload/index.js +5 -0
  117. package/dist/XUpload/provider.d.ts +26 -0
  118. package/dist/XUpload/provider.js +43 -0
  119. package/dist/XUpload/types.d.ts +31 -0
  120. package/dist/XUpload/types.js +4 -0
  121. package/dist/XWheel/index.d.ts +49 -0
  122. package/dist/XWheel/index.js +263 -0
  123. package/dist/index.d.ts +113 -0
  124. package/dist/index.js +127 -0
  125. package/dist/theme.d.ts +164 -0
  126. package/dist/theme.js +161 -0
  127. package/package.json +78 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ============================================================================
3
+ * XRecord —— 录音(duxui Recorder 的 expo-audio 重写版)
4
+ * ============================================================================
5
+ *
6
+ * 【与 duxui Recorder 的关系】
7
+ * duxui 用 Taro getRecorderManager()(RN 端依赖 duxapp 原生录音模块),
8
+ * Expo 项目换官方 expo-audio:useAudioRecorder + RecordingPresets +
9
+ * useAudioRecorderState(轮询 RecorderState 拿时长,SDK 57 实测 API)。
10
+ * 交互保留 duxui 的"三段式":待录音 → 录音中(计时/停止)→ 完成
11
+ * (试听/重录/使用),并补充受控 value 支持。
12
+ *
13
+ * 【取值】onChange({uri, duration})——uri 为本机临时文件,
14
+ * 上传可配合 XUpload 适配器(业务自行接管 uri)。
15
+ * ============================================================================
16
+ */
17
+ import React from 'react';
18
+ /** 录音结果 */
19
+ export interface XRecordValue {
20
+ /** 本机文件地址 file://... */
21
+ uri: string;
22
+ /** 秒 */
23
+ duration: number;
24
+ }
25
+ export interface XRecordProps {
26
+ /** 最长录音秒数,默认 60(duxui 同默认) */
27
+ max?: number;
28
+ /** 受控值(回显已上抛的录音) */
29
+ value?: XRecordValue | null;
30
+ /** 录音完成并点"使用"后回传 */
31
+ onChange?: (value: XRecordValue) => void;
32
+ /** 麦克风权限被拒回调 */
33
+ onPermissionDenied?: () => void;
34
+ disabled?: boolean;
35
+ }
36
+ export declare function XRecord({ max, value, onChange, onPermissionDenied, disabled }: XRecordProps): React.JSX.Element;
37
+ export default XRecord;
@@ -0,0 +1,160 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XRecord —— 录音(duxui Recorder 的 expo-audio 重写版)
5
+ * ============================================================================
6
+ *
7
+ * 【与 duxui Recorder 的关系】
8
+ * duxui 用 Taro getRecorderManager()(RN 端依赖 duxapp 原生录音模块),
9
+ * Expo 项目换官方 expo-audio:useAudioRecorder + RecordingPresets +
10
+ * useAudioRecorderState(轮询 RecorderState 拿时长,SDK 57 实测 API)。
11
+ * 交互保留 duxui 的"三段式":待录音 → 录音中(计时/停止)→ 完成
12
+ * (试听/重录/使用),并补充受控 value 支持。
13
+ *
14
+ * 【取值】onChange({uri, duration})——uri 为本机临时文件,
15
+ * 上传可配合 XUpload 适配器(业务自行接管 uri)。
16
+ * ============================================================================
17
+ */
18
+ import { useCallback, useEffect, useRef, useState } from 'react';
19
+ import { StyleSheet, Text, View } from 'react-native';
20
+ import { RecordingPresets, requestRecordingPermissionsAsync, useAudioPlayer, useAudioPlayerStatus, useAudioRecorder, useAudioRecorderState, } from 'expo-audio';
21
+ import { useXTheme } from '../theme';
22
+ import { useXLocale } from '../XLocale';
23
+ import { XButton } from '../XButton';
24
+ /** 秒 → mm:ss */
25
+ const fmt = (s) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
26
+ export function XRecord({ max = 60, value, onChange, onPermissionDenied, disabled = false }) {
27
+ const t = useXTheme();
28
+ const { t: i18n } = useXLocale();
29
+ /** 三段式状态:idle 待录音 / recording 录音中 / done 录完待确认 */
30
+ const [phase, setPhase] = useState('idle');
31
+ /** 录完待确认的草稿 */
32
+ const [draft, setDraft] = useState(null);
33
+ /** 权限拒绝提示 */
34
+ const [denied, setDenied] = useState(false);
35
+ /** 录音停止瞬间锁存的时长(state 轮询停更后仍能取到正确值) */
36
+ const secondsRef = useRef(0);
37
+ const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
38
+ /** 轮询录音状态(200ms):时长计时 + 上限判断 */
39
+ const recState = useAudioRecorderState(recorder, 200);
40
+ const seconds = recState.durationMillis / 1000;
41
+ /** 停止录音 → 生成草稿 */
42
+ const finishRecording = useCallback(async () => {
43
+ try {
44
+ await recorder.stop();
45
+ const uri = recorder.uri ?? recState.url;
46
+ if (uri) {
47
+ setDraft({ uri, duration: Math.max(1, Math.round(Math.min(secondsRef.current || seconds, max))) });
48
+ setPhase('done');
49
+ }
50
+ else {
51
+ setPhase('idle');
52
+ }
53
+ }
54
+ catch {
55
+ setPhase('idle');
56
+ }
57
+ }, [recorder, recState.url, seconds, max]);
58
+ /** 上限自动停(isRecording 中且到达 max) */
59
+ useEffect(() => {
60
+ if (phase === 'recording' && recState.isRecording && seconds >= max) {
61
+ secondsRef.current = max;
62
+ finishRecording();
63
+ }
64
+ else if (recState.isRecording) {
65
+ secondsRef.current = seconds;
66
+ }
67
+ }, [phase, recState.isRecording, seconds, max, finishRecording]);
68
+ /** 开始录音:先请求权限 */
69
+ const startRecording = useCallback(async () => {
70
+ if (disabled)
71
+ return;
72
+ setDenied(false);
73
+ const { granted } = await requestRecordingPermissionsAsync();
74
+ if (!granted) {
75
+ setDenied(true);
76
+ onPermissionDenied?.();
77
+ return;
78
+ }
79
+ secondsRef.current = 0;
80
+ await recorder.prepareToRecordAsync();
81
+ recorder.record();
82
+ setPhase('recording');
83
+ }, [recorder, disabled, onPermissionDenied]);
84
+ /** 重录:回到待录音 */
85
+ const reset = useCallback(() => {
86
+ setDraft(null);
87
+ secondsRef.current = 0;
88
+ setPhase('idle');
89
+ }, []);
90
+ /** 使用:上抛结果 */
91
+ const confirm = useCallback(() => {
92
+ if (draft)
93
+ onChange?.(draft);
94
+ setPhase('idle');
95
+ setDraft(null);
96
+ }, [draft, onChange]);
97
+ // 录音中:计时 + 上限提示 + 停止
98
+ if (phase === 'recording') {
99
+ return (_jsxs(View, { style: [styles.box, { backgroundColor: t.colorErrorBg, borderColor: t.colorErrorBg }], children: [_jsx(View, { style: [styles.dot, { backgroundColor: t.colorError }] }), _jsx(Text, { style: [styles.timer, { color: t.colorError }], allowFontScaling: false, children: fmt(seconds) }), _jsx(Text, { style: [styles.tip, { color: t.colorTextSecondary }], allowFontScaling: false, children: i18n('maxDurationTip', { n: max }) }), _jsx(XButton, { type: 'primary', danger: true, size: 'small', onPress: finishRecording, children: i18n('stopRecord') })] }));
100
+ }
101
+ // 录完待确认:试听 + 重录 + 使用
102
+ if (phase === 'done' && draft) {
103
+ return _jsx(PreviewRow, { draft: draft, onRerecord: reset, onUse: confirm, disabled: disabled });
104
+ }
105
+ // 待录音(或受控回显)
106
+ return (_jsxs(View, { style: styles.wrap, children: [value ? (_jsx(PreviewRow, { draft: value, onRerecord: reset, disabled: disabled, usedMode: true })) : (_jsx(XButton, { type: 'default', block: true, onPress: startRecording, disabled: disabled, children: i18n('tapToRecord') })), denied && _jsx(Text, { style: [styles.denied, { color: t.colorError }], children: i18n('micPermissionTip') })] }));
107
+ }
108
+ /** 完成态行:播放/暂停 + 时长 + 重录(+ 使用) */
109
+ function PreviewRow({ draft, onRerecord, onUse, disabled, usedMode = false, }) {
110
+ const t = useXTheme();
111
+ const { t: i18n } = useXLocale();
112
+ const player = useAudioPlayer({ uri: draft.uri });
113
+ const status = useAudioPlayerStatus(player);
114
+ const playing = status?.playing ?? false;
115
+ const togglePlay = () => {
116
+ if (!player)
117
+ return;
118
+ if (playing) {
119
+ player.pause();
120
+ }
121
+ else {
122
+ player.seekTo(0);
123
+ player.play();
124
+ }
125
+ };
126
+ return (_jsxs(View, { style: [styles.box, { borderColor: t.colorSplit }], children: [_jsx(Text, { style: [styles.timer, { color: t.colorText }], allowFontScaling: false, children: fmt(draft.duration) }), _jsx(XButton, { size: 'small', type: playing ? 'primary' : 'default', onPress: togglePlay, disabled: disabled, children: playing ? i18n('pause') : i18n('play') }), !usedMode && (_jsxs(_Fragment, { children: [_jsx(XButton, { size: 'small', onPress: onRerecord, disabled: disabled, children: i18n('rerecord') }), _jsx(XButton, { size: 'small', type: 'primary', onPress: () => onUse?.(), disabled: disabled, children: i18n('useRecording') })] }))] }));
127
+ }
128
+ const styles = StyleSheet.create({
129
+ wrap: {
130
+ width: '100%',
131
+ },
132
+ box: {
133
+ flexDirection: 'row',
134
+ alignItems: 'center',
135
+ borderWidth: StyleSheet.hairlineWidth,
136
+ borderRadius: 8,
137
+ paddingHorizontal: 12,
138
+ paddingVertical: 8,
139
+ gap: 8,
140
+ },
141
+ dot: {
142
+ width: 10,
143
+ height: 10,
144
+ borderRadius: 5,
145
+ },
146
+ timer: {
147
+ fontSize: 16,
148
+ fontWeight: '600',
149
+ fontVariant: ['tabular-nums'],
150
+ },
151
+ tip: {
152
+ fontSize: 12,
153
+ flex: 1,
154
+ },
155
+ denied: {
156
+ fontSize: 12,
157
+ marginTop: 6,
158
+ },
159
+ });
160
+ export default XRecord;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ============================================================================
3
+ * XSignatureSkia —— Skia 逐字签名(changhu/SkiaSignature 正式迁移版)
4
+ * ============================================================================
5
+ *
6
+ * 【迁移改造清单】(相对 changhu 原版)
7
+ * 1. 弹层:RN Modal → XPullView(底部拉起,与项目弹窗族动画/遮罩一致);
8
+ * 2. 底部按钮:5 个等宽蓝色 TouchableOpacity → XButton 组合
9
+ * (关闭=text / 重写=text / 存字=default / 清空=danger text / 完成=primary),
10
+ * 颜色全部走 useXTheme,暗黑自适应;
11
+ * 3. 去业务耦合:uploadAsset/XLoadingModalService/api 全部移除 ——
12
+ * 「完成」只做截图导出 onExport(base64),上传交给业务层或 XUpload 适配器;
13
+ * 4. RNFS → 移除(导出纯 base64,写临时文件由上传侧按需处理);
14
+ * 5. 文案 → i18n(中英),色值 → token。
15
+ *
16
+ * 【算法】采点/贝塞尔平滑/字符变换 100% 保留(见 ./core.tsx)。
17
+ * ============================================================================
18
+ */
19
+ import React from 'react';
20
+ import { XSignatureCharacter, XSignatureSkiaValue, XSignatureStroke } from './core';
21
+ export type { XSignatureSkiaValue, XSignatureCharacter, XSignatureStroke };
22
+ export interface XSignatureSkiaProps {
23
+ /** 受控值(结构化字符集,可回填表单) */
24
+ value?: XSignatureSkiaValue | null;
25
+ /** 字符集变化 */
26
+ onChange?: (value: XSignatureSkiaValue | null) => void;
27
+ /** 触发按钮文案,默认"签名" */
28
+ buttonText?: string;
29
+ /** 笔迹颜色(白纸上),默认 #111 */
30
+ strokeColor?: string;
31
+ /** 笔迹宽度,默认 4 */
32
+ strokeWidth?: number;
33
+ /** 点完成:导出签名 PNG base64(不含 data: 前缀) */
34
+ onExport?: (base64: string) => void;
35
+ /** 只弹签名板(不渲染触发按钮/预览),默认 false */
36
+ inline?: boolean;
37
+ }
38
+ export declare function XSignatureSkia({ value, onChange, buttonText, strokeColor, strokeWidth, onExport, inline, }: XSignatureSkiaProps): React.JSX.Element;
39
+ export default XSignatureSkia;
@@ -0,0 +1,184 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XSignatureSkia —— Skia 逐字签名(changhu/SkiaSignature 正式迁移版)
5
+ * ============================================================================
6
+ *
7
+ * 【迁移改造清单】(相对 changhu 原版)
8
+ * 1. 弹层:RN Modal → XPullView(底部拉起,与项目弹窗族动画/遮罩一致);
9
+ * 2. 底部按钮:5 个等宽蓝色 TouchableOpacity → XButton 组合
10
+ * (关闭=text / 重写=text / 存字=default / 清空=danger text / 完成=primary),
11
+ * 颜色全部走 useXTheme,暗黑自适应;
12
+ * 3. 去业务耦合:uploadAsset/XLoadingModalService/api 全部移除 ——
13
+ * 「完成」只做截图导出 onExport(base64),上传交给业务层或 XUpload 适配器;
14
+ * 4. RNFS → 移除(导出纯 base64,写临时文件由上传侧按需处理);
15
+ * 5. 文案 → i18n(中英),色值 → token。
16
+ *
17
+ * 【算法】采点/贝塞尔平滑/字符变换 100% 保留(见 ./core.tsx)。
18
+ * ============================================================================
19
+ */
20
+ import { useCallback, useMemo, useState } from 'react';
21
+ import { ImageFormat, useCanvasRef } from '@shopify/react-native-skia';
22
+ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
23
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
24
+ import Svg, { Line } from 'react-native-svg';
25
+ import { XButton } from '../XButton';
26
+ import { XPullView } from '../XPullView';
27
+ import { useXTheme } from '../theme';
28
+ import { useXLocale } from '../XLocale';
29
+ import { SignatureCanvas, transformCharacters, } from './core';
30
+ const emptyValue = () => ({ version: 1, characters: [] });
31
+ export function XSignatureSkia({ value, onChange, buttonText = '签名', strokeColor = '#111111', strokeWidth = 4, onExport, inline = false, }) {
32
+ const t = useXTheme();
33
+ const { t: i18n } = useXLocale();
34
+ const insets = useSafeAreaInsets();
35
+ const signatureValue = value?.characters ? value : emptyValue();
36
+ /** 预览画布 ref(完成时截图用) */
37
+ const previewCanvasRef = useCanvasRef();
38
+ const [visible, setVisible] = useState(false);
39
+ const [previewSize, setPreviewSize] = useState({ width: 0, height: 0 });
40
+ const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
41
+ /** 当前正在写的字(未"存字"的笔画) */
42
+ const [currentStrokes, setCurrentStrokes] = useState([]);
43
+ /** 预览:字符集拼到预览画布 */
44
+ const previewStrokes = useMemo(() => transformCharacters(signatureValue.characters, previewSize), [previewSize, signatureValue.characters]);
45
+ const emitCharacters = useCallback((characters) => {
46
+ onChange?.(characters.length ? { version: 1, characters } : null);
47
+ }, [onChange]);
48
+ /** 一笔画完 */
49
+ const handleStrokeComplete = useCallback((points, size) => {
50
+ setCurrentStrokes(strokes => [
51
+ ...strokes,
52
+ {
53
+ id: `${Date.now()}-${strokes.length}`,
54
+ color: strokeColor,
55
+ width: strokeWidth,
56
+ points,
57
+ size,
58
+ },
59
+ ]);
60
+ }, [strokeColor, strokeWidth]);
61
+ /** 存字:当前笔画收进字符集 */
62
+ const handleSaveCharacter = useCallback(() => {
63
+ if (!currentStrokes.length)
64
+ return;
65
+ const nextCharacter = {
66
+ id: `${Date.now()}`,
67
+ strokes: currentStrokes,
68
+ size: currentStrokes[0].size,
69
+ };
70
+ emitCharacters([...signatureValue.characters, nextCharacter]);
71
+ setCurrentStrokes([]);
72
+ }, [currentStrokes, signatureValue.characters, emitCharacters]);
73
+ /** 重写:只清当前字 */
74
+ const handleClearCurrent = useCallback(() => setCurrentStrokes([]), []);
75
+ /** 清空:连已存的字一起清 */
76
+ const handleClearAll = useCallback(() => {
77
+ setCurrentStrokes([]);
78
+ emitCharacters([]);
79
+ }, [emitCharacters]);
80
+ /** 长按字符 chip 删除该字 */
81
+ const handleRemoveCharacter = useCallback((id) => {
82
+ emitCharacters(signatureValue.characters.filter(character => character.id !== id));
83
+ }, [signatureValue.characters, emitCharacters]);
84
+ /** 完成:先收尾当前字 → 等预览渲染 → 截图导出 */
85
+ const handleDone = useCallback(async () => {
86
+ let finalCharacters = signatureValue.characters;
87
+ if (currentStrokes.length) {
88
+ finalCharacters = [
89
+ ...finalCharacters,
90
+ { id: `${Date.now()}`, strokes: currentStrokes, size: currentStrokes[0].size },
91
+ ];
92
+ emitCharacters(finalCharacters);
93
+ setCurrentStrokes([]);
94
+ }
95
+ if (!finalCharacters.length) {
96
+ setVisible(false);
97
+ return;
98
+ }
99
+ // 等预览 Canvas 渲染出最新字符(changhu 原版同款时序)
100
+ await new Promise(resolve => setTimeout(resolve, 100));
101
+ const image = previewCanvasRef.current?.makeImageSnapshot();
102
+ if (image) {
103
+ onExport?.(image.encodeToBase64(ImageFormat.PNG));
104
+ }
105
+ setVisible(false);
106
+ }, [signatureValue.characters, currentStrokes, emitCharacters, previewCanvasRef, onExport]);
107
+ /** 签名板内容(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: t.colorSplit, strokeWidth: '1', strokeDasharray: [4, 4] }), _jsx(Line, { x1: '50%', y1: '0%', x2: '50%', y2: '100%', stroke: t.colorSplit, strokeWidth: '1', strokeDasharray: [4, 4] }), _jsx(Line, { x1: '0%', y1: '0%', x2: '100%', y2: '100%', stroke: t.colorSplit, strokeWidth: '1', strokeDasharray: [4, 4] }), _jsx(Line, { x1: '100%', y1: '0%', x2: '0%', y2: '100%', stroke: t.colorSplit, 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
+ // inline 模式:外部自己控制 XPullView(board 由 renderBoard 暴露)——当前版本仅内部用
110
+ if (inline) {
111
+ return board;
112
+ }
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: '88%' }, children: board }) })] }));
114
+ }
115
+ /** 单个已存字的 chip 缩略渲染 */
116
+ function CharacterChip({ character }) {
117
+ const [size, setSize] = useState({ width: 0, height: 0 });
118
+ const strokes = useMemo(() => transformCharacters([character], size), [character, size]);
119
+ return (_jsx(View, { style: styles.characterChipInner, children: _jsx(SignatureCanvas, { strokes: strokes, strokeColor: '#111111', strokeWidth: 3, onLayoutSize: setSize }) }));
120
+ }
121
+ const styles = StyleSheet.create({
122
+ container: {
123
+ gap: 8,
124
+ },
125
+ openButton: {
126
+ alignSelf: 'flex-start',
127
+ },
128
+ preview: {
129
+ height: 120,
130
+ borderWidth: StyleSheet.hairlineWidth,
131
+ borderRadius: 8,
132
+ overflow: 'hidden',
133
+ justifyContent: 'center',
134
+ },
135
+ placeholder: {
136
+ position: 'absolute',
137
+ alignSelf: 'center',
138
+ fontSize: 13,
139
+ },
140
+ board: {
141
+ flex: 1,
142
+ },
143
+ characterStrip: {
144
+ height: 96,
145
+ flexGrow: 0,
146
+ },
147
+ characterStripContent: {
148
+ alignItems: 'center',
149
+ gap: 8,
150
+ paddingHorizontal: 12,
151
+ },
152
+ savedCount: {
153
+ fontSize: 12,
154
+ },
155
+ characterChip: {
156
+ width: 64,
157
+ height: 64,
158
+ borderRadius: 6,
159
+ borderWidth: StyleSheet.hairlineWidth,
160
+ alignItems: 'center',
161
+ justifyContent: 'center',
162
+ overflow: 'hidden',
163
+ },
164
+ characterChipInner: {
165
+ width: '100%',
166
+ height: '100%',
167
+ },
168
+ signArea: {
169
+ flex: 1,
170
+ },
171
+ grid: {
172
+ position: 'absolute',
173
+ top: 0,
174
+ left: 0,
175
+ },
176
+ footer: {
177
+ flexDirection: 'row',
178
+ alignItems: 'center',
179
+ justifyContent: 'space-around',
180
+ paddingTop: 8,
181
+ gap: 4,
182
+ },
183
+ });
184
+ export default XSignatureSkia;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * ============================================================================
3
+ * XSignature 共享内核 —— Skia 画布/贝塞尔/字符变换(逐字签名与普通签名共用)
4
+ * ============================================================================
5
+ *
6
+ * 【移植自 changhu/SkiaSignature】核心算法原样保留:
7
+ * - makePath:中点二次贝塞尔平滑(quadTo),单点画圆;
8
+ * - transformCharacters:每个"字"按独立 cell 等比缩放居中拼到目标画布;
9
+ * - SignatureCanvas:PanResponder 采点(locationX/Y)+ forceTick 触发重绘,
10
+ * 已提交笔画 useMemo 化的 Skia Path 渲染。
11
+ * ============================================================================
12
+ */
13
+ import React from 'react';
14
+ import { type CanvasRef } from '@shopify/react-native-skia';
15
+ export interface XSignaturePoint {
16
+ x: number;
17
+ y: number;
18
+ }
19
+ /** 一笔 */
20
+ export interface XSignatureStroke {
21
+ id: string;
22
+ color: string;
23
+ width: number;
24
+ points: XSignaturePoint[];
25
+ size: {
26
+ width: number;
27
+ height: number;
28
+ };
29
+ }
30
+ /** 一个"字"(逐字签名用) */
31
+ export interface XSignatureCharacter {
32
+ id: string;
33
+ strokes: XSignatureStroke[];
34
+ size: {
35
+ width: number;
36
+ height: number;
37
+ };
38
+ }
39
+ /** 逐字签名结构化取值(可直接进表单) */
40
+ export interface XSignatureSkiaValue {
41
+ version: 1;
42
+ characters: XSignatureCharacter[];
43
+ }
44
+ /** 中点二次贝塞尔平滑(changhu 原算法) */
45
+ export declare function makeSignaturePath(points: XSignaturePoint[]): import("@shopify/react-native-skia").SkPath;
46
+ /** 字符集 → 目标画布笔画(等比缩放居中,changhu 原算法) */
47
+ export declare function transformCharacters(characters: XSignatureCharacter[] | undefined | null, targetSize: {
48
+ width: number;
49
+ height: number;
50
+ }): XSignatureStroke[];
51
+ /** 签名画布(采集 + 渲染;touchEnabled=false 时为静态预览) */
52
+ export declare function SignatureCanvas({ strokes, currentPoints, touchEnabled, strokeColor, strokeWidth, onStrokeComplete, onLayoutSize, canvasRef, paperColor, }: {
53
+ strokes: XSignatureStroke[];
54
+ currentPoints?: XSignaturePoint[];
55
+ touchEnabled?: boolean;
56
+ strokeColor: string;
57
+ strokeWidth: number;
58
+ onStrokeComplete?: (points: XSignaturePoint[], size: {
59
+ width: number;
60
+ height: number;
61
+ }) => void;
62
+ onLayoutSize?: (size: {
63
+ width: number;
64
+ height: number;
65
+ }) => void;
66
+ canvasRef?: React.RefObject<CanvasRef | null>;
67
+ /** 纸面颜色(签名图默认白纸,不随主题) */
68
+ paperColor?: string;
69
+ }): React.JSX.Element;
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XSignature 共享内核 —— Skia 画布/贝塞尔/字符变换(逐字签名与普通签名共用)
5
+ * ============================================================================
6
+ *
7
+ * 【移植自 changhu/SkiaSignature】核心算法原样保留:
8
+ * - makePath:中点二次贝塞尔平滑(quadTo),单点画圆;
9
+ * - transformCharacters:每个"字"按独立 cell 等比缩放居中拼到目标画布;
10
+ * - SignatureCanvas:PanResponder 采点(locationX/Y)+ forceTick 触发重绘,
11
+ * 已提交笔画 useMemo 化的 Skia Path 渲染。
12
+ * ============================================================================
13
+ */
14
+ import { useMemo, useRef, useState } from 'react';
15
+ import { PanResponder, StyleSheet, View } from 'react-native';
16
+ import { Canvas, Fill, Path, Skia } from '@shopify/react-native-skia';
17
+ /** 中点二次贝塞尔平滑(changhu 原算法) */
18
+ export function makeSignaturePath(points) {
19
+ const path = Skia.Path.Make();
20
+ if (points.length === 0)
21
+ return path;
22
+ path.moveTo(points[0].x, points[0].y);
23
+ if (points.length === 1) {
24
+ path.addCircle(points[0].x, points[0].y, 0.5);
25
+ return path;
26
+ }
27
+ for (let i = 1; i < points.length - 1; i++) {
28
+ const current = points[i];
29
+ const next = points[i + 1];
30
+ const midX = (current.x + next.x) / 2;
31
+ const midY = (current.y + next.y) / 2;
32
+ path.quadTo(current.x, current.y, midX, midY);
33
+ }
34
+ const last = points[points.length - 1];
35
+ path.lineTo(last.x, last.y);
36
+ return path;
37
+ }
38
+ /** 字符集 → 目标画布笔画(等比缩放居中,changhu 原算法) */
39
+ export function transformCharacters(characters, targetSize) {
40
+ if (!characters || !characters.length || !targetSize.width || !targetSize.height)
41
+ return [];
42
+ const cellWidth = targetSize.width / characters.length;
43
+ const padding = Math.min(cellWidth, targetSize.height) * 0.12;
44
+ const targetWidth = Math.max(cellWidth - padding * 2, 1);
45
+ const targetHeight = Math.max(targetSize.height - padding * 2, 1);
46
+ return characters.flatMap((character, charIndex) => {
47
+ const scale = Math.min(targetWidth / character.size.width, targetHeight / character.size.height);
48
+ const drawWidth = character.size.width * scale;
49
+ const drawHeight = character.size.height * scale;
50
+ const offsetX = charIndex * cellWidth + (cellWidth - drawWidth) / 2;
51
+ const offsetY = (targetSize.height - drawHeight) / 2;
52
+ return character.strokes.map(stroke => ({
53
+ ...stroke,
54
+ width: Math.max(stroke.width * scale, 1),
55
+ size: targetSize,
56
+ points: stroke.points.map(point => ({
57
+ x: point.x * scale + offsetX,
58
+ y: point.y * scale + offsetY,
59
+ })),
60
+ }));
61
+ });
62
+ }
63
+ /** 签名画布(采集 + 渲染;touchEnabled=false 时为静态预览) */
64
+ export function SignatureCanvas({ strokes, currentPoints = [], touchEnabled = false, strokeColor, strokeWidth, onStrokeComplete, onLayoutSize, canvasRef, paperColor = '#FFFFFF', }) {
65
+ const sizeRef = useRef({ width: 0, height: 0 });
66
+ const pointsRef = useRef([]);
67
+ const [, forceTick] = useState(0);
68
+ const panResponder = useMemo(() => PanResponder.create({
69
+ onStartShouldSetPanResponder: () => touchEnabled,
70
+ onMoveShouldSetPanResponder: () => touchEnabled,
71
+ onPanResponderGrant: event => {
72
+ const point = { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY };
73
+ pointsRef.current = [point];
74
+ forceTick(tick => tick + 1);
75
+ },
76
+ onPanResponderMove: event => {
77
+ const point = { x: event.nativeEvent.locationX, y: event.nativeEvent.locationY };
78
+ pointsRef.current = [...pointsRef.current, point];
79
+ forceTick(tick => tick + 1);
80
+ },
81
+ onPanResponderRelease: () => {
82
+ if (pointsRef.current.length) {
83
+ onStrokeComplete?.(pointsRef.current, sizeRef.current);
84
+ }
85
+ pointsRef.current = [];
86
+ forceTick(tick => tick + 1);
87
+ },
88
+ onPanResponderTerminate: () => {
89
+ pointsRef.current = [];
90
+ forceTick(tick => tick + 1);
91
+ },
92
+ }), [onStrokeComplete, touchEnabled]);
93
+ const activePoints = touchEnabled ? pointsRef.current : currentPoints;
94
+ return (_jsx(View, { style: styles.canvasHost, onLayout: event => {
95
+ const { width, height } = event.nativeEvent.layout;
96
+ const size = { width, height };
97
+ sizeRef.current = size;
98
+ onLayoutSize?.(size);
99
+ }, ...(touchEnabled ? panResponder.panHandlers : {}), children: _jsxs(Canvas, { ref: canvasRef, style: styles.canvas, children: [_jsx(Fill, { color: paperColor }), strokes.map(stroke => (_jsx(Path, { path: makeSignaturePath(stroke.points), color: stroke.color, style: 'stroke', strokeWidth: stroke.width, strokeCap: 'round', strokeJoin: 'round' }, stroke.id))), activePoints.length > 0 && (_jsx(Path, { path: makeSignaturePath(activePoints), color: strokeColor, style: 'stroke', strokeWidth: strokeWidth, strokeCap: 'round', strokeJoin: 'round' }))] }) }));
100
+ }
101
+ const styles = StyleSheet.create({
102
+ canvasHost: {
103
+ flex: 1,
104
+ },
105
+ canvas: {
106
+ flex: 1,
107
+ },
108
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ============================================================================
3
+ * XSignature —— 普通签名(单画布连笔,非逐字)
4
+ * ============================================================================
5
+ *
6
+ * 【与 XSignatureSkia 的分工】
7
+ * - XSignatureSkia:逐字签名(一字一格、可单字重写、结构化 value);
8
+ * - XSignature:普通连笔签名(一块白纸写到底),适合"签个名确认"场景;
9
+ * 两者共用 ./core.tsx 的采集/贝塞尔/渲染内核。
10
+ *
11
+ * 【API】
12
+ * <XSignature height={200} onExport={base64 => upload(base64)} />
13
+ * 内置 撤销/清空/完成 三个动作按钮(XButton + theme + i18n)。
14
+ * ============================================================================
15
+ */
16
+ import React from 'react';
17
+ import { StyleProp, ViewStyle } from 'react-native';
18
+ export interface XSignatureProps {
19
+ /** 画布高度,默认 200 */
20
+ height?: number;
21
+ /** 笔迹颜色(白纸上),默认 #111 */
22
+ strokeColor?: string;
23
+ /** 笔迹宽度,默认 4 */
24
+ strokeWidth?: number;
25
+ /** 点完成:导出 PNG base64(不含 data: 前缀) */
26
+ onExport?: (base64: string) => void;
27
+ /** 每笔画完回调(业务可拿笔迹做存储) */
28
+ onStrokeEnd?: (strokeCount: number) => void;
29
+ disabled?: boolean;
30
+ style?: StyleProp<ViewStyle>;
31
+ }
32
+ export declare function XSignature({ height, strokeColor, strokeWidth, onExport, onStrokeEnd, disabled, style, }: XSignatureProps): React.JSX.Element;
33
+ export default XSignature;