59 changed files with 3399 additions and 573 deletions
@ -1,4 +1,7 @@
|
||||
# 开发环境配置 |
||||
EXPO_PUBLIC_API_URL=/ |
||||
# EXPO_PUBLIC_API_URL=/api # 注释掉,让 config.ts 根据平台自动选择 |
||||
EXPO_PUBLIC_API_TIMEOUT=10000 |
||||
|
||||
# 测试环境的域名 |
||||
API_TARGET=https://51zhh5.notbug.org |
||||
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
import { StyleSheet, ScrollView } from 'react-native'; |
||||
import { Stack, useRouter } from 'expo-router'; |
||||
import { ThemedText, ThemedView } from '@/components'; |
||||
|
||||
/** |
||||
* 测试页面 |
||||
*
|
||||
* 这是一个独立的业务页面示例,不包含底部 tabs |
||||
*
|
||||
* 特点: |
||||
* - 带有返回按钮的 header |
||||
* - 不包含底部导航栏 |
||||
* - 可以作为业务页面的模板 |
||||
*/ |
||||
export default function TestPage() { |
||||
const router = useRouter(); |
||||
|
||||
return ( |
||||
<> |
||||
{/* 配置页面 header */} |
||||
<Stack.Screen |
||||
options={{ |
||||
title: '测试页面', |
||||
headerShown: true, |
||||
headerBackTitle: '返回', |
||||
}} |
||||
/> |
||||
|
||||
<ThemedView style={styles.container}> |
||||
<ScrollView style={styles.scrollView}> |
||||
<ThemedView style={styles.content}> |
||||
<ThemedText type="title" style={styles.title}> |
||||
测试页面 |
||||
</ThemedText> |
||||
|
||||
<ThemedText style={styles.description}> |
||||
这是一个独立的业务页面示例,展示了如何创建不包含底部 tabs 的页面。 |
||||
</ThemedText> |
||||
|
||||
<ThemedView style={styles.section}> |
||||
<ThemedText type="subtitle">页面特点</ThemedText> |
||||
<ThemedText style={styles.item}>✅ 带有返回按钮的 header</ThemedText> |
||||
<ThemedText style={styles.item}>✅ 不包含底部导航栏</ThemedText> |
||||
<ThemedText style={styles.item}>✅ 支持主题切换</ThemedText> |
||||
<ThemedText style={styles.item}>✅ 可以作为业务页面模板</ThemedText> |
||||
</ThemedView> |
||||
|
||||
<ThemedView style={styles.section}> |
||||
<ThemedText type="subtitle">使用场景</ThemedText> |
||||
<ThemedText style={styles.item}>• 详情页面</ThemedText> |
||||
<ThemedText style={styles.item}>• 表单页面</ThemedText> |
||||
<ThemedText style={styles.item}>• 设置页面</ThemedText> |
||||
<ThemedText style={styles.item}>• 其他业务页面</ThemedText> |
||||
</ThemedView> |
||||
|
||||
<ThemedView style={styles.section}> |
||||
<ThemedText type="subtitle">路由说明</ThemedText> |
||||
<ThemedText style={styles.item}> |
||||
文件路径: app/test-page.tsx |
||||
</ThemedText> |
||||
<ThemedText style={styles.item}> |
||||
访问路径: /test-page |
||||
</ThemedText> |
||||
<ThemedText style={styles.item}> |
||||
跳转方式: router.push('/test-page') |
||||
</ThemedText> |
||||
</ThemedView> |
||||
|
||||
<ThemedView style={styles.infoBox}> |
||||
<ThemedText type="defaultSemiBold">💡 提示</ThemedText> |
||||
<ThemedText style={styles.infoText}> |
||||
对于复杂的业务页面,建议在 screens/ 目录下创建独立的组件, |
||||
然后在 app/ 目录下的路由文件中引用。 |
||||
</ThemedText> |
||||
</ThemedView> |
||||
</ThemedView> |
||||
</ScrollView> |
||||
</ThemedView> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
const styles = StyleSheet.create({ |
||||
container: { |
||||
flex: 1, |
||||
}, |
||||
scrollView: { |
||||
flex: 1, |
||||
}, |
||||
content: { |
||||
padding: 20, |
||||
}, |
||||
title: { |
||||
marginBottom: 16, |
||||
}, |
||||
description: { |
||||
marginBottom: 24, |
||||
lineHeight: 24, |
||||
}, |
||||
section: { |
||||
marginBottom: 24, |
||||
}, |
||||
item: { |
||||
marginTop: 8, |
||||
marginLeft: 8, |
||||
lineHeight: 24, |
||||
}, |
||||
infoBox: { |
||||
padding: 16, |
||||
borderRadius: 8, |
||||
backgroundColor: 'rgba(0, 122, 255, 0.1)', |
||||
marginTop: 8, |
||||
}, |
||||
infoText: { |
||||
marginTop: 8, |
||||
lineHeight: 22, |
||||
}, |
||||
}); |
||||
|
||||
@ -0,0 +1,16 @@
|
||||
/** |
||||
* Components 统一导出 |
||||
*/ |
||||
|
||||
// 主题组件
|
||||
export { ThemedText, ThemedView, Text, View, useThemeColor } from './Themed'; |
||||
export type { ThemedTextProps, ThemedViewProps, TextProps, ViewProps } from './Themed'; |
||||
|
||||
// 主题演示
|
||||
export { ThemeDemo } from './ThemeDemo'; |
||||
|
||||
// 工具组件
|
||||
export { ExternalLink } from './ExternalLink'; |
||||
export { MonoText } from './StyledText'; |
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
// This function is web-only as native doesn't currently support server (or build-time) rendering.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C { |
||||
return client; |
||||
} |
||||
@ -1,12 +0,0 @@
|
||||
import React from 'react'; |
||||
|
||||
// `useEffect` is not invoked during server rendering, meaning
|
||||
// we can use this to determine if we're on the server or not.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C { |
||||
const [value, setValue] = React.useState<S | C>(server); |
||||
React.useEffect(() => { |
||||
setValue(client); |
||||
}, [client]); |
||||
|
||||
return value; |
||||
} |
||||
@ -1 +0,0 @@
|
||||
export { useColorScheme } from 'react-native'; |
||||
@ -1,8 +0,0 @@
|
||||
// NOTE: The default React Native styling doesn't support server rendering.
|
||||
// Server rendered styles should not change between the first render of the HTML
|
||||
// and the first render on the client. Typically, web developers will use CSS media queries
|
||||
// to render different styles on the client and server, these aren't directly supported in React Native
|
||||
// but can be achieved using a styling library like Nativewind.
|
||||
export function useColorScheme() { |
||||
return 'light'; |
||||
} |
||||
@ -1,19 +1,112 @@
|
||||
const tintColorLight = '#2f95dc'; |
||||
const tintColorDark = '#fff'; |
||||
/** |
||||
* 主题颜色配置 |
||||
* |
||||
* 支持 light 和 dark 两种主题 |
||||
* 可以通过 settingsStore 切换主题 |
||||
*/ |
||||
|
||||
const tintColorLight = '#007AFF'; |
||||
const tintColorDark = '#0A84FF'; |
||||
|
||||
export default { |
||||
light: { |
||||
text: '#000', |
||||
background: '#fff', |
||||
// 文本颜色
|
||||
text: '#000000', |
||||
textSecondary: '#666666', |
||||
textTertiary: '#999999', |
||||
textInverse: '#FFFFFF', |
||||
|
||||
// 背景颜色
|
||||
background: '#FFFFFF', |
||||
backgroundSecondary: '#F5F5F5', |
||||
backgroundTertiary: '#E5E5E5', |
||||
|
||||
// 主题色
|
||||
tint: tintColorLight, |
||||
tabIconDefault: '#ccc', |
||||
primary: '#007AFF', |
||||
secondary: '#5856D6', |
||||
success: '#34C759', |
||||
warning: '#FF9500', |
||||
error: '#FF3B30', |
||||
info: '#5AC8FA', |
||||
|
||||
// 边框颜色
|
||||
border: '#E5E5E5', |
||||
borderSecondary: '#D1D1D6', |
||||
|
||||
// Tab 图标
|
||||
tabIconDefault: '#8E8E93', |
||||
tabIconSelected: tintColorLight, |
||||
|
||||
// 卡片
|
||||
card: '#FFFFFF', |
||||
cardShadow: 'rgba(0, 0, 0, 0.1)', |
||||
|
||||
// 输入框
|
||||
inputBackground: '#FFFFFF', |
||||
inputBorder: '#D1D1D6', |
||||
inputPlaceholder: '#C7C7CC', |
||||
|
||||
// 按钮
|
||||
buttonPrimary: '#007AFF', |
||||
buttonSecondary: '#5856D6', |
||||
buttonDisabled: '#E5E5E5', |
||||
buttonText: '#FFFFFF', |
||||
|
||||
// 分隔线
|
||||
separator: '#E5E5E5', |
||||
|
||||
// 覆盖层
|
||||
overlay: 'rgba(0, 0, 0, 0.5)', |
||||
}, |
||||
dark: { |
||||
text: '#fff', |
||||
background: '#000', |
||||
// 文本颜色
|
||||
text: '#FFFFFF', |
||||
textSecondary: '#AEAEB2', |
||||
textTertiary: '#8E8E93', |
||||
textInverse: '#000000', |
||||
|
||||
// 背景颜色
|
||||
background: '#000000', |
||||
backgroundSecondary: '#1C1C1E', |
||||
backgroundTertiary: '#2C2C2E', |
||||
|
||||
// 主题色
|
||||
tint: tintColorDark, |
||||
tabIconDefault: '#ccc', |
||||
primary: '#0A84FF', |
||||
secondary: '#5E5CE6', |
||||
success: '#32D74B', |
||||
warning: '#FF9F0A', |
||||
error: '#FF453A', |
||||
info: '#64D2FF', |
||||
|
||||
// 边框颜色
|
||||
border: '#38383A', |
||||
borderSecondary: '#48484A', |
||||
|
||||
// Tab 图标
|
||||
tabIconDefault: '#8E8E93', |
||||
tabIconSelected: tintColorDark, |
||||
|
||||
// 卡片
|
||||
card: '#1C1C1E', |
||||
cardShadow: 'rgba(0, 0, 0, 0.3)', |
||||
|
||||
// 输入框
|
||||
inputBackground: '#1C1C1E', |
||||
inputBorder: '#38383A', |
||||
inputPlaceholder: '#636366', |
||||
|
||||
// 按钮
|
||||
buttonPrimary: '#0A84FF', |
||||
buttonSecondary: '#5E5CE6', |
||||
buttonDisabled: '#38383A', |
||||
buttonText: '#FFFFFF', |
||||
|
||||
// 分隔线
|
||||
separator: '#38383A', |
||||
|
||||
// 覆盖层
|
||||
overlay: 'rgba(0, 0, 0, 0.7)', |
||||
}, |
||||
}; |
||||
|
||||
@ -0,0 +1,26 @@
|
||||
/** |
||||
* Hooks 模块统一导出 |
||||
*/ |
||||
|
||||
// Debounce
|
||||
export { useDebounce, useDebouncedCallback } from './useDebounce'; |
||||
|
||||
// Throttle
|
||||
export { useThrottle } from './useThrottle'; |
||||
|
||||
// Haptics
|
||||
export { useHaptics } from './useHaptics'; |
||||
|
||||
// Request
|
||||
export { useRequest } from './useRequest'; |
||||
|
||||
// Theme
|
||||
export { |
||||
useColorScheme, |
||||
useThemeColor, |
||||
useThemeColors, |
||||
useThemeInfo, |
||||
} from './useTheme'; |
||||
|
||||
// Client-only value (for SSR/Web compatibility)
|
||||
export { useClientOnlyValue } from './useClientOnlyValue'; |
||||
@ -0,0 +1,23 @@
|
||||
/** |
||||
* Client-only value Hook |
||||
*
|
||||
* This hook is used to provide different values for server-side rendering (SSR) |
||||
* and client-side rendering. It's particularly useful for React Native Web |
||||
* to prevent hydration errors. |
||||
*
|
||||
* @param server - Value to use during server-side rendering |
||||
* @param client - Value to use during client-side rendering |
||||
* @returns The appropriate value based on the rendering context |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* // Disable header on server, enable on client
|
||||
* headerShown: useClientOnlyValue(false, true) |
||||
* ``` |
||||
*/ |
||||
|
||||
// This function is web-only as native doesn't currently support server (or build-time) rendering.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C { |
||||
return client; |
||||
} |
||||
|
||||
@ -0,0 +1,34 @@
|
||||
/** |
||||
* Client-only value Hook (Web version) |
||||
*
|
||||
* This hook is used to provide different values for server-side rendering (SSR) |
||||
* and client-side rendering on web platforms. |
||||
*
|
||||
* On web, we use `useEffect` to detect if we're on the client or server, |
||||
* since `useEffect` is not invoked during server rendering. |
||||
*
|
||||
* @param server - Value to use during server-side rendering |
||||
* @param client - Value to use during client-side rendering |
||||
* @returns The appropriate value based on the rendering context |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* // Disable header on server, enable on client
|
||||
* headerShown: useClientOnlyValue(false, true) |
||||
* ``` |
||||
*/ |
||||
|
||||
import React from 'react'; |
||||
|
||||
// `useEffect` is not invoked during server rendering, meaning
|
||||
// we can use this to determine if we're on the server or not.
|
||||
export function useClientOnlyValue<S, C>(server: S, client: C): S | C { |
||||
const [value, setValue] = React.useState<S | C>(server); |
||||
|
||||
React.useEffect(() => { |
||||
setValue(client); |
||||
}, [client]); |
||||
|
||||
return value; |
||||
} |
||||
|
||||
@ -0,0 +1,100 @@
|
||||
/** |
||||
* 主题 Hooks |
||||
*
|
||||
* 提供统一的主题访问接口 |
||||
*/ |
||||
|
||||
import { useMemo } from 'react'; |
||||
import { useColorScheme as useSystemColorScheme } from 'react-native'; |
||||
import { useTheme as useThemeStore } from '@/stores'; |
||||
import Colors from '@/constants/Colors'; |
||||
|
||||
/** |
||||
* 获取当前颜色方案(light | dark) |
||||
*
|
||||
* 从 settingsStore 读取用户设置的主题 |
||||
* 支持 'light' | 'dark' | 'auto' 三种模式 |
||||
*/ |
||||
export function useColorScheme(): 'light' | 'dark' { |
||||
const userTheme = useThemeStore(); |
||||
const systemTheme = useSystemColorScheme(); |
||||
|
||||
// 如果用户选择了 'auto',则使用系统主题
|
||||
if (userTheme === 'auto') { |
||||
return systemTheme === 'dark' ? 'dark' : 'light'; |
||||
} |
||||
|
||||
// 否则使用用户选择的主题
|
||||
return userTheme; |
||||
} |
||||
|
||||
/** |
||||
* 获取主题颜色 |
||||
*
|
||||
* @param props - 可选的自定义颜色 { light?: string; dark?: string } |
||||
* @param colorName - Colors 中定义的颜色名称 |
||||
* @returns 当前主题对应的颜色值 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const textColor = useThemeColor({}, 'text'); |
||||
* const customColor = useThemeColor({ light: '#000', dark: '#fff' }, 'text'); |
||||
* ``` |
||||
*/ |
||||
export function useThemeColor( |
||||
props: { light?: string; dark?: string }, |
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark |
||||
): string { |
||||
const theme = useColorScheme(); |
||||
const colorFromProps = props[theme]; |
||||
|
||||
if (colorFromProps) { |
||||
return colorFromProps; |
||||
} else { |
||||
return Colors[theme][colorName]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 获取完整的主题颜色对象 |
||||
*
|
||||
* @returns 当前主题的所有颜色配置 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const colors = useThemeColors(); |
||||
* <View style={{ backgroundColor: colors.background }}> |
||||
* <Text style={{ color: colors.text }}>Hello</Text> |
||||
* </View> |
||||
* ``` |
||||
*/ |
||||
export function useThemeColors() { |
||||
const theme = useColorScheme(); |
||||
|
||||
return useMemo(() => { |
||||
return Colors[theme]; |
||||
}, [theme]); |
||||
} |
||||
|
||||
/** |
||||
* 获取主题相关的所有信息 |
||||
*
|
||||
* @returns 主题信息对象 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const { theme, colors, isDark } = useThemeInfo(); |
||||
* ``` |
||||
*/ |
||||
export function useThemeInfo() { |
||||
const theme = useColorScheme(); |
||||
const colors = useThemeColors(); |
||||
|
||||
return useMemo(() => ({ |
||||
theme, |
||||
colors, |
||||
isDark: theme === 'dark', |
||||
isLight: theme === 'light', |
||||
}), [theme, colors]); |
||||
} |
||||
|
||||
@ -120,6 +120,9 @@ importers:
|
||||
concurrently: |
||||
specifier: ^9.2.1 |
||||
version: 9.2.1 |
||||
cors: |
||||
specifier: ^2.8.5 |
||||
version: 2.8.5 |
||||
express: |
||||
specifier: ^5.1.0 |
||||
version: 5.1.0 |
||||
@ -1641,6 +1644,10 @@ packages:
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} |
||||
engines: {node: '>= 0.10'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} |
||||
|
||||
@ -5656,6 +5663,11 @@ snapshots:
|
||||
dependencies: |
||||
browserslist: 4.27.0 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
object-assign: 4.1.1 |
||||
vary: 1.1.2 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
node-fetch: 2.7.0 |
||||
|
||||
@ -0,0 +1,36 @@
|
||||
/** |
||||
* Schemas 模块统一导出 |
||||
*/ |
||||
|
||||
// Auth Schemas
|
||||
export { |
||||
loginSchema, |
||||
registerSchema, |
||||
forgotPasswordSchema, |
||||
resetPasswordSchema, |
||||
changePasswordSchema, |
||||
phoneLoginSchema, |
||||
} from './auth'; |
||||
export type { |
||||
LoginFormData, |
||||
RegisterFormData, |
||||
ForgotPasswordFormData, |
||||
ResetPasswordFormData, |
||||
ChangePasswordFormData, |
||||
PhoneLoginFormData, |
||||
} from './auth'; |
||||
|
||||
// User Schemas
|
||||
export { |
||||
userSchema, |
||||
updateUserSchema, |
||||
bindPhoneSchema, |
||||
bindEmailSchema, |
||||
} from './user'; |
||||
export type { |
||||
UserFormData, |
||||
UpdateUserFormData, |
||||
BindPhoneFormData, |
||||
BindEmailFormData, |
||||
} from './user'; |
||||
|
||||
@ -0,0 +1,36 @@
|
||||
/** |
||||
* Screens 统一导出 |
||||
*
|
||||
* 这个目录用于存放复杂的业务页面组件 |
||||
*
|
||||
* 目录结构建议: |
||||
* screens/ |
||||
* ├── index.ts # 统一导出 |
||||
* ├── TestScreen/ # 测试页面 |
||||
* │ ├── index.tsx # 页面主组件 |
||||
* │ ├── components/ # 页面私有组件 |
||||
* │ └── styles.ts # 页面样式 |
||||
* ├── ProfileScreen/ # 个人资料页面 |
||||
* └── SettingsScreen/ # 设置页面 |
||||
*
|
||||
* 使用方式: |
||||
* 1. 在 screens/ 目录下创建页面组件 |
||||
* 2. 在 app/ 目录下创建路由文件,引用 screens/ 中的组件 |
||||
* 3. 从这里统一导出,方便管理 |
||||
*
|
||||
* 示例: |
||||
* ```typescript
|
||||
* // screens/TestScreen/index.tsx
|
||||
* export default function TestScreen() { |
||||
* return <View>...</View>; |
||||
* } |
||||
*
|
||||
* // app/test.tsx
|
||||
* import TestScreen from '@/screens/TestScreen'; |
||||
* export default TestScreen; |
||||
* ``` |
||||
*/ |
||||
|
||||
// 当前暂无导出,等待添加业务页面组件
|
||||
export {}; |
||||
|
||||
@ -0,0 +1,8 @@
|
||||
/** |
||||
* Services 模块统一导出 |
||||
*/ |
||||
|
||||
export { default as authService } from './authService'; |
||||
export { default as userService } from './userService'; |
||||
export { default as tenantService } from './tenantService'; |
||||
|
||||
@ -0,0 +1,43 @@
|
||||
/** |
||||
* 租户服务 |
||||
* 处理租户相关的 API 请求 |
||||
*/ |
||||
|
||||
import { request } from '@/utils/network/api'; |
||||
// import type { User, UpdateProfileFormData } from '@/schemas/user';
|
||||
|
||||
/** |
||||
* API 响应接口 |
||||
*/ |
||||
interface ApiResponse<T = any> { |
||||
code: number; |
||||
message: string; |
||||
data: T; |
||||
} |
||||
|
||||
/** |
||||
* tenant 服务类 |
||||
*/ |
||||
class TenantService { |
||||
/** |
||||
* 获取平台信息 |
||||
*/ |
||||
getPlatformData(params?: Record<string, any>): Promise<ApiResponse> { |
||||
const data = { |
||||
language: '0', |
||||
...params |
||||
}; |
||||
return request.post('/v2', data, { |
||||
headers: { |
||||
cmdId: 371130, |
||||
headerType: 1, |
||||
apiName: 'getPlatformData', |
||||
tid: '', |
||||
}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// 导出单例
|
||||
export const tenantService = new TenantService(); |
||||
export default tenantService; |
||||
@ -1,39 +0,0 @@
|
||||
/** |
||||
* 统一导出所有模块 |
||||
*/ |
||||
|
||||
// Utils
|
||||
export { |
||||
default as api, |
||||
request, |
||||
cancelAllRequests, |
||||
cancelRequest, |
||||
createRetryRequest, |
||||
} from './utils/network/api'; |
||||
export type { ApiResponse, ApiError, RequestConfig } from './utils/network/api'; |
||||
export { default as Storage, STORAGE_KEYS } from './utils/storage'; |
||||
export { default as config, printConfig } from './utils/config'; |
||||
export * from './utils/date'; |
||||
|
||||
// Stores
|
||||
export * from './stores/userStore'; |
||||
export * from './stores/settingsStore'; |
||||
|
||||
// Schemas
|
||||
export * from './schemas/auth'; |
||||
export * from './schemas/user'; |
||||
|
||||
// Services
|
||||
export { default as authService } from './services/authService'; |
||||
export { default as userService } from './services/userService'; |
||||
export { default as appService } from './services/appService'; |
||||
|
||||
// Hooks
|
||||
export * from './hooks/useDebounce'; |
||||
export * from './hooks/useThrottle'; |
||||
export * from './hooks/useHaptics'; |
||||
export * from './hooks/useRequest'; |
||||
|
||||
// Types
|
||||
export * from './types'; |
||||
export * from './types/api'; |
||||
@ -1,39 +0,0 @@
|
||||
/** |
||||
* 基础服务 |
||||
* 处理应用相关的 API 请求 |
||||
*/ |
||||
|
||||
import { request } from '@/src/utils/network/api'; |
||||
import type { User, UpdateProfileFormData } from '@/src/schemas/user'; |
||||
|
||||
/** |
||||
* API 响应接口 |
||||
*/ |
||||
interface ApiResponse<T = any> { |
||||
code: number; |
||||
message: string; |
||||
data: T; |
||||
} |
||||
|
||||
/** |
||||
* 用户服务类 |
||||
*/ |
||||
class AppService { |
||||
/** |
||||
* 获取当前用户信息 |
||||
*/ |
||||
getPlatformData(data?: Record<string, any>): Promise<any> { |
||||
return request.post('/v2', data, { |
||||
headers: { |
||||
cmdId: 371130, |
||||
headerType: 1, |
||||
apiName: 'getPlatformData', |
||||
tid: '', |
||||
}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// 导出单例
|
||||
export const appService = new AppService(); |
||||
export default appService; |
||||
@ -1,144 +0,0 @@
|
||||
/** |
||||
* 应用设置状态管理 |
||||
* 使用 Zustand + AsyncStorage 持久化 |
||||
*/ |
||||
|
||||
import { create } from 'zustand'; |
||||
import { persist, createJSONStorage } from 'zustand/middleware'; |
||||
import AsyncStorage from '@react-native-async-storage/async-storage'; |
||||
|
||||
/** |
||||
* 主题类型 |
||||
*/ |
||||
export type Theme = 'light' | 'dark' | 'auto'; |
||||
|
||||
/** |
||||
* 语言类型 |
||||
*/ |
||||
export type Language = 'zh-CN' | 'en-US'; |
||||
|
||||
/** |
||||
* 设置状态接口 |
||||
*/ |
||||
interface SettingsState { |
||||
// 状态
|
||||
theme: Theme; |
||||
language: Language; |
||||
notificationsEnabled: boolean; |
||||
soundEnabled: boolean; |
||||
hapticsEnabled: boolean; |
||||
|
||||
// 操作
|
||||
setTheme: (theme: Theme) => void; |
||||
setLanguage: (language: Language) => void; |
||||
setNotificationsEnabled: (enabled: boolean) => void; |
||||
setSoundEnabled: (enabled: boolean) => void; |
||||
setHapticsEnabled: (enabled: boolean) => void; |
||||
resetSettings: () => void; |
||||
} |
||||
|
||||
/** |
||||
* 默认设置 |
||||
*/ |
||||
const DEFAULT_SETTINGS = { |
||||
theme: 'auto' as Theme, |
||||
language: 'zh-CN' as Language, |
||||
notificationsEnabled: true, |
||||
soundEnabled: true, |
||||
hapticsEnabled: true, |
||||
}; |
||||
|
||||
/** |
||||
* 设置状态 Store |
||||
*/ |
||||
export const useSettingsStore = create<SettingsState>()( |
||||
persist( |
||||
(set) => ({ |
||||
// 初始状态
|
||||
...DEFAULT_SETTINGS, |
||||
|
||||
// 设置主题
|
||||
setTheme: (theme) => { |
||||
set({ theme }); |
||||
if (__DEV__) { |
||||
console.log('🎨 Theme changed:', theme); |
||||
} |
||||
}, |
||||
|
||||
// 设置语言
|
||||
setLanguage: (language) => { |
||||
set({ language }); |
||||
if (__DEV__) { |
||||
console.log('🌐 Language changed:', language); |
||||
} |
||||
}, |
||||
|
||||
// 设置通知开关
|
||||
setNotificationsEnabled: (enabled) => { |
||||
set({ notificationsEnabled: enabled }); |
||||
if (__DEV__) { |
||||
console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled'); |
||||
} |
||||
}, |
||||
|
||||
// 设置声音开关
|
||||
setSoundEnabled: (enabled) => { |
||||
set({ soundEnabled: enabled }); |
||||
if (__DEV__) { |
||||
console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled'); |
||||
} |
||||
}, |
||||
|
||||
// 设置触觉反馈开关
|
||||
setHapticsEnabled: (enabled) => { |
||||
set({ hapticsEnabled: enabled }); |
||||
if (__DEV__) { |
||||
console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled'); |
||||
} |
||||
}, |
||||
|
||||
// 重置所有设置
|
||||
resetSettings: () => { |
||||
set(DEFAULT_SETTINGS); |
||||
if (__DEV__) { |
||||
console.log('🔄 Settings reset to default'); |
||||
} |
||||
}, |
||||
}), |
||||
{ |
||||
name: 'settings-storage', |
||||
storage: createJSONStorage(() => AsyncStorage), |
||||
} |
||||
) |
||||
); |
||||
|
||||
/** |
||||
* 选择器 Hooks |
||||
*/ |
||||
|
||||
// 获取主题
|
||||
export const useTheme = () => useSettingsStore((state) => state.theme); |
||||
|
||||
// 获取语言
|
||||
export const useLanguage = () => useSettingsStore((state) => state.language); |
||||
|
||||
// 获取通知状态
|
||||
export const useNotificationsEnabled = () => |
||||
useSettingsStore((state) => state.notificationsEnabled); |
||||
|
||||
// 获取声音状态
|
||||
export const useSoundEnabled = () => useSettingsStore((state) => state.soundEnabled); |
||||
|
||||
// 获取触觉反馈状态
|
||||
export const useHapticsEnabled = () => useSettingsStore((state) => state.hapticsEnabled); |
||||
|
||||
// 获取设置操作方法
|
||||
export const useSettingsActions = () => |
||||
useSettingsStore((state) => ({ |
||||
setTheme: state.setTheme, |
||||
setLanguage: state.setLanguage, |
||||
setNotificationsEnabled: state.setNotificationsEnabled, |
||||
setSoundEnabled: state.setSoundEnabled, |
||||
setHapticsEnabled: state.setHapticsEnabled, |
||||
resetSettings: state.resetSettings, |
||||
})); |
||||
@ -1,141 +0,0 @@
|
||||
/** |
||||
* 用户状态管理 |
||||
* 使用 Zustand + AsyncStorage 持久化 |
||||
*/ |
||||
|
||||
import { create } from 'zustand'; |
||||
import { persist, createJSONStorage } from 'zustand/middleware'; |
||||
import AsyncStorage from '@react-native-async-storage/async-storage'; |
||||
|
||||
/** |
||||
* 用户信息接口 |
||||
*/ |
||||
export interface User { |
||||
id: string; |
||||
username: string; |
||||
email: string; |
||||
avatar?: string; |
||||
nickname?: string; |
||||
phone?: string; |
||||
createdAt?: string; |
||||
} |
||||
|
||||
/** |
||||
* 用户状态接口 |
||||
*/ |
||||
interface UserState { |
||||
// 状态
|
||||
user: User | null; |
||||
isLoggedIn: boolean; |
||||
token: string | null; |
||||
|
||||
// 操作
|
||||
setUser: (user: User) => void; |
||||
setToken: (token: string) => void; |
||||
login: (user: User, token: string) => void; |
||||
logout: () => void; |
||||
updateUser: (updates: Partial<User>) => void; |
||||
} |
||||
|
||||
/** |
||||
* 用户状态 Store |
||||
*/ |
||||
export const useUserStore = create<UserState>()( |
||||
persist( |
||||
(set, get) => ({ |
||||
// 初始状态
|
||||
user: null, |
||||
isLoggedIn: false, |
||||
token: null, |
||||
|
||||
// 设置用户信息
|
||||
setUser: (user) => { |
||||
set({ user, isLoggedIn: true }); |
||||
}, |
||||
|
||||
// 设置 token
|
||||
setToken: (token) => { |
||||
set({ token }); |
||||
}, |
||||
|
||||
// 登录
|
||||
login: (user, token) => { |
||||
set({ |
||||
user, |
||||
token, |
||||
isLoggedIn: true, |
||||
}); |
||||
|
||||
// 同时保存 token 到 AsyncStorage(用于 API 请求)
|
||||
AsyncStorage.setItem('auth_token', token); |
||||
|
||||
if (__DEV__) { |
||||
console.log('✅ User logged in:', user.username); |
||||
} |
||||
}, |
||||
|
||||
// 登出
|
||||
logout: () => { |
||||
set({ |
||||
user: null, |
||||
token: null, |
||||
isLoggedIn: false, |
||||
}); |
||||
|
||||
// 清除 AsyncStorage 中的 token
|
||||
AsyncStorage.removeItem('auth_token'); |
||||
|
||||
if (__DEV__) { |
||||
console.log('👋 User logged out'); |
||||
} |
||||
}, |
||||
|
||||
// 更新用户信息
|
||||
updateUser: (updates) => { |
||||
const currentUser = get().user; |
||||
if (currentUser) { |
||||
set({ |
||||
user: { ...currentUser, ...updates }, |
||||
}); |
||||
|
||||
if (__DEV__) { |
||||
console.log('📝 User updated:', updates); |
||||
} |
||||
} |
||||
}, |
||||
}), |
||||
{ |
||||
name: 'user-storage', // AsyncStorage 中的键名
|
||||
storage: createJSONStorage(() => AsyncStorage), |
||||
// 可以选择性地持久化某些字段
|
||||
partialize: (state) => ({ |
||||
user: state.user, |
||||
token: state.token, |
||||
isLoggedIn: state.isLoggedIn, |
||||
}), |
||||
} |
||||
) |
||||
); |
||||
|
||||
/** |
||||
* 选择器 Hooks(优化性能,避免不必要的重渲染) |
||||
*/ |
||||
|
||||
// 获取用户信息
|
||||
export const useUser = () => useUserStore((state) => state.user); |
||||
|
||||
// 获取登录状态
|
||||
export const useIsLoggedIn = () => useUserStore((state) => state.isLoggedIn); |
||||
|
||||
// 获取 token
|
||||
export const useToken = () => useUserStore((state) => state.token); |
||||
|
||||
// 获取用户操作方法
|
||||
export const useUserActions = () => |
||||
useUserStore((state) => ({ |
||||
setUser: state.setUser, |
||||
setToken: state.setToken, |
||||
login: state.login, |
||||
logout: state.logout, |
||||
updateUser: state.updateUser, |
||||
})); |
||||
@ -0,0 +1,36 @@
|
||||
/** |
||||
* Stores 模块统一导出 |
||||
*/ |
||||
|
||||
// User Store
|
||||
export { |
||||
useUserStore, |
||||
useUser, |
||||
useIsLoggedIn, |
||||
useToken, |
||||
useUserActions, |
||||
restoreUserState, |
||||
} from './userStore'; |
||||
export type { User } from './userStore'; |
||||
|
||||
// Settings Store
|
||||
export { |
||||
useSettingsStore, |
||||
useTheme, |
||||
useLanguage, |
||||
useNotificationsEnabled, |
||||
useSoundEnabled, |
||||
useHapticsEnabled, |
||||
useSettingsActions, |
||||
restoreSettingsState, |
||||
} from './settingsStore'; |
||||
export type { Theme, Language } from './settingsStore'; |
||||
|
||||
// Tenant Store
|
||||
export { |
||||
default as useTenantStore, |
||||
useTenantInfo, |
||||
useTenantStates, |
||||
useTenantActions, |
||||
restoreTenantState, |
||||
} from './tenantStore'; |
||||
@ -0,0 +1,167 @@
|
||||
/** |
||||
* 应用设置状态管理 |
||||
* 使用 Zustand + AsyncStorage 持久化 |
||||
*/ |
||||
|
||||
import { create } from 'zustand'; |
||||
import { useShallow } from 'zustand/react/shallow'; |
||||
import AsyncStorage from '@react-native-async-storage/async-storage'; |
||||
|
||||
/** |
||||
* 主题类型 |
||||
*/ |
||||
export type Theme = 'light' | 'dark' | 'auto'; |
||||
|
||||
/** |
||||
* 语言类型 |
||||
*/ |
||||
export type Language = 'zh-CN' | 'en-US'; |
||||
|
||||
/** |
||||
* 设置状态接口 |
||||
*/ |
||||
interface SettingsState { |
||||
// 状态
|
||||
theme: Theme; |
||||
language: Language; |
||||
notificationsEnabled: boolean; |
||||
soundEnabled: boolean; |
||||
hapticsEnabled: boolean; |
||||
|
||||
// 操作
|
||||
setTheme: (theme: Theme) => void; |
||||
setLanguage: (language: Language) => void; |
||||
setNotificationsEnabled: (enabled: boolean) => void; |
||||
setSoundEnabled: (enabled: boolean) => void; |
||||
setHapticsEnabled: (enabled: boolean) => void; |
||||
resetSettings: () => void; |
||||
} |
||||
|
||||
/** |
||||
* 默认设置 |
||||
*/ |
||||
const DEFAULT_SETTINGS = { |
||||
theme: 'auto' as Theme, |
||||
language: 'zh-CN' as Language, |
||||
notificationsEnabled: true, |
||||
soundEnabled: true, |
||||
hapticsEnabled: true, |
||||
}; |
||||
|
||||
/** |
||||
* 设置状态 Store |
||||
*/ |
||||
export const useSettingsStore = create<SettingsState>()((set, get) => ({ |
||||
// 初始状态
|
||||
...DEFAULT_SETTINGS, |
||||
|
||||
// 设置主题
|
||||
setTheme: (theme) => { |
||||
set({ theme }); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get())); |
||||
if (__DEV__) { |
||||
console.log('🎨 Theme changed:', theme); |
||||
} |
||||
}, |
||||
|
||||
// 设置语言
|
||||
setLanguage: (language) => { |
||||
set({ language }); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get())); |
||||
if (__DEV__) { |
||||
console.log('🌐 Language changed:', language); |
||||
} |
||||
}, |
||||
|
||||
// 设置通知开关
|
||||
setNotificationsEnabled: (enabled) => { |
||||
set({ notificationsEnabled: enabled }); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get())); |
||||
if (__DEV__) { |
||||
console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled'); |
||||
} |
||||
}, |
||||
|
||||
// 设置声音开关
|
||||
setSoundEnabled: (enabled) => { |
||||
set({ soundEnabled: enabled }); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get())); |
||||
if (__DEV__) { |
||||
console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled'); |
||||
} |
||||
}, |
||||
|
||||
// 设置触觉反馈开关
|
||||
setHapticsEnabled: (enabled) => { |
||||
set({ hapticsEnabled: enabled }); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get())); |
||||
if (__DEV__) { |
||||
console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled'); |
||||
} |
||||
}, |
||||
|
||||
// 重置所有设置
|
||||
resetSettings: () => { |
||||
set(DEFAULT_SETTINGS); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('settings-storage', JSON.stringify(DEFAULT_SETTINGS)); |
||||
if (__DEV__) { |
||||
console.log('🔄 Settings reset to default'); |
||||
} |
||||
}, |
||||
})); |
||||
|
||||
// 从 AsyncStorage 恢复状态的函数
|
||||
export const restoreSettingsState = async () => { |
||||
try { |
||||
const stored = await AsyncStorage.getItem('settings-storage'); |
||||
if (stored) { |
||||
const state = JSON.parse(stored); |
||||
useSettingsStore.setState(state); |
||||
if (__DEV__) { |
||||
console.log('✅ Settings state restored from storage'); |
||||
} |
||||
} |
||||
} catch (error) { |
||||
console.error('Failed to restore settings state:', error); |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* 选择器 Hooks |
||||
*/ |
||||
|
||||
// 获取主题
|
||||
export const useTheme = () => useSettingsStore((state) => state.theme); |
||||
|
||||
// 获取语言
|
||||
export const useLanguage = () => useSettingsStore((state) => state.language); |
||||
|
||||
// 获取通知状态
|
||||
export const useNotificationsEnabled = () => |
||||
useSettingsStore((state) => state.notificationsEnabled); |
||||
|
||||
// 获取声音状态
|
||||
export const useSoundEnabled = () => useSettingsStore((state) => state.soundEnabled); |
||||
|
||||
// 获取触觉反馈状态
|
||||
export const useHapticsEnabled = () => useSettingsStore((state) => state.hapticsEnabled); |
||||
|
||||
// 获取设置操作方法
|
||||
// 使用 useShallow 避免每次渲染都返回新对象
|
||||
export const useSettingsActions = () => |
||||
useSettingsStore( |
||||
useShallow((state) => ({ |
||||
setTheme: state.setTheme, |
||||
setLanguage: state.setLanguage, |
||||
setNotificationsEnabled: state.setNotificationsEnabled, |
||||
setSoundEnabled: state.setSoundEnabled, |
||||
setHapticsEnabled: state.setHapticsEnabled, |
||||
resetSettings: state.resetSettings, |
||||
})) |
||||
); |
||||
@ -0,0 +1,122 @@
|
||||
/** |
||||
* 租户状态管理 |
||||
* 使用 Zustand + AsyncStorage 持久化 |
||||
*/ |
||||
|
||||
import { create } from 'zustand'; |
||||
import { useShallow } from 'zustand/react/shallow'; |
||||
import AsyncStorage from '@react-native-async-storage/async-storage'; |
||||
import { STORAGE_KEYS } from '@/utils/storage'; |
||||
import { tenantService } from '@/services/tenantService'; |
||||
import { useEffect } from 'react'; |
||||
|
||||
/** |
||||
* 租户信息接口 |
||||
*/ |
||||
// export interface Tenant {
|
||||
// id: string;
|
||||
// username: string;
|
||||
// email: string;
|
||||
// avatar?: string;
|
||||
// nickname?: string;
|
||||
// phone?: string;
|
||||
// createdAt?: string;
|
||||
// }
|
||||
|
||||
/** |
||||
* 租户状态接口 |
||||
*/ |
||||
interface TenantState { |
||||
// 状态
|
||||
tenantInfo: Record<string, any> | null; |
||||
|
||||
// 操作
|
||||
setTenantInfo: (data: Record<string, any>) => void; |
||||
requestTenantInfo: (data?: Record<string, any>) => Promise<any>; |
||||
} |
||||
|
||||
/** |
||||
* 租户状态 Store |
||||
*/ |
||||
const useTenantStore = create<TenantState>()((set, get) => ({ |
||||
// 初始状态
|
||||
tenantInfo: null, |
||||
|
||||
|
||||
// 设置租户信息(通用方法,包含持久化逻辑)
|
||||
setTenantInfo: (data: any) => { |
||||
set({ tenantInfo: data }); |
||||
// 手动持久化
|
||||
// AsyncStorage.setItem(STORAGE_KEYS.TENANT_STORE, JSON.stringify({ tenantInfo: data }));
|
||||
|
||||
if (__DEV__) { |
||||
console.log('💾 Tenant info saved:', data); |
||||
} |
||||
}, |
||||
|
||||
// 获取租户信息(调用 API 并使用 setTenantInfo 保存)
|
||||
requestTenantInfo: async () => { |
||||
try { |
||||
const params = { |
||||
domain_addr: 'https://51zhh5.notbug.org', |
||||
}; |
||||
const { data } = await tenantService.getPlatformData(params); |
||||
|
||||
// 调用 setTenantInfo 来保存数据,避免重复代码
|
||||
get().setTenantInfo(data); |
||||
|
||||
if (__DEV__) { |
||||
console.log('✅ Tenant info loaded:', data); |
||||
} |
||||
return Promise.resolve(data); |
||||
} catch (error) { |
||||
console.error('Failed to request tenant info:', error); |
||||
return Promise.reject(error); |
||||
} |
||||
}, |
||||
})); |
||||
|
||||
// 从 AsyncStorage 恢复状态的函数
|
||||
export const restoreTenantState = async () => { |
||||
try { |
||||
const stored = await AsyncStorage.getItem(STORAGE_KEYS.TENANT_STORE); |
||||
if (stored) { |
||||
const state = JSON.parse(stored); |
||||
useTenantStore.setState(state); |
||||
if (__DEV__) { |
||||
console.log('✅ Tenant state restored from storage'); |
||||
} |
||||
} |
||||
} catch (error) { |
||||
console.error('Failed to restore tenant state:', error); |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* 选择器 Hooks(优化性能,避免不必要的重渲染) |
||||
*/ |
||||
|
||||
// 获取用户信息
|
||||
export const useTenantInfo = () => useTenantStore((state) => state.tenantInfo); |
||||
|
||||
|
||||
// 获取租户状态
|
||||
export const useTenantStates = () => |
||||
useTenantStore( |
||||
useShallow((state) => ({ |
||||
tenantInfo: state.tenantInfo, |
||||
tenantLoad: !!state.tenantInfo?.tid || !!state.tenantInfo?.create_time, |
||||
})) |
||||
); |
||||
|
||||
// 获取租户操作方法
|
||||
// 使用 useShallow 避免每次渲染都返回新对象
|
||||
export const useTenantActions = () => |
||||
useTenantStore( |
||||
useShallow((state) => ({ |
||||
setTenantInfo: state.setTenantInfo, |
||||
requestTenantInfo: state.requestTenantInfo, |
||||
})) |
||||
); |
||||
|
||||
export default useTenantStore; |
||||
@ -0,0 +1,161 @@
|
||||
/** |
||||
* 用户状态管理 |
||||
* 使用 Zustand + AsyncStorage 持久化 |
||||
*/ |
||||
|
||||
import { create } from 'zustand'; |
||||
import { useShallow } from 'zustand/react/shallow'; |
||||
import AsyncStorage from '@react-native-async-storage/async-storage'; |
||||
|
||||
/** |
||||
* 用户信息接口 |
||||
*/ |
||||
export interface User { |
||||
id: string; |
||||
username: string; |
||||
email: string; |
||||
avatar?: string; |
||||
nickname?: string; |
||||
phone?: string; |
||||
createdAt?: string; |
||||
} |
||||
|
||||
/** |
||||
* 用户状态接口 |
||||
*/ |
||||
interface UserState { |
||||
// 状态
|
||||
user: User | null; |
||||
isLoggedIn: boolean; |
||||
token: string | null; |
||||
|
||||
// 操作
|
||||
setUser: (user: User) => void; |
||||
setToken: (token: string) => void; |
||||
login: (user: User, token: string) => void; |
||||
logout: () => void; |
||||
updateUser: (updates: Partial<User>) => void; |
||||
} |
||||
|
||||
/** |
||||
* 用户状态 Store |
||||
*/ |
||||
export const useUserStore = create<UserState>()((set, get) => ({ |
||||
// 初始状态
|
||||
user: null, |
||||
isLoggedIn: false, |
||||
token: null, |
||||
|
||||
// 设置用户信息
|
||||
setUser: (user) => { |
||||
const newState = { user, isLoggedIn: true }; |
||||
set(newState); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('user-storage', JSON.stringify(newState)); |
||||
}, |
||||
|
||||
// 设置 token
|
||||
setToken: (token) => { |
||||
set({ token }); |
||||
// 手动持久化 - 延迟执行以确保状态已更新
|
||||
setTimeout(() => { |
||||
const state = get(); |
||||
AsyncStorage.setItem('user-storage', JSON.stringify(state)); |
||||
}, 0); |
||||
}, |
||||
|
||||
// 登录
|
||||
login: (user, token) => { |
||||
const newState = { |
||||
user, |
||||
token, |
||||
isLoggedIn: true, |
||||
}; |
||||
set(newState); |
||||
|
||||
// 同时保存 token 到 AsyncStorage(用于 API 请求)
|
||||
AsyncStorage.setItem('auth_token', token); |
||||
// 手动持久化整个状态
|
||||
AsyncStorage.setItem('user-storage', JSON.stringify(newState)); |
||||
|
||||
if (__DEV__) { |
||||
console.log('✅ User logged in:', user.username); |
||||
} |
||||
}, |
||||
|
||||
// 登出
|
||||
logout: () => { |
||||
const newState = { |
||||
user: null, |
||||
token: null, |
||||
isLoggedIn: false, |
||||
}; |
||||
set(newState); |
||||
|
||||
// 清除 AsyncStorage 中的 token
|
||||
AsyncStorage.removeItem('auth_token'); |
||||
// 清除持久化状态
|
||||
AsyncStorage.removeItem('user-storage'); |
||||
|
||||
if (__DEV__) { |
||||
console.log('👋 User logged out'); |
||||
} |
||||
}, |
||||
|
||||
// 更新用户信息
|
||||
updateUser: (updates) => { |
||||
const currentUser = get().user; |
||||
if (currentUser) { |
||||
const newUser = { ...currentUser, ...updates }; |
||||
set({ user: newUser }); |
||||
// 手动持久化
|
||||
AsyncStorage.setItem('user-storage', JSON.stringify({ ...get(), user: newUser })); |
||||
|
||||
if (__DEV__) { |
||||
console.log('📝 User updated:', updates); |
||||
} |
||||
} |
||||
}, |
||||
})); |
||||
|
||||
// 从 AsyncStorage 恢复状态的函数
|
||||
export const restoreUserState = async () => { |
||||
try { |
||||
const stored = await AsyncStorage.getItem('user-storage'); |
||||
if (stored) { |
||||
const state = JSON.parse(stored); |
||||
useUserStore.setState(state); |
||||
if (__DEV__) { |
||||
console.log('✅ User state restored from storage'); |
||||
} |
||||
} |
||||
} catch (error) { |
||||
console.error('Failed to restore user state:', error); |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* 选择器 Hooks(优化性能,避免不必要的重渲染) |
||||
*/ |
||||
|
||||
// 获取用户信息
|
||||
export const useUser = () => useUserStore((state) => state.user); |
||||
|
||||
// 获取登录状态
|
||||
export const useIsLoggedIn = () => useUserStore((state) => state.isLoggedIn); |
||||
|
||||
// 获取 token
|
||||
export const useToken = () => useUserStore((state) => state.token); |
||||
|
||||
// 获取用户操作方法
|
||||
// 使用 useShallow 避免每次渲染都返回新对象
|
||||
export const useUserActions = () => |
||||
useUserStore( |
||||
useShallow((state) => ({ |
||||
setUser: state.setUser, |
||||
setToken: state.setToken, |
||||
login: state.login, |
||||
logout: state.logout, |
||||
updateUser: state.updateUser, |
||||
})) |
||||
); |
||||
@ -0,0 +1,36 @@
|
||||
/** |
||||
* 主题系统统一导出 |
||||
*
|
||||
* 提供主题配置、工具函数和类型定义 |
||||
*/ |
||||
|
||||
// 导出颜色配置
|
||||
export { default as Colors } from '@/constants/Colors'; |
||||
|
||||
// 导出主题 Hooks
|
||||
export { |
||||
useColorScheme, |
||||
useThemeColor, |
||||
useThemeColors, |
||||
useThemeInfo, |
||||
} from '@/hooks/useTheme'; |
||||
|
||||
// 导出主题组件
|
||||
export { |
||||
ThemedText, |
||||
ThemedView, |
||||
Text as ThemeText, |
||||
View as ThemeView, |
||||
} from '@/components/Themed'; |
||||
|
||||
export type { |
||||
ThemedTextProps, |
||||
ThemedViewProps, |
||||
TextProps as ThemeTextProps, |
||||
ViewProps as ThemeViewProps, |
||||
} from '@/components/Themed'; |
||||
|
||||
// 导出主题工具函数
|
||||
export * from './utils'; |
||||
export * from './styles'; |
||||
|
||||
@ -0,0 +1,261 @@
|
||||
/** |
||||
* 主题样式工厂 |
||||
*
|
||||
* 提供创建主题感知样式的工具函数 |
||||
*
|
||||
* React Native 不支持 CSS 类名,但可以通过样式工厂函数实现类似效果 |
||||
*/ |
||||
|
||||
import { StyleSheet, TextStyle, ViewStyle } from 'react-native'; |
||||
import Colors from '@/constants/Colors'; |
||||
|
||||
/** |
||||
* 主题样式类型 |
||||
*/ |
||||
export type ThemeStyles = { |
||||
light: any; |
||||
dark: any; |
||||
}; |
||||
|
||||
/** |
||||
* 创建主题样式 |
||||
*
|
||||
* 类似于 CSS 类名的概念,但使用函数式方法 |
||||
*
|
||||
* @param createStyles - 样式创建函数,接收颜色对象 |
||||
* @returns 主题样式对象 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const styles = createThemeStyles((colors) => ({ |
||||
* container: { |
||||
* backgroundColor: colors.background, |
||||
* padding: 16, |
||||
* }, |
||||
* text: { |
||||
* color: colors.text, |
||||
* fontSize: 16, |
||||
* }, |
||||
* })); |
||||
*
|
||||
* // 使用
|
||||
* const theme = useColorScheme(); |
||||
* <View style={styles[theme].container}> |
||||
* <Text style={styles[theme].text}>Hello</Text> |
||||
* </View> |
||||
* ``` |
||||
*/ |
||||
export function createThemeStyles<T extends StyleSheet.NamedStyles<T>>( |
||||
createStyles: (colors: typeof Colors.light) => T |
||||
): ThemeStyles { |
||||
return { |
||||
light: StyleSheet.create(createStyles(Colors.light)), |
||||
dark: StyleSheet.create(createStyles(Colors.dark)), |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 创建响应式主题样式 |
||||
*
|
||||
* 允许为不同主题定义完全不同的样式 |
||||
*
|
||||
* @param lightStyles - 浅色主题样式 |
||||
* @param darkStyles - 深色主题样式 |
||||
* @returns 主题样式对象 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const styles = createResponsiveThemeStyles( |
||||
* { |
||||
* container: { backgroundColor: '#fff', padding: 16 }, |
||||
* text: { color: '#000', fontSize: 14 }, |
||||
* }, |
||||
* { |
||||
* container: { backgroundColor: '#000', padding: 20 }, |
||||
* text: { color: '#fff', fontSize: 16 }, |
||||
* } |
||||
* ); |
||||
* ``` |
||||
*/ |
||||
export function createResponsiveThemeStyles<T extends StyleSheet.NamedStyles<T>>( |
||||
lightStyles: T, |
||||
darkStyles: T |
||||
): ThemeStyles { |
||||
return { |
||||
light: StyleSheet.create(lightStyles), |
||||
dark: StyleSheet.create(darkStyles), |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 预定义的通用样式类 |
||||
*
|
||||
* 类似于 Tailwind CSS 的工具类 |
||||
*/ |
||||
export const commonStyles = createThemeStyles((colors) => ({ |
||||
// 容器样式
|
||||
container: { |
||||
flex: 1, |
||||
backgroundColor: colors.background, |
||||
}, |
||||
containerPadded: { |
||||
flex: 1, |
||||
backgroundColor: colors.background, |
||||
padding: 16, |
||||
}, |
||||
containerCentered: { |
||||
flex: 1, |
||||
backgroundColor: colors.background, |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
}, |
||||
|
||||
// 卡片样式
|
||||
card: { |
||||
backgroundColor: colors.card, |
||||
borderRadius: 8, |
||||
padding: 16, |
||||
borderWidth: 1, |
||||
borderColor: colors.border, |
||||
}, |
||||
cardElevated: { |
||||
backgroundColor: colors.card, |
||||
borderRadius: 8, |
||||
padding: 16, |
||||
shadowColor: '#000', |
||||
shadowOffset: { width: 0, height: 2 }, |
||||
shadowOpacity: 0.1, |
||||
shadowRadius: 4, |
||||
elevation: 3, |
||||
}, |
||||
|
||||
// 文本样式
|
||||
textPrimary: { |
||||
color: colors.text, |
||||
fontSize: 16, |
||||
} as TextStyle, |
||||
textSecondary: { |
||||
color: colors.textSecondary, |
||||
fontSize: 14, |
||||
} as TextStyle, |
||||
textTertiary: { |
||||
color: colors.textTertiary, |
||||
fontSize: 12, |
||||
} as TextStyle, |
||||
textTitle: { |
||||
color: colors.text, |
||||
fontSize: 24, |
||||
fontWeight: 'bold', |
||||
} as TextStyle, |
||||
textSubtitle: { |
||||
color: colors.text, |
||||
fontSize: 18, |
||||
fontWeight: '600', |
||||
} as TextStyle, |
||||
|
||||
// 按钮样式
|
||||
button: { |
||||
backgroundColor: colors.buttonPrimary, |
||||
paddingVertical: 12, |
||||
paddingHorizontal: 24, |
||||
borderRadius: 8, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
} as ViewStyle, |
||||
buttonOutline: { |
||||
backgroundColor: 'transparent', |
||||
paddingVertical: 12, |
||||
paddingHorizontal: 24, |
||||
borderRadius: 8, |
||||
borderWidth: 1, |
||||
borderColor: colors.buttonPrimary, |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
} as ViewStyle, |
||||
buttonText: { |
||||
color: '#FFFFFF', |
||||
fontSize: 16, |
||||
fontWeight: '600', |
||||
} as TextStyle, |
||||
buttonTextOutline: { |
||||
color: colors.buttonPrimary, |
||||
fontSize: 16, |
||||
fontWeight: '600', |
||||
} as TextStyle, |
||||
|
||||
// 输入框样式
|
||||
input: { |
||||
backgroundColor: colors.inputBackground, |
||||
borderWidth: 1, |
||||
borderColor: colors.inputBorder, |
||||
borderRadius: 8, |
||||
paddingVertical: 12, |
||||
paddingHorizontal: 16, |
||||
fontSize: 16, |
||||
color: colors.text, |
||||
} as TextStyle, |
||||
inputFocused: { |
||||
backgroundColor: colors.inputBackground, |
||||
borderWidth: 2, |
||||
borderColor: colors.primary, |
||||
borderRadius: 8, |
||||
paddingVertical: 12, |
||||
paddingHorizontal: 16, |
||||
fontSize: 16, |
||||
color: colors.text, |
||||
} as TextStyle, |
||||
|
||||
// 分隔线
|
||||
separator: { |
||||
height: 1, |
||||
backgroundColor: colors.separator, |
||||
} as ViewStyle, |
||||
separatorVertical: { |
||||
width: 1, |
||||
backgroundColor: colors.separator, |
||||
} as ViewStyle, |
||||
|
||||
// 间距
|
||||
spacingXs: { height: 4 } as ViewStyle, |
||||
spacingSm: { height: 8 } as ViewStyle, |
||||
spacingMd: { height: 16 } as ViewStyle, |
||||
spacingLg: { height: 24 } as ViewStyle, |
||||
spacingXl: { height: 32 } as ViewStyle, |
||||
|
||||
// 布局
|
||||
row: { |
||||
flexDirection: 'row', |
||||
alignItems: 'center', |
||||
} as ViewStyle, |
||||
rowBetween: { |
||||
flexDirection: 'row', |
||||
alignItems: 'center', |
||||
justifyContent: 'space-between', |
||||
} as ViewStyle, |
||||
column: { |
||||
flexDirection: 'column', |
||||
} as ViewStyle, |
||||
center: { |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
} as ViewStyle, |
||||
})); |
||||
|
||||
/** |
||||
* 获取主题样式 |
||||
*
|
||||
* @param styles - 主题样式对象 |
||||
* @param theme - 当前主题 |
||||
* @returns 当前主题的样式 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const theme = useColorScheme(); |
||||
* const style = getThemeStyle(styles, theme); |
||||
* <View style={style.container} /> |
||||
* ``` |
||||
*/ |
||||
export function getThemeStyle<T>(styles: ThemeStyles, theme: 'light' | 'dark'): T { |
||||
return styles[theme]; |
||||
} |
||||
|
||||
@ -0,0 +1,122 @@
|
||||
/** |
||||
* 主题工具函数 |
||||
*
|
||||
* 提供主题相关的辅助函数 |
||||
*/ |
||||
|
||||
import Colors from '@/constants/Colors'; |
||||
|
||||
/** |
||||
* 根据主题获取颜色 |
||||
*
|
||||
* @param theme - 主题类型 'light' | 'dark' |
||||
* @param colorName - 颜色名称 |
||||
* @returns 颜色值 |
||||
*/ |
||||
export function getThemeColor( |
||||
theme: 'light' | 'dark', |
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark |
||||
): string { |
||||
return Colors[theme][colorName]; |
||||
} |
||||
|
||||
/** |
||||
* 根据主题获取所有颜色 |
||||
*
|
||||
* @param theme - 主题类型 'light' | 'dark' |
||||
* @returns 颜色对象 |
||||
*/ |
||||
export function getThemeColors(theme: 'light' | 'dark') { |
||||
return Colors[theme]; |
||||
} |
||||
|
||||
/** |
||||
* 创建主题感知的样式 |
||||
*
|
||||
* @param lightStyle - 浅色主题样式 |
||||
* @param darkStyle - 深色主题样式 |
||||
* @param theme - 当前主题 |
||||
* @returns 合并后的样式 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const style = createThemedStyle( |
||||
* { backgroundColor: '#fff' }, |
||||
* { backgroundColor: '#000' }, |
||||
* theme |
||||
* ); |
||||
* ``` |
||||
*/ |
||||
export function createThemedStyle<T>( |
||||
lightStyle: T, |
||||
darkStyle: T, |
||||
theme: 'light' | 'dark' |
||||
): T { |
||||
return theme === 'dark' ? darkStyle : lightStyle; |
||||
} |
||||
|
||||
/** |
||||
* 根据主题选择值 |
||||
*
|
||||
* @param lightValue - 浅色主题值 |
||||
* @param darkValue - 深色主题值 |
||||
* @param theme - 当前主题 |
||||
* @returns 选中的值 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const fontSize = selectByTheme(14, 16, theme); |
||||
* ``` |
||||
*/ |
||||
export function selectByTheme<T>( |
||||
lightValue: T, |
||||
darkValue: T, |
||||
theme: 'light' | 'dark' |
||||
): T { |
||||
return theme === 'dark' ? darkValue : lightValue; |
||||
} |
||||
|
||||
/** |
||||
* 颜色透明度调整 |
||||
*
|
||||
* @param color - 十六进制颜色值 |
||||
* @param opacity - 透明度 0-1 |
||||
* @returns 带透明度的颜色值 |
||||
*
|
||||
* @example |
||||
* ```tsx
|
||||
* const color = withOpacity('#000000', 0.5); // rgba(0, 0, 0, 0.5)
|
||||
* ``` |
||||
*/ |
||||
export function withOpacity(color: string, opacity: number): string { |
||||
// 移除 # 号
|
||||
const hex = color.replace('#', ''); |
||||
|
||||
// 转换为 RGB
|
||||
const r = parseInt(hex.substring(0, 2), 16); |
||||
const g = parseInt(hex.substring(2, 4), 16); |
||||
const b = parseInt(hex.substring(4, 6), 16); |
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`; |
||||
} |
||||
|
||||
/** |
||||
* 判断是否为深色主题 |
||||
*
|
||||
* @param theme - 主题类型 |
||||
* @returns 是否为深色主题 |
||||
*/ |
||||
export function isDarkTheme(theme: 'light' | 'dark'): boolean { |
||||
return theme === 'dark'; |
||||
} |
||||
|
||||
/** |
||||
* 判断是否为浅色主题 |
||||
*
|
||||
* @param theme - 主题类型 |
||||
* @returns 是否为浅色主题 |
||||
*/ |
||||
export function isLightTheme(theme: 'light' | 'dark'): boolean { |
||||
return theme === 'light'; |
||||
} |
||||
|
||||
@ -0,0 +1,38 @@
|
||||
/** |
||||
* Utils 模块统一导出 |
||||
*/ |
||||
|
||||
// Network API
|
||||
export { |
||||
default as api, |
||||
request, |
||||
cancelAllRequests, |
||||
cancelRequest, |
||||
createRetryRequest, |
||||
} from './network/api'; |
||||
export type { ApiResponse, ApiError, RequestConfig } from './network/api'; |
||||
|
||||
// Storage
|
||||
export { default as Storage, STORAGE_KEYS } from './storage'; |
||||
export { default as SessionStorage, SESSION_KEYS } from './sessionStorage'; |
||||
export { default as StorageManager } from './storageManager'; |
||||
export type { StorageType, StorageOptions } from './storageManager'; |
||||
|
||||
// Config
|
||||
export { default as config, printConfig } from './config'; |
||||
|
||||
// Date utilities
|
||||
export { |
||||
formatDate, |
||||
formatRelativeTime, |
||||
formatChatTime, |
||||
parseDate, |
||||
isToday, |
||||
isYesterday, |
||||
isSameDay, |
||||
addDays, |
||||
subtractDays, |
||||
startOfDay, |
||||
endOfDay, |
||||
} from './date'; |
||||
|
||||
Loading…
Reference in new issue