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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
import { useXTheme, useXThemeMode, setXThemeMode } from '../theme';
|
|
4
|
+
import { useXBrandStore, setXBrandByName, X_BRAND_PRESETS } from './BrandColor';
|
|
5
|
+
import { useXLocale, setXLocale } from '../XLocale';
|
|
6
|
+
/**
|
|
7
|
+
* 暗黑切换:三态循环 📱跟随系统 → 🌙暗 → ☀️亮 → 📱。
|
|
8
|
+
* 用户点过两态按钮会永久脱离系统跟随(bug 根因),三态保证能回到"自动"。
|
|
9
|
+
*/
|
|
10
|
+
function ThemeModeButton({ compact }) {
|
|
11
|
+
const t = useXTheme();
|
|
12
|
+
const mode = useXThemeMode();
|
|
13
|
+
const next = mode === 'system' ? 'dark' : mode === 'dark' ? 'light' : 'system';
|
|
14
|
+
return (_jsx(Pressable, { onPress: () => setXThemeMode(next), hitSlop: 8, style: ({ pressed }) => [
|
|
15
|
+
compact ? styles.btnCompact : styles.btn,
|
|
16
|
+
{ borderColor: t.colorBorder, backgroundColor: pressed ? t.colorBgLayout : 'transparent' },
|
|
17
|
+
], children: _jsx(Text, { style: [compact ? styles.icon : styles.iconBig, { color: t.colorText }], children: mode === 'system' ? '📱' : mode === 'dark' ? '🌙' : '☀️' }) }));
|
|
18
|
+
}
|
|
19
|
+
/** 主题色循环切换:4 套预设品牌色 */
|
|
20
|
+
function BrandColorButton({ compact }) {
|
|
21
|
+
const t = useXTheme();
|
|
22
|
+
const brand = useXBrandStore(s => s.brand);
|
|
23
|
+
return (_jsx(Pressable, { onPress: () => {
|
|
24
|
+
const idx = X_BRAND_PRESETS.findIndex(p => p.name === brand.name);
|
|
25
|
+
const next = X_BRAND_PRESETS[(idx + 1) % X_BRAND_PRESETS.length];
|
|
26
|
+
setXBrandByName(next.name);
|
|
27
|
+
}, hitSlop: 8, style: ({ pressed }) => [
|
|
28
|
+
compact ? styles.btnCompact : styles.btn,
|
|
29
|
+
{ borderColor: t.colorBorder, backgroundColor: pressed ? t.colorBgLayout : 'transparent' },
|
|
30
|
+
], children: _jsx(View, { style: {
|
|
31
|
+
width: compact ? 14 : 18,
|
|
32
|
+
height: compact ? 14 : 18,
|
|
33
|
+
borderRadius: compact ? 7 : 9,
|
|
34
|
+
backgroundColor: brand.primary,
|
|
35
|
+
} }) }));
|
|
36
|
+
}
|
|
37
|
+
/** 语言切换:中 ↔ 英 */
|
|
38
|
+
function LocaleButton({ compact }) {
|
|
39
|
+
const t = useXTheme();
|
|
40
|
+
const { locale } = useXLocale();
|
|
41
|
+
const next = locale === 'zh-CN' ? 'en-US' : 'zh-CN';
|
|
42
|
+
return (_jsx(Pressable, { onPress: () => setXLocale(next), hitSlop: 8, style: ({ pressed }) => [
|
|
43
|
+
compact ? styles.btnCompact : styles.btn,
|
|
44
|
+
{ borderColor: t.colorBorder, backgroundColor: pressed ? t.colorBgLayout : 'transparent' },
|
|
45
|
+
], children: _jsx(Text, { style: [compact ? styles.icon : styles.iconBig, { color: t.colorText, fontWeight: '600' }], children: locale === 'zh-CN' ? 'EN' : '中' }) }));
|
|
46
|
+
}
|
|
47
|
+
/** 三个按钮组合 */
|
|
48
|
+
export function ThemeControls({ compact, style }) {
|
|
49
|
+
return (_jsxs(View, { style: [compact ? styles.rowCompact : styles.row, compact && styles.rowFlex], children: [_jsx(ThemeModeButton, { compact: compact }), _jsx(BrandColorButton, { compact: compact }), _jsx(LocaleButton, { compact: compact })] }));
|
|
50
|
+
}
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
row: {
|
|
53
|
+
flexDirection: 'row',
|
|
54
|
+
gap: 10,
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
},
|
|
57
|
+
rowCompact: {
|
|
58
|
+
flexDirection: 'row',
|
|
59
|
+
gap: 6,
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
},
|
|
62
|
+
rowFlex: {
|
|
63
|
+
flex: 1,
|
|
64
|
+
},
|
|
65
|
+
btn: {
|
|
66
|
+
width: 44,
|
|
67
|
+
height: 36,
|
|
68
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
69
|
+
borderRadius: 18,
|
|
70
|
+
alignItems: 'center',
|
|
71
|
+
justifyContent: 'center',
|
|
72
|
+
},
|
|
73
|
+
btnCompact: {
|
|
74
|
+
width: 32,
|
|
75
|
+
height: 32,
|
|
76
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
77
|
+
borderRadius: 16,
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
justifyContent: 'center',
|
|
80
|
+
},
|
|
81
|
+
icon: {
|
|
82
|
+
fontSize: 14,
|
|
83
|
+
},
|
|
84
|
+
iconBig: {
|
|
85
|
+
fontSize: 16,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
export default ThemeControls;
|
|
@@ -13,11 +13,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
* ============================================================================
|
|
14
14
|
*/
|
|
15
15
|
import { useCallback, useRef, useState } from 'react';
|
|
16
|
-
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
16
|
+
import { Alert, Image, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
17
17
|
import { useXTheme } from '../theme';
|
|
18
18
|
import { useXLocale } from '../XLocale';
|
|
19
19
|
import { useXUploadAdapter } from './provider';
|
|
20
|
-
import {
|
|
20
|
+
import { showXActionSheet } from '../XActionSheet/global';
|
|
21
|
+
import { XConfirmForm } from '../XConfirmForm';
|
|
22
|
+
import { XImagePreviewService } from '../XProviders';
|
|
23
|
+
import { compressImage, pickImageFiles, takePicture } from './helpers';
|
|
21
24
|
const CELL_GAP = 8;
|
|
22
25
|
export function XUploadImage({ value, onChange, max = 9, compress = true, compressWidth = 1280, quality = 0.6, disabled = false, columns = 3, adapter: localAdapter, onItemPress, style, }) {
|
|
23
26
|
const t = useXTheme();
|
|
@@ -38,14 +41,32 @@ export function XUploadImage({ value, onChange, max = 9, compress = true, compre
|
|
|
38
41
|
setInner(next);
|
|
39
42
|
onChange?.(next);
|
|
40
43
|
}, [onChange]);
|
|
41
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* 加号:弹 ActionSheet(拍照/相册/取消),再选 → 压缩 → 串行上传
|
|
46
|
+
*/
|
|
42
47
|
const handleAdd = useCallback(async () => {
|
|
43
48
|
if (disabled || tasks.length)
|
|
44
49
|
return;
|
|
45
50
|
const remaining = max - currentRef.current.length;
|
|
46
51
|
if (remaining <= 0)
|
|
47
52
|
return;
|
|
48
|
-
const
|
|
53
|
+
const source = await showXActionSheet({
|
|
54
|
+
title: '选择图片来源',
|
|
55
|
+
options: [
|
|
56
|
+
{ label: '📷 拍照', value: 'camera' },
|
|
57
|
+
{ label: '🖼️ 从相册选择', value: 'album' },
|
|
58
|
+
{ label: '取消', value: null },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
let picked = [];
|
|
62
|
+
if (source === 'camera') {
|
|
63
|
+
const file = await takePicture({ quality: 0.9 });
|
|
64
|
+
if (file)
|
|
65
|
+
picked = [file];
|
|
66
|
+
}
|
|
67
|
+
else if (source === 'album') {
|
|
68
|
+
picked = await pickImageFiles({ max: remaining, quality: 0.9 });
|
|
69
|
+
}
|
|
49
70
|
for (const raw of picked) {
|
|
50
71
|
if (currentRef.current.length >= max)
|
|
51
72
|
break;
|
|
@@ -60,15 +81,24 @@ export function XUploadImage({ value, onChange, max = 9, compress = true, compre
|
|
|
60
81
|
}
|
|
61
82
|
catch (e) {
|
|
62
83
|
console.warn('[XUploadImage] upload failed:', e);
|
|
84
|
+
Alert.alert('上传失败', String(e.message ?? e));
|
|
63
85
|
}
|
|
64
86
|
finally {
|
|
65
87
|
setTasks(prev => prev.filter(task => task.id !== taskId));
|
|
66
88
|
}
|
|
67
89
|
}
|
|
68
90
|
}, [disabled, max, tasks.length, compress, compressWidth, quality, adapter, emit]);
|
|
69
|
-
/**
|
|
91
|
+
/** 删除:先弹命令式确认框,确认后才删 */
|
|
70
92
|
const handleRemove = useCallback((index) => {
|
|
71
|
-
|
|
93
|
+
XConfirmForm.show({
|
|
94
|
+
title: '删除图片',
|
|
95
|
+
content: '确定要删除这张图片吗?',
|
|
96
|
+
confirmText: '删除',
|
|
97
|
+
danger: true,
|
|
98
|
+
}).then(ok => {
|
|
99
|
+
if (ok)
|
|
100
|
+
emit(currentRef.current.filter((_, i) => i !== index));
|
|
101
|
+
});
|
|
72
102
|
}, [emit]);
|
|
73
103
|
const handleLayout = useCallback((e) => {
|
|
74
104
|
setGridWidth(e.nativeEvent.layout.width);
|
|
@@ -76,16 +106,28 @@ export function XUploadImage({ value, onChange, max = 9, compress = true, compre
|
|
|
76
106
|
/** 精确列宽:容器宽 - 间隙后均分 */
|
|
77
107
|
const cellWidth = gridWidth > 0 ? (gridWidth - CELL_GAP * (columns - 1)) / columns : undefined;
|
|
78
108
|
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: () =>
|
|
109
|
+
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: () => {
|
|
110
|
+
// 默认调用内置 XImagePreviewService 预览;也可通过 onItemPress 覆盖
|
|
111
|
+
if (onItemPress) {
|
|
112
|
+
onItemPress(item, index);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const urls = items
|
|
116
|
+
.map(it => it?.url ?? it.uri)
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
const initialIndex = index;
|
|
119
|
+
XImagePreviewService.show({ images: urls, initialIndex });
|
|
120
|
+
}
|
|
121
|
+
}, 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
122
|
styles.cell,
|
|
81
123
|
styles.uploadingCell,
|
|
82
124
|
{ 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 }) => [
|
|
125
|
+
], children: _jsx(View, { style: styles.cellCenter, 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
126
|
styles.cell,
|
|
85
127
|
styles.addCell,
|
|
86
128
|
{ borderRadius: t.borderRadius, borderColor: t.colorBorder, width: cellWidth },
|
|
87
129
|
pressed && { backgroundColor: t.colorBgLayout },
|
|
88
|
-
], children: _jsx(Text, { style: [styles.addText, { color: t.colorTextTertiary }], allowFontScaling: false, children: "\uFF0B" }) }))] }));
|
|
130
|
+
], children: _jsx(View, { style: styles.cellCenter, children: _jsx(Text, { style: [styles.addText, { color: t.colorTextTertiary }], allowFontScaling: false, children: "\uFF0B" }) }) }))] }));
|
|
89
131
|
}
|
|
90
132
|
const styles = StyleSheet.create({
|
|
91
133
|
grid: {
|
|
@@ -125,14 +167,18 @@ const styles = StyleSheet.create({
|
|
|
125
167
|
alignItems: 'center',
|
|
126
168
|
justifyContent: 'center',
|
|
127
169
|
},
|
|
170
|
+
/** 单元格内容统一用 flex:1 居中容器(修复安卓上加号仅水平居中问题) */
|
|
171
|
+
cellCenter: {
|
|
172
|
+
flex: 1,
|
|
173
|
+
alignItems: 'center',
|
|
174
|
+
justifyContent: 'center',
|
|
175
|
+
},
|
|
128
176
|
progressText: {
|
|
129
177
|
fontSize: 12,
|
|
130
178
|
fontWeight: '600',
|
|
131
179
|
},
|
|
132
180
|
addCell: {
|
|
133
181
|
borderStyle: 'dashed',
|
|
134
|
-
alignItems: 'center',
|
|
135
|
-
justifyContent: 'center',
|
|
136
182
|
},
|
|
137
183
|
addText: {
|
|
138
184
|
fontSize: 26,
|
|
@@ -12,7 +12,9 @@ import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
|
12
12
|
import { useXTheme } from '../theme';
|
|
13
13
|
import { useXLocale } from '../XLocale';
|
|
14
14
|
import { useXUploadAdapter } from './provider';
|
|
15
|
-
import {
|
|
15
|
+
import { showXActionSheet } from '../XActionSheet/global';
|
|
16
|
+
import { pickVideoFile, takeVideo } from './helpers';
|
|
17
|
+
import { XVideoPreview } from '../XVideoPreview';
|
|
16
18
|
export function XUploadVideo({ value, onChange, videoMaxDuration = 60, disabled = false, adapter: localAdapter, style, }) {
|
|
17
19
|
const t = useXTheme();
|
|
18
20
|
const { t: i18n } = useXLocale();
|
|
@@ -21,16 +23,32 @@ export function XUploadVideo({ value, onChange, videoMaxDuration = 60, disabled
|
|
|
21
23
|
const current = value !== undefined ? value : inner;
|
|
22
24
|
const [progress, setProgress] = useState(null);
|
|
23
25
|
const [failed, setFailed] = useState(false);
|
|
26
|
+
/** 视频预览弹层 */
|
|
27
|
+
const [previewUri, setPreviewUri] = useState(null);
|
|
24
28
|
const emit = useCallback((item) => {
|
|
25
29
|
setInner(item);
|
|
26
30
|
onChange?.(item);
|
|
27
31
|
}, [onChange]);
|
|
28
|
-
/**
|
|
32
|
+
/** 弹 ActionSheet → 拍照 / 相册 → 上传 */
|
|
29
33
|
const handlePick = useCallback(async () => {
|
|
30
34
|
if (disabled || progress !== null)
|
|
31
35
|
return;
|
|
32
36
|
setFailed(false);
|
|
33
|
-
const
|
|
37
|
+
const source = await showXActionSheet({
|
|
38
|
+
title: '选择视频来源',
|
|
39
|
+
options: [
|
|
40
|
+
{ label: '🎥 拍视频', value: 'camera' },
|
|
41
|
+
{ label: '🎞️ 从相册选择', value: 'album' },
|
|
42
|
+
{ label: '取消', value: null },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
let file = null;
|
|
46
|
+
if (source === 'camera') {
|
|
47
|
+
file = await takeVideo({ videoMaxDuration });
|
|
48
|
+
}
|
|
49
|
+
else if (source === 'album') {
|
|
50
|
+
file = await pickVideoFile({ videoMaxDuration });
|
|
51
|
+
}
|
|
34
52
|
if (!file)
|
|
35
53
|
return;
|
|
36
54
|
setProgress(0);
|
|
@@ -46,11 +64,11 @@ export function XUploadVideo({ value, onChange, videoMaxDuration = 60, disabled
|
|
|
46
64
|
setProgress(null);
|
|
47
65
|
}
|
|
48
66
|
}, [disabled, progress, videoMaxDuration, adapter, emit]);
|
|
49
|
-
return (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
return (_jsxs(View, { style: style, children: [current ? (_jsxs(Pressable, { onPress: () => setPreviewUri(current.url ?? current.uri ?? null), 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, { hitSlop: 6, onPress: () => emit(null), style: styles.deleteBtn, children: _jsx(Text, { style: [styles.removeText, { color: t.colorError }], children: i18n('delete') }) }))] })) : (_jsx(Pressable, { onPress: handlePick, disabled: disabled, style: ({ pressed }) => [
|
|
68
|
+
styles.pickBox,
|
|
69
|
+
{ borderColor: t.colorBorder, borderRadius: t.borderRadius },
|
|
70
|
+
pressed && { backgroundColor: t.colorBgLayout },
|
|
71
|
+
], 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') })) })), _jsx(XVideoPreview, { visible: !!previewUri, uri: previewUri ?? '', onClose: () => setPreviewUri(null) })] }));
|
|
54
72
|
}
|
|
55
73
|
const styles = StyleSheet.create({
|
|
56
74
|
resultBox: {
|
|
@@ -69,6 +87,9 @@ const styles = StyleSheet.create({
|
|
|
69
87
|
removeText: {
|
|
70
88
|
fontSize: 13,
|
|
71
89
|
},
|
|
90
|
+
deleteBtn: {
|
|
91
|
+
paddingHorizontal: 4,
|
|
92
|
+
},
|
|
72
93
|
pickBox: {
|
|
73
94
|
height: 64,
|
|
74
95
|
borderWidth: StyleSheet.hairlineWidth,
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* ============================================================================
|
|
10
10
|
*/
|
|
11
11
|
import type { XUploadAdapter, XUploadFile, XUploadResult } from './types';
|
|
12
|
+
/** 拍照 */
|
|
13
|
+
export declare function takePicture(options?: {
|
|
14
|
+
quality?: number;
|
|
15
|
+
}): Promise<XUploadFile | null>;
|
|
16
|
+
/** 拍视频 */
|
|
17
|
+
export declare function takeVideo(options?: {
|
|
18
|
+
videoMaxDuration?: number;
|
|
19
|
+
}): Promise<XUploadFile | null>;
|
|
12
20
|
/** 选图(可多选),返回本机文件列表 */
|
|
13
21
|
export declare function pickImageFiles(options?: {
|
|
14
22
|
/** 最多可选,默认 9 */
|
package/dist/XUpload/helpers.js
CHANGED
|
@@ -21,6 +21,29 @@ function toUploadFile(asset) {
|
|
|
21
21
|
size: asset.fileSize ?? undefined,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
/** 拍照 */
|
|
25
|
+
export async function takePicture(options) {
|
|
26
|
+
// 拍照权限(Android 自动授予,iOS 第一次会弹)
|
|
27
|
+
await ImagePicker.requestCameraPermissionsAsync();
|
|
28
|
+
const res = await ImagePicker.launchCameraAsync({
|
|
29
|
+
mediaTypes: ['images'],
|
|
30
|
+
quality: options?.quality ?? 0.9,
|
|
31
|
+
});
|
|
32
|
+
if (res.canceled || !res.assets.length)
|
|
33
|
+
return null;
|
|
34
|
+
return toUploadFile(res.assets[0]);
|
|
35
|
+
}
|
|
36
|
+
/** 拍视频 */
|
|
37
|
+
export async function takeVideo(options) {
|
|
38
|
+
await ImagePicker.requestCameraPermissionsAsync();
|
|
39
|
+
const res = await ImagePicker.launchCameraAsync({
|
|
40
|
+
mediaTypes: ['videos'],
|
|
41
|
+
videoMaxDuration: options?.videoMaxDuration ?? 60,
|
|
42
|
+
});
|
|
43
|
+
if (res.canceled || !res.assets.length)
|
|
44
|
+
return null;
|
|
45
|
+
return toUploadFile(res.assets[0]);
|
|
46
|
+
}
|
|
24
47
|
/** 选图(可多选),返回本机文件列表 */
|
|
25
48
|
export async function pickImageFiles(options) {
|
|
26
49
|
const res = await ImagePicker.launchImageLibraryAsync({
|
package/dist/XUpload/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { XUploadAdapter, XUploadFile, XUploadResult } from './types';
|
|
|
5
5
|
export { createMinioPresignedAdapter, createFormDataUploadAdapter, createMockUploadAdapter } from './adapters';
|
|
6
6
|
export type { MinioPresignedAdapterOptions, FormDataAdapterOptions } from './adapters';
|
|
7
7
|
export { XUploadProvider, setXUploadAdapter, getXUploadAdapter, useXUploadAdapter, createMinioAdapter } from './provider';
|
|
8
|
-
export { pickImageFiles, pickVideoFile, compressImage, uploadBase64 } from './helpers';
|
|
8
|
+
export { pickImageFiles, pickVideoFile, compressImage, uploadBase64, takePicture, takeVideo } from './helpers';
|
|
9
9
|
export { XUploadImage } from './XUploadImage';
|
|
10
10
|
export type { XUploadImageProps } from './XUploadImage';
|
|
11
11
|
export { XUploadVideo } from './XUploadVideo';
|
package/dist/XUpload/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createMinioPresignedAdapter, createFormDataUploadAdapter, createMockUploadAdapter } from './adapters';
|
|
2
2
|
export { XUploadProvider, setXUploadAdapter, getXUploadAdapter, useXUploadAdapter, createMinioAdapter } from './provider';
|
|
3
|
-
export { pickImageFiles, pickVideoFile, compressImage, uploadBase64 } from './helpers';
|
|
3
|
+
export { pickImageFiles, pickVideoFile, compressImage, uploadBase64, takePicture, takeVideo } from './helpers';
|
|
4
4
|
export { XUploadImage } from './XUploadImage';
|
|
5
5
|
export { XUploadVideo } from './XUploadVideo';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* XVideoPreview —— 视频全屏预览(基于 expo-video + XPullView 居中弹层)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* 用法:受控组件
|
|
6
|
+
* <XVideoPreview visible={visible} onClose={...} uri="..." />
|
|
7
|
+
* XUploadVideo 内部直接复用。
|
|
8
|
+
* ============================================================================
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
export interface XVideoPreviewProps {
|
|
12
|
+
visible: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
uri: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function XVideoPreview({ visible, onClose, uri }: XVideoPreviewProps): React.JSX.Element;
|
|
17
|
+
export default XVideoPreview;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* XVideoPreview —— 视频全屏预览(基于 expo-video + XPullView 居中弹层)
|
|
5
|
+
* ============================================================================
|
|
6
|
+
* 用法:受控组件
|
|
7
|
+
* <XVideoPreview visible={visible} onClose={...} uri="..." />
|
|
8
|
+
* XUploadVideo 内部直接复用。
|
|
9
|
+
* ============================================================================
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect } from 'react';
|
|
12
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
13
|
+
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
14
|
+
import { XPullView } from '../XPullView';
|
|
15
|
+
export function XVideoPreview({ visible, onClose, uri }) {
|
|
16
|
+
const player = useVideoPlayer(uri, p => {
|
|
17
|
+
p.loop = false;
|
|
18
|
+
p.play();
|
|
19
|
+
});
|
|
20
|
+
// 关闭时停止播放 + 卸载时清理
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
return () => {
|
|
23
|
+
try {
|
|
24
|
+
player?.pause();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* noop */
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, [player]);
|
|
31
|
+
// 每次打开自动播放
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (visible && player) {
|
|
34
|
+
try {
|
|
35
|
+
player.play();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* noop */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (!visible && player) {
|
|
42
|
+
try {
|
|
43
|
+
player.pause();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* noop */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}, [visible, player]);
|
|
50
|
+
return (_jsx(XPullView, { visible: visible, onClose: onClose, side: 'center', overlayOpacity: 0.85, mask: true, children: _jsxs(View, { style: styles.overlay, children: [_jsx(VideoView, { player: player, style: StyleSheet.absoluteFill, contentFit: 'contain', nativeControls: true }), _jsx(Pressable, { onPress: onClose, hitSlop: 12, style: styles.close, children: _jsx(Text, { style: styles.closeText, children: "\u2715" }) })] }) }));
|
|
51
|
+
}
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
overlay: {
|
|
54
|
+
width: '100%',
|
|
55
|
+
aspectRatio: 16 / 9,
|
|
56
|
+
maxHeight: '80%',
|
|
57
|
+
backgroundColor: '#000',
|
|
58
|
+
overflow: 'hidden',
|
|
59
|
+
borderRadius: 12,
|
|
60
|
+
},
|
|
61
|
+
close: {
|
|
62
|
+
position: 'absolute',
|
|
63
|
+
top: 12,
|
|
64
|
+
right: 12,
|
|
65
|
+
width: 36,
|
|
66
|
+
height: 36,
|
|
67
|
+
borderRadius: 18,
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
justifyContent: 'center',
|
|
70
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
71
|
+
},
|
|
72
|
+
closeText: {
|
|
73
|
+
color: '#fff',
|
|
74
|
+
fontSize: 18,
|
|
75
|
+
fontWeight: '600',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
export default XVideoPreview;
|
package/dist/index.d.ts
CHANGED
|
@@ -90,8 +90,6 @@ export { XCarousel } from './XCarousel';
|
|
|
90
90
|
export type { XCarouselProps, XCarouselItem } from './XCarousel';
|
|
91
91
|
export { XTabs, XTabPane } from './XTabs';
|
|
92
92
|
export type { XTabsProps, XTabPaneProps } from './XTabs';
|
|
93
|
-
export { XElevator } from './XElevator';
|
|
94
|
-
export type { XElevatorProps, XElevatorSection } from './XElevator';
|
|
95
93
|
export { default as XDropdownMenu, XDropdownMenuItem } from './XDropdownMenu';
|
|
96
94
|
export type { XDropdownMenuProps, XDropdownMenuItemProps, XMenuOption } from './XDropdownMenu';
|
|
97
95
|
export { XCalendar } from './XCalendar';
|
|
@@ -111,3 +109,8 @@ export type { XSignatureProps } from './XSignature';
|
|
|
111
109
|
export { XSignatureSkia } from './XSignature/XSignatureSkia';
|
|
112
110
|
export type { XSignatureSkiaProps, XSignatureSkiaValue, XSignatureCharacter, XSignatureStroke, } from './XSignature/XSignatureSkia';
|
|
113
111
|
export * from './XUpload';
|
|
112
|
+
export { ThemeControls } from './XTheme/ThemeControls';
|
|
113
|
+
export { useXBrandStore, setXBrand, setXBrandByName, X_BRAND_PRESETS } from './XTheme/BrandColor';
|
|
114
|
+
export type { XBrandPreset } from './XTheme/BrandColor';
|
|
115
|
+
export { XVideoPreview } from './XVideoPreview';
|
|
116
|
+
export type { XVideoPreviewProps } from './XVideoPreview';
|
package/dist/index.js
CHANGED
|
@@ -90,7 +90,7 @@ export { useXLocale, setXLocale, getXLocale, xT, xMonthTitle, X_WEEKDAYS } from
|
|
|
90
90
|
// ---------------------------------------------------------------------------
|
|
91
91
|
export { XCarousel } from './XCarousel';
|
|
92
92
|
export { XTabs, XTabPane } from './XTabs';
|
|
93
|
-
|
|
93
|
+
// XElevator 已废弃(实现质量不达标,代码保留在 src/_deprecated/),不再导出/发布
|
|
94
94
|
// ---------------------------------------------------------------------------
|
|
95
95
|
// 下拉菜单
|
|
96
96
|
// ---------------------------------------------------------------------------
|
|
@@ -125,3 +125,12 @@ export { XSignatureSkia } from './XSignature/XSignatureSkia';
|
|
|
125
125
|
// 上传(适配器模式:普通 multipart + MinIO 预签名直传)
|
|
126
126
|
// ---------------------------------------------------------------------------
|
|
127
127
|
export * from './XUpload';
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// 主题色 + 全局主题控件
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
export { ThemeControls } from './XTheme/ThemeControls';
|
|
132
|
+
export { useXBrandStore, setXBrand, setXBrandByName, X_BRAND_PRESETS } from './XTheme/BrandColor';
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// 视频预览
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
export { XVideoPreview } from './XVideoPreview';
|
package/dist/theme.d.ts
CHANGED
|
@@ -150,14 +150,6 @@ export declare function setXThemeMode(mode: XThemeMode): void;
|
|
|
150
150
|
export declare function getXThemeMode(): XThemeMode;
|
|
151
151
|
/** 订阅当前模式(React 组件里用) */
|
|
152
152
|
export declare function useXThemeMode(): XThemeMode;
|
|
153
|
-
/**
|
|
154
|
-
* 【核心 Hook】当前模式下的完整 token 集。
|
|
155
|
-
*
|
|
156
|
-
* - 返回值是模块级常量对象(lightTokens / darkTokens),
|
|
157
|
-
* 引用稳定、不会触发无谓重渲染;模式切换时组件自然重渲染取到新对象;
|
|
158
|
-
* - 解析规则:mode==='system' 时跟随 useColorScheme(),
|
|
159
|
-
* 否则用强制值(未挂 Provider 也能工作,状态全局单例)。
|
|
160
|
-
*/
|
|
161
153
|
export declare function useXTheme(): XTheme;
|
|
162
154
|
/** 当前实际生效的明暗方案(isDark 便捷判断用) */
|
|
163
155
|
export declare function useXThemeScheme(): XThemeScheme;
|
package/dist/theme.js
CHANGED
|
@@ -95,7 +95,7 @@ export const darkTokens = {
|
|
|
95
95
|
colorTextQuaternary: 'rgba(255, 255, 255, 0.25)',
|
|
96
96
|
colorTextLightSolid: '#FFFFFF',
|
|
97
97
|
colorBorder: 'rgba(255, 255, 255, 0.22)',
|
|
98
|
-
colorSplit: 'rgba(255, 255, 255, 0.
|
|
98
|
+
colorSplit: 'rgba(255, 255, 255, 0.15)',
|
|
99
99
|
colorProgressTrack: 'rgba(255, 255, 255, 0.12)',
|
|
100
100
|
colorBgContainer: '#212225',
|
|
101
101
|
colorBgLayout: '#000000',
|
|
@@ -142,16 +142,32 @@ export function useXThemeMode() {
|
|
|
142
142
|
/**
|
|
143
143
|
* 【核心 Hook】当前模式下的完整 token 集。
|
|
144
144
|
*
|
|
145
|
-
* -
|
|
146
|
-
*
|
|
147
|
-
* -
|
|
148
|
-
*
|
|
145
|
+
* - 返回值按 (scheme × 品牌色) 缓存,**引用稳定**:同组合永远返回同一对象,
|
|
146
|
+
* 避免每次渲染新对象导致订阅组件级联重渲染(曾引发日历连点卡顿);
|
|
147
|
+
* - mode==='system' 时跟随 useColorScheme(),否则用强制值;
|
|
148
|
+
* - 注入运行时品牌色(zustand XBrand)覆盖 colorPrimary 等键。
|
|
149
149
|
*/
|
|
150
|
+
import { useXBrandStore } from './XTheme/BrandColor';
|
|
151
|
+
const themeCache = new Map();
|
|
150
152
|
export function useXTheme() {
|
|
151
153
|
const mode = useXThemeModeStore(s => s.mode);
|
|
152
154
|
const systemScheme = useColorScheme();
|
|
153
155
|
const scheme = mode === 'system' ? (systemScheme === 'dark' ? 'dark' : 'light') : mode;
|
|
154
|
-
|
|
156
|
+
const brand = useXBrandStore(s => s.brand);
|
|
157
|
+
const key = `${scheme}:${brand.name}`;
|
|
158
|
+
let cached = themeCache.get(key);
|
|
159
|
+
if (!cached) {
|
|
160
|
+
const base = scheme === 'dark' ? darkTokens : lightTokens;
|
|
161
|
+
cached = {
|
|
162
|
+
...base,
|
|
163
|
+
colorPrimary: scheme === 'dark' ? brand.primaryDark : brand.primary,
|
|
164
|
+
colorPrimaryActive: scheme === 'dark' ? brand.primaryDark : brand.primary,
|
|
165
|
+
colorPrimaryBg: brand.primaryBg,
|
|
166
|
+
colorPrimaryDisabled: scheme === 'dark' ? 'rgba(255,255,255,0.18)' : 'rgba(32,128,240,0.28)',
|
|
167
|
+
};
|
|
168
|
+
themeCache.set(key, cached);
|
|
169
|
+
}
|
|
170
|
+
return cached;
|
|
155
171
|
}
|
|
156
172
|
/** 当前实际生效的明暗方案(isDark 便捷判断用) */
|
|
157
173
|
export function useXThemeScheme() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-x-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Ant Design 风格的 React Native (Expo) 组件库:表单/弹层(TopView)/日历/Skia逐字签名/上传适配器(MinIO预签名)/轻量图表,内置暗黑模式与中英双语。",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"react-native": "dist/index.js",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"react-native": ">=0.74.0",
|
|
41
41
|
"react-native-reanimated": ">=3.17.0",
|
|
42
42
|
"react-native-safe-area-context": ">=4.0.0",
|
|
43
|
-
"react-native-svg": ">=14.0.0"
|
|
43
|
+
"react-native-svg": ">=14.0.0",
|
|
44
|
+
"react-native-gesture-handler": ">=2.0.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependenciesMeta": {
|
|
46
47
|
"@shopify/react-native-skia": {
|
|
@@ -61,9 +62,6 @@
|
|
|
61
62
|
"expo-image-picker": {
|
|
62
63
|
"optional": true
|
|
63
64
|
},
|
|
64
|
-
"react-native-gesture-handler": {
|
|
65
|
-
"optional": true
|
|
66
|
-
},
|
|
67
65
|
"react-native-linear-gradient": {
|
|
68
66
|
"optional": true
|
|
69
67
|
},
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================================
|
|
3
|
-
* XElevator —— 电梯楼层/字母导航列表(duxui Elevator 的 RN 原生重写版)
|
|
4
|
-
* ============================================================================
|
|
5
|
-
*
|
|
6
|
-
* 【与 duxui Elevator 的关系】
|
|
7
|
-
* duxui 用 ScrollView + Layout 手动累加每组高度算 scrollTop;
|
|
8
|
-
* RN 端 SectionList 原生支持分组吸顶 + scrollToLocation,
|
|
9
|
-
* 这里直接用 SectionList 重写:代码量减半、吸顶/定位更稳。
|
|
10
|
-
*
|
|
11
|
-
* 【右侧导航条】
|
|
12
|
-
* - 触摸(按下/滑动)定位:容器拦截 responder 事件,用 locationY 与
|
|
13
|
-
* 各标签 onLayout 记录的 y 区间比对,命中即 scrollToLocation;
|
|
14
|
-
* - 滚动联动高亮:onViewableItemsChanged 取第一个可见项所属 section。
|
|
15
|
-
*
|
|
16
|
-
* 【API 对标 duxui】list -> sections({title, data}[])、onItemClick、
|
|
17
|
-
* showNav、renderTop/Header/Footer/Empty;扩展 navLabel(导航条短标签,
|
|
18
|
-
* 默认取 title 首字符)、keyExtractor。
|
|
19
|
-
* ============================================================================
|
|
20
|
-
*/
|
|
21
|
-
import React from 'react';
|
|
22
|
-
import { StyleProp, ViewStyle } from 'react-native';
|
|
23
|
-
export interface XElevatorSection<T = {
|
|
24
|
-
name: string;
|
|
25
|
-
}> {
|
|
26
|
-
/** 分组标题(吸顶显示) */
|
|
27
|
-
title: string;
|
|
28
|
-
/** 右侧导航条短标签,默认取 title 首字符 */
|
|
29
|
-
navLabel?: string;
|
|
30
|
-
/** 数据 */
|
|
31
|
-
data: T[];
|
|
32
|
-
}
|
|
33
|
-
export interface XElevatorProps<T> {
|
|
34
|
-
sections: XElevatorSection<T>[];
|
|
35
|
-
/** 自定义行渲染,默认显示 item.name */
|
|
36
|
-
renderItem?: (item: T, section: XElevatorSection<T>, index: number) => React.ReactNode;
|
|
37
|
-
keyExtractor?: (item: T, index: number) => string;
|
|
38
|
-
onItemClick?: (item: T, section: XElevatorSection<T>) => void;
|
|
39
|
-
/** 是否显示右侧导航条,默认 true */
|
|
40
|
-
showNav?: boolean;
|
|
41
|
-
/** 列表顶部(搜索框等) */
|
|
42
|
-
renderTop?: () => React.ReactNode;
|
|
43
|
-
/** 列表底部 */
|
|
44
|
-
renderFooter?: () => React.ReactNode;
|
|
45
|
-
/** 空态 */
|
|
46
|
-
renderEmpty?: () => React.ReactNode;
|
|
47
|
-
style?: StyleProp<ViewStyle>;
|
|
48
|
-
}
|
|
49
|
-
export declare function XElevator<T = {
|
|
50
|
-
name: string;
|
|
51
|
-
}>({ sections, renderItem, keyExtractor, onItemClick, showNav, renderTop, renderFooter, renderEmpty, style, }: XElevatorProps<T>): React.JSX.Element;
|
|
52
|
-
export default XElevator;
|