Compare commits
5 Commits
61252cdf36
...
b3a06f7f9e
| Author | SHA1 | Date |
|---|---|---|
|
|
b3a06f7f9e | 1 month ago |
|
|
170a08e5d1 | 1 month ago |
|
|
855f289579 | 1 month ago |
|
|
c0d54b8513 | 1 month ago |
|
|
ce324c9bb5 | 1 month ago |
73 changed files with 6372 additions and 895 deletions
@ -0,0 +1,7 @@
|
||||
# 开发环境配置 |
||||
# EXPO_PUBLIC_API_URL=/api # 注释掉,让 config.ts 根据平台自动选择 |
||||
EXPO_PUBLIC_API_TIMEOUT=10000 |
||||
|
||||
# 测试环境的域名 |
||||
API_TARGET=https://51zhh5.notbug.org |
||||
|
||||
@ -1,11 +1,34 @@
|
||||
# 环境变量示例文件 |
||||
# 复制此文件为 .env 并填入实际值 |
||||
|
||||
# ============================================ |
||||
# API 配置 |
||||
EXPO_PUBLIC_API_URL=https://api.example.com |
||||
# ============================================ |
||||
|
||||
# API 基础 URL |
||||
# 开发环境推荐使用相对路径 "/" 配合代理服务器 |
||||
# 生产环境根据实际情况配置: |
||||
# - 如果前后端同域:使用 "/" |
||||
# - 如果前后端分离:使用完整 URL "https://api.yourdomain.com/api" |
||||
EXPO_PUBLIC_API_URL=/ |
||||
|
||||
# API 请求超时时间(毫秒) |
||||
EXPO_PUBLIC_API_TIMEOUT=10000 |
||||
|
||||
# ============================================ |
||||
# 应用信息 |
||||
# ============================================ |
||||
|
||||
EXPO_PUBLIC_APP_NAME=RN Demo |
||||
EXPO_PUBLIC_APP_VERSION=1.0.0 |
||||
|
||||
# 其他配置 |
||||
# ============================================ |
||||
# 其他配置(可选) |
||||
# ============================================ |
||||
|
||||
# Sentry 错误追踪 |
||||
# EXPO_PUBLIC_SENTRY_DSN= |
||||
|
||||
# 分析工具 ID |
||||
# EXPO_PUBLIC_ANALYTICS_ID= |
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
# 生产环境配置 |
||||
EXPO_PUBLIC_API_URL=/ |
||||
EXPO_PUBLIC_API_TIMEOUT=10000 |
||||
|
||||
@ -0,0 +1,55 @@
|
||||
# Dependencies |
||||
node_modules |
||||
.pnp |
||||
.pnp.js |
||||
|
||||
# Build outputs |
||||
.expo |
||||
.expo-shared |
||||
dist |
||||
build |
||||
*.tsbuildinfo |
||||
|
||||
# Cache |
||||
.cache |
||||
.parcel-cache |
||||
.next |
||||
.nuxt |
||||
|
||||
# Logs |
||||
*.log |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
pnpm-debug.log* |
||||
|
||||
# OS |
||||
.DS_Store |
||||
Thumbs.db |
||||
|
||||
# IDE |
||||
.vscode |
||||
.idea |
||||
*.swp |
||||
*.swo |
||||
*~ |
||||
|
||||
# Environment |
||||
.env |
||||
.env.local |
||||
.env.*.local |
||||
|
||||
# Generated files |
||||
coverage |
||||
.nyc_output |
||||
|
||||
# Lock files |
||||
package-lock.json |
||||
yarn.lock |
||||
pnpm-lock.yaml |
||||
|
||||
# Misc |
||||
*.min.js |
||||
*.min.css |
||||
public |
||||
|
||||
@ -0,0 +1,15 @@
|
||||
{ |
||||
"semi": true, |
||||
"trailingComma": "es5", |
||||
"singleQuote": true, |
||||
"printWidth": 100, |
||||
"tabWidth": 2, |
||||
"useTabs": false, |
||||
"arrowParens": "always", |
||||
"bracketSpacing": true, |
||||
"endOfLine": "lf", |
||||
"jsxSingleQuote": false, |
||||
"bracketSameLine": false, |
||||
"quoteProps": "as-needed", |
||||
"proseWrap": "preserve" |
||||
} |
||||
@ -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,38 @@
|
||||
// 请求相关
|
||||
|
||||
export enum NetworkTypeEnum { |
||||
ERROR = 'error', |
||||
SUCCESS = 'success', |
||||
} |
||||
|
||||
// 冻结账号相关接口
|
||||
export const FREEZE_CMDID = [ |
||||
'314501', |
||||
'7242031', |
||||
'621116', |
||||
'396101', |
||||
'420029', |
||||
'724209', |
||||
'621112', |
||||
'377003', |
||||
'7242026', |
||||
'390004', |
||||
'3740012', |
||||
'321543', |
||||
'310400', |
||||
'325308', |
||||
]; |
||||
|
||||
export const WITHDRAWAL_CMDID = ['325308']; |
||||
|
||||
export const NO_CANCEL_CMDID = ['370730']; // 不需要取消的请求集合
|
||||
|
||||
export const TIPS_CON = [ |
||||
'请完成短信验证之后再参与', |
||||
'请填写真实姓名之后再参与', |
||||
'请完成绑定银行卡之后再参与', |
||||
'请完成生日设置之后再参与', |
||||
'请绑定虚拟货币之后再参与', |
||||
'请绑定收款方式之后再参与', |
||||
'同登录IP仅可领取一次,不可重复领取', |
||||
]; |
||||
@ -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,291 @@
|
||||
/** |
||||
* 请求 Hook |
||||
* 提供统一的请求状态管理 |
||||
*/ |
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'; |
||||
import { AxiosError } from 'axios'; |
||||
import type { RequestConfig } from '@/utils/network/api'; |
||||
|
||||
/** |
||||
* 请求状态 |
||||
*/ |
||||
export interface RequestState<T> { |
||||
data: T | null; |
||||
loading: boolean; |
||||
error: Error | null; |
||||
} |
||||
|
||||
/** |
||||
* 请求选项 |
||||
*/ |
||||
export interface UseRequestOptions<T> extends RequestConfig { |
||||
/** 是否立即执行 */ |
||||
immediate?: boolean; |
||||
/** 成功回调 */ |
||||
onSuccess?: (data: T) => void; |
||||
/** 失败回调 */ |
||||
onError?: (error: Error) => void; |
||||
/** 完成回调(无论成功失败) */ |
||||
onFinally?: () => void; |
||||
/** 默认数据 */ |
||||
defaultData?: T; |
||||
} |
||||
|
||||
/** |
||||
* 请求 Hook |
||||
* |
||||
* @example |
||||
* ```tsx
|
||||
* const { data, loading, error, run, refresh } = useRequest( |
||||
* () => request.get('/api/users'), |
||||
* { immediate: true } |
||||
* ); |
||||
* ``` |
||||
*/ |
||||
export function useRequest<T = any>( |
||||
requestFn: () => Promise<T>, |
||||
options: UseRequestOptions<T> = {} |
||||
) { |
||||
const { immediate = false, onSuccess, onError, onFinally, defaultData = null } = options; |
||||
|
||||
const [state, setState] = useState<RequestState<T>>({ |
||||
data: defaultData, |
||||
loading: false, |
||||
error: null, |
||||
}); |
||||
|
||||
const requestRef = useRef(requestFn); |
||||
requestRef.current = requestFn; |
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null); |
||||
|
||||
/** |
||||
* 执行请求 |
||||
*/ |
||||
const run = useCallback( |
||||
async (...args: any[]) => { |
||||
// 取消之前的请求
|
||||
if (abortControllerRef.current) { |
||||
abortControllerRef.current.abort(); |
||||
} |
||||
|
||||
// 创建新的 AbortController
|
||||
abortControllerRef.current = new AbortController(); |
||||
|
||||
setState((prev) => ({ |
||||
...prev, |
||||
loading: true, |
||||
error: null, |
||||
})); |
||||
|
||||
try { |
||||
const data = await requestRef.current(); |
||||
|
||||
setState({ |
||||
data, |
||||
loading: false, |
||||
error: null, |
||||
}); |
||||
|
||||
onSuccess?.(data); |
||||
|
||||
return data; |
||||
} catch (error) { |
||||
const err = error as Error; |
||||
|
||||
setState((prev) => ({ |
||||
...prev, |
||||
loading: false, |
||||
error: err, |
||||
})); |
||||
|
||||
onError?.(err); |
||||
|
||||
throw error; |
||||
} finally { |
||||
onFinally?.(); |
||||
} |
||||
}, |
||||
[onSuccess, onError, onFinally] |
||||
); |
||||
|
||||
/** |
||||
* 刷新(重新执行请求) |
||||
*/ |
||||
const refresh = useCallback(() => { |
||||
return run(); |
||||
}, [run]); |
||||
|
||||
/** |
||||
* 重置状态 |
||||
*/ |
||||
const reset = useCallback(() => { |
||||
setState({ |
||||
data: defaultData, |
||||
loading: false, |
||||
error: null, |
||||
}); |
||||
}, [defaultData]); |
||||
|
||||
/** |
||||
* 取消请求 |
||||
*/ |
||||
const cancel = useCallback(() => { |
||||
if (abortControllerRef.current) { |
||||
abortControllerRef.current.abort(); |
||||
} |
||||
}, []); |
||||
|
||||
// 立即执行
|
||||
useEffect(() => { |
||||
if (immediate) { |
||||
run(); |
||||
} |
||||
|
||||
// 组件卸载时取消请求
|
||||
return () => { |
||||
cancel(); |
||||
}; |
||||
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return { |
||||
...state, |
||||
run, |
||||
refresh, |
||||
reset, |
||||
cancel, |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 分页请求 Hook |
||||
* |
||||
* @example |
||||
* ```tsx
|
||||
* const { data, loading, loadMore, refresh, hasMore } = usePagination( |
||||
* (page, pageSize) => request.get('/api/users', { params: { page, pageSize } }) |
||||
* ); |
||||
* ``` |
||||
*/ |
||||
export function usePagination<T = any>( |
||||
requestFn: ( |
||||
page: number, |
||||
pageSize: number |
||||
) => Promise<{ |
||||
list: T[]; |
||||
total: number; |
||||
hasMore: boolean; |
||||
}>, |
||||
options: { |
||||
pageSize?: number; |
||||
immediate?: boolean; |
||||
onSuccess?: (data: T[]) => void; |
||||
onError?: (error: Error) => void; |
||||
} = {} |
||||
) { |
||||
const { pageSize = 20, immediate = false, onSuccess, onError } = options; |
||||
|
||||
const [state, setState] = useState({ |
||||
data: [] as T[], |
||||
loading: false, |
||||
loadingMore: false, |
||||
error: null as Error | null, |
||||
page: 1, |
||||
total: 0, |
||||
hasMore: true, |
||||
}); |
||||
|
||||
/** |
||||
* 加载数据 |
||||
*/ |
||||
const load = useCallback( |
||||
async (page: number, append = false) => { |
||||
setState((prev) => ({ |
||||
...prev, |
||||
loading: !append, |
||||
loadingMore: append, |
||||
error: null, |
||||
})); |
||||
|
||||
try { |
||||
const result = await requestFn(page, pageSize); |
||||
|
||||
setState((prev) => ({ |
||||
...prev, |
||||
data: append ? [...prev.data, ...result.list] : result.list, |
||||
loading: false, |
||||
loadingMore: false, |
||||
page, |
||||
total: result.total, |
||||
hasMore: result.hasMore, |
||||
})); |
||||
|
||||
onSuccess?.(result.list); |
||||
|
||||
return result; |
||||
} catch (error) { |
||||
const err = error as Error; |
||||
|
||||
setState((prev) => ({ |
||||
...prev, |
||||
loading: false, |
||||
loadingMore: false, |
||||
error: err, |
||||
})); |
||||
|
||||
onError?.(err); |
||||
|
||||
throw error; |
||||
} |
||||
}, |
||||
[requestFn, pageSize, onSuccess, onError] |
||||
); |
||||
|
||||
/** |
||||
* 加载更多 |
||||
*/ |
||||
const loadMore = useCallback(async () => { |
||||
if (state.loadingMore || !state.hasMore) { |
||||
return; |
||||
} |
||||
|
||||
return load(state.page + 1, true); |
||||
}, [state.loadingMore, state.hasMore, state.page, load]); |
||||
|
||||
/** |
||||
* 刷新(重新加载第一页) |
||||
*/ |
||||
const refresh = useCallback(async () => { |
||||
return load(1, false); |
||||
}, [load]); |
||||
|
||||
/** |
||||
* 重置 |
||||
*/ |
||||
const reset = useCallback(() => { |
||||
setState({ |
||||
data: [], |
||||
loading: false, |
||||
loadingMore: false, |
||||
error: null, |
||||
page: 1, |
||||
total: 0, |
||||
hasMore: true, |
||||
}); |
||||
}, []); |
||||
|
||||
// 立即执行
|
||||
useEffect(() => { |
||||
if (immediate) { |
||||
load(1, false); |
||||
} |
||||
}, [immediate]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return { |
||||
...state, |
||||
loadMore, |
||||
refresh, |
||||
reset, |
||||
}; |
||||
} |
||||
@ -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]); |
||||
} |
||||
|
||||
@ -0,0 +1,19 @@
|
||||
const { getDefaultConfig } = require('expo/metro-config'); |
||||
|
||||
/** @type {import('expo/metro-config').MetroConfig} */ |
||||
const config = getDefaultConfig(__dirname); |
||||
|
||||
// 自定义 Metro 配置
|
||||
config.resolver = { |
||||
...config.resolver, |
||||
// 可以在这里添加自定义解析规则
|
||||
}; |
||||
|
||||
// 开发服务器配置
|
||||
config.server = { |
||||
...config.server, |
||||
// Metro 服务器端口(默认 8081)
|
||||
port: 8081, |
||||
}; |
||||
|
||||
module.exports = config; |
||||
@ -23,6 +23,9 @@ importers:
|
||||
axios: |
||||
specifier: ^1.13.1 |
||||
version: 1.13.1 |
||||
crypto-js: |
||||
specifier: ^4.2.0 |
||||
version: 4.2.0 |
||||
dayjs: |
||||
specifier: ^1.11.19 |
||||
version: 1.11.19 |
||||
@ -62,6 +65,9 @@ importers:
|
||||
lodash-es: |
||||
specifier: ^4.17.21 |
||||
version: 4.17.21 |
||||
md5: |
||||
specifier: ^2.3.0 |
||||
version: 2.3.0 |
||||
react: |
||||
specifier: 19.1.0 |
||||
version: 19.1.0 |
||||
@ -99,12 +105,33 @@ importers:
|
||||
specifier: ^5.0.8 |
||||
version: 5.0.8(@types/[email protected])([email protected])([email protected]([email protected])) |
||||
devDependencies: |
||||
'@types/crypto-js': |
||||
specifier: ^4.2.2 |
||||
version: 4.2.2 |
||||
'@types/lodash-es': |
||||
specifier: ^4.17.12 |
||||
version: 4.17.12 |
||||
'@types/md5': |
||||
specifier: ^2.3.6 |
||||
version: 2.3.6 |
||||
'@types/react': |
||||
specifier: ~19.1.0 |
||||
version: 19.1.17 |
||||
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 |
||||
http-proxy-middleware: |
||||
specifier: ^3.0.5 |
||||
version: 3.0.5 |
||||
prettier: |
||||
specifier: ^3.6.2 |
||||
version: 3.6.2 |
||||
react-test-renderer: |
||||
specifier: 19.1.0 |
||||
version: 19.1.0([email protected]) |
||||
@ -1192,9 +1219,15 @@ packages:
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} |
||||
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} |
||||
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} |
||||
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} |
||||
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} |
||||
|
||||
@ -1210,6 +1243,9 @@ packages:
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} |
||||
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA==} |
||||
|
||||
'@types/[email protected]': |
||||
resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} |
||||
|
||||
@ -1248,6 +1284,10 @@ packages:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} |
||||
engines: {node: '>=0.4.0'} |
||||
@ -1409,6 +1449,10 @@ packages:
|
||||
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} |
||||
engines: {node: '>=0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} |
||||
engines: {node: '>=18'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} |
||||
|
||||
@ -1452,6 +1496,10 @@ packages:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} |
||||
engines: {node: '>=6'} |
||||
@ -1471,6 +1519,9 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} |
||||
engines: {node: '>=10'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} |
||||
engines: {node: '>=18'} |
||||
@ -1562,16 +1613,41 @@ packages:
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} |
||||
engines: {node: '>=18'} |
||||
hasBin: true |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} |
||||
engines: {node: '>= 0.10.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} |
||||
engines: {node: '>=6.6.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[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==} |
||||
|
||||
@ -1579,6 +1655,12 @@ packages:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} |
||||
engines: {node: '>= 8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} |
||||
engines: {node: '>=8'} |
||||
@ -1749,6 +1831,9 @@ packages:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} |
||||
engines: {node: '>=6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==} |
||||
|
||||
@ -1918,6 +2003,10 @@ packages:
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} |
||||
engines: {node: '>= 18'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} |
||||
|
||||
@ -1945,6 +2034,10 @@ packages:
|
||||
resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} |
||||
engines: {node: '>= 0.8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} |
||||
engines: {node: '>= 0.8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} |
||||
engines: {node: '>=8'} |
||||
@ -1976,6 +2069,10 @@ packages:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} |
||||
engines: {node: '>= 6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==} |
||||
engines: {node: '>=8'} |
||||
@ -1984,6 +2081,10 @@ packages:
|
||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} |
||||
engines: {node: '>= 0.8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} |
||||
|
||||
@ -2085,6 +2186,14 @@ packages:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} |
||||
engines: {node: '>= 0.8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==} |
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} |
||||
engines: {node: '>=8.0.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} |
||||
engines: {node: '>= 14'} |
||||
@ -2092,6 +2201,14 @@ packages:
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} |
||||
engines: {node: '>=0.10.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} |
||||
engines: {node: '>=0.10.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} |
||||
|
||||
@ -2124,9 +2241,16 @@ packages:
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} |
||||
engines: {node: '>= 0.10'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} |
||||
engines: {node: '>= 0.4'} |
||||
@ -2136,10 +2260,18 @@ packages:
|
||||
engines: {node: '>=8'} |
||||
hasBin: true |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} |
||||
engines: {node: '>=0.10.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} |
||||
engines: {node: '>=8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} |
||||
engines: {node: '>=0.10.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} |
||||
engines: {node: '>=0.12.0'} |
||||
@ -2148,6 +2280,13 @@ packages:
|
||||
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} |
||||
engines: {node: '>=8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} |
||||
engines: {node: '>=0.10.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} |
||||
engines: {node: '>=8'} |
||||
@ -2358,12 +2497,23 @@ packages:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} |
||||
engines: {node: '>= 0.8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} |
||||
engines: {node: '>=18'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} |
||||
engines: {node: '>=10'} |
||||
@ -2503,6 +2653,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} |
||||
engines: {node: '>=4'} |
||||
@ -2557,6 +2711,10 @@ packages:
|
||||
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} |
||||
|
||||
@ -2602,6 +2760,10 @@ packages:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} |
||||
engines: {node: '>=0.10.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
on-[email protected]: |
||||
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} |
||||
engines: {node: '>= 0.8'} |
||||
@ -2683,6 +2845,9 @@ packages:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} |
||||
engines: {node: '>=16 || 14 >=14.18'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} |
||||
|
||||
@ -2713,6 +2878,11 @@ packages:
|
||||
resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} |
||||
engines: {node: ^10 || ^12 || >=14} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} |
||||
engines: {node: '>=14'} |
||||
hasBin: true |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} |
||||
engines: {node: '>=6'} |
||||
@ -2739,6 +2909,10 @@ packages:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} |
||||
engines: {node: '>= 6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} |
||||
engines: {node: '>= 0.10'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} |
||||
|
||||
@ -2750,6 +2924,10 @@ packages:
|
||||
resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} |
||||
hasBin: true |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} |
||||
engines: {node: '>=0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} |
||||
engines: {node: '>=6'} |
||||
@ -2761,6 +2939,10 @@ packages:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} |
||||
engines: {node: '>= 0.10'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} |
||||
hasBin: true |
||||
@ -2930,6 +3112,9 @@ packages:
|
||||
resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} |
||||
engines: {node: '>= 4.0.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} |
||||
engines: {node: '>=8'} |
||||
@ -2962,9 +3147,19 @@ packages:
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported |
||||
hasBin: true |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} |
||||
engines: {node: '>= 18'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-FySGAa0RGcFiN6zfrO9JvK1r7TB59xuzCcTHOBXBNoKgDejlOQCR2KL/FGk3/iDlsqyYg1ELZpOmlg09B01Czw==} |
||||
|
||||
@ -2998,6 +3193,10 @@ packages:
|
||||
resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} |
||||
engines: {node: '>= 0.8.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} |
||||
engines: {node: '>= 18'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} |
||||
engines: {node: '>=0.10.0'} |
||||
@ -3006,6 +3205,10 @@ packages:
|
||||
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} |
||||
engines: {node: '>= 0.8.0'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} |
||||
engines: {node: '>= 18'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} |
||||
|
||||
@ -3034,6 +3237,22 @@ packages:
|
||||
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} |
||||
engines: {node: '>= 0.4'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} |
||||
|
||||
@ -3207,6 +3426,10 @@ packages:
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} |
||||
hasBin: true |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} |
||||
|
||||
@ -3225,6 +3448,10 @@ packages:
|
||||
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} |
||||
engines: {node: '>=8'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} |
||||
engines: {node: '>= 0.6'} |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} |
||||
engines: {node: '>=14.17'} |
||||
@ -4938,10 +5165,16 @@ snapshots:
|
||||
dependencies: |
||||
'@babel/types': 7.28.5 |
||||
|
||||
'@types/[email protected]': {} |
||||
|
||||
'@types/[email protected]': |
||||
dependencies: |
||||
'@types/node': 24.10.0 |
||||
|
||||
'@types/[email protected]': |
||||
dependencies: |
||||
'@types/node': 24.10.0 |
||||
|
||||
'@types/[email protected]': {} |
||||
|
||||
'@types/[email protected]': |
||||
@ -4958,6 +5191,8 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {} |
||||
|
||||
'@types/[email protected]': {} |
||||
|
||||
'@types/[email protected]': |
||||
dependencies: |
||||
undici-types: 7.16.0 |
||||
@ -4999,6 +5234,11 @@ snapshots:
|
||||
mime-types: 2.1.35 |
||||
negotiator: 0.6.3 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
mime-types: 3.0.1 |
||||
negotiator: 1.0.0 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -5056,7 +5296,7 @@ snapshots:
|
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
follow-redirects: 1.15.11 |
||||
follow-redirects: 1.15.11([email protected]) |
||||
form-data: 4.0.4 |
||||
proxy-from-env: 1.1.0 |
||||
transitivePeerDependencies: |
||||
@ -5201,6 +5441,20 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
bytes: 3.1.2 |
||||
content-type: 1.0.5 |
||||
debug: 4.4.3 |
||||
http-errors: 2.0.0 |
||||
iconv-lite: 0.6.3 |
||||
on-finished: 2.4.1 |
||||
qs: 6.14.0 |
||||
raw-body: 3.0.1 |
||||
type-is: 2.0.1 |
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
stream-buffers: 2.2.0 |
||||
@ -5252,6 +5506,11 @@ snapshots:
|
||||
es-errors: 1.3.0 |
||||
function-bind: 1.1.2 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
call-bind-apply-helpers: 1.0.2 |
||||
get-intrinsic: 1.3.0 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -5269,6 +5528,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0 |
||||
supports-color: 7.2.0 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
@ -5368,6 +5629,15 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
chalk: 4.1.2 |
||||
rxjs: 7.8.2 |
||||
shell-quote: 1.8.3 |
||||
supports-color: 8.1.1 |
||||
tree-kill: 1.2.2 |
||||
yargs: 17.7.2 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
debug: 2.6.9 |
||||
@ -5377,12 +5647,27 @@ snapshots:
|
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
safe-buffer: 5.2.1 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
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 |
||||
@ -5395,6 +5680,10 @@ snapshots:
|
||||
shebang-command: 2.0.0 |
||||
which: 2.0.2 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
@ -5504,6 +5793,8 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]([email protected])([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]): |
||||
@ -5718,6 +6009,38 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
accepts: 2.0.0 |
||||
body-parser: 2.2.0 |
||||
content-disposition: 1.0.0 |
||||
content-type: 1.0.5 |
||||
cookie: 0.7.2 |
||||
cookie-signature: 1.2.2 |
||||
debug: 4.4.3 |
||||
encodeurl: 2.0.0 |
||||
escape-html: 1.0.3 |
||||
etag: 1.8.1 |
||||
finalhandler: 2.1.0 |
||||
fresh: 2.0.0 |
||||
http-errors: 2.0.0 |
||||
merge-descriptors: 2.0.0 |
||||
mime-types: 3.0.1 |
||||
on-finished: 2.4.1 |
||||
once: 1.4.0 |
||||
parseurl: 1.3.3 |
||||
proxy-addr: 2.0.7 |
||||
qs: 6.14.0 |
||||
range-parser: 1.2.1 |
||||
router: 2.2.0 |
||||
send: 1.2.0 |
||||
serve-static: 2.2.0 |
||||
statuses: 2.0.1 |
||||
type-is: 2.0.1 |
||||
vary: 1.1.2 |
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -5758,6 +6081,17 @@ snapshots:
|
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
debug: 4.4.3 |
||||
encodeurl: 2.0.0 |
||||
escape-html: 1.0.3 |
||||
on-finished: 2.4.1 |
||||
parseurl: 1.3.3 |
||||
statuses: 2.0.1 |
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
locate-path: 5.0.0 |
||||
@ -5770,7 +6104,9 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
[email protected]([email protected]): |
||||
optionalDependencies: |
||||
debug: 4.4.3 |
||||
|
||||
[email protected]: {} |
||||
|
||||
@ -5787,10 +6123,14 @@ snapshots:
|
||||
hasown: 2.0.2 |
||||
mime-types: 2.1.35 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
@ -5894,6 +6234,25 @@ snapshots:
|
||||
statuses: 2.0.1 |
||||
toidentifier: 1.0.1 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
'@types/http-proxy': 1.17.17 |
||||
debug: 4.4.3 |
||||
http-proxy: 1.18.1([email protected]) |
||||
is-glob: 4.0.3 |
||||
is-plain-object: 5.0.0 |
||||
micromatch: 4.0.8 |
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]([email protected]): |
||||
dependencies: |
||||
eventemitter3: 4.0.7 |
||||
follow-redirects: 1.15.11([email protected]) |
||||
requires-port: 1.0.0 |
||||
transitivePeerDependencies: |
||||
- debug |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
agent-base: 7.1.4 |
||||
@ -5903,6 +6262,14 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
safer-buffer: 2.1.2 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
safer-buffer: 2.1.2 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -5930,20 +6297,34 @@ snapshots:
|
||||
dependencies: |
||||
loose-envify: 1.4.0 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
hasown: 2.0.2 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
is-extglob: 2.1.1 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
is-docker: 2.2.1 |
||||
@ -6159,10 +6540,20 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
charenc: 0.0.2 |
||||
crypt: 0.0.2 |
||||
is-buffer: 1.1.6 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
is-plain-obj: 2.1.0 |
||||
@ -6532,6 +6923,10 @@ snapshots:
|
||||
dependencies: |
||||
mime-db: 1.52.0 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
mime-db: 1.54.0 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -6570,6 +6965,8 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
@ -6603,6 +7000,8 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
on-[email protected]: |
||||
dependencies: |
||||
ee-first: 1.1.1 |
||||
@ -6680,6 +7079,8 @@ snapshots:
|
||||
lru-cache: 10.4.3 |
||||
minipass: 7.1.2 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -6704,6 +7105,8 @@ snapshots:
|
||||
picocolors: 1.1.1 |
||||
source-map-js: 1.2.1 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
@ -6729,12 +7132,21 @@ snapshots:
|
||||
kleur: 3.0.3 |
||||
sisteransi: 1.0.5 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
forwarded: 0.2.0 |
||||
ipaddr.js: 1.9.1 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
side-channel: 1.1.0 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
decode-uri-component: 0.2.2 |
||||
@ -6748,6 +7160,13 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
bytes: 3.1.2 |
||||
http-errors: 2.0.0 |
||||
iconv-lite: 0.7.0 |
||||
unpipe: 1.0.0 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
deep-extend: 0.6.0 |
||||
@ -6971,6 +7390,8 @@ snapshots:
|
||||
rc: 1.2.8 |
||||
resolve: 1.7.1 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
@ -7000,8 +7421,24 @@ snapshots:
|
||||
dependencies: |
||||
glob: 7.2.3 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
debug: 4.4.3 |
||||
depd: 2.0.0 |
||||
is-promise: 4.0.0 |
||||
parseurl: 1.3.3 |
||||
path-to-regexp: 8.3.0 |
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
tslib: 2.8.1 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -7050,6 +7487,22 @@ snapshots:
|
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
debug: 4.4.3 |
||||
encodeurl: 2.0.0 |
||||
escape-html: 1.0.3 |
||||
etag: 1.8.1 |
||||
fresh: 2.0.0 |
||||
http-errors: 2.0.0 |
||||
mime-types: 3.0.1 |
||||
ms: 2.1.3 |
||||
on-finished: 2.4.1 |
||||
range-parser: 1.2.1 |
||||
statuses: 2.0.1 |
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
@ -7061,6 +7514,15 @@ snapshots:
|
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
encodeurl: 2.0.0 |
||||
escape-html: 1.0.3 |
||||
parseurl: 1.3.3 |
||||
send: 1.2.0 |
||||
transitivePeerDependencies: |
||||
- supports-color |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -7079,6 +7541,34 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
es-errors: 1.3.0 |
||||
object-inspect: 1.13.4 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
call-bound: 1.0.4 |
||||
es-errors: 1.3.0 |
||||
get-intrinsic: 1.3.0 |
||||
object-inspect: 1.13.4 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
call-bound: 1.0.4 |
||||
es-errors: 1.3.0 |
||||
get-intrinsic: 1.3.0 |
||||
object-inspect: 1.13.4 |
||||
side-channel-map: 1.0.1 |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
es-errors: 1.3.0 |
||||
object-inspect: 1.13.4 |
||||
side-channel-list: 1.0.0 |
||||
side-channel-map: 1.0.1 |
||||
side-channel-weakmap: 1.0.2 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -7239,6 +7729,8 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
@ -7249,6 +7741,12 @@ snapshots:
|
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: |
||||
dependencies: |
||||
content-type: 1.0.5 |
||||
media-typer: 1.1.0 |
||||
mime-types: 3.0.1 |
||||
|
||||
[email protected]: {} |
||||
|
||||
[email protected]: {} |
||||
|
||||
@ -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,111 @@
|
||||
/** |
||||
* 开发环境代理服务器 |
||||
* 用于解决跨域问题和统一 API 请求 |
||||
*/ |
||||
|
||||
const express = require('express'); |
||||
const { createProxyMiddleware } = require('http-proxy-middleware'); |
||||
const cors = require('cors'); |
||||
|
||||
const app = express(); |
||||
const PORT = 8086; |
||||
|
||||
// 启用 CORS
|
||||
app.use(cors({ |
||||
origin: '*', // 允许所有源,开发环境使用
|
||||
credentials: true, |
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], |
||||
allowedHeaders: [ |
||||
'Content-Type', |
||||
'Authorization', |
||||
'X-Requested-With', |
||||
'Accept', |
||||
// 自定义请求头
|
||||
'cmdId', |
||||
'datetime', |
||||
'pwds', |
||||
'aseqId', |
||||
'nc', |
||||
'apiName', |
||||
'tid', |
||||
'custId', |
||||
'reqId', |
||||
'isMobileOpen', |
||||
'languageNum', |
||||
'project', |
||||
'platform', |
||||
'checkOr', |
||||
'tbc', |
||||
'reqKey', |
||||
'signature', |
||||
'authorization', |
||||
], |
||||
exposedHeaders: ['cmdId', 'datetime', 'pwds', 'aseqId', 'nc', 'checkOr', 'checkor'], |
||||
})); |
||||
|
||||
// 目标 API 服务器地址
|
||||
const API_TARGET = process.env.API_TARGET || 'https://51zhh5.notbug.org'; |
||||
|
||||
// 代理路径列表
|
||||
const PROXY_PATHS = ['/api/v1', '/api/v2', '/api/v3']; |
||||
|
||||
// 为每个路径配置代理
|
||||
PROXY_PATHS.forEach((path) => { |
||||
app.use( |
||||
path, |
||||
createProxyMiddleware({ |
||||
target: API_TARGET, |
||||
changeOrigin: true, |
||||
secure: false, // 如果目标服务器使用自签名证书,设置为 false
|
||||
pathRewrite: (pathStr, req) => { |
||||
// Express 会自动去掉匹配的前缀,所以需要加回来
|
||||
const fullPath = path + pathStr; |
||||
console.log(`[Proxy] Path rewrite: ${pathStr} → ${fullPath}`); |
||||
return fullPath; |
||||
}, |
||||
onProxyReq: (proxyReq, req, res) => { |
||||
const fullPath = path + req.url; |
||||
console.log(`[Proxy] ${req.method} ${fullPath} → ${API_TARGET}${fullPath}`); |
||||
}, |
||||
onProxyRes: (proxyRes, req, res) => { |
||||
const fullPath = path + req.url; |
||||
console.log(`[Proxy] ${req.method} ${fullPath} ← ${proxyRes.statusCode}`); |
||||
|
||||
// 确保 CORS 头被正确设置
|
||||
res.setHeader('Access-Control-Allow-Origin', '*'); |
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH'); |
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, cmdId, datetime, pwds, aseqId, nc, apiName, tid, custId, reqId, isMobileOpen, languageNum, project, platform, checkOr, tbc, reqKey, signature, authorization'); |
||||
res.setHeader('Access-Control-Expose-Headers', 'cmdId, datetime, pwds, aseqId, nc, checkOr, checkor'); |
||||
res.setHeader('Access-Control-Allow-Credentials', 'true'); |
||||
}, |
||||
onError: (err, req, res) => { |
||||
console.error('[Proxy Error]', err.message); |
||||
res.status(500).json({ |
||||
error: 'Proxy Error', |
||||
message: err.message, |
||||
}); |
||||
}, |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => { |
||||
res.json({ |
||||
status: 'ok', |
||||
proxy: API_TARGET, |
||||
timestamp: new Date().toISOString(), |
||||
}); |
||||
}); |
||||
|
||||
app.listen(PORT, () => { |
||||
console.log(` |
||||
╔════════════════════════════════════════════════════════╗ |
||||
║ 🚀 Proxy Server Running ║ |
||||
╠════════════════════════════════════════════════════════╣ |
||||
║ Local: http://localhost:${PORT} ║
|
||||
║ Target: ${API_TARGET.padEnd(40)} ║ |
||||
║ Health: http://localhost:${PORT}/health ║
|
||||
╚════════════════════════════════════════════════════════╝ |
||||
`);
|
||||
}); |
||||
@ -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,29 +0,0 @@
|
||||
/** |
||||
* 统一导出所有模块 |
||||
*/ |
||||
|
||||
// Utils
|
||||
export { default as api, request } from './utils/api'; |
||||
export { default as Storage, STORAGE_KEYS } from './utils/storage'; |
||||
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'; |
||||
|
||||
// Hooks
|
||||
export * from './hooks/useDebounce'; |
||||
export * from './hooks/useThrottle'; |
||||
export * from './hooks/useHaptics'; |
||||
|
||||
// Types
|
||||
export * from './types'; |
||||
|
||||
@ -1,147 +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,142 +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, |
||||
})); |
||||
|
||||
@ -1,144 +0,0 @@
|
||||
/** |
||||
* Axios API 配置 |
||||
* 统一管理 HTTP 请求 |
||||
*/ |
||||
|
||||
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; |
||||
import AsyncStorage from '@react-native-async-storage/async-storage'; |
||||
|
||||
// API 基础配置
|
||||
const API_CONFIG = { |
||||
baseURL: process.env.EXPO_PUBLIC_API_URL || 'https://api.example.com', |
||||
timeout: 10000, |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
}; |
||||
|
||||
// 创建 axios 实例
|
||||
const api = axios.create(API_CONFIG); |
||||
|
||||
/** |
||||
* 请求拦截器 |
||||
* 在请求发送前添加 token 等信息 |
||||
*/ |
||||
api.interceptors.request.use( |
||||
async (config) => { |
||||
try { |
||||
// 从本地存储获取 token
|
||||
const token = await AsyncStorage.getItem('auth_token'); |
||||
|
||||
if (token) { |
||||
config.headers.Authorization = `Bearer ${token}`; |
||||
} |
||||
|
||||
// 打印请求信息(开发环境)
|
||||
if (__DEV__) { |
||||
console.log('📤 API Request:', { |
||||
method: config.method?.toUpperCase(), |
||||
url: config.url, |
||||
data: config.data, |
||||
}); |
||||
} |
||||
|
||||
return config; |
||||
} catch (error) { |
||||
console.error('Request interceptor error:', error); |
||||
return config; |
||||
} |
||||
}, |
||||
(error) => { |
||||
console.error('Request error:', error); |
||||
return Promise.reject(error); |
||||
} |
||||
); |
||||
|
||||
/** |
||||
* 响应拦截器 |
||||
* 统一处理响应和错误 |
||||
*/ |
||||
api.interceptors.response.use( |
||||
(response: AxiosResponse) => { |
||||
// 打印响应信息(开发环境)
|
||||
if (__DEV__) { |
||||
console.log('📥 API Response:', { |
||||
url: response.config.url, |
||||
status: response.status, |
||||
data: response.data, |
||||
}); |
||||
} |
||||
|
||||
// 返回响应数据
|
||||
return response.data; |
||||
}, |
||||
async (error: AxiosError) => { |
||||
// 打印错误信息
|
||||
console.error('❌ API Error:', { |
||||
url: error.config?.url, |
||||
status: error.response?.status, |
||||
message: error.message, |
||||
data: error.response?.data, |
||||
}); |
||||
|
||||
// 处理不同的错误状态码
|
||||
if (error.response) { |
||||
switch (error.response.status) { |
||||
case 401: |
||||
// 未授权,清除 token 并跳转到登录页
|
||||
await AsyncStorage.removeItem('auth_token'); |
||||
// TODO: 导航到登录页
|
||||
// router.replace('/login');
|
||||
break; |
||||
|
||||
case 403: |
||||
// 禁止访问
|
||||
console.error('Access forbidden'); |
||||
break; |
||||
|
||||
case 404: |
||||
// 资源不存在
|
||||
console.error('Resource not found'); |
||||
break; |
||||
|
||||
case 500: |
||||
// 服务器错误
|
||||
console.error('Server error'); |
||||
break; |
||||
|
||||
default: |
||||
console.error('Unknown error'); |
||||
} |
||||
} else if (error.request) { |
||||
// 请求已发送但没有收到响应
|
||||
console.error('No response received'); |
||||
} else { |
||||
// 请求配置出错
|
||||
console.error('Request configuration error'); |
||||
} |
||||
|
||||
return Promise.reject(error); |
||||
} |
||||
); |
||||
|
||||
/** |
||||
* 通用请求方法 |
||||
*/ |
||||
export const request = { |
||||
get: <T = any>(url: string, config?: AxiosRequestConfig) =>
|
||||
api.get<T, T>(url, config), |
||||
|
||||
post: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||
api.post<T, T>(url, data, config), |
||||
|
||||
put: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||
api.put<T, T>(url, data, config), |
||||
|
||||
delete: <T = any>(url: string, config?: AxiosRequestConfig) =>
|
||||
api.delete<T, T>(url, config), |
||||
|
||||
patch: <T = any>(url: string, data?: any, config?: AxiosRequestConfig) =>
|
||||
api.patch<T, T>(url, data, config), |
||||
}; |
||||
|
||||
export default api; |
||||
|
||||
@ -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,73 @@
|
||||
/** |
||||
* API 相关类型定义 |
||||
*/ |
||||
|
||||
/** |
||||
* 分页请求参数 |
||||
*/ |
||||
export interface PaginationParams { |
||||
page: number; |
||||
pageSize: number; |
||||
sortBy?: string; |
||||
sortOrder?: 'asc' | 'desc'; |
||||
} |
||||
|
||||
/** |
||||
* 分页响应数据 |
||||
*/ |
||||
export interface PaginationResponse<T> { |
||||
list: T[]; |
||||
total: number; |
||||
page: number; |
||||
pageSize: number; |
||||
totalPages: number; |
||||
hasMore: boolean; |
||||
} |
||||
|
||||
/** |
||||
* 列表响应数据 |
||||
*/ |
||||
export interface ListResponse<T> { |
||||
items: T[]; |
||||
total: number; |
||||
} |
||||
|
||||
/** |
||||
* ID 参数 |
||||
*/ |
||||
export interface IdParams { |
||||
id: string | number; |
||||
} |
||||
|
||||
/** |
||||
* 批量操作参数 |
||||
*/ |
||||
export interface BatchParams { |
||||
ids: (string | number)[]; |
||||
} |
||||
|
||||
/** |
||||
* 搜索参数 |
||||
*/ |
||||
export interface SearchParams { |
||||
keyword: string; |
||||
filters?: Record<string, any>; |
||||
} |
||||
|
||||
/** |
||||
* 上传响应 |
||||
*/ |
||||
export interface UploadResponse { |
||||
url: string; |
||||
filename: string; |
||||
size: number; |
||||
mimeType: string; |
||||
} |
||||
|
||||
/** |
||||
* 通用操作响应 |
||||
*/ |
||||
export interface OperationResponse { |
||||
success: boolean; |
||||
message?: string; |
||||
} |
||||
@ -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'; |
||||
|
||||
@ -0,0 +1,589 @@
|
||||
/** |
||||
* Axios API 配置 |
||||
* 统一管理 HTTP 请求 |
||||
* |
||||
* 功能特性: |
||||
* - 自动添加 Token |
||||
* - Token 自动刷新 |
||||
* - 请求重试机制 |
||||
* - 请求取消功能 |
||||
* - 统一错误处理 |
||||
* - 请求/响应日志 |
||||
* - Loading 状态管理 |
||||
*/ |
||||
|
||||
import axios, { |
||||
AxiosError, |
||||
AxiosRequestConfig, |
||||
AxiosResponse, |
||||
InternalAxiosRequestConfig, |
||||
CancelTokenSource, |
||||
AxiosRequestHeaders, |
||||
} from 'axios'; |
||||
import AsyncStorage from '@react-native-async-storage/async-storage'; |
||||
import { router } from 'expo-router'; |
||||
import { config } from '../config'; |
||||
import { transformRequest, parseResponse } from './helper'; |
||||
import { cloneDeep, pick } from 'lodash-es'; |
||||
import md5 from 'md5'; |
||||
|
||||
/** |
||||
* API 响应数据结构 |
||||
*/ |
||||
export interface ApiResponse<T = any> { |
||||
code: number; |
||||
message: string; |
||||
data: T; |
||||
success?: boolean; |
||||
} |
||||
|
||||
/** |
||||
* API 错误响应 |
||||
*/ |
||||
export interface ApiError { |
||||
code: number; |
||||
message: string; |
||||
errors?: Record<string, string[]>; |
||||
} |
||||
|
||||
/** |
||||
* 请求配置扩展 |
||||
*/ |
||||
export interface RequestConfig extends AxiosRequestConfig { |
||||
/** 是否显示 loading */ |
||||
showLoading?: boolean; |
||||
/** 是否显示错误提示 */ |
||||
showError?: boolean; |
||||
/** 是否重试 */ |
||||
retry?: boolean; |
||||
/** 重试次数 */ |
||||
retryCount?: number; |
||||
/** 是否需要 token */ |
||||
requiresAuth?: boolean; |
||||
/** 自定义错误处理 */ |
||||
customErrorHandler?: (error: AxiosError<ApiError>) => void; |
||||
} |
||||
|
||||
// API 基础配置
|
||||
const API_CONFIG = { |
||||
baseURL: config.api.baseURL, |
||||
timeout: config.api.timeout, |
||||
headers: { |
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', |
||||
Accept: 'application/json, application/xml, text/play, text/html, *.*', |
||||
}, |
||||
}; |
||||
|
||||
// 创建 axios 实例
|
||||
const api = axios.create(API_CONFIG); |
||||
|
||||
// 请求队列(用于取消请求)
|
||||
const pendingRequests = new Map<string, CancelTokenSource>(); |
||||
|
||||
// 是否正在刷新 token
|
||||
let isRefreshing = false; |
||||
|
||||
// 刷新 token 时的请求队列
|
||||
let refreshSubscribers: Array<(token: string) => void> = []; |
||||
|
||||
/** |
||||
* 生成请求唯一标识 |
||||
*/ |
||||
function generateRequestKey(config: InternalAxiosRequestConfig): string { |
||||
const cmdId = config.headers.cmdId || config.url; |
||||
const data = cloneDeep(config.method === 'post' ? config.data : config.params); |
||||
return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`; |
||||
} |
||||
|
||||
/** |
||||
* 添加请求到队列 |
||||
*/ |
||||
function addPendingRequest(config: InternalAxiosRequestConfig): void { |
||||
const requestKey = generateRequestKey(config); |
||||
|
||||
// 如果已存在相同请求,取消之前的请求
|
||||
if (pendingRequests.has(requestKey)) { |
||||
const source = pendingRequests.get(requestKey); |
||||
source?.cancel('重复请求已取消'); |
||||
} |
||||
|
||||
// 创建新的取消令牌
|
||||
const source = axios.CancelToken.source(); |
||||
config.cancelToken = source.token; |
||||
pendingRequests.set(requestKey, source); |
||||
} |
||||
|
||||
/** |
||||
* 从队列中移除请求 |
||||
*/ |
||||
function removePendingRequest(config: InternalAxiosRequestConfig | AxiosRequestConfig): void { |
||||
const requestKey = generateRequestKey(config as InternalAxiosRequestConfig); |
||||
pendingRequests.delete(requestKey); |
||||
} |
||||
|
||||
/** |
||||
* 订阅 token 刷新 |
||||
*/ |
||||
function subscribeTokenRefresh(callback: (token: string) => void): void { |
||||
refreshSubscribers.push(callback); |
||||
} |
||||
|
||||
/** |
||||
* 通知所有订阅者 token 已刷新 |
||||
*/ |
||||
function onTokenRefreshed(token: string): void { |
||||
refreshSubscribers.forEach((callback) => callback(token)); |
||||
refreshSubscribers = []; |
||||
} |
||||
|
||||
/** |
||||
* 刷新 token |
||||
*/ |
||||
async function refreshAccessToken(): Promise<string | null> { |
||||
try { |
||||
const refreshToken = await AsyncStorage.getItem('refresh_token'); |
||||
|
||||
if (!refreshToken) { |
||||
throw new Error('No refresh token'); |
||||
} |
||||
|
||||
// 调用刷新 token 接口
|
||||
const response = await axios.post<ApiResponse<{ token: string; refreshToken: string }>>( |
||||
`${config.api.baseURL}/auth/refresh-token`, |
||||
{ refreshToken } |
||||
); |
||||
|
||||
const { token, refreshToken: newRefreshToken } = response.data.data; |
||||
|
||||
// 保存新的 token
|
||||
await AsyncStorage.setItem('auth_token', token); |
||||
await AsyncStorage.setItem('refresh_token', newRefreshToken); |
||||
|
||||
return token; |
||||
} catch (error) { |
||||
// 刷新失败,清除所有 token
|
||||
await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 请求拦截器 |
||||
* 在请求发送前添加 token 等信息 |
||||
*/ |
||||
api.interceptors.request.use( |
||||
async (config: InternalAxiosRequestConfig) => { |
||||
try { |
||||
// 添加到请求队列(防止重复请求)
|
||||
addPendingRequest(config); |
||||
|
||||
const { apiName } = config.headers; |
||||
|
||||
const { headers, data } = transformRequest(pick(config, ['headers', 'data'])); |
||||
|
||||
config.headers = { |
||||
...headers, |
||||
...(__DEV__ ? { apiName } : {}), |
||||
} as AxiosRequestHeaders; |
||||
|
||||
config.data = data; |
||||
|
||||
if (Number(config.headers.cmdId) !== 381120) { |
||||
config.url = '/v2/'; |
||||
} |
||||
|
||||
// if (__DEV__ && apiName) {
|
||||
// config.url = `${config.url}?${apiName}`;
|
||||
// }
|
||||
|
||||
// // 从本地存储获取 token
|
||||
// const token = await AsyncStorage.getItem('auth_token');
|
||||
//
|
||||
// // 添加 token 到请求头
|
||||
// if (token && config.headers) {
|
||||
// config.headers.Authorization = `Bearer ${token}`;
|
||||
// }
|
||||
|
||||
// 添加请求时间戳(用于计算请求耗时)
|
||||
(config as any).metadata = { startTime: Date.now() }; |
||||
|
||||
// 打印请求信息(开发环境)
|
||||
if (__DEV__) { |
||||
console.log('📤 API Request:', { |
||||
method: config.method?.toUpperCase(), |
||||
url: config.url, |
||||
baseURL: config.baseURL, |
||||
fullURL: `${config.baseURL}${config.url}`, |
||||
params: config.params, |
||||
headers: config.headers, |
||||
}); |
||||
} |
||||
|
||||
return config; |
||||
} catch (error) { |
||||
console.error('❌ Request interceptor error:', error); |
||||
return Promise.reject(error); |
||||
} |
||||
}, |
||||
(error) => { |
||||
console.error('❌ Request error:', error); |
||||
return Promise.reject(error); |
||||
} |
||||
); |
||||
|
||||
/** |
||||
* 响应拦截器 |
||||
* 统一处理响应和错误 |
||||
*/ |
||||
api.interceptors.response.use( |
||||
async (response: AxiosResponse) => { |
||||
// 从请求队列中移除
|
||||
removePendingRequest(response.config); |
||||
|
||||
// 计算请求耗时
|
||||
const duration = Date.now() - (response.config as any).metadata?.startTime; |
||||
|
||||
const resData: any = await parseResponse(response); |
||||
|
||||
// 打印响应信息(开发环境)
|
||||
// if (__DEV__) {
|
||||
// console.log('📥 API Response:', {
|
||||
// url: response.config.url,
|
||||
// status: response.status,
|
||||
// duration: `${duration}ms`,
|
||||
// data: response.data,
|
||||
// });
|
||||
// }
|
||||
|
||||
// 统一处理响应数据格式
|
||||
// const apiResponse = response.data as ApiResponse;
|
||||
|
||||
// 如果后端返回的数据结构包含 code 和 data
|
||||
// if (apiResponse && typeof apiResponse === 'object' && 'code' in apiResponse) {
|
||||
// // 检查业务状态码
|
||||
// if (apiResponse.code !== 0 && apiResponse.code !== 200) {
|
||||
// // 业务错误
|
||||
// const error = new Error(apiResponse.message || '请求失败') as any;
|
||||
// error.code = apiResponse.code;
|
||||
// error.response = response;
|
||||
// return Promise.reject(error);
|
||||
// }
|
||||
//
|
||||
// // 返回 data 字段
|
||||
// return apiResponse.data;
|
||||
// }
|
||||
|
||||
// 直接返回响应数据
|
||||
// return response.data;
|
||||
return Promise.resolve(resData); |
||||
}, |
||||
async (error: AxiosError<ApiError>) => { |
||||
// 从请求队列中移除
|
||||
if (error.config) { |
||||
removePendingRequest(error.config); |
||||
} |
||||
|
||||
// 如果是取消的请求,直接返回
|
||||
if (axios.isCancel(error)) { |
||||
if (__DEV__) { |
||||
console.log('🚫 Request cancelled:', error.message); |
||||
} |
||||
return Promise.reject(error); |
||||
} |
||||
|
||||
const originalRequest = error.config as RequestConfig & { _retry?: boolean }; |
||||
|
||||
// 打印错误信息
|
||||
if (__DEV__) { |
||||
console.error('❌ API Error:', { |
||||
method: error.config?.method, |
||||
cmdId: error.config?.headers?.cmdId, |
||||
status: error.response?.status, |
||||
message: error.message, |
||||
data: error.response?.data, |
||||
}); |
||||
} |
||||
|
||||
// 处理不同的错误状态码
|
||||
if (error.response) { |
||||
const { status, data } = error.response; |
||||
|
||||
switch (status) { |
||||
case 401: { |
||||
// Token 过期,尝试刷新
|
||||
if (!originalRequest._retry) { |
||||
if (isRefreshing) { |
||||
// 如果正在刷新,将请求加入队列
|
||||
return new Promise((resolve) => { |
||||
subscribeTokenRefresh((token: string) => { |
||||
if (originalRequest.headers) { |
||||
originalRequest.headers.Authorization = `Bearer ${token}`; |
||||
} |
||||
resolve(api(originalRequest)); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
originalRequest._retry = true; |
||||
isRefreshing = true; |
||||
|
||||
try { |
||||
const newToken = await refreshAccessToken(); |
||||
|
||||
if (newToken) { |
||||
// Token 刷新成功
|
||||
isRefreshing = false; |
||||
onTokenRefreshed(newToken); |
||||
|
||||
// 重试原请求
|
||||
if (originalRequest.headers) { |
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`; |
||||
} |
||||
return api(originalRequest); |
||||
} else { |
||||
// Token 刷新失败,跳转到登录页
|
||||
isRefreshing = false; |
||||
await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); |
||||
|
||||
// 跳转到登录页
|
||||
if (router.canGoBack()) { |
||||
router.replace('/(auth)/login' as any); |
||||
} |
||||
} |
||||
} catch (refreshError) { |
||||
isRefreshing = false; |
||||
await AsyncStorage.multiRemove(['auth_token', 'refresh_token']); |
||||
|
||||
// 跳转到登录页
|
||||
if (router.canGoBack()) { |
||||
router.replace('/(auth)/login' as any); |
||||
} |
||||
|
||||
return Promise.reject(refreshError); |
||||
} |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 403: |
||||
// 禁止访问
|
||||
console.error('❌ 403: 没有权限访问该资源'); |
||||
break; |
||||
|
||||
case 404: |
||||
// 资源不存在
|
||||
console.error('❌ 404: 请求的资源不存在'); |
||||
break; |
||||
|
||||
case 422: |
||||
// 表单验证错误
|
||||
console.error('❌ 422: 表单验证失败', data); |
||||
break; |
||||
|
||||
case 429: |
||||
// 请求过于频繁
|
||||
console.error('❌ 429: 请求过于频繁,请稍后再试'); |
||||
break; |
||||
|
||||
case 500: |
||||
// 服务器错误
|
||||
console.error('❌ 500: 服务器内部错误'); |
||||
break; |
||||
|
||||
case 502: |
||||
// 网关错误
|
||||
console.error('❌ 502: 网关错误'); |
||||
break; |
||||
|
||||
case 503: |
||||
// 服务不可用
|
||||
console.error('❌ 503: 服务暂时不可用'); |
||||
break; |
||||
|
||||
default: |
||||
console.error(`❌ ${status}: 未知错误`); |
||||
} |
||||
} else if (error.request) { |
||||
// 请求已发送但没有收到响应
|
||||
console.error('❌ 网络错误: 请检查网络连接'); |
||||
} else { |
||||
// 请求配置出错
|
||||
console.error('❌ 请求配置错误:', error.message); |
||||
} |
||||
|
||||
// 自定义错误处理
|
||||
if (originalRequest?.customErrorHandler) { |
||||
originalRequest.customErrorHandler(error); |
||||
} |
||||
|
||||
return Promise.reject(error); |
||||
} |
||||
); |
||||
|
||||
/** |
||||
* 取消所有待处理的请求 |
||||
*/ |
||||
export function cancelAllRequests(message = '请求已取消'): void { |
||||
pendingRequests.forEach((source) => { |
||||
source.cancel(message); |
||||
}); |
||||
pendingRequests.clear(); |
||||
} |
||||
|
||||
/** |
||||
* 取消指定 URL 的请求 |
||||
*/ |
||||
export function cancelRequest(url: string): void { |
||||
pendingRequests.forEach((source, key) => { |
||||
if (key.includes(url)) { |
||||
source.cancel('请求已取消'); |
||||
pendingRequests.delete(key); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 通用请求方法(增强版) |
||||
*/ |
||||
export const request = { |
||||
/** |
||||
* GET 请求 |
||||
*/ |
||||
get: <T = any>(url: string, config?: RequestConfig) => api.get<T, T>(url, config), |
||||
|
||||
/** |
||||
* POST 请求 |
||||
*/ |
||||
post: <T = any>(url: string, data?: any, config?: RequestConfig) => |
||||
api.post<T, T>(url, data, config), |
||||
|
||||
/** |
||||
* PUT 请求 |
||||
*/ |
||||
put: <T = any>(url: string, data?: any, config?: RequestConfig) => |
||||
api.put<T, T>(url, data, config), |
||||
|
||||
/** |
||||
* DELETE 请求 |
||||
*/ |
||||
delete: <T = any>(url: string, config?: RequestConfig) => api.delete<T, T>(url, config), |
||||
|
||||
/** |
||||
* PATCH 请求 |
||||
*/ |
||||
patch: <T = any>(url: string, data?: any, config?: RequestConfig) => |
||||
api.patch<T, T>(url, data, config), |
||||
|
||||
/** |
||||
* 上传文件 |
||||
*/ |
||||
upload: <T = any>( |
||||
url: string, |
||||
file: File | Blob, |
||||
onProgress?: (progress: number) => void, |
||||
config?: RequestConfig |
||||
) => { |
||||
const formData = new FormData(); |
||||
formData.append('file', file); |
||||
|
||||
return api.post<T, T>(url, formData, { |
||||
...config, |
||||
headers: { |
||||
'Content-Type': 'multipart/form-data', |
||||
...config?.headers, |
||||
}, |
||||
onUploadProgress: (progressEvent) => { |
||||
if (onProgress && progressEvent.total) { |
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); |
||||
onProgress(progress); |
||||
} |
||||
}, |
||||
}); |
||||
}, |
||||
|
||||
/** |
||||
* 下载文件 |
||||
*/ |
||||
download: async ( |
||||
url: string, |
||||
filename?: string, |
||||
onProgress?: (progress: number) => void, |
||||
config?: RequestConfig |
||||
) => { |
||||
const response: any = await api.get(url, { |
||||
...config, |
||||
responseType: 'blob', |
||||
onDownloadProgress: (progressEvent) => { |
||||
if (onProgress && progressEvent.total) { |
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); |
||||
onProgress(progress); |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([response]); |
||||
const downloadUrl = window.URL.createObjectURL(blob); |
||||
const link = document.createElement('a'); |
||||
link.href = downloadUrl; |
||||
link.download = filename || 'download'; |
||||
document.body.appendChild(link); |
||||
link.click(); |
||||
document.body.removeChild(link); |
||||
window.URL.revokeObjectURL(downloadUrl); |
||||
|
||||
return response; |
||||
}, |
||||
|
||||
/** |
||||
* 并发请求 |
||||
*/ |
||||
all: <T = any>(requests: Promise<T>[]) => Promise.all(requests), |
||||
|
||||
/** |
||||
* 串行请求 |
||||
*/ |
||||
series: async <T = any>(requests: (() => Promise<T>)[]): Promise<T[]> => { |
||||
const results: T[] = []; |
||||
for (const request of requests) { |
||||
const result = await request(); |
||||
results.push(result); |
||||
} |
||||
return results; |
||||
}, |
||||
}; |
||||
|
||||
/** |
||||
* 创建带重试的请求 |
||||
*/ |
||||
export function createRetryRequest<T = any>( |
||||
requestFn: () => Promise<T>, |
||||
maxRetries = 3, |
||||
retryDelay = 1000 |
||||
): Promise<T> { |
||||
return new Promise((resolve, reject) => { |
||||
let retries = 0; |
||||
|
||||
const attempt = async () => { |
||||
try { |
||||
const result = await requestFn(); |
||||
resolve(result); |
||||
} catch (error) { |
||||
retries++; |
||||
|
||||
if (retries < maxRetries) { |
||||
if (__DEV__) { |
||||
console.log(`🔄 Retrying request (${retries}/${maxRetries})...`); |
||||
} |
||||
setTimeout(attempt, retryDelay * retries); |
||||
} else { |
||||
reject(error); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
attempt(); |
||||
}); |
||||
} |
||||
|
||||
export default api; |
||||
@ -0,0 +1,492 @@
|
||||
// @ts-nocheck
|
||||
//des
|
||||
//this takes the key, the message, and whether to encrypt or decrypt
|
||||
|
||||
export function des(key, message, encrypt, mode, iv, padding) { |
||||
if (encrypt) |
||||
//如果是加密的话,首先转换编码
|
||||
message = unescape(encodeURIComponent(message)); |
||||
//declaring this locally speeds things up a bit
|
||||
const spfunction1 = [ |
||||
0x1010400, 0, 0x10000, 0x1010404, 0x1010004, 0x10404, 0x4, 0x10000, 0x400, 0x1010400, 0x1010404, |
||||
0x400, 0x1000404, 0x1010004, 0x1000000, 0x4, 0x404, 0x1000400, 0x1000400, 0x10400, 0x10400, |
||||
0x1010000, 0x1010000, 0x1000404, 0x10004, 0x1000004, 0x1000004, 0x10004, 0, 0x404, 0x10404, |
||||
0x1000000, 0x10000, 0x1010404, 0x4, 0x1010000, 0x1010400, 0x1000000, 0x1000000, 0x400, |
||||
0x1010004, 0x10000, 0x10400, 0x1000004, 0x400, 0x4, 0x1000404, 0x10404, 0x1010404, 0x10004, |
||||
0x1010000, 0x1000404, 0x1000004, 0x404, 0x10404, 0x1010400, 0x404, 0x1000400, 0x1000400, 0, |
||||
0x10004, 0x10400, 0, 0x1010004, |
||||
]; |
||||
const spfunction2 = [ |
||||
-0x7fef7fe0, -0x7fff8000, 0x8000, 0x108020, 0x100000, 0x20, -0x7fefffe0, -0x7fff7fe0, |
||||
-0x7fffffe0, -0x7fef7fe0, -0x7fef8000, -0x80000000, -0x7fff8000, 0x100000, 0x20, -0x7fefffe0, |
||||
0x108000, 0x100020, -0x7fff7fe0, 0, -0x80000000, 0x8000, 0x108020, -0x7ff00000, 0x100020, |
||||
-0x7fffffe0, 0, 0x108000, 0x8020, -0x7fef8000, -0x7ff00000, 0x8020, 0, 0x108020, -0x7fefffe0, |
||||
0x100000, -0x7fff7fe0, -0x7ff00000, -0x7fef8000, 0x8000, -0x7ff00000, -0x7fff8000, 0x20, |
||||
-0x7fef7fe0, 0x108020, 0x20, 0x8000, -0x80000000, 0x8020, -0x7fef8000, 0x100000, -0x7fffffe0, |
||||
0x100020, -0x7fff7fe0, -0x7fffffe0, 0x100020, 0x108000, 0, -0x7fff8000, 0x8020, -0x80000000, |
||||
-0x7fefffe0, -0x7fef7fe0, 0x108000, |
||||
]; |
||||
const spfunction3 = [ |
||||
0x208, 0x8020200, 0, 0x8020008, 0x8000200, 0, 0x20208, 0x8000200, 0x20008, 0x8000008, 0x8000008, |
||||
0x20000, 0x8020208, 0x20008, 0x8020000, 0x208, 0x8000000, 0x8, 0x8020200, 0x200, 0x20200, |
||||
0x8020000, 0x8020008, 0x20208, 0x8000208, 0x20200, 0x20000, 0x8000208, 0x8, 0x8020208, 0x200, |
||||
0x8000000, 0x8020200, 0x8000000, 0x20008, 0x208, 0x20000, 0x8020200, 0x8000200, 0, 0x200, |
||||
0x20008, 0x8020208, 0x8000200, 0x8000008, 0x200, 0, 0x8020008, 0x8000208, 0x20000, 0x8000000, |
||||
0x8020208, 0x8, 0x20208, 0x20200, 0x8000008, 0x8020000, 0x8000208, 0x208, 0x8020000, 0x20208, |
||||
0x8, 0x8020008, 0x20200, |
||||
]; |
||||
const spfunction4 = [ |
||||
0x802001, 0x2081, 0x2081, 0x80, 0x802080, 0x800081, 0x800001, 0x2001, 0, 0x802000, 0x802000, |
||||
0x802081, 0x81, 0, 0x800080, 0x800001, 0x1, 0x2000, 0x800000, 0x802001, 0x80, 0x800000, 0x2001, |
||||
0x2080, 0x800081, 0x1, 0x2080, 0x800080, 0x2000, 0x802080, 0x802081, 0x81, 0x800080, 0x800001, |
||||
0x802000, 0x802081, 0x81, 0, 0, 0x802000, 0x2080, 0x800080, 0x800081, 0x1, 0x802001, 0x2081, |
||||
0x2081, 0x80, 0x802081, 0x81, 0x1, 0x2000, 0x800001, 0x2001, 0x802080, 0x800081, 0x2001, 0x2080, |
||||
0x800000, 0x802001, 0x80, 0x800000, 0x2000, 0x802080, |
||||
]; |
||||
const spfunction5 = [ |
||||
0x100, 0x2080100, 0x2080000, 0x42000100, 0x80000, 0x100, 0x40000000, 0x2080000, 0x40080100, |
||||
0x80000, 0x2000100, 0x40080100, 0x42000100, 0x42080000, 0x80100, 0x40000000, 0x2000000, |
||||
0x40080000, 0x40080000, 0, 0x40000100, 0x42080100, 0x42080100, 0x2000100, 0x42080000, |
||||
0x40000100, 0, 0x42000000, 0x2080100, 0x2000000, 0x42000000, 0x80100, 0x80000, 0x42000100, |
||||
0x100, 0x2000000, 0x40000000, 0x2080000, 0x42000100, 0x40080100, 0x2000100, 0x40000000, |
||||
0x42080000, 0x2080100, 0x40080100, 0x100, 0x2000000, 0x42080000, 0x42080100, 0x80100, |
||||
0x42000000, 0x42080100, 0x2080000, 0, 0x40080000, 0x42000000, 0x80100, 0x2000100, 0x40000100, |
||||
0x80000, 0, 0x40080000, 0x2080100, 0x40000100, |
||||
]; |
||||
const spfunction6 = [ |
||||
0x20000010, 0x20400000, 0x4000, 0x20404010, 0x20400000, 0x10, 0x20404010, 0x400000, 0x20004000, |
||||
0x404010, 0x400000, 0x20000010, 0x400010, 0x20004000, 0x20000000, 0x4010, 0, 0x400010, |
||||
0x20004010, 0x4000, 0x404000, 0x20004010, 0x10, 0x20400010, 0x20400010, 0, 0x404010, 0x20404000, |
||||
0x4010, 0x404000, 0x20404000, 0x20000000, 0x20004000, 0x10, 0x20400010, 0x404000, 0x20404010, |
||||
0x400000, 0x4010, 0x20000010, 0x400000, 0x20004000, 0x20000000, 0x4010, 0x20000010, 0x20404010, |
||||
0x404000, 0x20400000, 0x404010, 0x20404000, 0, 0x20400010, 0x10, 0x4000, 0x20400000, 0x404010, |
||||
0x4000, 0x400010, 0x20004010, 0, 0x20404000, 0x20000000, 0x400010, 0x20004010, |
||||
]; |
||||
const spfunction7 = [ |
||||
0x200000, 0x4200002, 0x4000802, 0, 0x800, 0x4000802, 0x200802, 0x4200800, 0x4200802, 0x200000, |
||||
0, 0x4000002, 0x2, 0x4000000, 0x4200002, 0x802, 0x4000800, 0x200802, 0x200002, 0x4000800, |
||||
0x4000002, 0x4200000, 0x4200800, 0x200002, 0x4200000, 0x800, 0x802, 0x4200802, 0x200800, 0x2, |
||||
0x4000000, 0x200800, 0x4000000, 0x200800, 0x200000, 0x4000802, 0x4000802, 0x4200002, 0x4200002, |
||||
0x2, 0x200002, 0x4000000, 0x4000800, 0x200000, 0x4200800, 0x802, 0x200802, 0x4200800, 0x802, |
||||
0x4000002, 0x4200802, 0x4200000, 0x200800, 0, 0x2, 0x4200802, 0, 0x200802, 0x4200000, 0x800, |
||||
0x4000002, 0x4000800, 0x800, 0x200002, |
||||
]; |
||||
const spfunction8 = [ |
||||
0x10001040, 0x1000, 0x40000, 0x10041040, 0x10000000, 0x10001040, 0x40, 0x10000000, 0x40040, |
||||
0x10040000, 0x10041040, 0x41000, 0x10041000, 0x41040, 0x1000, 0x40, 0x10040000, 0x10000040, |
||||
0x10001000, 0x1040, 0x41000, 0x40040, 0x10040040, 0x10041000, 0x1040, 0, 0, 0x10040040, |
||||
0x10000040, 0x10001000, 0x41040, 0x40000, 0x41040, 0x40000, 0x10041000, 0x1000, 0x40, |
||||
0x10040040, 0x1000, 0x41040, 0x10001000, 0x40, 0x10000040, 0x10040000, 0x10040040, 0x10000000, |
||||
0x40000, 0x10001040, 0, 0x10041040, 0x40040, 0x10000040, 0x10040000, 0x10001000, 0x10001040, 0, |
||||
0x10041040, 0x41000, 0x41000, 0x1040, 0x1040, 0x40040, 0x10000000, 0x10041000, |
||||
]; |
||||
|
||||
//create the 16 or 48 subkeys we will need
|
||||
const keys = des_createKeys(key); |
||||
let m = 0, |
||||
i, |
||||
j, |
||||
temp, |
||||
temp2, |
||||
right1, |
||||
right2, |
||||
left, |
||||
right, |
||||
looping; |
||||
let cbcleft, cbcleft2, cbcright, cbcright2; |
||||
let endloop, loopinc; |
||||
var len = message.length; |
||||
let chunk = 0; |
||||
//set up the loops for single and triple des
|
||||
const iterations = keys.length == 32 ? 3 : 9; //single or triple des
|
||||
if (iterations == 3) { |
||||
looping = encrypt ? [0, 32, 2] : [30, -2, -2]; |
||||
} else { |
||||
looping = encrypt ? [0, 32, 2, 62, 30, -2, 64, 96, 2] : [94, 62, -2, 32, 64, 2, 30, -2, -2]; |
||||
} |
||||
|
||||
//pad the message depending on the padding parameter
|
||||
if (padding == 2) |
||||
message += ' '; //pad the message with spaces
|
||||
else if (padding == 1) { |
||||
if (encrypt) { |
||||
temp = 8 - (len % 8); |
||||
message += String.fromCharCode(temp, temp, temp, temp, temp, temp, temp, temp); |
||||
if (temp === 8) len += 8; |
||||
} |
||||
} //PKCS7 padding
|
||||
else if (!padding) message += '\0\0\0\0\0\0\0\0'; //pad the message out with null bytes
|
||||
|
||||
//store the result here
|
||||
let result = ''; |
||||
let tempresult = ''; |
||||
|
||||
if (mode == 1) { |
||||
//CBC mode
|
||||
cbcleft = |
||||
(iv.charCodeAt(m++) << 24) | |
||||
(iv.charCodeAt(m++) << 16) | |
||||
(iv.charCodeAt(m++) << 8) | |
||||
iv.charCodeAt(m++); |
||||
cbcright = |
||||
(iv.charCodeAt(m++) << 24) | |
||||
(iv.charCodeAt(m++) << 16) | |
||||
(iv.charCodeAt(m++) << 8) | |
||||
iv.charCodeAt(m++); |
||||
m = 0; |
||||
} |
||||
|
||||
//loop through each 64 bit chunk of the message
|
||||
while (m < len) { |
||||
left = |
||||
(message.charCodeAt(m++) << 24) | |
||||
(message.charCodeAt(m++) << 16) | |
||||
(message.charCodeAt(m++) << 8) | |
||||
message.charCodeAt(m++); |
||||
right = |
||||
(message.charCodeAt(m++) << 24) | |
||||
(message.charCodeAt(m++) << 16) | |
||||
(message.charCodeAt(m++) << 8) | |
||||
message.charCodeAt(m++); |
||||
|
||||
//for Cipher Block Chaining mode, xor the message with the previous result
|
||||
if (mode == 1) { |
||||
if (encrypt) { |
||||
left ^= cbcleft; |
||||
right ^= cbcright; |
||||
} else { |
||||
cbcleft2 = cbcleft; |
||||
cbcright2 = cbcright; |
||||
cbcleft = left; |
||||
cbcright = right; |
||||
} |
||||
} |
||||
|
||||
//first each 64 but chunk of the message must be permuted according to IP
|
||||
temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; |
||||
right ^= temp; |
||||
left ^= temp << 4; |
||||
temp = ((left >>> 16) ^ right) & 0x0000ffff; |
||||
right ^= temp; |
||||
left ^= temp << 16; |
||||
temp = ((right >>> 2) ^ left) & 0x33333333; |
||||
left ^= temp; |
||||
right ^= temp << 2; |
||||
temp = ((right >>> 8) ^ left) & 0x00ff00ff; |
||||
left ^= temp; |
||||
right ^= temp << 8; |
||||
temp = ((left >>> 1) ^ right) & 0x55555555; |
||||
right ^= temp; |
||||
left ^= temp << 1; |
||||
|
||||
left = (left << 1) | (left >>> 31); |
||||
right = (right << 1) | (right >>> 31); |
||||
|
||||
//do this either 1 or 3 times for each chunk of the message
|
||||
for (j = 0; j < iterations; j += 3) { |
||||
endloop = looping[j + 1]; |
||||
loopinc = looping[j + 2]; |
||||
//now go through and perform the encryption or decryption
|
||||
for (i = looping[j]; i != endloop; i += loopinc) { |
||||
//for efficiency
|
||||
right1 = right ^ keys[i]; |
||||
right2 = ((right >>> 4) | (right << 28)) ^ keys[i + 1]; |
||||
//the result is attained by passing these bytes through the S selection functions
|
||||
temp = left; |
||||
left = right; |
||||
right = |
||||
temp ^ |
||||
(spfunction2[(right1 >>> 24) & 0x3f] | |
||||
spfunction4[(right1 >>> 16) & 0x3f] | |
||||
spfunction6[(right1 >>> 8) & 0x3f] | |
||||
spfunction8[right1 & 0x3f] | |
||||
spfunction1[(right2 >>> 24) & 0x3f] | |
||||
spfunction3[(right2 >>> 16) & 0x3f] | |
||||
spfunction5[(right2 >>> 8) & 0x3f] | |
||||
spfunction7[right2 & 0x3f]); |
||||
} |
||||
temp = left; |
||||
left = right; |
||||
right = temp; //unreverse left and right
|
||||
} //for either 1 or 3 iterations
|
||||
|
||||
//move then each one bit to the right
|
||||
left = (left >>> 1) | (left << 31); |
||||
right = (right >>> 1) | (right << 31); |
||||
|
||||
//now perform IP-1, which is IP in the opposite direction
|
||||
temp = ((left >>> 1) ^ right) & 0x55555555; |
||||
right ^= temp; |
||||
left ^= temp << 1; |
||||
temp = ((right >>> 8) ^ left) & 0x00ff00ff; |
||||
left ^= temp; |
||||
right ^= temp << 8; |
||||
temp = ((right >>> 2) ^ left) & 0x33333333; |
||||
left ^= temp; |
||||
right ^= temp << 2; |
||||
temp = ((left >>> 16) ^ right) & 0x0000ffff; |
||||
right ^= temp; |
||||
left ^= temp << 16; |
||||
temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; |
||||
right ^= temp; |
||||
left ^= temp << 4; |
||||
|
||||
//for Cipher Block Chaining mode, xor the message with the previous result
|
||||
if (mode == 1) { |
||||
if (encrypt) { |
||||
cbcleft = left; |
||||
cbcright = right; |
||||
} else { |
||||
left ^= cbcleft2; |
||||
right ^= cbcright2; |
||||
} |
||||
} |
||||
tempresult += String.fromCharCode( |
||||
left >>> 24, |
||||
(left >>> 16) & 0xff, |
||||
(left >>> 8) & 0xff, |
||||
left & 0xff, |
||||
right >>> 24, |
||||
(right >>> 16) & 0xff, |
||||
(right >>> 8) & 0xff, |
||||
right & 0xff |
||||
); |
||||
|
||||
chunk += 8; |
||||
if (chunk == 512) { |
||||
result += tempresult; |
||||
tempresult = ''; |
||||
chunk = 0; |
||||
} |
||||
} //for every 8 characters, or 64 bits in the message
|
||||
|
||||
//return the result as an array
|
||||
result += tempresult; |
||||
if (!encrypt) result = result.replace(/\0*$/g, ''); |
||||
|
||||
if (!encrypt) { |
||||
//如果是解密的话,解密结束后对PKCS7 padding进行解码,并转换成utf-8编码
|
||||
if (padding === 1) { |
||||
//PKCS7 padding解码
|
||||
var len = result.length, |
||||
paddingChars = 0; |
||||
len && (paddingChars = result.charCodeAt(len - 1)); |
||||
paddingChars <= 8 && (result = result.substring(0, len - paddingChars)); |
||||
} |
||||
//转换成UTF-8编码
|
||||
result = decodeURIComponent(escape(result)); |
||||
} |
||||
|
||||
return result; |
||||
} //end of des
|
||||
|
||||
//des_createKeys
|
||||
//this takes as input a 64 bit key (even though only 56 bits are used)
|
||||
//as an array of 2 integers, and returns 16 48 bit keys
|
||||
function des_createKeys(key) { |
||||
//declaring this locally speeds things up a bit
|
||||
const pc2bytes0 = [ |
||||
0, 0x4, 0x20000000, 0x20000004, 0x10000, 0x10004, 0x20010000, 0x20010004, 0x200, 0x204, |
||||
0x20000200, 0x20000204, 0x10200, 0x10204, 0x20010200, 0x20010204, |
||||
]; |
||||
const pc2bytes1 = [ |
||||
0, 0x1, 0x100000, 0x100001, 0x4000000, 0x4000001, 0x4100000, 0x4100001, 0x100, 0x101, 0x100100, |
||||
0x100101, 0x4000100, 0x4000101, 0x4100100, 0x4100101, |
||||
]; |
||||
const pc2bytes2 = [ |
||||
0, 0x8, 0x800, 0x808, 0x1000000, 0x1000008, 0x1000800, 0x1000808, 0, 0x8, 0x800, 0x808, |
||||
0x1000000, 0x1000008, 0x1000800, 0x1000808, |
||||
]; |
||||
const pc2bytes3 = [ |
||||
0, 0x200000, 0x8000000, 0x8200000, 0x2000, 0x202000, 0x8002000, 0x8202000, 0x20000, 0x220000, |
||||
0x8020000, 0x8220000, 0x22000, 0x222000, 0x8022000, 0x8222000, |
||||
]; |
||||
const pc2bytes4 = [ |
||||
0, 0x40000, 0x10, 0x40010, 0, 0x40000, 0x10, 0x40010, 0x1000, 0x41000, 0x1010, 0x41010, 0x1000, |
||||
0x41000, 0x1010, 0x41010, |
||||
]; |
||||
const pc2bytes5 = [ |
||||
0, 0x400, 0x20, 0x420, 0, 0x400, 0x20, 0x420, 0x2000000, 0x2000400, 0x2000020, 0x2000420, |
||||
0x2000000, 0x2000400, 0x2000020, 0x2000420, |
||||
]; |
||||
const pc2bytes6 = [ |
||||
0, 0x10000000, 0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002, 0, 0x10000000, |
||||
0x80000, 0x10080000, 0x2, 0x10000002, 0x80002, 0x10080002, |
||||
]; |
||||
const pc2bytes7 = [ |
||||
0, 0x10000, 0x800, 0x10800, 0x20000000, 0x20010000, 0x20000800, 0x20010800, 0x20000, 0x30000, |
||||
0x20800, 0x30800, 0x20020000, 0x20030000, 0x20020800, 0x20030800, |
||||
]; |
||||
const pc2bytes8 = [ |
||||
0, 0x40000, 0, 0x40000, 0x2, 0x40002, 0x2, 0x40002, 0x2000000, 0x2040000, 0x2000000, 0x2040000, |
||||
0x2000002, 0x2040002, 0x2000002, 0x2040002, |
||||
]; |
||||
const pc2bytes9 = [ |
||||
0, 0x10000000, 0x8, 0x10000008, 0, 0x10000000, 0x8, 0x10000008, 0x400, 0x10000400, 0x408, |
||||
0x10000408, 0x400, 0x10000400, 0x408, 0x10000408, |
||||
]; |
||||
const pc2bytes10 = [ |
||||
0, 0x20, 0, 0x20, 0x100000, 0x100020, 0x100000, 0x100020, 0x2000, 0x2020, 0x2000, 0x2020, |
||||
0x102000, 0x102020, 0x102000, 0x102020, |
||||
]; |
||||
const pc2bytes11 = [ |
||||
0, 0x1000000, 0x200, 0x1000200, 0x200000, 0x1200000, 0x200200, 0x1200200, 0x4000000, 0x5000000, |
||||
0x4000200, 0x5000200, 0x4200000, 0x5200000, 0x4200200, 0x5200200, |
||||
]; |
||||
const pc2bytes12 = [ |
||||
0, 0x1000, 0x8000000, 0x8001000, 0x80000, 0x81000, 0x8080000, 0x8081000, 0x10, 0x1010, |
||||
0x8000010, 0x8001010, 0x80010, 0x81010, 0x8080010, 0x8081010, |
||||
]; |
||||
const pc2bytes13 = [ |
||||
0, 0x4, 0x100, 0x104, 0, 0x4, 0x100, 0x104, 0x1, 0x5, 0x101, 0x105, 0x1, 0x5, 0x101, 0x105, |
||||
]; |
||||
|
||||
//how many iterations (1 for des, 3 for triple des)
|
||||
const iterations = key.length > 8 ? 3 : 1; //changed by Paul 16/6/2007 to use Triple DES for 9+ byte keys
|
||||
//stores the return keys
|
||||
const keys = new Array(32 * iterations); |
||||
//now define the left shifts which need to be done
|
||||
const shifts = [0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0]; |
||||
//other variables
|
||||
let lefttemp, |
||||
righttemp, |
||||
m = 0, |
||||
n = 0, |
||||
temp; |
||||
|
||||
for (let j = 0; j < iterations; j++) { |
||||
//either 1 or 3 iterations
|
||||
let left = |
||||
(key.charCodeAt(m++) << 24) | |
||||
(key.charCodeAt(m++) << 16) | |
||||
(key.charCodeAt(m++) << 8) | |
||||
key.charCodeAt(m++); |
||||
let right = |
||||
(key.charCodeAt(m++) << 24) | |
||||
(key.charCodeAt(m++) << 16) | |
||||
(key.charCodeAt(m++) << 8) | |
||||
key.charCodeAt(m++); |
||||
|
||||
temp = ((left >>> 4) ^ right) & 0x0f0f0f0f; |
||||
right ^= temp; |
||||
left ^= temp << 4; |
||||
temp = ((right >>> -16) ^ left) & 0x0000ffff; |
||||
left ^= temp; |
||||
right ^= temp << -16; |
||||
temp = ((left >>> 2) ^ right) & 0x33333333; |
||||
right ^= temp; |
||||
left ^= temp << 2; |
||||
temp = ((right >>> -16) ^ left) & 0x0000ffff; |
||||
left ^= temp; |
||||
right ^= temp << -16; |
||||
temp = ((left >>> 1) ^ right) & 0x55555555; |
||||
right ^= temp; |
||||
left ^= temp << 1; |
||||
temp = ((right >>> 8) ^ left) & 0x00ff00ff; |
||||
left ^= temp; |
||||
right ^= temp << 8; |
||||
temp = ((left >>> 1) ^ right) & 0x55555555; |
||||
right ^= temp; |
||||
left ^= temp << 1; |
||||
|
||||
//the right side needs to be shifted and to get the last four bits of the left side
|
||||
temp = (left << 8) | ((right >>> 20) & 0x000000f0); |
||||
//left needs to be put upside down
|
||||
left = |
||||
(right << 24) | |
||||
((right << 8) & 0xff0000) | |
||||
((right >>> 8) & 0xff00) | |
||||
((right >>> 24) & 0xf0); |
||||
right = temp; |
||||
|
||||
//now go through and perform these shifts on the left and right keys
|
||||
for (let i = 0; i < shifts.length; i++) { |
||||
//shift the keys either one or two bits to the left
|
||||
if (shifts[i]) { |
||||
left = (left << 2) | (left >>> 26); |
||||
right = (right << 2) | (right >>> 26); |
||||
} else { |
||||
left = (left << 1) | (left >>> 27); |
||||
right = (right << 1) | (right >>> 27); |
||||
} |
||||
left &= -0xf; |
||||
right &= -0xf; |
||||
|
||||
//now apply PC-2, in such a way that E is easier when encrypting or decrypting
|
||||
//this conversion will look like PC-2 except only the last 6 bits of each byte are used
|
||||
//rather than 48 consecutive bits and the order of lines will be according to
|
||||
//how the S selection functions will be applied: S2, S4, S6, S8, S1, S3, S5, S7
|
||||
lefttemp = |
||||
pc2bytes0[left >>> 28] | |
||||
pc2bytes1[(left >>> 24) & 0xf] | |
||||
pc2bytes2[(left >>> 20) & 0xf] | |
||||
pc2bytes3[(left >>> 16) & 0xf] | |
||||
pc2bytes4[(left >>> 12) & 0xf] | |
||||
pc2bytes5[(left >>> 8) & 0xf] | |
||||
pc2bytes6[(left >>> 4) & 0xf]; |
||||
righttemp = |
||||
pc2bytes7[right >>> 28] | |
||||
pc2bytes8[(right >>> 24) & 0xf] | |
||||
pc2bytes9[(right >>> 20) & 0xf] | |
||||
pc2bytes10[(right >>> 16) & 0xf] | |
||||
pc2bytes11[(right >>> 12) & 0xf] | |
||||
pc2bytes12[(right >>> 8) & 0xf] | |
||||
pc2bytes13[(right >>> 4) & 0xf]; |
||||
temp = ((righttemp >>> 16) ^ lefttemp) & 0x0000ffff; |
||||
keys[n++] = lefttemp ^ temp; |
||||
keys[n++] = righttemp ^ (temp << 16); |
||||
} |
||||
} //for each iterations
|
||||
//return the keys we've created
|
||||
return keys; |
||||
} //end of des_createKeys
|
||||
function genkey(key, start, end) { |
||||
//8 byte / 64 bit Key (DES) or 192 bit Key
|
||||
return { key: pad(key.slice(start, end)), vector: 1 }; |
||||
} |
||||
|
||||
function pad(key) { |
||||
for (let i = key.length; i < 24; i++) { |
||||
key += '0'; |
||||
} |
||||
return key; |
||||
} |
||||
//3DES加密,使用PKCS7 padding
|
||||
function encrypt_3des(key, input) { |
||||
const genKey = genkey(key, 0, 24); |
||||
return btoa(des(genKey.key, input, 1, 0, 0, 1)); |
||||
} |
||||
//3DES解密,使用PKCS7 padding
|
||||
function decrypt_3des(key, input) { |
||||
const genKey = genkey(key, 0, 24); |
||||
return des(genKey.key, atob(input), 0, 0, 0, 1); |
||||
} |
||||
|
||||
////////////////////////////// TEST //////////////////////////////
|
||||
export function stringToHex(s) { |
||||
let r = '0x'; |
||||
const hexes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; |
||||
for (let i = 0; i < s.length; i++) { |
||||
r += hexes[s.charCodeAt(i) >> 4] + hexes[s.charCodeAt(i) & 0xf]; |
||||
} |
||||
return r; |
||||
} |
||||
|
||||
export function stringToHexArray(s) { |
||||
const arr = []; |
||||
const hexes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; |
||||
for (let i = 0; i < s.length; i++) { |
||||
arr.push(hexes[s.charCodeAt(i) >> 4] + hexes[s.charCodeAt(i) & 0xf]); |
||||
} |
||||
return arr; |
||||
} |
||||
|
||||
export function hexToString(h) { |
||||
let r = ''; |
||||
for (let i = h.substr(0, 2) == '0x' ? 2 : 0; i < h.length; i += 2) { |
||||
r += String.fromCharCode(parseInt(h.substr(i, 2), 16)); |
||||
} |
||||
return r; |
||||
} |
||||
|
||||
/* eslint-enable */ |
||||
@ -0,0 +1,119 @@
|
||||
// import { useTenantStore, useUserStore, useModalStore } from '@star/stores';
|
||||
// import { mb_t } from '@star/languages';
|
||||
// import { router } from '@routers';
|
||||
import { endsWith, includes, startsWith } from 'lodash-es'; |
||||
// import { ModalTypeEnum } from '@star/constants';
|
||||
|
||||
// 错误提示
|
||||
export default class NetworkError { |
||||
tag: string; |
||||
key: string; |
||||
info: string; |
||||
level: string; |
||||
origin: string; |
||||
cmd: string; |
||||
code: number; |
||||
logout: boolean; |
||||
update: boolean; |
||||
time: string; |
||||
|
||||
constructor(options: any) { |
||||
this.tag = ''; |
||||
this.key = ''; |
||||
this.info = ''; |
||||
this.level = ''; |
||||
this.origin = ''; |
||||
this.cmd = ''; |
||||
// this.code = 0
|
||||
this.code = -1; |
||||
this.logout = false; |
||||
this.update = false; |
||||
this.time = new Date().toLocaleString().split('GMT')[0]; |
||||
if (options) { |
||||
if (options.key) { |
||||
this.key = options.key; |
||||
} |
||||
if (options.tag) { |
||||
this.tag = options.tag; |
||||
} |
||||
if (options.info) { |
||||
if (startsWith(options.info, '{') && endsWith(options.info, '}')) { |
||||
const result = JSON.parse(options.info); |
||||
this.info = result?.msg || result?.exMessage || ''; |
||||
} else { |
||||
this.info = options.info; |
||||
} |
||||
} |
||||
if (options.level) { |
||||
this.level = options.level; |
||||
} |
||||
if (options.origin) { |
||||
this.origin = options.origin; |
||||
} |
||||
if (options.cmd) { |
||||
this.cmd = options.cmd; |
||||
} |
||||
if (options.code) { |
||||
this.code = options.code; |
||||
} |
||||
if (options.logout) { |
||||
this.logout = options.logout; |
||||
} |
||||
if (options.update) { |
||||
this.update = options.update; |
||||
} |
||||
} |
||||
// const tenantStore = useTenantStore();
|
||||
// if ([371130, 381119].includes(Number(this.cmd))) {
|
||||
// tenantStore.setRepairStatus(true);
|
||||
// }
|
||||
if (this.info.indexOf('invalid_token') !== -1) { |
||||
this.info = '亲,麻烦重新登录哦'; |
||||
} else if (this.info.indexOf('Request failed with status code 500') !== -1) { |
||||
this.info = '网络不给力,请检查网络' + '(500)'; |
||||
} else if (this.info.indexOf('Request failed with status code 502') !== -1) { |
||||
this.info = '网络不给力,请检查网络' + '(502)'; |
||||
} else if (this.info.indexOf('Error 503 Service Unavailable') !== -1) { |
||||
this.info = '网络不给力,请检查网络' + '(503)'; |
||||
} else if (this.info.indexOf('Request failed with status code 504') !== -1) { |
||||
this.info = '网络不给力,请检查网络' + '(504)'; |
||||
} else if (this.info.indexOf('timeout of 20000ms') !== -1) { |
||||
this.info = '请求超时,请检查网络'; |
||||
} else if (this.info.indexOf('Error 400 Bad Request') !== -1) { |
||||
this.info = '网络不给力,请重新尝试'; |
||||
} else if (this.info.indexOf('Network Error') !== -1) { |
||||
this.info = '网络错误,请检查网络'; |
||||
} else if (options?.code === '1115') { |
||||
// tenantStore.setRepairData(options.origin);
|
||||
// tenantStore.setRepairStatus(true);
|
||||
} else { |
||||
console.error('err.info:', this.cmd, this.info, options); |
||||
// this.info = this.info
|
||||
// if (this.info) {
|
||||
// showFailToast(mb_t(this.info));
|
||||
// }
|
||||
// if (this.info && this.info.includes('重新登录')) {
|
||||
// const userStore = useUserStore();
|
||||
// userStore?.clearUserInfo?.();
|
||||
// if (
|
||||
// !startsWith(router.currentRoute.value?.path, '/activity') &&
|
||||
// !includes(['home', 'mine', 'activity'], router.currentRoute.value?.name)
|
||||
// ) {
|
||||
// const modalStore = useModalStore();
|
||||
// const path = router.currentRoute.value?.fullPath;
|
||||
// modalStore.showRegisterModal({ redirect: path });
|
||||
// router.replace?.('/home');
|
||||
// }
|
||||
// }
|
||||
// if (this.info.includes('访问限制')) {
|
||||
// tenantStore.setIPLimitStatus(true);
|
||||
// }
|
||||
// [371130, 370433]返回了请求没有任何数据 才跳维护 其他接口忽略
|
||||
// if ([371130, 370433].includes(Number(this.cmd))) {
|
||||
// // console.log(371130, 370433, this.info, '????????????????????????????');
|
||||
// tenantStore.setRepairStatus(true);
|
||||
// }
|
||||
} |
||||
// this.info = mb_t(this.info);
|
||||
} |
||||
} |
||||
@ -0,0 +1,434 @@
|
||||
import { HmacMD5 } from 'crypto-js'; |
||||
import Base64 from 'crypto-js/enc-base64'; |
||||
import Latin1 from 'crypto-js/enc-latin1'; |
||||
import md5 from 'md5'; |
||||
import { AxiosResponse } from 'axios'; |
||||
import * as des from './des'; |
||||
import NetworkError from './error'; |
||||
import { toNumber, toString, startsWith, isString, isNumber } from 'lodash-es'; |
||||
import { NetworkTypeEnum } from '@/constants/network'; |
||||
import appConfig from '../config'; |
||||
|
||||
// import NetworkError from './error'
|
||||
// import { storeToRefs, useTenantStore, useUserStore, useAppStore, start } from '../index';
|
||||
// import { isMobile, getBetPlatform } from '@star/utils';
|
||||
// import { langToNum } from '@star/languages';
|
||||
|
||||
// 请求到的数据返回
|
||||
export type NetworkResponse<T> = { |
||||
type: NetworkTypeEnum; |
||||
data: T; |
||||
}; |
||||
|
||||
export const getBetPlatform = (isReturnIndex = false) => { |
||||
// 5=PC; 7=HOMESCREEN_IOS; 6=HOMESCREEN_ANDROID; 4=H5_IOS 3=IOS 2=H5_ANDROID; 1=ANDROID 8=马甲包
|
||||
return 'H5_IOS'; |
||||
// const platform = new URLSearchParams(window.location.search).get('platform');
|
||||
// if (platform) {
|
||||
// return platform?.includes('IOS') ? 'IOS' : platform?.includes('ANDROID') ? 'ANDROID' : '';
|
||||
// }
|
||||
// if (isAppMJB()) {
|
||||
// return 'APPS_ANDROID';
|
||||
// }
|
||||
// if (isPWA()) {
|
||||
// if (isIOS()) {
|
||||
// return isReturnIndex ? 4 : 'HS_IOS';
|
||||
// } else {
|
||||
// return isReturnIndex ? 2 : 'HS_ANDROID';
|
||||
// }
|
||||
// }
|
||||
// if (isAndroid()) {
|
||||
// if (BASE_CONFIG.appVersion > 0) {
|
||||
// return isReturnIndex ? 1 : 'ANDROID';
|
||||
// } else {
|
||||
// return isReturnIndex ? 2 : 'H5_ANDROID';
|
||||
// }
|
||||
// }
|
||||
// if (isIOS()) {
|
||||
// if (BASE_CONFIG.appVersion > 0) {
|
||||
// return isReturnIndex ? 3 : 'IOS';
|
||||
// } else {
|
||||
// return isReturnIndex ? 4 : 'H5_IOS';
|
||||
// }
|
||||
// }
|
||||
// return isReturnIndex ? 5 : 'PC';
|
||||
}; |
||||
|
||||
const uuid = (len: number, radix: number) => { |
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); |
||||
const uuid: any[] = []; |
||||
radix = radix || chars.length; |
||||
|
||||
if (len) { |
||||
// Compact form
|
||||
for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]; |
||||
} else { |
||||
// rfc4122, version 4 form
|
||||
/* eslint-disable */ |
||||
let r; |
||||
|
||||
// rfc4122 requires these characters
|
||||
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; |
||||
uuid[14] = '4'; |
||||
|
||||
// Fill in random data. At i==19 set the high bits of clock sequence as
|
||||
// per rfc4122, sec. 4.1.5
|
||||
for (let i = 0; i < 36; i++) { |
||||
if (!uuid[i]) { |
||||
r = 0 | (Math.random() * 16); |
||||
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r]; |
||||
} |
||||
} |
||||
/* eslint-enable */ |
||||
} |
||||
return uuid.join(''); |
||||
}; |
||||
|
||||
// 格式化要发送的数据
|
||||
export const formatSendData = (data: any, type: number = 0) => { |
||||
// url code
|
||||
if (type === 0) { |
||||
const arr: any[] = []; |
||||
for (const k in data) { |
||||
const v = data[k]; |
||||
if (v instanceof Array) { |
||||
for (const subK in v) { |
||||
arr.push(`${k}[]=${v[subK]}`); |
||||
} |
||||
} else { |
||||
arr.push(`${k}=${data[k]}`); |
||||
} |
||||
} |
||||
return arr.join('&'); |
||||
} else if (type === 2) { |
||||
return data.join('/'); |
||||
} |
||||
// json
|
||||
return JSON.stringify(data); |
||||
}; |
||||
|
||||
export const getP = (p: any) => { |
||||
return HmacMD5(p, '7NEkojNzfkk=').toString(); |
||||
}; |
||||
|
||||
export const enD = (rk: string, str: string) => { |
||||
const enc = des.des(rk, str, 1, 0, null, 1); |
||||
return Base64.stringify(Latin1.parse(enc)); |
||||
}; |
||||
|
||||
export const dnD = (rk: string, str: string) => { |
||||
const s = Latin1.stringify(Base64.parse(str)); |
||||
const d = des.des(rk, s, 0, 0, null, 1); |
||||
return d; |
||||
}; |
||||
|
||||
export const enP = (rk: string, vk: string, t: number) => { |
||||
const enc = des.des(vk, rk + t, 1, 0, null, 1); |
||||
return Base64.stringify(Latin1.parse(enc)); |
||||
}; |
||||
|
||||
export const dnP = (vk: string, str: string) => { |
||||
const s = Latin1.stringify(Base64.parse(str)); |
||||
const p = des.des(vk, s, 0, 0, null, 1); |
||||
return p; |
||||
}; |
||||
|
||||
export const enC = (rk: string, vk: string, m: string) => { |
||||
const enc = HmacMD5(m + rk, vk); |
||||
return Base64.stringify(enc); |
||||
}; |
||||
|
||||
export const getRequestKey = (cmdId: number, data: any) => { |
||||
return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`; |
||||
}; |
||||
|
||||
// 加工请求数据
|
||||
export const transformRequest = (config: any) => { |
||||
const { headerType = 2, paramType = 0, cmdId, tid, ...reset } = config.headers; |
||||
const headers: Record<string, any> = {}; |
||||
// const { tenantInfo } = storeToRefs(useTenantStore());
|
||||
// const { userInfo } = storeToRefs(useUserStore());
|
||||
// const { language } = storeToRefs(useAppStore());
|
||||
const t = new Date().getTime(); |
||||
const rk = md5(toString(Math.random() + t)).substring(0, 8); |
||||
const vk = appConfig.app.vk as string; |
||||
const pwds = enP(rk, vk, t); |
||||
|
||||
const tenantInfo = { |
||||
tid: 3, |
||||
}; |
||||
let userInfo = { |
||||
cust_id: '', |
||||
cust_name: '', |
||||
access_token: '' |
||||
}; |
||||
|
||||
// if (['17', '3310052', '310111', '310122', '310400', '4', '402', '401', '310635'].includes(cmdId)) {
|
||||
// const publicParams = {
|
||||
// tid: tid ?? tenantInfo.value?.tid ?? '',
|
||||
// cust_id: (config.data?.cust_id === 0 ? '0' : '') || userInfo?.value?.cust_id || '',
|
||||
// cust_name: (config.data?.cust_name === 0 ? '0' : config.data?.cust_name) || userInfo?.value?.cust_name || '',
|
||||
// oper_code: (config.data?.cust_id === 0 ? '0' : '') || userInfo?.value?.cust_id || '',
|
||||
// oper_name: (config.data?.cust_name === 0 ? '0' : config.data?.cust_name) || userInfo?.value?.cust_name || '',
|
||||
// };
|
||||
// const localData = localStorage.getItem('publicData') || '';
|
||||
// const publicData = localData ? JSON.parse(localData) : {};
|
||||
// if (['17', '3310052', '310111', '310122'].includes(cmdId)) {
|
||||
// if (['4', '402', '401'].includes(cmdId)) {
|
||||
// publicParams.tid = publicParams?.tid.toString();
|
||||
// publicData.client_type = publicData?.client_type.toString();
|
||||
// publicData.oper_class_type = publicData?.oper_class_type.toString();
|
||||
// publicData.oper_source_type = publicData?.oper_source_type.toString();
|
||||
// publicData.mark = publicData?.mark.toString();
|
||||
// // publicData.ori_value = "";
|
||||
// // publicData.new_value = "";
|
||||
// // publicData.trans_type = "101";
|
||||
// // publicData.busi_type = "206";
|
||||
// // publicData.oper_method = "11";
|
||||
// }
|
||||
//
|
||||
// Object.assign(config.data, publicParams, publicData);
|
||||
// } else if (['310400'].includes(cmdId)) {
|
||||
// console.log('publicParams', publicParams, publicData);
|
||||
// config.data.memberOperInfoVo = {
|
||||
// ...publicParams,
|
||||
// ...publicData,
|
||||
// ori_value: '',
|
||||
// new_value: '',
|
||||
// oper_method: 1,
|
||||
// trans_type: 105,
|
||||
// busi_type: 222,
|
||||
// };
|
||||
// } else if (['310635', '4', '402', '401'].includes(cmdId)) {
|
||||
// config.data.trans_type = '101';
|
||||
// config.data.client_type = publicData.client_type;
|
||||
// config.data.browser_brand = publicData.browser_brand;
|
||||
// config.data.os_name = publicData.os_name;
|
||||
// config.data.os_version = publicData.os_version;
|
||||
// config.data.device_platform = publicData.device_platform;
|
||||
// config.data.device_model = publicData.device_model;
|
||||
// config.data.device_number = publicData.device_number;
|
||||
// config.data.device_fingerprint = publicData.device_fingerprint;
|
||||
// }
|
||||
//
|
||||
// console.log('config.data', config.data);
|
||||
// }
|
||||
|
||||
const sendStr = enD(rk, formatSendData(config.data, toNumber(paramType))); |
||||
const sendDateStr = enD(rk, t.toString()); |
||||
const checkOr = enC(rk, vk, sendDateStr); |
||||
|
||||
headers.cmdId = cmdId; |
||||
headers.aseqId = appConfig.app.aseqId; |
||||
headers.nc = appConfig.app.nc; |
||||
headers.tid = tid ?? tenantInfo.tid ?? ''; |
||||
// 试玩游戏cust_id=0 header需要保持一致
|
||||
headers.custId = (config.data?.cust_id === 0 ? '0' : '') || userInfo?.cust_id || ''; |
||||
headers.reqId = uuid(32, 16); |
||||
// headers.isMobileOpen = isMobile() ? '1' : '0';
|
||||
headers.isMobileOpen = '1'; |
||||
// headers.languageNum = langToNum(language.value);
|
||||
headers.languageNum = 0; |
||||
headers.project = 'tiyu-app'; |
||||
headers.platform = getBetPlatform(); // 哪端
|
||||
headers.checkOr = checkOr; |
||||
headers.pwds = pwds; |
||||
headers.datetime = t; |
||||
headers.tbc = md5(t + 'fT6phq0wkOPRlAoyToidAnkogUV7tBBD'); |
||||
headers.reqKey = getRequestKey(cmdId, config.data); // 每一个接口请求的唯一key,前端用
|
||||
|
||||
if (toNumber(headerType) === 1) { |
||||
const grantType = 'password'; |
||||
const scope = 'read write'; |
||||
const signature = `react_clientgrant_type=${grantType}scope=${scope}cmd_id=${cmdId}react`; |
||||
headers.signature = md5(signature); |
||||
} else if (toNumber(headerType) === 2) { |
||||
const authorization = `Bearer ${userInfo?.access_token || ''}`; |
||||
const signature = `react_clientauthorization=${authorization}cmd_id=${cmdId}react`; |
||||
headers.authorization = authorization; |
||||
headers.signature = md5(signature); |
||||
} else if (toNumber(headerType) === 3) { |
||||
const grantType = 'refresh_token'; |
||||
const scope = ''; |
||||
const signature = `react_clientgrant_type=${grantType}scope=${scope}cmd_id=${cmdId}react`; |
||||
headers.signature = md5(signature); |
||||
} |
||||
|
||||
// console.log(headers, cmdId, '<------ request headers');
|
||||
// localStorage.getItem('SHOW_DATA') &&
|
||||
// console.error(cmdId, '最终请求参数', formatSendData(config.data, toNumber(paramType))); // 查看最终请求参数
|
||||
console.log(cmdId, 'request data --->', config.data); |
||||
return { |
||||
headers: { ...reset, ...headers }, |
||||
data: sendStr, |
||||
}; |
||||
}; |
||||
|
||||
// 解释响应数据
|
||||
export const parseResponse = (response: AxiosResponse): Promise<NetworkResponse<any>> => { |
||||
try { |
||||
const { headers, data } = response; |
||||
const reqHeaders = response.config?.headers || {}; |
||||
// if (reqHeaders.cmdId == 310122) {
|
||||
// console.log(data, reqHeaders.cmdId, '<<<<<<<<<<<<<<<<<<<<<<<<<< parseResponse data');
|
||||
// }
|
||||
if (isString(data) && data.length > 0 && !startsWith(data, '<html>')) { |
||||
// 检查 headers 是否存在必要的属性
|
||||
if (!headers || !headers.pwds || !headers.datetime) { |
||||
console.error('Response headers missing required fields:', headers); |
||||
throw new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 0', |
||||
info: '响应头缺少必要字段', |
||||
origin: '', |
||||
cmd: reqHeaders.cmdId, |
||||
}); |
||||
} |
||||
|
||||
const drk = dnP(appConfig.app.vk, headers.pwds).substring(0, 8); |
||||
const dm = dnD(drk, data); |
||||
const dc = enC(drk, appConfig.app.vk, enD(drk, toString(headers.datetime))); |
||||
// if (reqHeaders.cmdId == 310122) {
|
||||
// console.log(dm, JSON.parse(dm), reqHeaders.cmdId);
|
||||
// }
|
||||
// localStorage.getItem('SHOW_DATA') &&
|
||||
// console.error(reqHeaders.cmdId, dm ? JSON.parse(dm) : dm); // 查看请求返回的数据
|
||||
if (!dm) { |
||||
throw new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 1', |
||||
info: '数据为空', |
||||
origin: '', |
||||
cmd: reqHeaders.cmdId, |
||||
}); |
||||
} |
||||
if (dc !== headers.checkor) { |
||||
throw new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 2', |
||||
info: '返回数据异常', |
||||
origin: '', |
||||
cmd: reqHeaders.cmdId, |
||||
}); |
||||
} |
||||
|
||||
const resData: any = JSON.parse(dm); |
||||
console.log( |
||||
JSON.parse(JSON.stringify(resData)), |
||||
reqHeaders.cmdId, |
||||
'<<<<<<<<<<<<<<<<<<<<<<<<<< parseResponse resData' |
||||
); |
||||
if (resData) { |
||||
if (resData.exLevel) { |
||||
// 接口请求的错误处理
|
||||
let err: any; |
||||
// 预存奖励领取的时候 返回'0'提示
|
||||
if ([724209].includes(+reqHeaders.cmdId)) { |
||||
throw { |
||||
key: '', |
||||
tag: 'Network error 3', |
||||
info: resData.exMessage, |
||||
origin: resData, |
||||
cmd: reqHeaders.cmdId, |
||||
code: resData.exCode, |
||||
}; |
||||
} else { |
||||
err = new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 3', |
||||
info: resData.exMessage, |
||||
origin: resData, |
||||
cmd: reqHeaders.cmdId, |
||||
code: resData.exCode, |
||||
}); |
||||
} |
||||
// 版本号不对,强制更新
|
||||
if (err.code === 1108) { |
||||
err.update = true; |
||||
} else if ([1200, 1201, 1109, 1112, 1202, 1007].includes(+err.code)) { |
||||
// 1200 refresh_token错误
|
||||
// 1201 access_token错误
|
||||
// 1112 IP限制
|
||||
err.logout = true; // 退出登录
|
||||
err.tag = 'Network error 4'; |
||||
err.logoutMessage = '返回异常'; |
||||
} |
||||
throw err; |
||||
} else if (resData.error) { |
||||
throw new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 5', |
||||
info: resData.error_description, |
||||
origin: resData, |
||||
cmd: reqHeaders.cmdId, |
||||
code: resData.exCode, |
||||
}); |
||||
} else if (resData.response_code === '0') { |
||||
// 回收第三方金额 没有的的时候 也是返回'0'
|
||||
// 预存奖励领取的时候 返回'0'提示
|
||||
if ([3911381, 724209].includes(+reqHeaders.cmdId)) { |
||||
resData.response_code = '1'; |
||||
} else { |
||||
throw new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 8', |
||||
info: resData.msg || '操作失败', |
||||
origin: resData, |
||||
cmd: reqHeaders.cmdId, |
||||
code: resData.exCode, |
||||
}); |
||||
} |
||||
} |
||||
} else { |
||||
if (!isNumber(toNumber(resData))) { |
||||
throw new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 1', |
||||
info: '数据为空', |
||||
origin: '', |
||||
cmd: reqHeaders.cmdId, |
||||
}); |
||||
} |
||||
} |
||||
return Promise.resolve({ type: NetworkTypeEnum.SUCCESS, data: resData }); |
||||
} else { |
||||
localStorage.getItem('SHOW_DATA') && console.error(reqHeaders.cmdId, data); // 查看请求返回的原始数据
|
||||
throw new NetworkError({ |
||||
key: '', |
||||
tag: 'Network error 7', |
||||
info: '请求没有返回任何数据', |
||||
// origin: context || response.toString(),
|
||||
cmd: reqHeaders.cmdId, |
||||
code: -1, |
||||
}); |
||||
} |
||||
} catch (e) { |
||||
// // 404等错误处理
|
||||
// if (typeof context === 'object') {
|
||||
// errStr = JSON.stringify(context);
|
||||
// } else {
|
||||
// const arr = context.match(/title[\s\S]*?title/g);
|
||||
// if (arr && arr.length !== 0) {
|
||||
// errStr = arr[0].replace(/title>/g, '').replace(/<\/title/g, '');
|
||||
// }
|
||||
// }
|
||||
// let logout = false;
|
||||
// if (errStr.indexOf('invalid_token') !== -1) {
|
||||
// logout = true;
|
||||
// }
|
||||
// // 类似这种错误的时候 跳到登录页 "error":"access_denied" "error":"unauthorized"
|
||||
// if (errStr.indexOf('access_denied') !== -1 || errStr.indexOf('unauthorized') !== -1) {
|
||||
// logout = true;
|
||||
// }
|
||||
// err = new NetworkError({
|
||||
// key: key,
|
||||
// tag: 'Network error 6',
|
||||
// info: errStr,
|
||||
// origin: context,
|
||||
// cmd: reqHeaders.cmdId,
|
||||
// code: -1,
|
||||
// logout: logout,
|
||||
// });
|
||||
return Promise.reject({ type: NetworkTypeEnum.ERROR, data: e }); |
||||
} |
||||
}; |
||||
Loading…
Reference in new issue