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,49 @@
1
+ /**
2
+ * ============================================================================
3
+ * XTopViewStore —— 全局弹窗宿主的状态仓库(zustand)
4
+ * ============================================================================
5
+ *
6
+ * 【为什么需要它?】
7
+ * 弹窗如果渲染在页面内部(比如 ScrollView 里),会遇到两个问题:
8
+ * 1. 遮罩的 absoluteFill 只相对最近的定位父级生效,页面后面的兄弟节点
9
+ * (比如 Button)会画在遮罩上面 → 遮不住;
10
+ * 2. 弹窗会跟着页面滚动,也无法覆盖到导航头/TabBar 等页面之外的区域。
11
+ *
12
+ * 解决方案(也是 duxui XTopView 的原理):把弹窗"托管"到一个全局 store,
13
+ * 再由挂在应用根部的 <XTopView /> 渲染出来。这样弹窗永远在最顶层,
14
+ * 遮罩天然覆盖整个屏幕,与它在哪个页面里调用无关。
15
+ *
16
+ * 【三个 API 的含义】
17
+ * add(element) -> 挂载一个弹窗,返回唯一 key(之后用 key 来操作它)
18
+ * update(key, el) -> 替换指定 key 的弹窗元素(用于"关闭时切换成离场态")
19
+ * remove(key) -> 从宿主中移除(离场动画播完后再调用)
20
+ *
21
+ * 【zustand 用法速记】
22
+ * create<T>()(set => ({...})) 返回一个 hook:useXTopViewStore(s => s.xxx)
23
+ * - 选择器 s => s.add 只在 add 引用变化时才触发组件重渲染;
24
+ * - 组件内也能用 useXTopViewStore.getState().add(...) 直接调(不用 hook)。
25
+ * ============================================================================
26
+ */
27
+ import { create } from 'zustand';
28
+ /** 自增 key 计数器:保证每次 add 的 key 全局唯一 */
29
+ let keyValue = 0;
30
+ export const useXTopViewStore = create()(set => ({
31
+ /** 初始状态:没有任何弹窗 */
32
+ items: [],
33
+ /** 挂载:追加到 items 末尾(数组尾部 = 绘制在最上层),返回 key */
34
+ add: element => {
35
+ const key = ++keyValue;
36
+ set(state => ({ items: [...state.items, { key, element }] }));
37
+ return key;
38
+ },
39
+ /**
40
+ * 更新:用 map 把 key 匹配的那一项替换成新元素。
41
+ * 注意是替换整个元素对象(新的 React 元素),React 会按 key 复用组件实例,
42
+ * 只更新 props —— XPullView 正是靠这个把 visible=false 传给已挂载的弹层。
43
+ */
44
+ update: (key, element) => set(state => ({
45
+ items: state.items.map(item => (item.key === key ? { ...item, element } : item)),
46
+ })),
47
+ /** 移除:用 filter 过滤掉目标 key,触发 <XTopView /> 重渲染 */
48
+ remove: key => set(state => ({ items: state.items.filter(item => item.key !== key) })),
49
+ }));
@@ -0,0 +1,37 @@
1
+ /**
2
+ * ============================================================================
3
+ * XUploadImage —— 图片上传(九宫格,适配器驱动)
4
+ * ============================================================================
5
+ *
6
+ * 【重构自 changhu/ImageUpload】原实现 UI 与 MinIO 上传逻辑耦合在组件内,
7
+ * 且用 '__uploading__' 占位 hack 展示进度。重构后:
8
+ * - UI 只关心"文件列表 + 进度",上传走 useXUploadAdapter()(局部/全局可换);
9
+ * - 进度用独立 overlay 状态,不污染 value;
10
+ * - 连续上传多张时用本地累加器 currentRef,避免闭包旧值覆盖;
11
+ * - 图片自动压缩(expo-image-manipulator,可关);列宽按容器实测精确计算。
12
+ * ============================================================================
13
+ */
14
+ import React from 'react';
15
+ import { StyleProp, ViewStyle } from 'react-native';
16
+ import type { XUploadAdapter, XUploadResult } from './types';
17
+ export interface XUploadImageProps {
18
+ /** 受控结果列表 */
19
+ value?: XUploadResult[];
20
+ onChange?: (items: XUploadResult[]) => void;
21
+ /** 最多张数,默认 9 */
22
+ max?: number;
23
+ /** 是否压缩(默认 true,缩到 compressWidth 宽 + JPEG quality) */
24
+ compress?: boolean;
25
+ compressWidth?: number;
26
+ quality?: number;
27
+ disabled?: boolean;
28
+ /** 列数,默认 3 */
29
+ columns?: number;
30
+ /** 局部适配器覆盖(不传用全局/Provider) */
31
+ adapter?: XUploadAdapter;
32
+ /** 点击已上传项(业务可接 XImagePreview 等) */
33
+ onItemPress?: (item: XUploadResult, index: number) => void;
34
+ style?: StyleProp<ViewStyle>;
35
+ }
36
+ export declare function XUploadImage({ value, onChange, max, compress, compressWidth, quality, disabled, columns, adapter: localAdapter, onItemPress, style, }: XUploadImageProps): React.JSX.Element;
37
+ export default XUploadImage;
@@ -0,0 +1,142 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XUploadImage —— 图片上传(九宫格,适配器驱动)
5
+ * ============================================================================
6
+ *
7
+ * 【重构自 changhu/ImageUpload】原实现 UI 与 MinIO 上传逻辑耦合在组件内,
8
+ * 且用 '__uploading__' 占位 hack 展示进度。重构后:
9
+ * - UI 只关心"文件列表 + 进度",上传走 useXUploadAdapter()(局部/全局可换);
10
+ * - 进度用独立 overlay 状态,不污染 value;
11
+ * - 连续上传多张时用本地累加器 currentRef,避免闭包旧值覆盖;
12
+ * - 图片自动压缩(expo-image-manipulator,可关);列宽按容器实测精确计算。
13
+ * ============================================================================
14
+ */
15
+ import { useCallback, useRef, useState } from 'react';
16
+ import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
17
+ import { useXTheme } from '../theme';
18
+ import { useXLocale } from '../XLocale';
19
+ import { useXUploadAdapter } from './provider';
20
+ import { compressImage, pickImageFiles } from './helpers';
21
+ const CELL_GAP = 8;
22
+ export function XUploadImage({ value, onChange, max = 9, compress = true, compressWidth = 1280, quality = 0.6, disabled = false, columns = 3, adapter: localAdapter, onItemPress, style, }) {
23
+ const t = useXTheme();
24
+ const { t: i18n } = useXLocale();
25
+ const adapter = useXUploadAdapter(localAdapter);
26
+ /** 非受控兜底 */
27
+ const [inner, setInner] = useState([]);
28
+ const items = value ?? inner;
29
+ /** 上传中的任务(进度 overlay) */
30
+ const [tasks, setTasks] = useState([]);
31
+ /** 连续上传时跨轮次累加(闭包旧值防线) */
32
+ const currentRef = useRef(items);
33
+ currentRef.current = items;
34
+ /** 容器实测宽度(精确列宽) */
35
+ const [gridWidth, setGridWidth] = useState(0);
36
+ const emit = useCallback((next) => {
37
+ currentRef.current = next;
38
+ setInner(next);
39
+ onChange?.(next);
40
+ }, [onChange]);
41
+ /** 加号:选图 → 逐张压缩上传(串行,进度独立展示) */
42
+ const handleAdd = useCallback(async () => {
43
+ if (disabled || tasks.length)
44
+ return;
45
+ const remaining = max - currentRef.current.length;
46
+ if (remaining <= 0)
47
+ return;
48
+ const picked = await pickImageFiles({ max: remaining, quality: 0.9 });
49
+ for (const raw of picked) {
50
+ if (currentRef.current.length >= max)
51
+ break;
52
+ const file = compress ? await compressImage(raw, compressWidth, quality) : raw;
53
+ const taskId = `${Date.now()}-${Math.random()}`;
54
+ setTasks(prev => [...prev, { id: taskId, progress: 0 }]);
55
+ try {
56
+ const result = await adapter.upload(file, percent => {
57
+ setTasks(prev => prev.map(task => (task.id === taskId ? { ...task, progress: percent } : task)));
58
+ });
59
+ emit([...currentRef.current, result]);
60
+ }
61
+ catch (e) {
62
+ console.warn('[XUploadImage] upload failed:', e);
63
+ }
64
+ finally {
65
+ setTasks(prev => prev.filter(task => task.id !== taskId));
66
+ }
67
+ }
68
+ }, [disabled, max, tasks.length, compress, compressWidth, quality, adapter, emit]);
69
+ /** 删除 */
70
+ const handleRemove = useCallback((index) => {
71
+ emit(currentRef.current.filter((_, i) => i !== index));
72
+ }, [emit]);
73
+ const handleLayout = useCallback((e) => {
74
+ setGridWidth(e.nativeEvent.layout.width);
75
+ }, []);
76
+ /** 精确列宽:容器宽 - 间隙后均分 */
77
+ const cellWidth = gridWidth > 0 ? (gridWidth - CELL_GAP * (columns - 1)) / columns : undefined;
78
+ const showAdd = !disabled && items.length + tasks.length < max;
79
+ return (_jsxs(View, { style: [styles.grid, style], onLayout: handleLayout, children: [items.map((item, index) => (_jsxs(View, { style: [styles.cell, { borderRadius: t.borderRadius, borderColor: t.colorSplit, width: cellWidth }], children: [_jsx(Pressable, { style: styles.cellInner, onPress: () => onItemPress?.(item, index), children: _jsx(Image, { source: { uri: item.url ?? item.uri }, style: styles.image, resizeMode: 'cover' }) }), !disabled && (_jsx(Pressable, { style: styles.remove, onPress: () => handleRemove(index), hitSlop: 6, children: _jsx(Text, { style: styles.removeText, allowFontScaling: false, children: "\u2715" }) }))] }, `${item.objectKey ?? item.url ?? index}`))), tasks.map(task => (_jsx(View, { style: [
80
+ styles.cell,
81
+ styles.uploadingCell,
82
+ { borderRadius: t.borderRadius, borderColor: t.colorSplit, backgroundColor: t.colorBgLayout, width: cellWidth },
83
+ ], children: _jsx(Text, { style: [styles.progressText, { color: t.colorPrimary }], allowFontScaling: false, children: i18n('uploadingPercent', { n: task.progress }) }) }, task.id))), showAdd && (_jsx(Pressable, { onPress: handleAdd, style: ({ pressed }) => [
84
+ styles.cell,
85
+ styles.addCell,
86
+ { borderRadius: t.borderRadius, borderColor: t.colorBorder, width: cellWidth },
87
+ pressed && { backgroundColor: t.colorBgLayout },
88
+ ], children: _jsx(Text, { style: [styles.addText, { color: t.colorTextTertiary }], allowFontScaling: false, children: "\uFF0B" }) }))] }));
89
+ }
90
+ const styles = StyleSheet.create({
91
+ grid: {
92
+ flexDirection: 'row',
93
+ flexWrap: 'wrap',
94
+ gap: CELL_GAP,
95
+ },
96
+ cell: {
97
+ aspectRatio: 1,
98
+ borderWidth: StyleSheet.hairlineWidth,
99
+ overflow: 'hidden',
100
+ },
101
+ cellInner: {
102
+ flex: 1,
103
+ },
104
+ image: {
105
+ width: '100%',
106
+ height: '100%',
107
+ },
108
+ remove: {
109
+ position: 'absolute',
110
+ top: 0,
111
+ right: 0,
112
+ width: 20,
113
+ height: 20,
114
+ borderBottomLeftRadius: 8,
115
+ alignItems: 'center',
116
+ justifyContent: 'center',
117
+ backgroundColor: 'rgba(0,0,0,0.45)',
118
+ },
119
+ removeText: {
120
+ color: '#fff',
121
+ fontSize: 11,
122
+ lineHeight: 13,
123
+ },
124
+ uploadingCell: {
125
+ alignItems: 'center',
126
+ justifyContent: 'center',
127
+ },
128
+ progressText: {
129
+ fontSize: 12,
130
+ fontWeight: '600',
131
+ },
132
+ addCell: {
133
+ borderStyle: 'dashed',
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ },
137
+ addText: {
138
+ fontSize: 26,
139
+ lineHeight: 30,
140
+ },
141
+ });
142
+ export default XUploadImage;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ============================================================================
3
+ * XUploadVideo —— 视频上传(单个,适配器驱动)
4
+ * ============================================================================
5
+ * 与 XUploadImage 同一适配器协议;不压缩(视频压缩超出组件库职责),
6
+ * 选择即上传,展示进度与结果(文件名 + 删除)。
7
+ * ============================================================================
8
+ */
9
+ import React from 'react';
10
+ import { StyleProp, ViewStyle } from 'react-native';
11
+ import type { XUploadAdapter, XUploadResult } from './types';
12
+ export interface XUploadVideoProps {
13
+ /** 受控结果 */
14
+ value?: XUploadResult | null;
15
+ onChange?: (item: XUploadResult | null) => void;
16
+ /** 最长视频秒数(picker 侧限制),默认 60 */
17
+ videoMaxDuration?: number;
18
+ disabled?: boolean;
19
+ /** 局部适配器覆盖 */
20
+ adapter?: XUploadAdapter;
21
+ style?: StyleProp<ViewStyle>;
22
+ }
23
+ export declare function XUploadVideo({ value, onChange, videoMaxDuration, disabled, adapter: localAdapter, style, }: XUploadVideoProps): React.JSX.Element;
24
+ export default XUploadVideo;
@@ -0,0 +1,83 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XUploadVideo —— 视频上传(单个,适配器驱动)
5
+ * ============================================================================
6
+ * 与 XUploadImage 同一适配器协议;不压缩(视频压缩超出组件库职责),
7
+ * 选择即上传,展示进度与结果(文件名 + 删除)。
8
+ * ============================================================================
9
+ */
10
+ import { useCallback, useState } from 'react';
11
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
12
+ import { useXTheme } from '../theme';
13
+ import { useXLocale } from '../XLocale';
14
+ import { useXUploadAdapter } from './provider';
15
+ import { pickVideoFile } from './helpers';
16
+ export function XUploadVideo({ value, onChange, videoMaxDuration = 60, disabled = false, adapter: localAdapter, style, }) {
17
+ const t = useXTheme();
18
+ const { t: i18n } = useXLocale();
19
+ const adapter = useXUploadAdapter(localAdapter);
20
+ const [inner, setInner] = useState(null);
21
+ const current = value !== undefined ? value : inner;
22
+ const [progress, setProgress] = useState(null);
23
+ const [failed, setFailed] = useState(false);
24
+ const emit = useCallback((item) => {
25
+ setInner(item);
26
+ onChange?.(item);
27
+ }, [onChange]);
28
+ /** 选择视频 → 上传 */
29
+ const handlePick = useCallback(async () => {
30
+ if (disabled || progress !== null)
31
+ return;
32
+ setFailed(false);
33
+ const file = await pickVideoFile({ videoMaxDuration });
34
+ if (!file)
35
+ return;
36
+ setProgress(0);
37
+ try {
38
+ const result = await adapter.upload(file, setProgress);
39
+ emit(result);
40
+ }
41
+ catch (e) {
42
+ console.warn('[XUploadVideo] upload failed:', e);
43
+ setFailed(true);
44
+ }
45
+ finally {
46
+ setProgress(null);
47
+ }
48
+ }, [disabled, progress, videoMaxDuration, adapter, emit]);
49
+ return (_jsx(View, { style: style, children: current ? (_jsxs(View, { style: [styles.resultBox, { borderColor: t.colorSplit, backgroundColor: t.colorBgLayout, borderRadius: t.borderRadius }], children: [_jsxs(Text, { numberOfLines: 1, style: [styles.resultText, { color: t.colorText }], children: ["\uD83C\uDFAC ", String(current.url ?? current.objectKey ?? '视频')] }), !disabled && (_jsx(Pressable, { onPress: () => emit(null), hitSlop: 6, children: _jsx(Text, { style: [styles.removeText, { color: t.colorError }], children: i18n('delete') }) }))] })) : (_jsx(Pressable, { onPress: handlePick, disabled: disabled, style: ({ pressed }) => [
50
+ styles.pickBox,
51
+ { borderColor: t.colorBorder, borderRadius: t.borderRadius },
52
+ pressed && { backgroundColor: t.colorBgLayout },
53
+ ], children: progress !== null ? (_jsx(Text, { style: [styles.pickText, { color: t.colorPrimary }], allowFontScaling: false, children: i18n('uploadingPercent', { n: progress }) })) : (_jsx(Text, { style: [styles.pickText, { color: failed ? t.colorError : t.colorTextSecondary }], allowFontScaling: false, children: failed ? `${i18n('uploadFailed')} · ${i18n('retry')}` : i18n('pleaseSelect') })) })) }));
54
+ }
55
+ const styles = StyleSheet.create({
56
+ resultBox: {
57
+ height: 48,
58
+ borderWidth: StyleSheet.hairlineWidth,
59
+ flexDirection: 'row',
60
+ alignItems: 'center',
61
+ justifyContent: 'space-between',
62
+ paddingHorizontal: 12,
63
+ },
64
+ resultText: {
65
+ fontSize: 14,
66
+ flex: 1,
67
+ marginRight: 8,
68
+ },
69
+ removeText: {
70
+ fontSize: 13,
71
+ },
72
+ pickBox: {
73
+ height: 64,
74
+ borderWidth: StyleSheet.hairlineWidth,
75
+ borderStyle: 'dashed',
76
+ alignItems: 'center',
77
+ justifyContent: 'center',
78
+ },
79
+ pickText: {
80
+ fontSize: 14,
81
+ },
82
+ });
83
+ export default XUploadVideo;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * ============================================================================
3
+ * XUpload 内置适配器
4
+ * ============================================================================
5
+ * - createMinioPresignedAdapter:MinIO 预签名直传(changhu 同款后端约定,
6
+ * 两步:换预签名 URL → XHR PUT 直传,可选 objectKey 换预览 URL);
7
+ * - createFormDataUploadAdapter:普通 multipart 直传;
8
+ * - createMockUploadAdapter:演示/开发用假上传。
9
+ *
10
+ * 所有适配器的进度都通过 XHR upload.onprogress 上报(RN 原生支持)。
11
+ * ============================================================================
12
+ */
13
+ import type { XUploadAdapter, XUploadResult } from './types';
14
+ export interface MinioPresignedAdapterOptions {
15
+ /**
16
+ * 换预签名 URL。changhu 后端约定示例:
17
+ * GET /file/getUploadUrl?fileName=xx&contentType=image/png
18
+ * → {objectKey, preSignedUrl}
19
+ */
20
+ getUploadUrl: (file: {
21
+ name: string;
22
+ mimeType: string;
23
+ }) => Promise<{
24
+ objectKey: string;
25
+ preSignedUrl: string;
26
+ }>;
27
+ /** 可选:objectKey → 预览地址(changhu: GET /file/getPreviewUrl) */
28
+ getPreviewUrl?: (objectKey: string) => Promise<string | undefined> | string | undefined;
29
+ /** PUT 请求头(预签名一般无需额外鉴权头) */
30
+ headers?: Record<string, string>;
31
+ }
32
+ /**
33
+ * MinIO 预签名适配器。
34
+ * 返回值:{objectKey, url}——url 优先用 getPreviewUrl 的回显地址,
35
+ * 否则取预签名 URL 去掉 query 的裸直链。
36
+ */
37
+ export declare function createMinioPresignedAdapter(options: MinioPresignedAdapterOptions): XUploadAdapter;
38
+ export interface FormDataAdapterOptions {
39
+ /** 上传接口地址(POST multipart/form-data) */
40
+ url: string;
41
+ /** 文件字段名,默认 'file' */
42
+ fieldName?: string;
43
+ headers?: Record<string, string>;
44
+ /** 附加表单字段 */
45
+ fields?: Record<string, string>;
46
+ /** 从响应中解析结果,默认取 JSON 的 {url | data.url} */
47
+ parseResponse?: (responseText: string) => XUploadResult;
48
+ }
49
+ export declare function createFormDataUploadAdapter(options: FormDataAdapterOptions): XUploadAdapter;
50
+ export declare function createMockUploadAdapter(durationMs?: number): XUploadAdapter;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * ============================================================================
3
+ * XUpload 内置适配器
4
+ * ============================================================================
5
+ * - createMinioPresignedAdapter:MinIO 预签名直传(changhu 同款后端约定,
6
+ * 两步:换预签名 URL → XHR PUT 直传,可选 objectKey 换预览 URL);
7
+ * - createFormDataUploadAdapter:普通 multipart 直传;
8
+ * - createMockUploadAdapter:演示/开发用假上传。
9
+ *
10
+ * 所有适配器的进度都通过 XHR upload.onprogress 上报(RN 原生支持)。
11
+ * ============================================================================
12
+ */
13
+ /** XHR 上传(带进度) */
14
+ function xhrUpload(url, method,
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ body, headers, onProgress) {
17
+ return new Promise((resolve, reject) => {
18
+ const xhr = new XMLHttpRequest();
19
+ xhr.open(method, url);
20
+ for (const [k, v] of Object.entries(headers ?? {})) {
21
+ xhr.setRequestHeader(k, v);
22
+ }
23
+ if (onProgress) {
24
+ xhr.upload.onprogress = e => {
25
+ if (e.lengthComputable)
26
+ onProgress(Math.min(100, Math.round((e.loaded / e.total) * 100)));
27
+ };
28
+ }
29
+ xhr.onload = () => {
30
+ if (xhr.status >= 200 && xhr.status < 300)
31
+ resolve();
32
+ else
33
+ reject(new Error(`upload failed: HTTP ${xhr.status}`));
34
+ };
35
+ xhr.onerror = () => reject(new Error('network error'));
36
+ xhr.send(body);
37
+ });
38
+ }
39
+ /** 本地文件 → Blob(RN 中 fetch file:// 协议可用) */
40
+ async function fileToBlob(file) {
41
+ const res = await fetch(file.uri);
42
+ return res.blob();
43
+ }
44
+ /**
45
+ * MinIO 预签名适配器。
46
+ * 返回值:{objectKey, url}——url 优先用 getPreviewUrl 的回显地址,
47
+ * 否则取预签名 URL 去掉 query 的裸直链。
48
+ */
49
+ export function createMinioPresignedAdapter(options) {
50
+ return {
51
+ async upload(file, onProgress) {
52
+ const { objectKey, preSignedUrl } = await options.getUploadUrl({ name: file.name, mimeType: file.mimeType });
53
+ const blob = await fileToBlob(file);
54
+ await xhrUpload(preSignedUrl, 'PUT', blob, options.headers, onProgress);
55
+ let previewUrl;
56
+ if (options.getPreviewUrl) {
57
+ previewUrl = await options.getPreviewUrl(objectKey);
58
+ }
59
+ return { objectKey, url: previewUrl ?? preSignedUrl.split('?')[0] };
60
+ },
61
+ };
62
+ }
63
+ export function createFormDataUploadAdapter(options) {
64
+ const parse = options.parseResponse ?? (text => {
65
+ try {
66
+ const json = JSON.parse(text);
67
+ return json?.data ?? json;
68
+ }
69
+ catch {
70
+ return {};
71
+ }
72
+ });
73
+ return {
74
+ async upload(file, onProgress) {
75
+ const formData = new FormData();
76
+ // RN 的 FormData 文件项格式
77
+ formData.append(options.fieldName ?? 'file', {
78
+ uri: file.uri,
79
+ name: file.name,
80
+ type: file.mimeType,
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ });
83
+ for (const [k, v] of Object.entries(options.fields ?? {})) {
84
+ formData.append(k, v);
85
+ }
86
+ await xhrUpload(options.url, 'POST', formData, options.headers, onProgress);
87
+ return {};
88
+ },
89
+ };
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Mock 适配器(演示用)
93
+ // ---------------------------------------------------------------------------
94
+ export function createMockUploadAdapter(durationMs = 1200) {
95
+ return {
96
+ async upload(file, onProgress) {
97
+ const start = Date.now();
98
+ return new Promise(resolve => {
99
+ const timer = setInterval(() => {
100
+ const ratio = Math.min(1, (Date.now() - start) / durationMs);
101
+ onProgress?.(Math.round(ratio * 100));
102
+ if (ratio >= 1) {
103
+ clearInterval(timer);
104
+ resolve({ url: file.uri });
105
+ }
106
+ }, 100);
107
+ });
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ============================================================================
3
+ * XUpload 辅助 —— 选图/选视频/压缩/base64 上传
4
+ * ============================================================================
5
+ * 全部基于 Expo 官方包(SDK 57 API,已按类型定义核对):
6
+ * - expo-image-picker:launchImageLibraryAsync(mediaTypes: ['images'] 新写法)
7
+ * - expo-image-manipulator:manipulateAsync 压缩(JPEG)
8
+ * - expo-file-system:新版 File API 写 base64 临时文件
9
+ * ============================================================================
10
+ */
11
+ import type { XUploadAdapter, XUploadFile, XUploadResult } from './types';
12
+ /** 选图(可多选),返回本机文件列表 */
13
+ export declare function pickImageFiles(options?: {
14
+ /** 最多可选,默认 9 */
15
+ max?: number;
16
+ quality?: number;
17
+ }): Promise<XUploadFile[]>;
18
+ /** 选视频(单个),取消返回 null */
19
+ export declare function pickVideoFile(options?: {
20
+ videoMaxDuration?: number;
21
+ }): Promise<XUploadFile | null>;
22
+ /**
23
+ * 压缩图片(等比缩到指定宽度 + JPEG 质量),返回新文件。
24
+ * 小于目标宽度的图不压缩直接返回。
25
+ */
26
+ export declare function compressImage(file: XUploadFile, maxWidth?: number, quality?: number): Promise<XUploadFile>;
27
+ /**
28
+ * base64 → 临时文件 → 适配器上传(签名等"内存产物"的接入通道)。
29
+ * 上传完成自动清理临时文件。
30
+ */
31
+ export declare function uploadBase64(base64: string, options?: {
32
+ name?: string;
33
+ mimeType?: string;
34
+ adapter?: XUploadAdapter;
35
+ }): Promise<XUploadResult>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ============================================================================
3
+ * XUpload 辅助 —— 选图/选视频/压缩/base64 上传
4
+ * ============================================================================
5
+ * 全部基于 Expo 官方包(SDK 57 API,已按类型定义核对):
6
+ * - expo-image-picker:launchImageLibraryAsync(mediaTypes: ['images'] 新写法)
7
+ * - expo-image-manipulator:manipulateAsync 压缩(JPEG)
8
+ * - expo-file-system:新版 File API 写 base64 临时文件
9
+ * ============================================================================
10
+ */
11
+ import * as ImagePicker from 'expo-image-picker';
12
+ import { SaveFormat, manipulateAsync } from 'expo-image-manipulator';
13
+ import { EncodingType, File, Paths } from 'expo-file-system';
14
+ import { getXUploadAdapter } from './provider';
15
+ /** picker 资产 → XUploadFile */
16
+ function toUploadFile(asset) {
17
+ return {
18
+ uri: asset.uri,
19
+ name: asset.fileName ?? asset.uri.split('/').pop() ?? `file-${Date.now()}`,
20
+ mimeType: asset.mimeType ?? 'application/octet-stream',
21
+ size: asset.fileSize ?? undefined,
22
+ };
23
+ }
24
+ /** 选图(可多选),返回本机文件列表 */
25
+ export async function pickImageFiles(options) {
26
+ const res = await ImagePicker.launchImageLibraryAsync({
27
+ mediaTypes: ['images'],
28
+ allowsMultipleSelection: true,
29
+ selectionLimit: options?.max ?? 9,
30
+ quality: options?.quality ?? 0.9,
31
+ });
32
+ if (res.canceled)
33
+ return [];
34
+ return res.assets.map(toUploadFile);
35
+ }
36
+ /** 选视频(单个),取消返回 null */
37
+ export async function pickVideoFile(options) {
38
+ const res = await ImagePicker.launchImageLibraryAsync({
39
+ mediaTypes: ['videos'],
40
+ videoMaxDuration: options?.videoMaxDuration ?? 60,
41
+ });
42
+ if (res.canceled || !res.assets.length)
43
+ return null;
44
+ return toUploadFile(res.assets[0]);
45
+ }
46
+ /**
47
+ * 压缩图片(等比缩到指定宽度 + JPEG 质量),返回新文件。
48
+ * 小于目标宽度的图不压缩直接返回。
49
+ */
50
+ export async function compressImage(file, maxWidth = 1280, quality = 0.6) {
51
+ try {
52
+ const result = await manipulateAsync(file.uri, [{ resize: { width: maxWidth } }], { compress: quality, format: SaveFormat.JPEG });
53
+ return {
54
+ uri: result.uri,
55
+ name: file.name.replace(/\.\w+$/, '') + '.jpg',
56
+ mimeType: 'image/jpeg',
57
+ size: undefined,
58
+ };
59
+ }
60
+ catch {
61
+ // 压缩失败兜底原图
62
+ return file;
63
+ }
64
+ }
65
+ /**
66
+ * base64 → 临时文件 → 适配器上传(签名等"内存产物"的接入通道)。
67
+ * 上传完成自动清理临时文件。
68
+ */
69
+ export async function uploadBase64(base64, options) {
70
+ const name = options?.name ?? `upload-${Date.now()}.png`;
71
+ const file = new File(Paths.cache, name);
72
+ file.write(base64, { encoding: EncodingType.Base64 });
73
+ try {
74
+ const adapter = options?.adapter ?? getXUploadAdapter();
75
+ return await adapter.upload({ uri: file.uri, name, mimeType: options?.mimeType ?? 'image/png' });
76
+ }
77
+ finally {
78
+ try {
79
+ file.delete();
80
+ }
81
+ catch {
82
+ // 清理失败不影响结果
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * XUpload 统一出口
3
+ */
4
+ export type { XUploadAdapter, XUploadFile, XUploadResult } from './types';
5
+ export { createMinioPresignedAdapter, createFormDataUploadAdapter, createMockUploadAdapter } from './adapters';
6
+ export type { MinioPresignedAdapterOptions, FormDataAdapterOptions } from './adapters';
7
+ export { XUploadProvider, setXUploadAdapter, getXUploadAdapter, useXUploadAdapter, createMinioAdapter } from './provider';
8
+ export { pickImageFiles, pickVideoFile, compressImage, uploadBase64 } from './helpers';
9
+ export { XUploadImage } from './XUploadImage';
10
+ export type { XUploadImageProps } from './XUploadImage';
11
+ export { XUploadVideo } from './XUploadVideo';
12
+ export type { XUploadVideoProps } from './XUploadVideo';
@@ -0,0 +1,5 @@
1
+ export { createMinioPresignedAdapter, createFormDataUploadAdapter, createMockUploadAdapter } from './adapters';
2
+ export { XUploadProvider, setXUploadAdapter, getXUploadAdapter, useXUploadAdapter, createMinioAdapter } from './provider';
3
+ export { pickImageFiles, pickVideoFile, compressImage, uploadBase64 } from './helpers';
4
+ export { XUploadImage } from './XUploadImage';
5
+ export { XUploadVideo } from './XUploadVideo';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ============================================================================
3
+ * XUpload Provider —— 上传适配器的注入/获取
4
+ * ============================================================================
5
+ *
6
+ * 【两级注入】
7
+ * 1. 全局默认:setXUploadAdapter(adapter)(App 启动时注入 MinIO/业务适配器);
8
+ * 2. 局部覆盖:<XUploadProvider adapter={...}>(子树内所有上传组件生效),
9
+ * 没有时回退全局默认;连全局也没注入时兜底 Mock 并 warn 一次。
10
+ * ============================================================================
11
+ */
12
+ import React from 'react';
13
+ import { createMinioPresignedAdapter } from './adapters';
14
+ import type { XUploadAdapter } from './types';
15
+ /** 注入全局默认适配器(App 启动时调用一次) */
16
+ export declare function setXUploadAdapter(adapter: XUploadAdapter): void;
17
+ /** 获取全局默认适配器 */
18
+ export declare function getXUploadAdapter(): XUploadAdapter;
19
+ export declare function XUploadProvider({ adapter, children }: {
20
+ adapter: XUploadAdapter;
21
+ children: React.ReactNode;
22
+ }): React.JSX.Element;
23
+ /** 上传组件内部取适配器:局部 > 全局 > Mock(仅首次 warn) */
24
+ export declare function useXUploadAdapter(localAdapter?: XUploadAdapter): XUploadAdapter;
25
+ /** 便捷构造:MinIO 预签名适配器(re-export) */
26
+ export declare const createMinioAdapter: typeof createMinioPresignedAdapter;