@coze-arch/cli 0.0.1-beta.5
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 +142 -0
- package/bin/main +2 -0
- package/lib/__templates__/expo/.coze +7 -0
- package/lib/__templates__/expo/.cozeproj/scripts/deploy_build.sh +109 -0
- package/lib/__templates__/expo/.cozeproj/scripts/deploy_run.sh +257 -0
- package/lib/__templates__/expo/README.md +13 -0
- package/lib/__templates__/expo/_gitignore +11 -0
- package/lib/__templates__/expo/app.json +63 -0
- package/lib/__templates__/expo/babel.config.js +9 -0
- package/lib/__templates__/expo/client/app/(tabs)/_layout.tsx +43 -0
- package/lib/__templates__/expo/client/app/(tabs)/home.tsx +1 -0
- package/lib/__templates__/expo/client/app/(tabs)/index.tsx +7 -0
- package/lib/__templates__/expo/client/app/+not-found.tsx +79 -0
- package/lib/__templates__/expo/client/app/_layout.tsx +33 -0
- package/lib/__templates__/expo/client/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/lib/__templates__/expo/client/assets/images/adaptive-icon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/default-avatar.png +0 -0
- package/lib/__templates__/expo/client/assets/images/favicon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/icon.png +0 -0
- package/lib/__templates__/expo/client/assets/images/partial-react-logo.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo@2x.png +0 -0
- package/lib/__templates__/expo/client/assets/images/react-logo@3x.png +0 -0
- package/lib/__templates__/expo/client/assets/images/splash-icon.png +0 -0
- package/lib/__templates__/expo/client/components/Screen.tsx +330 -0
- package/lib/__templates__/expo/client/components/SmartDateInput.tsx +238 -0
- package/lib/__templates__/expo/client/constants/theme.ts +118 -0
- package/lib/__templates__/expo/client/contexts/AuthContext.tsx +142 -0
- package/lib/__templates__/expo/client/hooks/useColorScheme.ts +1 -0
- package/lib/__templates__/expo/client/hooks/useTheme.ts +13 -0
- package/lib/__templates__/expo/client/index.js +11 -0
- package/lib/__templates__/expo/client/screens/home/index.tsx +54 -0
- package/lib/__templates__/expo/client/screens/home/styles.ts +332 -0
- package/lib/__templates__/expo/client/scripts/install-missing-deps.js +80 -0
- package/lib/__templates__/expo/client/utils/index.ts +55 -0
- package/lib/__templates__/expo/eslint-formatter-simple.mjs +49 -0
- package/lib/__templates__/expo/eslint.config.mjs +98 -0
- package/lib/__templates__/expo/metro.config.js +53 -0
- package/lib/__templates__/expo/package.json +100 -0
- package/lib/__templates__/expo/pnpm-lock.yaml +13978 -0
- package/lib/__templates__/expo/src/index.ts +12 -0
- package/lib/__templates__/expo/template.config.js +49 -0
- package/lib/__templates__/expo/tsconfig.json +24 -0
- package/lib/__templates__/nextjs/.coze +11 -0
- package/lib/__templates__/nextjs/.vscode/settings.json +121 -0
- package/lib/__templates__/nextjs/README.md +36 -0
- package/lib/__templates__/nextjs/_gitignore +99 -0
- package/lib/__templates__/nextjs/_npmrc +22 -0
- package/lib/__templates__/nextjs/eslint.config.mjs +18 -0
- package/lib/__templates__/nextjs/next-env.d.ts +6 -0
- package/lib/__templates__/nextjs/next.config.ts +7 -0
- package/lib/__templates__/nextjs/package.json +32 -0
- package/lib/__templates__/nextjs/pnpm-lock.yaml +4061 -0
- package/lib/__templates__/nextjs/postcss.config.mjs +7 -0
- package/lib/__templates__/nextjs/public/file.svg +1 -0
- package/lib/__templates__/nextjs/public/globe.svg +1 -0
- package/lib/__templates__/nextjs/public/next.svg +1 -0
- package/lib/__templates__/nextjs/public/vercel.svg +1 -0
- package/lib/__templates__/nextjs/public/window.svg +1 -0
- package/lib/__templates__/nextjs/scripts/build.sh +14 -0
- package/lib/__templates__/nextjs/scripts/dev.sh +51 -0
- package/lib/__templates__/nextjs/scripts/start.sh +15 -0
- package/lib/__templates__/nextjs/src/app/favicon.ico +0 -0
- package/lib/__templates__/nextjs/src/app/globals.css +26 -0
- package/lib/__templates__/nextjs/src/app/layout.tsx +34 -0
- package/lib/__templates__/nextjs/src/app/page.tsx +66 -0
- package/lib/__templates__/nextjs/template.config.js +55 -0
- package/lib/__templates__/nextjs/tsconfig.json +34 -0
- package/lib/__templates__/react-rsbuild/.coze +11 -0
- package/lib/__templates__/react-rsbuild/.vscode/settings.json +121 -0
- package/lib/__templates__/react-rsbuild/README.md +61 -0
- package/lib/__templates__/react-rsbuild/_gitignore +97 -0
- package/lib/__templates__/react-rsbuild/_npmrc +22 -0
- package/lib/__templates__/react-rsbuild/package.json +31 -0
- package/lib/__templates__/react-rsbuild/pnpm-lock.yaml +997 -0
- package/lib/__templates__/react-rsbuild/rsbuild.config.ts +13 -0
- package/lib/__templates__/react-rsbuild/scripts/build.sh +14 -0
- package/lib/__templates__/react-rsbuild/scripts/dev.sh +51 -0
- package/lib/__templates__/react-rsbuild/scripts/start.sh +15 -0
- package/lib/__templates__/react-rsbuild/src/App.tsx +60 -0
- package/lib/__templates__/react-rsbuild/src/index.css +21 -0
- package/lib/__templates__/react-rsbuild/src/index.html +12 -0
- package/lib/__templates__/react-rsbuild/src/index.tsx +16 -0
- package/lib/__templates__/react-rsbuild/tailwind.config.js +9 -0
- package/lib/__templates__/react-rsbuild/template.config.js +54 -0
- package/lib/__templates__/react-rsbuild/tsconfig.json +17 -0
- package/lib/__templates__/rsbuild/.coze +11 -0
- package/lib/__templates__/rsbuild/.vscode/settings.json +7 -0
- package/lib/__templates__/rsbuild/README.md +61 -0
- package/lib/__templates__/rsbuild/_gitignore +97 -0
- package/lib/__templates__/rsbuild/_npmrc +22 -0
- package/lib/__templates__/rsbuild/package.json +24 -0
- package/lib/__templates__/rsbuild/pnpm-lock.yaml +888 -0
- package/lib/__templates__/rsbuild/rsbuild.config.ts +12 -0
- package/lib/__templates__/rsbuild/scripts/build.sh +14 -0
- package/lib/__templates__/rsbuild/scripts/dev.sh +51 -0
- package/lib/__templates__/rsbuild/scripts/start.sh +15 -0
- package/lib/__templates__/rsbuild/src/index.css +21 -0
- package/lib/__templates__/rsbuild/src/index.html +12 -0
- package/lib/__templates__/rsbuild/src/index.ts +5 -0
- package/lib/__templates__/rsbuild/src/main.ts +65 -0
- package/lib/__templates__/rsbuild/tailwind.config.js +9 -0
- package/lib/__templates__/rsbuild/template.config.js +54 -0
- package/lib/__templates__/rsbuild/tsconfig.json +16 -0
- package/lib/__templates__/templates.json +100 -0
- package/lib/__templates__/vite/.coze +11 -0
- package/lib/__templates__/vite/.vscode/settings.json +7 -0
- package/lib/__templates__/vite/README.md +61 -0
- package/lib/__templates__/vite/_gitignore +66 -0
- package/lib/__templates__/vite/_npmrc +22 -0
- package/lib/__templates__/vite/index.html +13 -0
- package/lib/__templates__/vite/package.json +24 -0
- package/lib/__templates__/vite/pnpm-lock.yaml +1249 -0
- package/lib/__templates__/vite/postcss.config.js +6 -0
- package/lib/__templates__/vite/scripts/build.sh +14 -0
- package/lib/__templates__/vite/scripts/dev.sh +51 -0
- package/lib/__templates__/vite/scripts/start.sh +15 -0
- package/lib/__templates__/vite/src/index.css +21 -0
- package/lib/__templates__/vite/src/index.ts +5 -0
- package/lib/__templates__/vite/src/main.ts +65 -0
- package/lib/__templates__/vite/tailwind.config.js +9 -0
- package/lib/__templates__/vite/template.config.js +55 -0
- package/lib/__templates__/vite/tsconfig.json +16 -0
- package/lib/__templates__/vite/vite.config.ts +15 -0
- package/lib/cli.js +1575 -0
- package/package.json +70 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Keyboard,
|
|
8
|
+
Platform,
|
|
9
|
+
useColorScheme,
|
|
10
|
+
ViewStyle,
|
|
11
|
+
TextStyle
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import DateTimePickerModal from 'react-native-modal-datetime-picker';
|
|
14
|
+
import dayjs from 'dayjs';
|
|
15
|
+
import { FontAwesome6 } from '@expo/vector-icons';
|
|
16
|
+
|
|
17
|
+
// --------------------------------------------------------
|
|
18
|
+
// 1. 配置 Dayjs
|
|
19
|
+
// --------------------------------------------------------
|
|
20
|
+
// 即使服务端返回 '2023-10-20T10:00:00Z' (UTC),
|
|
21
|
+
// dayjs(utcString).format() 会自动转为手机当前的本地时区显示。
|
|
22
|
+
// 如果需要传回给后端,我们再转回 ISO 格式。
|
|
23
|
+
|
|
24
|
+
interface SmartDateInputProps {
|
|
25
|
+
label?: string; // 表单标题 (可选)
|
|
26
|
+
value?: string | null; // 服务端返回的时间字符串 (ISO 8601, 带 T)
|
|
27
|
+
onChange: (isoDate: string) => void; // 回调给父组件的值,依然是标准 ISO 字符串
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
mode?: 'date' | 'time' | 'datetime'; // 支持日期、时间、或两者
|
|
30
|
+
displayFormat?: string; // UI展示的格式,默认 YYYY-MM-DD
|
|
31
|
+
error?: string; // 错误信息
|
|
32
|
+
|
|
33
|
+
// 样式自定义(可选)
|
|
34
|
+
containerStyle?: ViewStyle; // 外层容器样式
|
|
35
|
+
inputStyle?: ViewStyle; // 输入框样式
|
|
36
|
+
textStyle?: TextStyle; // 文字样式
|
|
37
|
+
labelStyle?: TextStyle; // 标签样式
|
|
38
|
+
placeholderTextStyle?: TextStyle; // 占位符文字样式
|
|
39
|
+
errorTextStyle?: TextStyle; // 错误信息文字样式
|
|
40
|
+
iconColor?: string; // 图标颜色
|
|
41
|
+
iconSize?: number; // 图标大小
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const SmartDateInput = ({
|
|
45
|
+
label,
|
|
46
|
+
value,
|
|
47
|
+
onChange,
|
|
48
|
+
placeholder = '请选择',
|
|
49
|
+
mode = 'date',
|
|
50
|
+
displayFormat,
|
|
51
|
+
error,
|
|
52
|
+
containerStyle,
|
|
53
|
+
inputStyle,
|
|
54
|
+
textStyle,
|
|
55
|
+
labelStyle,
|
|
56
|
+
placeholderTextStyle,
|
|
57
|
+
errorTextStyle,
|
|
58
|
+
iconColor,
|
|
59
|
+
iconSize = 18
|
|
60
|
+
}: SmartDateInputProps) => {
|
|
61
|
+
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
|
|
62
|
+
const colorScheme = useColorScheme();
|
|
63
|
+
const isDark = colorScheme === 'dark';
|
|
64
|
+
|
|
65
|
+
// 默认展示格式
|
|
66
|
+
const format = displayFormat || (mode === 'time' ? 'HH:mm' : 'YYYY-MM-DD');
|
|
67
|
+
|
|
68
|
+
// --------------------------------------------------------
|
|
69
|
+
// 2. 核心:数据转换逻辑
|
|
70
|
+
// --------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
// 解析服务端值,确保无效值不传给控件;time 模式兼容仅时间字符串
|
|
73
|
+
const parsedValue = useMemo(() => {
|
|
74
|
+
if (!value) return null;
|
|
75
|
+
|
|
76
|
+
const direct = dayjs(value);
|
|
77
|
+
if (direct.isValid()) return direct;
|
|
78
|
+
|
|
79
|
+
if (mode === 'time') {
|
|
80
|
+
const timeOnly = dayjs(`1970-01-01T${value}`);
|
|
81
|
+
if (timeOnly.isValid()) return timeOnly;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}, [value, mode]);
|
|
86
|
+
|
|
87
|
+
// A. 将字符串转为 JS Date 对象给控件使用
|
|
88
|
+
// 如果 value 是空或无效,回退到当前时间
|
|
89
|
+
const dateObjectForPicker = useMemo(() => {
|
|
90
|
+
return parsedValue ? parsedValue.toDate() : new Date();
|
|
91
|
+
}, [parsedValue]);
|
|
92
|
+
|
|
93
|
+
// B. 将 Date 对象转为展示字符串
|
|
94
|
+
const displayString = useMemo(() => {
|
|
95
|
+
if (!parsedValue) return '';
|
|
96
|
+
return parsedValue.format(format);
|
|
97
|
+
}, [parsedValue, format]);
|
|
98
|
+
|
|
99
|
+
// --------------------------------------------------------
|
|
100
|
+
// 3. 核心:交互逻辑 (解决键盘遮挡/无法收起)
|
|
101
|
+
// --------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
const showDatePicker = () => {
|
|
104
|
+
// 【关键点】打开日期控件前,必须强制收起键盘!
|
|
105
|
+
// 否则键盘会遮挡 iOS 的底部滚轮,或者导致 Android 焦点混乱
|
|
106
|
+
Keyboard.dismiss();
|
|
107
|
+
setDatePickerVisibility(true);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const hideDatePicker = () => {
|
|
111
|
+
setDatePickerVisibility(false);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleConfirm = (date: Date) => {
|
|
115
|
+
hideDatePicker();
|
|
116
|
+
// 采用带本地偏移的 ISO 字符串,避免 date 模式在非 UTC 时区出现跨天
|
|
117
|
+
const serverString = dayjs(date).format(format);
|
|
118
|
+
onChange(serverString);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// 根据 mode 选择图标
|
|
122
|
+
const iconName = mode === 'time' ? 'clock' : 'calendar';
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<View style={[styles.container, containerStyle]}>
|
|
126
|
+
{/* 标题 */}
|
|
127
|
+
{label && <Text style={[styles.label, labelStyle]}>{label}</Text>}
|
|
128
|
+
|
|
129
|
+
{/*
|
|
130
|
+
这里用 TouchableOpacity 模拟 Input。
|
|
131
|
+
模拟组件永远不会唤起键盘。
|
|
132
|
+
*/}
|
|
133
|
+
<TouchableOpacity
|
|
134
|
+
style={[
|
|
135
|
+
styles.inputBox,
|
|
136
|
+
error ? styles.inputBoxError : null,
|
|
137
|
+
inputStyle
|
|
138
|
+
]}
|
|
139
|
+
onPress={showDatePicker}
|
|
140
|
+
activeOpacity={0.7}
|
|
141
|
+
>
|
|
142
|
+
<Text
|
|
143
|
+
style={[
|
|
144
|
+
styles.text,
|
|
145
|
+
textStyle,
|
|
146
|
+
!value && styles.placeholder,
|
|
147
|
+
!value && placeholderTextStyle
|
|
148
|
+
]}
|
|
149
|
+
numberOfLines={1}
|
|
150
|
+
>
|
|
151
|
+
{displayString || placeholder}
|
|
152
|
+
</Text>
|
|
153
|
+
|
|
154
|
+
<FontAwesome6
|
|
155
|
+
name={iconName}
|
|
156
|
+
size={iconSize}
|
|
157
|
+
color={iconColor || (value ? '#4B5563' : '#9CA3AF')}
|
|
158
|
+
style={styles.icon}
|
|
159
|
+
/>
|
|
160
|
+
</TouchableOpacity>
|
|
161
|
+
|
|
162
|
+
{error && <Text style={[styles.errorText, errorTextStyle]}>{error}</Text>}
|
|
163
|
+
|
|
164
|
+
{/*
|
|
165
|
+
DateTimePickerModal 是 React Native Modal。
|
|
166
|
+
它会覆盖在所有 View 之上。
|
|
167
|
+
*/}
|
|
168
|
+
<DateTimePickerModal
|
|
169
|
+
isVisible={isDatePickerVisible}
|
|
170
|
+
mode={mode}
|
|
171
|
+
date={dateObjectForPicker} // 传入 Date 对象
|
|
172
|
+
onConfirm={handleConfirm}
|
|
173
|
+
onCancel={hideDatePicker}
|
|
174
|
+
// iOS 只有用这个 display 样式才最稳,避免乱七八糟的 inline 样式
|
|
175
|
+
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
|
176
|
+
// 自动适配系统深色模式,或者根据 isDark 变量控制
|
|
177
|
+
isDarkModeEnabled={isDark}
|
|
178
|
+
// 强制使用中文环境
|
|
179
|
+
locale="zh-CN"
|
|
180
|
+
confirmTextIOS="确定"
|
|
181
|
+
cancelTextIOS="取消"
|
|
182
|
+
/>
|
|
183
|
+
</View>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// 设计样式
|
|
188
|
+
const styles = StyleSheet.create({
|
|
189
|
+
container: {
|
|
190
|
+
marginBottom: 20,
|
|
191
|
+
},
|
|
192
|
+
label: {
|
|
193
|
+
fontSize: 14,
|
|
194
|
+
fontWeight: '600',
|
|
195
|
+
color: '#374151', // Gray 700
|
|
196
|
+
marginBottom: 8,
|
|
197
|
+
marginLeft: 2,
|
|
198
|
+
},
|
|
199
|
+
inputBox: {
|
|
200
|
+
height: 52, // 增加高度提升触控体验
|
|
201
|
+
backgroundColor: '#FFFFFF',
|
|
202
|
+
borderRadius: 12, // 更圆润的角
|
|
203
|
+
flexDirection: 'row',
|
|
204
|
+
alignItems: 'center',
|
|
205
|
+
justifyContent: 'space-between',
|
|
206
|
+
paddingHorizontal: 16,
|
|
207
|
+
borderWidth: 1,
|
|
208
|
+
borderColor: '#E5E7EB', // Gray 200
|
|
209
|
+
// 增加轻微阴影提升层次感 (iOS)
|
|
210
|
+
shadowColor: '#000',
|
|
211
|
+
shadowOffset: { width: 0, height: 1 },
|
|
212
|
+
shadowOpacity: 0.05,
|
|
213
|
+
shadowRadius: 2,
|
|
214
|
+
// Android
|
|
215
|
+
elevation: 1,
|
|
216
|
+
},
|
|
217
|
+
inputBoxError: {
|
|
218
|
+
borderColor: '#EF4444', // Red 500
|
|
219
|
+
backgroundColor: '#FEF2F2', // Red 50
|
|
220
|
+
},
|
|
221
|
+
text: {
|
|
222
|
+
fontSize: 16,
|
|
223
|
+
color: '#111827', // Gray 900
|
|
224
|
+
flex: 1,
|
|
225
|
+
},
|
|
226
|
+
placeholder: {
|
|
227
|
+
color: '#9CA3AF', // Gray 400 - 标准占位符颜色
|
|
228
|
+
},
|
|
229
|
+
icon: {
|
|
230
|
+
marginLeft: 12,
|
|
231
|
+
},
|
|
232
|
+
errorText: {
|
|
233
|
+
marginTop: 4,
|
|
234
|
+
marginLeft: 2,
|
|
235
|
+
fontSize: 12,
|
|
236
|
+
color: '#EF4444',
|
|
237
|
+
}
|
|
238
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
const tintColorLight = "#007AFF";
|
|
4
|
+
const tintColorDark = "#0A84FF";
|
|
5
|
+
|
|
6
|
+
export const Colors = {
|
|
7
|
+
light: {
|
|
8
|
+
text: "#11181C",
|
|
9
|
+
buttonText: "#FFFFFF",
|
|
10
|
+
tabIconDefault: "#687076",
|
|
11
|
+
tabIconSelected: tintColorLight,
|
|
12
|
+
link: "#007AFF",
|
|
13
|
+
backgroundRoot: "#FFFFFF", // Elevation 0
|
|
14
|
+
backgroundDefault: "#F2F2F2", // Elevation 1
|
|
15
|
+
backgroundSecondary: "#E6E6E6", // Elevation 2
|
|
16
|
+
backgroundTertiary: "#D9D9D9", // Elevation 3
|
|
17
|
+
},
|
|
18
|
+
dark: {
|
|
19
|
+
text: "#ECEDEE",
|
|
20
|
+
buttonText: "#FFFFFF",
|
|
21
|
+
tabIconDefault: "#9BA1A6",
|
|
22
|
+
tabIconSelected: tintColorDark,
|
|
23
|
+
link: "#0A84FF",
|
|
24
|
+
backgroundRoot: "#1F2123", // Elevation 0
|
|
25
|
+
backgroundDefault: "#2A2C2E", // Elevation 1
|
|
26
|
+
backgroundSecondary: "#353739", // Elevation 2
|
|
27
|
+
backgroundTertiary: "#404244", // Elevation 3
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Spacing = {
|
|
32
|
+
xs: 4,
|
|
33
|
+
sm: 8,
|
|
34
|
+
md: 12,
|
|
35
|
+
lg: 16,
|
|
36
|
+
xl: 20,
|
|
37
|
+
"2xl": 24,
|
|
38
|
+
"3xl": 32,
|
|
39
|
+
"4xl": 40,
|
|
40
|
+
"5xl": 48,
|
|
41
|
+
inputHeight: 48,
|
|
42
|
+
buttonHeight: 52,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const BorderRadius = {
|
|
46
|
+
xs: 8,
|
|
47
|
+
sm: 12,
|
|
48
|
+
md: 18,
|
|
49
|
+
lg: 24,
|
|
50
|
+
xl: 30,
|
|
51
|
+
"2xl": 40,
|
|
52
|
+
"3xl": 50,
|
|
53
|
+
full: 9999,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Typography = {
|
|
57
|
+
h1: {
|
|
58
|
+
fontSize: 32,
|
|
59
|
+
lineHeight: 40,
|
|
60
|
+
fontWeight: "700" as const,
|
|
61
|
+
},
|
|
62
|
+
h2: {
|
|
63
|
+
fontSize: 28,
|
|
64
|
+
lineHeight: 36,
|
|
65
|
+
fontWeight: "700" as const,
|
|
66
|
+
},
|
|
67
|
+
h3: {
|
|
68
|
+
fontSize: 24,
|
|
69
|
+
lineHeight: 32,
|
|
70
|
+
fontWeight: "600" as const,
|
|
71
|
+
},
|
|
72
|
+
h4: {
|
|
73
|
+
fontSize: 20,
|
|
74
|
+
lineHeight: 28,
|
|
75
|
+
fontWeight: "600" as const,
|
|
76
|
+
},
|
|
77
|
+
body: {
|
|
78
|
+
fontSize: 16,
|
|
79
|
+
lineHeight: 24,
|
|
80
|
+
fontWeight: "400" as const,
|
|
81
|
+
},
|
|
82
|
+
small: {
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
lineHeight: 20,
|
|
85
|
+
fontWeight: "400" as const,
|
|
86
|
+
},
|
|
87
|
+
link: {
|
|
88
|
+
fontSize: 16,
|
|
89
|
+
lineHeight: 24,
|
|
90
|
+
fontWeight: "400" as const,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const Fonts = Platform.select({
|
|
95
|
+
ios: {
|
|
96
|
+
/** iOS `UIFontDescriptorSystemDesignDefault` */
|
|
97
|
+
sans: "system-ui",
|
|
98
|
+
/** iOS `UIFontDescriptorSystemDesignSerif` */
|
|
99
|
+
serif: "ui-serif",
|
|
100
|
+
/** iOS `UIFontDescriptorSystemDesignRounded` */
|
|
101
|
+
rounded: "ui-rounded",
|
|
102
|
+
/** iOS `UIFontDescriptorSystemDesignMonospaced` */
|
|
103
|
+
mono: "ui-monospace",
|
|
104
|
+
},
|
|
105
|
+
default: {
|
|
106
|
+
sans: "normal",
|
|
107
|
+
serif: "serif",
|
|
108
|
+
rounded: "normal",
|
|
109
|
+
mono: "monospace",
|
|
110
|
+
},
|
|
111
|
+
web: {
|
|
112
|
+
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
113
|
+
serif: "Georgia, 'Times New Roman', serif",
|
|
114
|
+
rounded:
|
|
115
|
+
"'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
|
|
116
|
+
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* 通用认证上下文
|
|
4
|
+
*
|
|
5
|
+
* 基于固定的 API 接口实现,可复用到其他项目
|
|
6
|
+
* 其他项目使用时,只需修改 @api 的导入路径指向项目的 api 模块
|
|
7
|
+
*/
|
|
8
|
+
import React, {
|
|
9
|
+
createContext,
|
|
10
|
+
useContext,
|
|
11
|
+
useState,
|
|
12
|
+
useEffect,
|
|
13
|
+
ReactNode,
|
|
14
|
+
} from "react";
|
|
15
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
16
|
+
//import { UserOut, UsersService, AuthenticationService } from "@api";
|
|
17
|
+
|
|
18
|
+
interface UserOut {
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AuthContextType {
|
|
23
|
+
user: UserOut | null;
|
|
24
|
+
token: string | null;
|
|
25
|
+
isAuthenticated: boolean;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
login: (token: string) => Promise<void>;
|
|
28
|
+
logout: () => Promise<void>;
|
|
29
|
+
updateUser: (userData: Partial<UserOut>) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
33
|
+
|
|
34
|
+
export const AuthProvider: React.FC<{ children: ReactNode }> = ({
|
|
35
|
+
children,
|
|
36
|
+
}) => {
|
|
37
|
+
const [user, setUser] = useState<UserOut | null>(null);
|
|
38
|
+
const [token, setToken] = useState<string | null>(null);
|
|
39
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
loadAuthData();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const loadAuthData = async () => {
|
|
46
|
+
try {
|
|
47
|
+
const results = await AsyncStorage.multiGet(["access_token", "user_data"]);
|
|
48
|
+
const storedToken = results?.[0]?.[1] ?? null;
|
|
49
|
+
const storedUser = results?.[1]?.[1] ?? null;
|
|
50
|
+
|
|
51
|
+
if (!storedToken) {
|
|
52
|
+
setToken(null);
|
|
53
|
+
setUser(null);
|
|
54
|
+
await AsyncStorage.multiRemove(["access_token", "user_data"]);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (storedToken && storedUser) {
|
|
59
|
+
setToken(storedToken);
|
|
60
|
+
setUser(JSON.parse(storedUser));
|
|
61
|
+
} else if (storedToken && !storedUser) {
|
|
62
|
+
// 若仅有 token,主动拉取当前用户信息
|
|
63
|
+
setToken(storedToken);
|
|
64
|
+
try {
|
|
65
|
+
// const me = await UsersService.getCurrentUserApiV1UsersMeGet();
|
|
66
|
+
//if (me?.success && me.data) {
|
|
67
|
+
// setUser(me.data);
|
|
68
|
+
// await AsyncStorage.setItem("user_data", JSON.stringify(me.data));
|
|
69
|
+
//}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// 拉取失败则保持未登录状态
|
|
72
|
+
console.error("Failed to fetch current user with stored token:", e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("Failed to load auth data:", error);
|
|
77
|
+
} finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const login = async (newToken: string) => {
|
|
83
|
+
try {
|
|
84
|
+
setToken(newToken);
|
|
85
|
+
// 统一写入 access_token,供 OpenAPI 读取并自动携带
|
|
86
|
+
await AsyncStorage.setItem("access_token", newToken);
|
|
87
|
+
// 登录后拉取当前用户并缓存
|
|
88
|
+
try {
|
|
89
|
+
const me = await UsersService.getCurrentUserApiV1UsersMeGet();
|
|
90
|
+
if (me?.success && me.data) {
|
|
91
|
+
setUser(me.data);
|
|
92
|
+
await AsyncStorage.setItem("user_data", JSON.stringify(me.data));
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error("Fetch current user after login failed:", e);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Login failed:", error);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const logout = async () => {
|
|
104
|
+
try {
|
|
105
|
+
// await AuthenticationService.logoutApiV1AuthLogoutPost(true);
|
|
106
|
+
} catch(error) {
|
|
107
|
+
console.warn('Logout failed:', error);
|
|
108
|
+
}
|
|
109
|
+
// remove token
|
|
110
|
+
setToken(null);
|
|
111
|
+
setUser(null);
|
|
112
|
+
await AsyncStorage.multiRemove(["auth_token", "access_token", "user_data"]);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const updateUser = (userData: Partial<UserOut>) => {
|
|
116
|
+
if (user) {
|
|
117
|
+
const updatedUser = { ...user, ...userData };
|
|
118
|
+
setUser(updatedUser);
|
|
119
|
+
AsyncStorage.setItem("user_data", JSON.stringify(updatedUser));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const value: AuthContextType = {
|
|
124
|
+
user,
|
|
125
|
+
token,
|
|
126
|
+
isAuthenticated: !!token && !!user,
|
|
127
|
+
isLoading,
|
|
128
|
+
login,
|
|
129
|
+
logout,
|
|
130
|
+
updateUser,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const useAuth = (): AuthContextType => {
|
|
137
|
+
const context = useContext(AuthContext);
|
|
138
|
+
if (context === undefined) {
|
|
139
|
+
throw new Error("useAuth must be used within an AuthProvider");
|
|
140
|
+
}
|
|
141
|
+
return context;
|
|
142
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useColorScheme } from "react-native";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Colors } from "@/constants/theme";
|
|
2
|
+
import { useColorScheme } from "@/hooks/useColorScheme";
|
|
3
|
+
|
|
4
|
+
export function useTheme() {
|
|
5
|
+
const colorScheme = useColorScheme();
|
|
6
|
+
const isDark = colorScheme === "dark";
|
|
7
|
+
const theme = Colors[colorScheme ?? "light"];
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
theme,
|
|
11
|
+
isDark,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { registerRootComponent } from 'expo';
|
|
2
|
+
import { ExpoRoot } from 'expo-router';
|
|
3
|
+
|
|
4
|
+
// 显式定义 App 组件
|
|
5
|
+
export function App() {
|
|
6
|
+
// eslint-disable-next-line no-undef
|
|
7
|
+
const ctx = require.context('./app');
|
|
8
|
+
return <ExpoRoot context={ctx} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
registerRootComponent(App);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
ScrollView,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
RefreshControl,
|
|
8
|
+
Image,
|
|
9
|
+
ActivityIndicator,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useRouter, useFocusEffect } from 'expo-router';
|
|
12
|
+
import { FontAwesome6 } from '@expo/vector-icons';
|
|
13
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
14
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
15
|
+
|
|
16
|
+
import { Screen } from '@/components/Screen';
|
|
17
|
+
|
|
18
|
+
import styles from './styles';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export default function HomeScreen() {
|
|
22
|
+
const insets = useSafeAreaInsets();
|
|
23
|
+
const { theme, isDark } = useTheme();
|
|
24
|
+
|
|
25
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
26
|
+
|
|
27
|
+
const loadData = useCallback(async () => {
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
useFocusEffect(
|
|
31
|
+
useCallback(() => {
|
|
32
|
+
loadData();
|
|
33
|
+
}, [loadData])
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const handleRefresh = useCallback(() => {
|
|
37
|
+
setIsRefreshing(true);
|
|
38
|
+
loadData();
|
|
39
|
+
}, [loadData]);
|
|
40
|
+
return (
|
|
41
|
+
<Screen backgroundColor={theme.backgroundRoot} statusBarStyle={isDark ? 'light' : 'dark'}>
|
|
42
|
+
<ScrollView
|
|
43
|
+
showsVerticalScrollIndicator={false}
|
|
44
|
+
contentContainerStyle={{}}
|
|
45
|
+
refreshControl={
|
|
46
|
+
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} colors={['#2563EB']} />
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
<View style={[styles.header]}>
|
|
50
|
+
</View>
|
|
51
|
+
</ScrollView>
|
|
52
|
+
</Screen>
|
|
53
|
+
);
|
|
54
|
+
}
|