feat: 初始化财务管理应用前端项目,包含账户、预算、交易、报表、设置等核心功能模块。
This commit is contained in:
1
copy/src/hooks/.gitkeep
Normal file
1
copy/src/hooks/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Custom React hooks
|
||||
9
copy/src/hooks/index.ts
Normal file
9
copy/src/hooks/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Custom hooks barrel export
|
||||
*/
|
||||
|
||||
export { useTheme } from './useTheme';
|
||||
export type { Theme } from './useTheme';
|
||||
|
||||
export { useDevice, useIsMobile, useIsDesktop, useIsTablet } from './useDevice';
|
||||
export type { DeviceInfo, DeviceType } from './useDevice';
|
||||
180
copy/src/hooks/useDevice.ts
Normal file
180
copy/src/hooks/useDevice.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Device type enum
|
||||
*/
|
||||
export type DeviceType = 'mobile' | 'tablet' | 'desktop';
|
||||
|
||||
/**
|
||||
* Device detection result
|
||||
*/
|
||||
export interface DeviceInfo {
|
||||
isMobile: boolean;
|
||||
isTablet: boolean;
|
||||
isDesktop: boolean;
|
||||
deviceType: DeviceType;
|
||||
isTouchDevice: boolean;
|
||||
screenWidth: number;
|
||||
screenHeight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Breakpoints for device detection
|
||||
*/
|
||||
const BREAKPOINTS = {
|
||||
mobile: 768, // < 768px
|
||||
tablet: 1024, // 768px - 1024px
|
||||
desktop: 1024, // >= 1024px
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect device type based on User Agent
|
||||
*/
|
||||
const detectDeviceFromUserAgent = (): DeviceType => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
|
||||
// Mobile devices
|
||||
if (/(android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini)/i.test(ua)) {
|
||||
// Distinguish between tablet and mobile
|
||||
if (/(ipad|tablet|playbook|silk)|(android(?!.*mobile))/i.test(ua)) {
|
||||
return 'tablet';
|
||||
}
|
||||
return 'mobile';
|
||||
}
|
||||
|
||||
return 'desktop';
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect device type based on screen width
|
||||
*/
|
||||
const detectDeviceFromScreenWidth = (width: number): DeviceType => {
|
||||
if (width < BREAKPOINTS.mobile) {
|
||||
return 'mobile';
|
||||
} else if (width < BREAKPOINTS.desktop) {
|
||||
return 'tablet';
|
||||
}
|
||||
return 'desktop';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if device supports touch
|
||||
*/
|
||||
const isTouchDevice = (): boolean => {
|
||||
return (
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
// @ts-ignore - for older browsers
|
||||
navigator.msMaxTouchPoints > 0
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current device information
|
||||
*/
|
||||
const getDeviceInfo = (): DeviceInfo => {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
// Combine User Agent and screen width detection
|
||||
const uaDevice = detectDeviceFromUserAgent();
|
||||
const screenDevice = detectDeviceFromScreenWidth(width);
|
||||
|
||||
// Prefer User Agent detection, but fall back to screen width
|
||||
const deviceType = uaDevice !== 'desktop' ? uaDevice : screenDevice;
|
||||
|
||||
return {
|
||||
isMobile: deviceType === 'mobile',
|
||||
isTablet: deviceType === 'tablet',
|
||||
isDesktop: deviceType === 'desktop',
|
||||
deviceType,
|
||||
isTouchDevice: isTouchDevice(),
|
||||
screenWidth: width,
|
||||
screenHeight: height,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to detect device type and screen size
|
||||
*
|
||||
* @returns DeviceInfo object with device detection results
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { isMobile, isDesktop, deviceType } = useDevice();
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isMobile && <MobileView />}
|
||||
* {isDesktop && <DesktopView />}
|
||||
* </div>
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export const useDevice = (): DeviceInfo => {
|
||||
const [deviceInfo, setDeviceInfo] = useState<DeviceInfo>(getDeviceInfo);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setDeviceInfo(getDeviceInfo());
|
||||
};
|
||||
|
||||
// Listen for window resize
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Listen for orientation change (mobile devices)
|
||||
window.addEventListener('orientationchange', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('orientationchange', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return deviceInfo;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if current device is mobile
|
||||
*
|
||||
* @returns boolean indicating if device is mobile
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const isMobile = useIsMobile();
|
||||
* ```
|
||||
*/
|
||||
export const useIsMobile = (): boolean => {
|
||||
const { isMobile } = useDevice();
|
||||
return isMobile;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if current device is desktop
|
||||
*
|
||||
* @returns boolean indicating if device is desktop
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const isDesktop = useIsDesktop();
|
||||
* ```
|
||||
*/
|
||||
export const useIsDesktop = (): boolean => {
|
||||
const { isDesktop } = useDevice();
|
||||
return isDesktop;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to check if current device is tablet
|
||||
*
|
||||
* @returns boolean indicating if device is tablet
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const isTablet = useIsTablet();
|
||||
* ```
|
||||
*/
|
||||
export const useIsTablet = (): boolean => {
|
||||
const { isTablet } = useDevice();
|
||||
return isTablet;
|
||||
};
|
||||
74
copy/src/hooks/useTheme.ts
Normal file
74
copy/src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Theme hook for dark/light mode support
|
||||
* Requirement: 8.3 - Support dark mode and light mode switching
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
export type Theme = 'light' | 'dark';
|
||||
|
||||
const THEME_STORAGE_KEY = 'accounting-app-theme';
|
||||
|
||||
/**
|
||||
* Get the initial theme from localStorage or system preference
|
||||
*/
|
||||
function getInitialTheme(): Theme {
|
||||
// Check localStorage first
|
||||
const storedTheme = localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
|
||||
if (storedTheme === 'light' || storedTheme === 'dark') {
|
||||
return storedTheme;
|
||||
}
|
||||
|
||||
// Fall back to system preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
return 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing theme (dark/light mode)
|
||||
*/
|
||||
export function useTheme() {
|
||||
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
|
||||
|
||||
// Apply theme to document
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
root.classList.add(theme);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
}, [theme]);
|
||||
|
||||
// Listen for system theme changes
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleChange = (e: MediaQueryListEvent) => {
|
||||
// Only auto-switch if user hasn't set a preference
|
||||
if (!localStorage.getItem(THEME_STORAGE_KEY)) {
|
||||
setThemeState(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleChange);
|
||||
}, []);
|
||||
|
||||
const setTheme = useCallback((newTheme: Theme) => {
|
||||
setThemeState(newTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setThemeState((prev) => (prev === 'light' ? 'dark' : 'light'));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
theme,
|
||||
setTheme,
|
||||
toggleTheme,
|
||||
isDark: theme === 'dark',
|
||||
};
|
||||
}
|
||||
|
||||
export default useTheme;
|
||||
Reference in New Issue
Block a user