+ {/* 顶部装饰线 */}
+
+
+ {/* 导航内容 */}
+
{navItems.map((item) => (
debouncedNavigate(item.id)}
+ onClick={() => handleNavigate(item.id)}
+ isFab={item.isFab}
+ colors={colors}
/>
))}
+
+ {/* 底部安全区域填充 */}
+
);
});
diff --git a/src/config/androidPerformance.ts b/src/config/androidPerformance.ts
new file mode 100644
index 00000000..4aaa8801
--- /dev/null
+++ b/src/config/androidPerformance.ts
@@ -0,0 +1,399 @@
+/**
+ * Android Performance Configuration - Android 性能优化配置
+ *
+ * 针对 Android 平台的性能优化设置
+ * 确保 Android 应用运行流畅,内存占用合理
+ */
+
+import { Capacitor } from '@capacitor/core';
+
+/**
+ * 检查是否为 Android 平台
+ */
+const isAndroid = (): boolean => {
+ return Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'android';
+};
+
+/**
+ * 性能配置接口
+ */
+export interface AndroidPerformanceConfig {
+ // 动画配置
+ animations: {
+ enabled: boolean;
+ reducedMotion: boolean;
+ maxFPS: number;
+ durationMultiplier: number;
+ };
+
+ // 图片配置
+ images: {
+ lazyLoadEnabled: boolean;
+ maxImageSize: number; // KB
+ compressionQuality: number; // 0-100
+ cacheSize: number; // MB
+ };
+
+ // 内存配置
+ memory: {
+ maxCacheItems: number;
+ cleanupInterval: number; // ms
+ warningThreshold: number; // MB
+ };
+
+ // 网络配置
+ network: {
+ requestTimeout: number; // ms
+ retryAttempts: number;
+ cacheEnabled: boolean;
+ prefetchEnabled: boolean;
+ };
+
+ // UI 配置
+ ui: {
+ virtualListThreshold: number;
+ debounceDelay: number; // ms
+ throttleDelay: number; // ms
+ hardwareAcceleration: boolean;
+ };
+
+ // 存储配置
+ storage: {
+ maxLocalStorageSize: number; // KB
+ useNativeStorage: boolean;
+ encryptionEnabled: boolean;
+ };
+}
+
+/**
+ * 默认 Android 性能配置
+ */
+const defaultAndroidConfig: AndroidPerformanceConfig = {
+ animations: {
+ enabled: true,
+ reducedMotion: false,
+ maxFPS: 60,
+ durationMultiplier: 1.0,
+ },
+
+ images: {
+ lazyLoadEnabled: true,
+ maxImageSize: 1024, // 1MB
+ compressionQuality: 80,
+ cacheSize: 50, // 50MB
+ },
+
+ memory: {
+ maxCacheItems: 100,
+ cleanupInterval: 30000, // 30秒
+ warningThreshold: 150, // 150MB
+ },
+
+ network: {
+ requestTimeout: 15000, // 15秒
+ retryAttempts: 3,
+ cacheEnabled: true,
+ prefetchEnabled: false,
+ },
+
+ ui: {
+ virtualListThreshold: 50,
+ debounceDelay: 300,
+ throttleDelay: 100,
+ hardwareAcceleration: true,
+ },
+
+ storage: {
+ maxLocalStorageSize: 5120, // 5MB
+ useNativeStorage: true,
+ encryptionEnabled: false,
+ },
+};
+
+/**
+ * 低端设备性能配置
+ */
+const lowEndDeviceConfig: AndroidPerformanceConfig = {
+ animations: {
+ enabled: false,
+ reducedMotion: true,
+ maxFPS: 30,
+ durationMultiplier: 0.5,
+ },
+
+ images: {
+ lazyLoadEnabled: true,
+ maxImageSize: 512, // 512KB
+ compressionQuality: 60,
+ cacheSize: 20, // 20MB
+ },
+
+ memory: {
+ maxCacheItems: 50,
+ cleanupInterval: 15000, // 15秒
+ warningThreshold: 100, // 100MB
+ },
+
+ network: {
+ requestTimeout: 20000, // 20秒
+ retryAttempts: 2,
+ cacheEnabled: true,
+ prefetchEnabled: false,
+ },
+
+ ui: {
+ virtualListThreshold: 30,
+ debounceDelay: 500,
+ throttleDelay: 200,
+ hardwareAcceleration: false,
+ },
+
+ storage: {
+ maxLocalStorageSize: 2048, // 2MB
+ useNativeStorage: true,
+ encryptionEnabled: false,
+ },
+};
+
+/**
+ * 高端设备性能配置
+ */
+const highEndDeviceConfig: AndroidPerformanceConfig = {
+ animations: {
+ enabled: true,
+ reducedMotion: false,
+ maxFPS: 60,
+ durationMultiplier: 1.0,
+ },
+
+ images: {
+ lazyLoadEnabled: true,
+ maxImageSize: 2048, // 2MB
+ compressionQuality: 90,
+ cacheSize: 100, // 100MB
+ },
+
+ memory: {
+ maxCacheItems: 200,
+ cleanupInterval: 60000, // 60秒
+ warningThreshold: 200, // 200MB
+ },
+
+ network: {
+ requestTimeout: 10000, // 10秒
+ retryAttempts: 3,
+ cacheEnabled: true,
+ prefetchEnabled: true,
+ },
+
+ ui: {
+ virtualListThreshold: 100,
+ debounceDelay: 200,
+ throttleDelay: 50,
+ hardwareAcceleration: true,
+ },
+
+ storage: {
+ maxLocalStorageSize: 10240, // 10MB
+ useNativeStorage: true,
+ encryptionEnabled: true,
+ },
+};
+
+/**
+ * 设备性能等级
+ */
+export type DevicePerformanceLevel = 'low' | 'medium' | 'high';
+
+/**
+ * 检测设备性能等级
+ */
+const detectPerformanceLevel = (): DevicePerformanceLevel => {
+ // 检测设备内存
+ const deviceMemory = navigator.deviceMemory || 4; // GB
+
+ // 检测硬件并发数
+ const hardwareConcurrency = navigator.hardwareConcurrency || 4;
+
+ // 检测连接速度
+ const connection = (navigator as any).connection;
+ const effectiveType = connection?.effectiveType || '4g';
+
+ console.log('[AndroidPerformance] Device info:', {
+ memory: deviceMemory,
+ cores: hardwareConcurrency,
+ connection: effectiveType,
+ });
+
+ // 低端设备判断
+ if (deviceMemory < 2 || hardwareConcurrency < 4 || effectiveType === '2g') {
+ return 'low';
+ }
+
+ // 高端设备判断
+ if (deviceMemory >= 6 && hardwareConcurrency >= 8 && effectiveType === '4g') {
+ return 'high';
+ }
+
+ // 默认中等设备
+ return 'medium';
+};
+
+/**
+ * 获取性能配置
+ */
+export const getAndroidPerformanceConfig = (): AndroidPerformanceConfig => {
+ // 非 Android 平台使用默认配置
+ if (!isAndroid()) {
+ return defaultAndroidConfig;
+ }
+
+ // 检测设备性能等级
+ const level = detectPerformanceLevel();
+ console.log('[AndroidPerformance] Detected performance level:', level);
+
+ // 根据性能等级返回配置
+ switch (level) {
+ case 'low':
+ return lowEndDeviceConfig;
+ case 'high':
+ return highEndDeviceConfig;
+ default:
+ return defaultAndroidConfig;
+ }
+};
+
+/**
+ * 应用性能配置到 DOM
+ */
+export const applyAndroidPerformanceConfig = (config: AndroidPerformanceConfig): void => {
+ if (!isAndroid()) return;
+
+ const root = document.documentElement;
+
+ // 应用动画配置
+ if (!config.animations.enabled || config.animations.reducedMotion) {
+ root.classList.add('reduce-motion');
+ }
+
+ // 应用硬件加速配置
+ if (config.ui.hardwareAcceleration) {
+ root.classList.add('gpu-accelerated');
+ } else {
+ root.classList.add('no-gpu-acceleration');
+ }
+
+ // 设置 CSS 变量
+ root.style.setProperty('--animation-duration-multiplier', config.animations.durationMultiplier.toString());
+ root.style.setProperty('--max-fps', config.animations.maxFPS.toString());
+
+ console.log('[AndroidPerformance] Applied performance config to DOM');
+};
+
+/**
+ * 性能监控
+ */
+export const startPerformanceMonitoring = (config: AndroidPerformanceConfig): void => {
+ if (!isAndroid()) return;
+
+ // 内存监控
+ const memoryCheckInterval = setInterval(() => {
+ if (performance.memory) {
+ const usedJSHeapSize = performance.memory.usedJSHeapSize / (1024 * 1024);
+ console.log('[AndroidPerformance] Memory usage:', usedJSHeapSize.toFixed(2), 'MB');
+
+ if (usedJSHeapSize > config.memory.warningThreshold) {
+ console.warn('[AndroidPerformance] Memory usage exceeds threshold!');
+ // 触发清理
+ triggerMemoryCleanup();
+ }
+ }
+ }, config.memory.cleanupInterval);
+
+ // FPS 监控
+ let lastTime = performance.now();
+ let frameCount = 0;
+
+ const fpsMonitor = () => {
+ frameCount++;
+ const currentTime = performance.now();
+
+ if (currentTime - lastTime >= 1000) {
+ const fps = frameCount;
+ console.log('[AndroidPerformance] FPS:', fps);
+
+ if (fps < config.animations.maxFPS * 0.5) {
+ console.warn('[AndroidPerformance] FPS is too low:', fps);
+ }
+
+ frameCount = 0;
+ lastTime = currentTime;
+ }
+
+ requestAnimationFrame(fpsMonitor);
+ };
+
+ requestAnimationFrame(fpsMonitor);
+
+ console.log('[AndroidPerformance] Performance monitoring started');
+};
+
+/**
+ * 触发内存清理
+ */
+const triggerMemoryCleanup = (): void => {
+ // 清理图片缓存
+ const images = document.querySelectorAll('img[data-loaded]');
+ images.forEach(img => {
+ if (img.getAttribute('data-loaded') === 'cached') {
+ img.removeAttribute('src');
+ img.removeAttribute('data-loaded');
+ }
+ });
+
+ // 清理隐藏元素
+ const hiddenElements = document.querySelectorAll('[style*="display: none"]');
+ hiddenElements.forEach(el => {
+ if (el.innerHTML.length > 1000) {
+ el.innerHTML = '';
+ }
+ });
+
+ console.log('[AndroidPerformance] Memory cleanup triggered');
+};
+
+/**
+ * 初始化 Android 性能优化
+ */
+export const initializeAndroidPerformance = (): void => {
+ if (!isAndroid()) {
+ console.log('[AndroidPerformance] Not Android platform, skipping performance initialization');
+ return;
+ }
+
+ console.log('[AndroidPerformance] Initializing Android performance optimization...');
+
+ const config = getAndroidPerformanceConfig();
+ applyAndroidPerformanceConfig(config);
+ startPerformanceMonitoring(config);
+
+ console.log('[AndroidPerformance] Android performance optimization initialized');
+};
+
+/**
+ * Android 性能优化 Hook
+ */
+export function useAndroidPerformance() {
+ const config = getAndroidPerformanceConfig();
+ const isAndroidDevice = isAndroid();
+ const performanceLevel = detectPerformanceLevel();
+
+ return {
+ config,
+ isAndroid: isAndroidDevice,
+ performanceLevel,
+ applyConfig: applyAndroidPerformanceConfig,
+ startMonitoring: startPerformanceMonitoring,
+ initialize: initializeAndroidPerformance,
+ };
+}
\ No newline at end of file
diff --git a/src/lib/animations.ts b/src/lib/animations.ts
new file mode 100644
index 00000000..0818f74e
--- /dev/null
+++ b/src/lib/animations.ts
@@ -0,0 +1,549 @@
+/**
+ * Apple Style Animation Library - Apple风格动画库
+ *
+ * 提供Apple级别的流畅动画效果
+ * 包括:页面过渡、手势动画、微交互等
+ */
+
+import { Capacitor } from '@capacitor/core';
+
+// 动画配置
+export const AnimationConfig = {
+ // 动画时长
+ duration: {
+ instant: 0,
+ fast: 150,
+ normal: 250,
+ slow: 350,
+ slower: 500,
+ slowest: 700,
+ },
+
+ // 动画曲线
+ easing: {
+ linear: 'linear',
+ easeIn: [0.42, 0, 1, 1],
+ easeOut: [0, 0, 0.58, 1],
+ easeInOut: [0.42, 0, 0.58, 1],
+ // Apple 特殊曲线
+ spring: [0.175, 0.885, 0.32, 1.275],
+ bounce: [0.68, -0.55, 0.265, 1.55],
+ smooth: [0.25, 0.1, 0.25, 1],
+ ios: [0.23, 1, 0.32, 1],
+ iosModal: [0.4, 0, 0.2, 1],
+ iosSheet: [0.32, 0.72, 0, 1],
+ },
+
+ // 原生平台优化
+ native: {
+ useHardwareAcceleration: true,
+ useWillChange: true,
+ },
+};
+
+// 检测是否为原生平台
+const isNative = () => Capacitor.isNativePlatform();
+
+// 将贝塞尔曲线转换为CSS格式
+const bezierToCSS = (bezier: number[]) =>
+ `cubic-bezier(${bezier.join(', ')})`;
+
+/**
+ * 页面过渡动画
+ */
+export const PageTransitions = {
+ // iOS 风格页面推入
+ push: {
+ enter: {
+ opacity: [0, 1],
+ transform: ['translateX(100%)', 'translateX(0)'],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.ios,
+ },
+ exit: {
+ opacity: [1, 0],
+ transform: ['translateX(0)', 'translateX(-30%)'],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.ios,
+ },
+ },
+
+ // iOS 风格页面弹出
+ pop: {
+ enter: {
+ opacity: [0, 1],
+ transform: ['translateX(-30%)', 'translateX(0)'],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.ios,
+ },
+ exit: {
+ opacity: [1, 0],
+ transform: ['translateX(0)', 'translateX(100%)'],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.ios,
+ },
+ },
+
+ // 模态框弹出
+ modal: {
+ enter: {
+ opacity: [0, 1],
+ transform: ['translateY(100%)', 'translateY(0)'],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.iosModal,
+ },
+ exit: {
+ opacity: [1, 0],
+ transform: ['translateY(0)', 'translateY(100%)'],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.iosModal,
+ },
+ },
+
+ // 底部弹出面板
+ sheet: {
+ enter: {
+ opacity: [0, 1],
+ transform: ['translateY(100%)', 'translateY(0)'],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.iosSheet,
+ },
+ exit: {
+ opacity: [1, 0],
+ transform: ['translateY(0)', 'translateY(100%)'],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.iosSheet,
+ },
+ },
+
+ // 淡入淡出
+ fade: {
+ enter: {
+ opacity: [0, 1],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.easeOut,
+ },
+ exit: {
+ opacity: [1, 0],
+ duration: AnimationConfig.duration.fast,
+ easing: AnimationConfig.easing.easeIn,
+ },
+ },
+};
+
+/**
+ * 微交互动画
+ */
+export const MicroInteractions = {
+ // 按钮点击
+ buttonPress: {
+ scale: [1, 0.97],
+ duration: AnimationConfig.duration.fast,
+ easing: AnimationConfig.easing.ios,
+ },
+
+ // 按钮释放
+ buttonRelease: {
+ scale: [0.97, 1],
+ duration: AnimationConfig.duration.fast,
+ easing: AnimationConfig.easing.spring,
+ },
+
+ // 卡片悬停
+ cardHover: {
+ transform: ['translateY(0)', 'translateY(-2px)'],
+ boxShadow: [
+ '0 2px 8px rgba(0,0,0,0.04)',
+ '0 8px 16px rgba(0,0,0,0.08)',
+ ],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.ios,
+ },
+
+ // 列表项点击
+ listItemPress: {
+ opacity: [1, 0.7],
+ duration: AnimationConfig.duration.fast,
+ easing: AnimationConfig.easing.ios,
+ },
+
+ // 开关切换
+ toggleSwitch: {
+ transform: ['translateX(0)', 'translateX(20px)'],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.spring,
+ },
+
+ // 图标旋转
+ iconRotate: {
+ transform: ['rotate(0deg)', 'rotate(180deg)'],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.ios,
+ },
+
+ // 数字变化
+ numberChange: {
+ opacity: [0, 1],
+ transform: ['translateY(-10px)', 'translateY(0)'],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.spring,
+ },
+};
+
+/**
+ * 手势动画
+ */
+export const GestureAnimations = {
+ // 拖拽跟随
+ dragFollow: {
+ transform: 'translateX(x) translateY(y)',
+ duration: AnimationConfig.duration.instant,
+ easing: AnimationConfig.easing.linear,
+ },
+
+ // 拖拽释放回弹
+ dragRelease: {
+ transform: ['translateX(x) translateY(y)', 'translateX(0) translateY(0)'],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.spring,
+ },
+
+ // 滑动删除
+ swipeDelete: {
+ opacity: [1, 0],
+ transform: ['translateX(0)', 'translateX(-100%)'],
+ height: ['auto', '0'],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.ios,
+ },
+
+ // 滑动取消
+ swipeCancel: {
+ transform: ['translateX(x)', 'translateX(0)'],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.spring,
+ },
+};
+
+/**
+ * 加载动画
+ */
+export const LoadingAnimations = {
+ // 骨架屏
+ skeleton: {
+ backgroundPosition: ['-200% 0', '200% 0'],
+ duration: AnimationConfig.duration.slower,
+ easing: AnimationConfig.easing.linear,
+ iterations: Infinity,
+ },
+
+ // 进度条
+ progress: {
+ width: ['0%', '100%'],
+ duration: AnimationConfig.duration.slowest,
+ easing: AnimationConfig.easing.easeInOut,
+ },
+
+ // 旋转加载
+ spinner: {
+ transform: ['rotate(0deg)', 'rotate(360deg)'],
+ duration: 1000,
+ easing: AnimationConfig.easing.linear,
+ iterations: Infinity,
+ },
+
+ // 脉冲
+ pulse: {
+ opacity: [1, 0.5, 1],
+ scale: [1, 1.05, 1],
+ duration: AnimationConfig.duration.slower,
+ easing: AnimationConfig.easing.easeInOut,
+ iterations: Infinity,
+ },
+};
+
+/**
+ * 反馈动画
+ */
+export const FeedbackAnimations = {
+ // 成功反馈
+ success: {
+ opacity: [0, 1],
+ scale: [0.5, 1],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.spring,
+ },
+
+ // 错误抖动
+ errorShake: {
+ transform: [
+ 'translateX(0)',
+ 'translateX(-10px)',
+ 'translateX(10px)',
+ 'translateX(-10px)',
+ 'translateX(10px)',
+ 'translateX(0)',
+ ],
+ duration: AnimationConfig.duration.slow,
+ easing: AnimationConfig.easing.easeInOut,
+ },
+
+ // 警告闪烁
+ warningFlash: {
+ opacity: [1, 0.3, 1],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.easeInOut,
+ },
+
+ // 点赞动画
+ like: {
+ scale: [1, 1.3, 1],
+ duration: AnimationConfig.duration.normal,
+ easing: AnimationConfig.easing.bounce,
+ },
+};
+
+/**
+ * 动画执行器
+ */
+export class AnimationExecutor {
+ private element: HTMLElement;
+ private useNative: boolean;
+
+ constructor(element: HTMLElement) {
+ this.element = element;
+ this.useNative = isNative();
+ }
+
+ // 执行动画
+ animate(config: {
+ opacity?: [number, number];
+ transform?: [string, string];
+ scale?: [number, number];
+ boxShadow?: [string, string];
+ width?: [string, string];
+ height?: [string, string];
+ backgroundPosition?: [string, string];
+ duration: number;
+ easing: number[] | string;
+ iterations?: number;
+ fill?: 'none' | 'forwards' | 'backwards' | 'both';
+ delay?: number;
+ }): Promise
{
+ return new Promise((resolve) => {
+ const keyframes: Keyframe[] = [];
+
+ // 构建起始帧
+ const startFrame: Keyframe = {};
+ const endFrame: Keyframe = { offset: 1 };
+
+ if (config.opacity) {
+ startFrame.opacity = config.opacity[0];
+ endFrame.opacity = config.opacity[1];
+ }
+
+ if (config.transform) {
+ startFrame.transform = config.transform[0];
+ endFrame.transform = config.transform[1];
+ }
+
+ if (config.scale) {
+ startFrame.transform = `scale(${config.scale[0]})`;
+ endFrame.transform = `scale(${config.scale[1]})`;
+ }
+
+ if (config.boxShadow) {
+ startFrame.boxShadow = config.boxShadow[0];
+ endFrame.boxShadow = config.boxShadow[1];
+ }
+
+ if (config.width) {
+ startFrame.width = config.width[0];
+ endFrame.width = config.width[1];
+ }
+
+ if (config.height) {
+ startFrame.height = config.height[0];
+ endFrame.height = config.height[1];
+ }
+
+ if (config.backgroundPosition) {
+ startFrame.backgroundPosition = config.backgroundPosition[0];
+ endFrame.backgroundPosition = config.backgroundPosition[1];
+ }
+
+ keyframes.push(startFrame, endFrame);
+
+ // 动画选项
+ const options: KeyframeAnimationOptions = {
+ duration: config.duration,
+ easing: typeof config.easing === 'string'
+ ? config.easing
+ : bezierToCSS(config.easing),
+ fill: config.fill || 'forwards',
+ iterations: config.iterations || 1,
+ delay: config.delay || 0,
+ };
+
+ // 原生平台优化
+ if (this.useNative && AnimationConfig.native.useHardwareAcceleration) {
+ this.element.style.willChange = 'transform, opacity';
+ }
+
+ // 执行动画
+ const animation = this.element.animate(keyframes, options);
+
+ animation.onfinish = () => {
+ if (this.useNative) {
+ this.element.style.willChange = 'auto';
+ }
+ resolve();
+ };
+ });
+ }
+
+ // 快捷方法
+ fadeIn(duration = AnimationConfig.duration.normal): Promise {
+ return this.animate({
+ opacity: [0, 1],
+ duration,
+ easing: AnimationConfig.easing.easeOut,
+ });
+ }
+
+ fadeOut(duration = AnimationConfig.duration.fast): Promise {
+ return this.animate({
+ opacity: [1, 0],
+ duration,
+ easing: AnimationConfig.easing.easeIn,
+ });
+ }
+
+ slideUp(duration = AnimationConfig.duration.slow): Promise {
+ return this.animate({
+ opacity: [0, 1],
+ transform: ['translateY(100%)', 'translateY(0)'],
+ duration,
+ easing: AnimationConfig.easing.iosModal,
+ });
+ }
+
+ slideDown(duration = AnimationConfig.duration.slow): Promise {
+ return this.animate({
+ opacity: [0, 1],
+ transform: ['translateY(-100%)', 'translateY(0)'],
+ duration,
+ easing: AnimationConfig.easing.iosModal,
+ });
+ }
+
+ scaleIn(duration = AnimationConfig.duration.normal): Promise {
+ return this.animate({
+ opacity: [0, 1],
+ scale: [0.9, 1],
+ duration,
+ easing: AnimationConfig.easing.spring,
+ });
+ }
+
+ scaleOut(duration = AnimationConfig.duration.fast): Promise {
+ return this.animate({
+ opacity: [1, 0],
+ scale: [1, 0.9],
+ duration,
+ easing: AnimationConfig.easing.easeIn,
+ });
+ }
+
+ bounce(duration = AnimationConfig.duration.slow): Promise {
+ return this.animate({
+ scale: [1, 1.1, 1],
+ duration,
+ easing: AnimationConfig.easing.bounce,
+ });
+ }
+
+ shake(duration = AnimationConfig.duration.slow): Promise {
+ return this.animate({
+ transform: [
+ 'translateX(0)',
+ 'translateX(-10px)',
+ 'translateX(10px)',
+ 'translateX(-10px)',
+ 'translateX(10px)',
+ 'translateX(0)',
+ ],
+ duration,
+ easing: AnimationConfig.easing.easeInOut,
+ });
+ }
+
+ press(): Promise {
+ return this.animate({
+ scale: [1, 0.97],
+ duration: AnimationConfig.duration.fast,
+ easing: AnimationConfig.easing.ios,
+ });
+ }
+
+ release(): Promise {
+ return this.animate({
+ scale: [0.97, 1],
+ duration: AnimationConfig.duration.fast,
+ easing: AnimationConfig.easing.spring,
+ });
+ }
+}
+
+/**
+ * 创建动画执行器
+ */
+export const createAnimation = (element: HTMLElement) =>
+ new AnimationExecutor(element);
+
+/**
+ * 批量动画
+ */
+export const animateAll = async (
+ elements: HTMLElement[],
+ animationFn: (executor: AnimationExecutor) => Promise
+): Promise => {
+ const promises = elements.map(el => animationFn(new AnimationExecutor(el)));
+ await Promise.all(promises);
+};
+
+/**
+ * 顺序动画
+ */
+export const animateSequence = async (
+ elements: HTMLElement[],
+ animationFn: (executor: AnimationExecutor) => Promise,
+ delay = AnimationConfig.duration.fast
+): Promise => {
+ for (let i = 0; i < elements.length; i++) {
+ await animationFn(new AnimationExecutor(elements[i]));
+ if (i < elements.length - 1) {
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+};
+
+/**
+ * React Hook - 使用动画
+ */
+export const useAnimation = () => {
+ const createExecutor = (element: HTMLElement | null) => {
+ if (!element) return null;
+ return new AnimationExecutor(element);
+ };
+
+ return {
+ createExecutor,
+ config: AnimationConfig,
+ transitions: PageTransitions,
+ micro: MicroInteractions,
+ gestures: GestureAnimations,
+ loading: LoadingAnimations,
+ feedback: FeedbackAnimations,
+ };
+};
\ No newline at end of file
diff --git a/src/lib/responsive.tsx b/src/lib/responsive.tsx
new file mode 100644
index 00000000..3ddfa142
--- /dev/null
+++ b/src/lib/responsive.tsx
@@ -0,0 +1,347 @@
+/**
+ * Responsive Design System - 响应式设计系统
+ *
+ * 多端分辨率适配,确保无变形
+ * 支持:iPhone SE / iPhone 标准 / iPhone Plus / iPad / iPad Pro
+ */
+
+import { useState, useEffect, useCallback } from 'react';
+import { Capacitor } from '@capacitor/core';
+
+// 设备类型
+export type DeviceType = 'phone' | 'tablet' | 'desktop';
+
+// 屏幕尺寸
+export type ScreenSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
+
+// 屏幕方向
+export type Orientation = 'portrait' | 'landscape';
+
+// 设备信息
+export interface DeviceInfo {
+ type: DeviceType;
+ size: ScreenSize;
+ orientation: Orientation;
+ width: number;
+ height: number;
+ pixelRatio: number;
+ isNative: boolean;
+ platform: 'web' | 'android' | 'ios';
+ safeArea: {
+ top: number;
+ bottom: number;
+ left: number;
+ right: number;
+ };
+}
+
+// 断点配置
+export const breakpoints = {
+ xs: 320, // iPhone SE
+ sm: 375, // iPhone 标准
+ md: 414, // iPhone Plus
+ lg: 768, // iPad
+ xl: 1024, // iPad Pro 11
+ '2xl': 1280, // iPad Pro 12.9
+};
+
+// 获取屏幕尺寸
+const getScreenSize = (width: number): ScreenSize => {
+ if (width < breakpoints.xs) return 'xs';
+ if (width < breakpoints.sm) return 'xs';
+ if (width < breakpoints.md) return 'sm';
+ if (width < breakpoints.lg) return 'md';
+ if (width < breakpoints.xl) return 'lg';
+ if (width < breakpoints['2xl']) return 'xl';
+ return '2xl';
+};
+
+// 获取设备类型
+const getDeviceType = (width: number): DeviceType => {
+ if (width < breakpoints.lg) return 'phone';
+ if (width < breakpoints['2xl']) return 'tablet';
+ return 'desktop';
+};
+
+// 获取屏幕方向
+const getOrientation = (width: number, height: number): Orientation =>
+ width > height ? 'landscape' : 'portrait';
+
+// 获取安全区域
+const getSafeArea = (): { top: number; bottom: number; left: number; right: number } => {
+ return {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ };
+};
+
+/**
+ * 获取设备信息
+ */
+export const getDeviceInfo = (): DeviceInfo => {
+ const width = window.innerWidth;
+ const height = window.innerHeight;
+ const pixelRatio = window.devicePixelRatio || 1;
+
+ return {
+ type: getDeviceType(width),
+ size: getScreenSize(width),
+ orientation: getOrientation(width, height),
+ width,
+ height,
+ pixelRatio,
+ isNative: Capacitor.isNativePlatform(),
+ platform: Capacitor.isNativePlatform()
+ ? Capacitor.getPlatform() as 'android' | 'ios'
+ : 'web',
+ safeArea: getSafeArea(),
+ };
+};
+
+/**
+ * 响应式 Hook
+ */
+export const useResponsive = () => {
+ const [deviceInfo, setDeviceInfo] = useState(getDeviceInfo());
+
+ const updateDeviceInfo = useCallback(() => {
+ setDeviceInfo(getDeviceInfo());
+ }, []);
+
+ useEffect(() => {
+ window.addEventListener('resize', updateDeviceInfo);
+ window.addEventListener('orientationchange', updateDeviceInfo);
+
+ return () => {
+ window.removeEventListener('resize', updateDeviceInfo);
+ window.removeEventListener('orientationchange', updateDeviceInfo);
+ };
+ }, [updateDeviceInfo]);
+
+ return {
+ ...deviceInfo,
+ isPhone: deviceInfo.type === 'phone',
+ isTablet: deviceInfo.type === 'tablet',
+ isDesktop: deviceInfo.type === 'desktop',
+ isPortrait: deviceInfo.orientation === 'portrait',
+ isLandscape: deviceInfo.orientation === 'landscape',
+ isSmall: deviceInfo.size === 'xs' || deviceInfo.size === 'sm',
+ isMedium: deviceInfo.size === 'md',
+ isLarge: deviceInfo.size === 'lg' || deviceInfo.size === 'xl' || deviceInfo.size === '2xl',
+ };
+};
+
+/**
+ * 响应式值 Hook
+ */
+export const useResponsiveValue = (values: Partial>, defaultValue: T): T => {
+ const { size } = useResponsive();
+ const sizeOrder: ScreenSize[] = [size, 'md', 'sm', 'xs'];
+
+ for (const s of sizeOrder) {
+ if (values[s] !== undefined) {
+ return values[s]!;
+ }
+ }
+
+ return defaultValue;
+};
+
+// 导出响应式样式Hook
+export { useResponsiveStyle } from './responsiveStyle';
+
+/**
+ * 响应式图片尺寸
+ */
+export const getResponsiveImageSize = (
+ originalWidth: number,
+ originalHeight: number,
+ containerWidth: number,
+ containerHeight?: number
+): { width: number; height: number } => {
+ // 保持比例,不变形
+ const aspectRatio = originalWidth / originalHeight;
+
+ if (containerHeight) {
+ // 有容器高度限制
+ const widthByHeight = containerHeight * aspectRatio;
+
+ if (widthByHeight <= containerWidth) {
+ return {
+ width: widthByHeight,
+ height: containerHeight,
+ };
+ } else {
+ return {
+ width: containerWidth,
+ height: containerWidth / aspectRatio,
+ };
+ }
+ } else {
+ // 只有宽度限制
+ return {
+ width: containerWidth,
+ height: containerWidth / aspectRatio,
+ };
+ }
+};
+
+/**
+ * 响应式字体缩放
+ */
+export const getResponsiveFontSize = (baseSize: number, pixelRatio: number): number => {
+ // 根据像素密度调整字体大小,确保清晰度
+ if (pixelRatio >= 3) {
+ // 高分辨率屏幕(如 iPhone Pro)
+ return baseSize;
+ } else if (pixelRatio >= 2) {
+ // 标准视网膜屏幕
+ return baseSize;
+ } else {
+ // 低分辨率屏幕
+ return Math.round(baseSize * 1.1);
+ }
+};
+
+/**
+ * CSS 响应式类名生成
+ */
+export const responsiveClasses = {
+ // 容器
+ container: (size: ScreenSize) => {
+ const maxWidths: Record = {
+ xs: 'max-w-xs',
+ sm: 'max-w-sm',
+ md: 'max-w-md',
+ lg: 'max-w-lg',
+ xl: 'max-w-xl',
+ '2xl': 'max-w-2xl',
+ };
+ return maxWidths[size];
+ },
+
+ // 网格列数
+ gridCols: (size: ScreenSize) => {
+ const cols: Record = {
+ xs: 'grid-cols-2',
+ sm: 'grid-cols-3',
+ md: 'grid-cols-4',
+ lg: 'grid-cols-6',
+ xl: 'grid-cols-8',
+ '2xl': 'grid-cols-10',
+ };
+ return cols[size];
+ },
+
+ // 间距
+ gap: (size: ScreenSize) => {
+ const gaps: Record = {
+ xs: 'gap-2',
+ sm: 'gap-3',
+ md: 'gap-4',
+ lg: 'gap-6',
+ xl: 'gap-8',
+ '2xl': 'gap-10',
+ };
+ return gaps[size];
+ },
+
+ // 内边距
+ padding: (size: ScreenSize) => {
+ const paddings: Record = {
+ xs: 'p-3',
+ sm: 'p-4',
+ md: 'p-5',
+ lg: 'p-6',
+ xl: 'p-8',
+ '2xl': 'p-10',
+ };
+ return paddings[size];
+ },
+
+ // 文字大小
+ text: (size: ScreenSize) => {
+ const texts: Record = {
+ xs: 'text-sm',
+ sm: 'text-base',
+ md: 'text-lg',
+ lg: 'text-xl',
+ xl: 'text-2xl',
+ '2xl': 'text-3xl',
+ };
+ return texts[size];
+ },
+};
+
+/**
+ * 响应式媒体查询 CSS
+ */
+export const responsiveMediaQueries = {
+ xs: '@media screen and (max-width: 319px)',
+ sm: '@media screen and (min-width: 320px) and (max-width: 374px)',
+ md: '@media screen and (min-width: 375px) and (max-width: 413px)',
+ lg: '@media screen and (min-width: 414px) and (max-width: 767px)',
+ xl: '@media screen and (min-width: 768px) and (max-width: 1023px)',
+ '2xl': '@media screen and (min-width: 1024px)',
+};
+
+/**
+ * 响应式图片 URL 生成
+ */
+export const getResponsiveImageUrl = (
+ baseUrl: string,
+ width: number,
+ options?: {
+ height?: number;
+ quality?: number;
+ format?: 'jpeg' | 'png' | 'webp';
+ }
+): string => {
+ // 如果是 Unsplash 图片,使用其 API 调整尺寸
+ if (baseUrl.includes('unsplash.com')) {
+ const params = new URLSearchParams({
+ w: width.toString(),
+ fit: 'crop',
+ crop: 'faces',
+ auto: 'format',
+ q: (options?.quality || 80).toString(),
+ });
+
+ if (options?.height) {
+ params.set('h', options.height.toString());
+ }
+
+ return `${baseUrl}?${params.toString()}`;
+ }
+
+ // 其他图片源,返回原 URL
+ return baseUrl;
+};
+
+/**
+ * 响应式组件包装器
+ */
+export const ResponsiveWrapper: React.FC<{
+ children: React.ReactNode;
+ className?: string;
+ style?: React.CSSProperties;
+}> = ({ children, className = '', style = {} }) => {
+ const { size, safeAreaPadding } = useResponsiveStyle();
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/src/lib/responsiveStyle.ts b/src/lib/responsiveStyle.ts
new file mode 100644
index 00000000..f3bf2a8c
--- /dev/null
+++ b/src/lib/responsiveStyle.ts
@@ -0,0 +1,137 @@
+/**
+ * Responsive Style Hook - 响应式样式Hook
+ *
+ * 提供响应式样式值,确保多端适配
+ */
+
+import { useMemo } from 'react';
+import { useResponsive } from './responsive';
+
+/**
+ * 安全区域Padding接口
+ */
+export interface SafeAreaPadding {
+ paddingTop: number;
+ paddingBottom: number;
+ paddingLeft: number;
+ paddingRight: number;
+}
+
+/**
+ * 响应式样式返回值接口
+ */
+export interface ResponsiveStyleValues {
+ fontSize: number;
+ spacing: number;
+ cardRadius: number;
+ buttonHeight: number;
+ avatarSize: number;
+ iconSize: number;
+ contentMaxWidth: number;
+ safeAreaPadding: SafeAreaPadding;
+ screenWidth: number;
+ screenHeight: number;
+ screenSize: string;
+ isPortrait: boolean;
+ isLandscape: boolean;
+ getResponsiveValue: (values: Partial>, defaultValue: T) => T;
+}
+
+/**
+ * 响应式样式Hook
+ */
+export const useResponsiveStyle = (): ResponsiveStyleValues => {
+ const { size, isPortrait, safeArea, width, height } = useResponsive();
+
+ return useMemo(() => {
+ // 根据屏幕尺寸计算样式值
+ const getResponsiveValue = (
+ values: Partial>,
+ defaultValue: T
+ ): T => {
+ const sizeOrder = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
+ const currentIndex = sizeOrder.indexOf(size);
+
+ // 从当前尺寸开始向下查找
+ for (let i = currentIndex; i >= 0; i--) {
+ if (values[sizeOrder[i]] !== undefined) {
+ return values[sizeOrder[i]]!;
+ }
+ }
+
+ // 向上查找
+ for (let i = currentIndex + 1; i < sizeOrder.length; i++) {
+ if (values[sizeOrder[i]] !== undefined) {
+ return values[sizeOrder[i]]!;
+ }
+ }
+
+ return defaultValue;
+ };
+
+ // 安全区域padding - 确保返回数值类型
+ const safeAreaPadding: SafeAreaPadding = {
+ paddingTop: typeof safeArea.top === 'number' ? safeArea.top : 0,
+ paddingBottom: typeof safeArea.bottom === 'number' ? safeArea.bottom : 0,
+ paddingLeft: typeof safeArea.left === 'number' ? safeArea.left : 0,
+ paddingRight: typeof safeArea.right === 'number' ? safeArea.right : 0,
+ };
+
+ return {
+ // 字体大小
+ fontSize: getResponsiveValue(
+ { xs: 14, sm: 15, md: 16, lg: 17, xl: 18 },
+ 15
+ ),
+
+ // 间距
+ spacing: getResponsiveValue(
+ { xs: 12, sm: 16, md: 18, lg: 24, xl: 28 },
+ 16
+ ),
+
+ // 卡片圆角
+ cardRadius: getResponsiveValue(
+ { xs: 12, sm: 14, md: 16, lg: 18, xl: 20 },
+ 14
+ ),
+
+ // 按钮高度
+ buttonHeight: getResponsiveValue(
+ { xs: 40, sm: 44, md: 48, lg: 52, xl: 56 },
+ 44
+ ),
+
+ // 头像大小
+ avatarSize: getResponsiveValue(
+ { xs: 48, sm: 56, md: 64, lg: 80, xl: 96 },
+ 56
+ ),
+
+ // 图标大小
+ iconSize: getResponsiveValue(
+ { xs: 20, sm: 24, md: 28, lg: 32, xl: 36 },
+ 24
+ ),
+
+ // 内容最大宽度
+ contentMaxWidth: getResponsiveValue(
+ { xs: 320, sm: 375, md: 414, lg: 768, xl: 1024 },
+ 375
+ ),
+
+ // 安全区域padding
+ safeAreaPadding,
+
+ // 屏幕信息
+ screenWidth: width,
+ screenHeight: height,
+ screenSize: size,
+ isPortrait,
+ isLandscape: !isPortrait,
+
+ // 工具函数
+ getResponsiveValue,
+ };
+ }, [size, isPortrait, safeArea, width, height]);
+};
\ No newline at end of file
diff --git a/src/pages/DeveloperInfoPage.tsx b/src/pages/DeveloperInfoPage.tsx
index 7922dd30..394d2eb8 100644
--- a/src/pages/DeveloperInfoPage.tsx
+++ b/src/pages/DeveloperInfoPage.tsx
@@ -15,7 +15,7 @@ interface DeveloperInfoPageProps {
export default function DeveloperInfoPage({ onNavigate }: DeveloperInfoPageProps) {
const developerInfo = {
- name: '热爱生活的小陈',
+ name: '带娃的小陈工',
title: '全栈开发者 & 宠物爱好者',
avatar: '👨💻',
bio: '一个热爱编程、热爱生活、热爱宠物的开发者。在带娃之余,用代码创造美好,让科技温暖每一个毛孩子。',
diff --git a/src/pages/DevicesPage.tsx b/src/pages/DevicesPage.tsx
new file mode 100644
index 00000000..723f07cc
--- /dev/null
+++ b/src/pages/DevicesPage.tsx
@@ -0,0 +1,235 @@
+import React, { useState, useEffect } from 'react';
+import {
+ ChevronLeft,
+ Plus,
+ Battery,
+ Wifi,
+ Settings,
+ MoreVertical,
+ Bell,
+ Camera,
+ Activity,
+ Droplets,
+ Utensils,
+ Zap,
+ Trash2,
+ Power
+} from 'lucide-react';
+import { useDevicesStore, type Device, type DeviceType } from '../store/devicesStore';
+
+interface DevicesPageProps {
+ onNavigate: (page: string) => void;
+}
+
+const DevicesPage: React.FC = ({ onNavigate }) => {
+ const {
+ devices,
+ isLoading,
+ isPairing,
+ getStats,
+ initialize,
+ startPairing,
+ cancelPairing,
+ removeDevice,
+ updateDeviceStatus,
+ } = useDevicesStore();
+
+ const [showAddModal, setShowAddModal] = useState(false);
+
+ useEffect(() => {
+ initialize();
+ }, [initialize]);
+
+ const stats = getStats() || { total: 0, online: 0, offline: 0, warning: 0 };
+
+ const getDeviceIcon = (type: string) => {
+ switch (type) {
+ case 'camera': return Camera;
+ case 'collar': return Activity;
+ case 'dispenser': return Droplets;
+ case 'bowl': return Utensils;
+ default: return Zap;
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'online': return 'bg-green-500';
+ case 'warning': return 'bg-yellow-500';
+ case 'offline': return 'bg-gray-400';
+ default: return 'bg-gray-400';
+ }
+ };
+
+ const getStatusText = (status: string) => {
+ switch (status) {
+ case 'online': return '在线';
+ case 'warning': return '注意';
+ case 'offline': return '离线';
+ default: return '未知';
+ }
+ };
+
+ const getBatteryColor = (battery: number) => {
+ if (battery > 60) return 'text-green-500';
+ if (battery > 20) return 'text-yellow-500';
+ return 'text-red-500';
+ };
+
+ return (
+
+ {/* 顶部导航 */}
+
+
+
+
我的设备
+
+
+
+
+ {/* 设备统计 */}
+
+
+
+
+
已连接设备
+
{stats.total || 0}
+
+
+
+
+
+
+
+
+
{stats.online || 0} 在线
+
+
+
+
{stats.warning || 0} 注意
+
+
+
+
+
+ {/* 设备列表 */}
+
+
设备列表
+
+ {devices.map((device) => {
+ const DeviceIcon = getDeviceIcon(device.type);
+ return (
+
+
+
+
+
+
+
+
+
{device.name}
+
+
+
{getStatusText(device.status)} · {device.lastActive}
+
+
+
+
+
+ {/* 设备状态详情 */}
+
+
+
+ {device.battery}%
+
+
+
+ {device.signal}%
+
+
+
+ {/* 快捷操作 */}
+
+ {device.type === 'camera' && (
+
+ )}
+
+
+
+
+ );
+ })}
+
+
+ {/* 添加设备按钮 */}
+
+
+
+ {/* 添加设备模态框 */}
+ {showAddModal && (
+
+
+
+
添加设备
+
+
+
+ {[
+ { icon: Camera, name: '摄像头', desc: '实时监控' },
+ { icon: Activity, name: '智能项圈', desc: '活动追踪' },
+ { icon: Droplets, name: '饮水机', desc: '智能喂水' },
+ { icon: Utensils, name: '喂食器', desc: '定时喂食' },
+ ].map((item, index) => (
+
+ ))}
+
+
+
+ )}
+
+ );
+};
+
+export default DevicesPage;
diff --git a/src/pages/DietDataPage.tsx b/src/pages/DietDataPage.tsx
new file mode 100644
index 00000000..14b7710d
--- /dev/null
+++ b/src/pages/DietDataPage.tsx
@@ -0,0 +1,799 @@
+import React, { useState, useEffect } from 'react';
+import {
+ ChevronLeft,
+ Calendar,
+ ChevronDown,
+ Utensils,
+ Droplets,
+ Flame,
+ Clock,
+ TrendingUp,
+ TrendingDown,
+ Minus,
+ PieChart,
+ BarChart3,
+ Plus,
+ Camera,
+ Trash2,
+ Edit3,
+ X,
+} from 'lucide-react';
+import { useDietStore, type TimeRange, type MealType, type DietRecord } from '../store/dietStore';
+import { usePetStore } from '../store/petStore';
+import { Card, Badge, EmptyState, Button } from '../components/DesignSystem';
+import { useResponsiveStyle } from '../lib/responsiveStyle';
+import { Camera as CameraPlugin, CameraResultType } from '@capacitor/camera';
+
+interface DietDataPageProps {
+ onNavigate: (page: string) => void;
+}
+
+// 餐食类型配置
+const MEAL_TYPE_CONFIG: Record = {
+ breakfast: { label: '早餐', icon: Utensils, color: '#F59E0B' },
+ lunch: { label: '午餐', icon: Utensils, color: '#10B981' },
+ dinner: { label: '晚餐', icon: Utensils, color: '#8B5CF6' },
+ snack: { label: '零食', icon: Utensils, color: '#EC4899' },
+ treat: { label: '奖励', icon: Utensils, color: '#06B6D4' },
+};
+
+export const DietDataPage: React.FC = ({ onNavigate }) => {
+ const responsive = useResponsiveStyle();
+ const { currentPetId } = usePetStore();
+ const {
+ timeRange,
+ setTimeRange,
+ selectedDate,
+ setSelectedDate,
+ records,
+ advices,
+ getStats,
+ getNutritionIntake,
+ getRecordsByDate,
+ getRecordsByRange,
+ getAdvicesByPet,
+ addRecord,
+ updateRecord,
+ deleteRecord,
+ initialize,
+ isLoading,
+ } = useDietStore();
+
+ const [showAddModal, setShowAddModal] = useState(false);
+ const [editingRecord, setEditingRecord] = useState(null);
+ const [showDatePicker, setShowDatePicker] = useState(false);
+ const [newRecord, setNewRecord] = useState<{
+ type: MealType;
+ food: string;
+ amount: number;
+ calories: number;
+ time: string;
+ note: string;
+ }>({
+ type: 'breakfast',
+ food: '',
+ amount: 100,
+ calories: 0,
+ time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
+ note: '',
+ });
+
+ useEffect(() => {
+ initialize();
+ }, [initialize]);
+
+ // 获取统计数据
+ const stats = currentPetId ? getStats(currentPetId, timeRange) : null;
+ const nutrition = currentPetId ? getNutritionIntake(currentPetId) : null;
+ const todayRecords = getRecordsByDate(selectedDate);
+ const rangeRecords = getRecordsByRange(timeRange);
+ const petAdvices = currentPetId ? getAdvicesByPet(currentPetId) : [];
+
+ // 计算变化趋势
+ const calculateTrend = (current: number, previous: number) => {
+ if (previous === 0) return 0;
+ return Math.round(((current - previous) / previous) * 100);
+ };
+
+ // 今日数据
+ const todayData = [
+ {
+ type: 'feeding',
+ label: '进食次数',
+ value: stats?.totalMeals || 0,
+ unit: '次',
+ change: 0,
+ changeLabel: '较昨日',
+ icon: Utensils,
+ color: '#3B82F6',
+ bgColor: '#EFF6FF',
+ },
+ {
+ type: 'drinking',
+ label: '进食总量',
+ value: stats?.totalAmount || 0,
+ unit: 'g',
+ change: 0,
+ changeLabel: '较昨日',
+ icon: Droplets,
+ color: '#10B981',
+ bgColor: '#ECFDF5',
+ },
+ {
+ type: 'calories',
+ label: '消耗卡路里',
+ value: stats?.totalCalories || 0,
+ unit: 'kcal',
+ change: 0,
+ changeLabel: '较昨日',
+ icon: Flame,
+ color: '#F59E0B',
+ bgColor: '#FFFBEB',
+ },
+ {
+ type: 'duration',
+ label: '进食时长',
+ value: stats?.totalDuration || 0,
+ unit: '分钟',
+ change: 0,
+ changeLabel: '较昨日',
+ icon: Clock,
+ color: '#8B5CF6',
+ bgColor: '#F5F3FF',
+ },
+ ];
+
+ // 周数据(从真实记录计算)
+ const getWeekData = () => {
+ const weekData: { date: string; feeding: number; drinking: number; calories: number; duration: number }[] = [];
+ const today = new Date();
+
+ for (let i = 6; i >= 0; i--) {
+ const date = new Date(today);
+ date.setDate(date.getDate() - i);
+ const dateStr = date.toISOString().split('T')[0];
+ const dayRecords = (records || []).filter(r => r?.date === dateStr);
+
+ const dayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
+ weekData.push({
+ date: dayNames[date.getDay()],
+ feeding: dayRecords.length || 0,
+ drinking: dayRecords.reduce((sum, r) => sum + (r?.amount || 0), 0),
+ calories: dayRecords.reduce((sum, r) => sum + (r?.calories || 0), 0),
+ duration: dayRecords.length * 1.5 || 0,
+ });
+ }
+
+ return weekData;
+ };
+
+ const weekData = getWeekData();
+
+ // 营养摄入分布
+ const nutritionData = nutrition ? [
+ { name: '蛋白质', value: nutrition.protein, color: '#3B82F6' },
+ { name: '脂肪', value: nutrition.fat, color: '#F59E0B' },
+ { name: '碳水化合物', value: nutrition.carbohydrate, color: '#10B981' },
+ { name: '纤维', value: nutrition.fiber, color: '#8B5CF6' },
+ ] : [];
+
+ // 获取变化趋势图标
+ const getTrendIcon = (change: number) => {
+ if (change > 0) return ;
+ if (change < 0) return ;
+ return ;
+ };
+
+ // 获取变化颜色
+ const getTrendColor = (change: number, type: string) => {
+ const positiveIncrease = ['feeding', 'calories', 'duration'];
+ const isPositive = positiveIncrease.includes(type);
+
+ if (change === 0) return 'text-gray-400';
+ if (isPositive) {
+ return change > 0 ? 'text-green-500' : 'text-red-500';
+ } else {
+ return change < 0 ? 'text-green-500' : 'text-red-500';
+ }
+ };
+
+ // 添加记录
+ const handleAddRecord = async () => {
+ if (!currentPetId || !newRecord.food) return;
+
+ await addRecord({
+ petId: currentPetId,
+ type: newRecord.type,
+ food: newRecord.food,
+ amount: newRecord.amount,
+ calories: newRecord.calories,
+ time: newRecord.time,
+ date: selectedDate,
+ note: newRecord.note,
+ });
+
+ setShowAddModal(false);
+ setNewRecord({
+ type: 'breakfast',
+ food: '',
+ amount: 100,
+ calories: 0,
+ time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
+ note: '',
+ });
+ };
+
+ // 更新记录
+ const handleUpdateRecord = async () => {
+ if (!editingRecord) return;
+
+ await updateRecord(editingRecord.id, {
+ type: newRecord.type,
+ food: newRecord.food,
+ amount: newRecord.amount,
+ calories: newRecord.calories,
+ time: newRecord.time,
+ note: newRecord.note,
+ });
+
+ setEditingRecord(null);
+ setShowAddModal(false);
+ setNewRecord({
+ type: 'breakfast',
+ food: '',
+ amount: 100,
+ calories: 0,
+ time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
+ note: '',
+ });
+ };
+
+ // 删除记录
+ const handleDeleteRecord = async (id: string) => {
+ await deleteRecord(id);
+ };
+
+ // 拍照添加记录
+ const handleTakePhoto = async () => {
+ try {
+ const photo = await CameraPlugin.getPhoto({
+ quality: 90,
+ allowEditing: true,
+ resultType: CameraResultType.Base64,
+ });
+
+ // 这里可以添加图片分析逻辑,识别食物类型和数量
+ console.log('Photo taken:', photo.base64String);
+
+ // 设置图片并打开添加模态框
+ setNewRecord(prev => ({
+ ...prev,
+ note: prev.note + ' [有图片]',
+ }));
+ setShowAddModal(true);
+ } catch (error) {
+ console.error('Camera error:', error);
+ }
+ };
+
+ // 编辑记录
+ const handleEditRecord = (record: DietRecord) => {
+ setEditingRecord(record);
+ setNewRecord({
+ type: record.type,
+ food: record.food,
+ amount: record.amount,
+ calories: record.calories || 0,
+ time: record.time,
+ note: record.note || '',
+ });
+ setShowAddModal(true);
+ };
+
+ // 格式化日期显示
+ const formatDateDisplay = (dateStr: string) => {
+ const date = new Date(dateStr);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ if (date.toDateString() === today.toDateString()) {
+ return '今天';
+ }
+
+ const yesterday = new Date(today);
+ yesterday.setDate(yesterday.getDate() - 1);
+ if (date.toDateString() === yesterday.toDateString()) {
+ return '昨天';
+ }
+
+ return date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' });
+ };
+
+ return (
+
+ {/* 顶部导航 */}
+
+
+
+
饮食数据
+
+
+
+
+
+
+
+ {/* 日期选择器 */}
+
+
+
+
+
+
+
+
+ {/* 日期快速选择 */}
+ {showDatePicker && (
+
+ {Array.from({ length: 7 }).map((_, i) => {
+ const date = new Date();
+ date.setDate(date.getDate() - i);
+ const dateStr = date.toISOString().split('T')[0];
+ const isSelected = selectedDate === dateStr;
+
+ return (
+
+ );
+ })}
+
+ )}
+
+
+ {/* 加载状态 */}
+ {isLoading && (
+
+ )}
+
+ {/* 今日数据概览 */}
+ {!isLoading && (
+
+
+ {timeRange === 'day' ? '今日概览' : timeRange === 'week' ? '本周概览' : '本月概览'}
+
+
+ {todayData.map((item, index) => (
+
+
+
+ {item.value}
+ {item.unit}
+
+
+ {getTrendIcon(item.change)}
+ {Math.abs(item.change)}%
+ {item.changeLabel}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* 今日饮食记录列表 */}
+ {!isLoading && timeRange === 'day' && (
+
+
今日饮食记录
+ {todayRecords.length === 0 ? (
+
setShowAddModal(true)}>
+ 添加记录
+
+ }
+ />
+ ) : (
+
+ {todayRecords.map((record) => {
+ const typeConfig = MEAL_TYPE_CONFIG[record.type];
+ return (
+
+
+
+
+
+
+
+
+ {record.food}
+
+ {typeConfig.label}
+
+
+
+ {record.amount}g
+ {record.calories && · {record.calories} kcal}
+
+ {record.time}
+
+ {record.note && (
+
{record.note}
+ )}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+ )}
+
+ {/* 周趋势图表 */}
+ {!isLoading && timeRange === 'week' && (
+
+
+
+
本周趋势
+
+
+
+ {/* 进食次数趋势 */}
+
+
+ 进食次数
+
+ 平均 {(weekData.reduce((sum, d) => sum + d.feeding, 0) / 7).toFixed(1)} 次/天
+
+
+
+ {weekData.map((day, index) => (
+
+ ))}
+
+
+
+ {/* 卡路里趋势 */}
+
+
+ 消耗卡路里
+
+ 平均 {Math.round(weekData.reduce((sum, d) => sum + d.calories, 0) / 7)} kcal/天
+
+
+
+ {weekData.map((day, index) => (
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* 营养摄入分布 */}
+ {!isLoading && nutritionData.length > 0 && (
+
+
+ 营养摄入分布
+
+ {/* 饼图示意 */}
+
+
+
+
+
+ {/* 图例 */}
+
+ {nutritionData.map((item, index) => (
+
+ ))}
+
+
+
+
+ )}
+
+ {/* 饮食建议 */}
+ {!isLoading && petAdvices.length > 0 && (
+
+
+
饮食建议
+ {petAdvices.map((advice) => (
+
+ {advice.content}
+
+ ))}
+
+
+
+ )}
+
+ {/* 添加/编辑记录模态框 */}
+ {showAddModal && (
+
+
+
+
+ {editingRecord ? '编辑饮食记录' : '添加饮食记录'}
+
+
+
+
+
+ {/* 餐食类型 */}
+
+
+
+ {Object.entries(MEAL_TYPE_CONFIG).map(([type, config]) => (
+
+ ))}
+
+
+
+ {/* 食物名称 */}
+
+
+ setNewRecord(prev => ({ ...prev, food: e.target.value }))}
+ placeholder="例如:狗粮、零食..."
+ className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-orange-500 focus:outline-none transition-colors"
+ />
+
+
+ {/* 数量 */}
+
+
+ setNewRecord(prev => ({ ...prev, amount: parseInt(e.target.value) || 0 }))}
+ className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-orange-500 focus:outline-none transition-colors"
+ />
+
+
+ {/* 卡路里 */}
+
+
+ setNewRecord(prev => ({ ...prev, calories: parseInt(e.target.value) || 0 }))}
+ className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-orange-500 focus:outline-none transition-colors"
+ />
+
+
+ {/* 时间 */}
+
+
+ setNewRecord(prev => ({ ...prev, time: e.target.value }))}
+ className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-orange-500 focus:outline-none transition-colors"
+ />
+
+
+ {/* 备注 */}
+
+
+ setNewRecord(prev => ({ ...prev, note: e.target.value }))}
+ placeholder="可选备注..."
+ className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-orange-500 focus:outline-none transition-colors"
+ />
+
+
+ {/* 提交按钮 */}
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default DietDataPage;
\ No newline at end of file
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
index 2cddffed..32bf8d34 100644
--- a/src/pages/HomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -1,584 +1,649 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
-import {
- ChevronRight,
- Heart,
- Bot,
- FileText,
- BookOpen,
- Calendar,
- Activity,
- Star,
- Clock,
- ArrowUpRight,
+/**
+ * HomePage - 首页
+ *
+ * 优化功能入口布局,确保所有功能真实完整
+ * Apple风格设计,完美适配多端
+ */
+
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ Stethoscope,
+ FileText,
+ Utensils,
+ PawPrint,
Camera,
- Video,
+ Activity,
+ Heart,
+ Bell,
MessageCircle,
- RefreshCw,
- Sparkles,
- Bell
+ Calendar,
+ ChevronRight,
+ Plus,
+ Battery,
+ Droplets,
+ Flame,
+ Clock,
+ Zap,
+ TrendingUp,
+ Eye,
+ Shield,
+ BookOpen,
+ Music,
} from 'lucide-react';
-import { GlassCard, SkeletonCard as _SkeletonCard, SkeletonQuickActions, LiquidHeroCard } from '../components/DesignSystem';
-import '../styles/animations.css';
import { useAppStore } from '../store/appStore';
-import { useBondStore } from '../store/bondStore';
import { usePetStore } from '../store/petStore';
-import { useReminderStore } from '../store/reminderStore';
-import { useHealthRecordStore } from '../store/healthRecordStore';
-import { cameraAdapterService } from '../services/cameraAdapterService';
-import type { CameraDevice } from '../types/camera';
-import { usePullToRefresh } from '../hooks/useGestures';
+import { useBondStore } from '../store/bondStore';
+import { useDevicesStore } from '../store/devicesStore';
+import { useDietStore } from '../store/dietStore';
+import { useRecordsStore } from '../store/recordsStore';
+import { useHealthStore } from '../store/healthStore';
+import { useResponsiveStyle } from '../lib/responsive';
+import { createAnimation, AnimationConfig } from '../lib/animations';
interface HomePageProps {
onNavigate: (page: string) => void;
}
-interface QuickAction {
+// 功能分类
+type FeatureCategory = 'health' | 'monitor' | 'care' | 'tools';
+
+// 功能项
+interface FeatureItem {
+ id: string;
icon: React.ElementType;
label: string;
description: string;
color: string;
- bgGradient: string;
+ bgColor: string;
page: string;
+ category: FeatureCategory;
badge?: string;
badgeColor?: string;
- isPrimary?: boolean;
+ isReal?: boolean; // 标记是否为真实功能
}
export const HomePage: React.FC = ({ onNavigate }) => {
- const { currentPet, currentEmotion, healthScore } = useAppStore();
- const { metrics, badges, totalPoints, streakDays } = useBondStore();
- const unlockedBadges = badges.filter(b => b.isUnlocked).length;
+ const { currentPet, healthScore } = useAppStore();
const { pets, currentPetId, setCurrentPet } = usePetStore();
- const { getUpcomingReminders } = useReminderStore();
- const { getFilteredRecords } = useHealthRecordStore();
- const [cameras, setCameras] = useState([]);
+ const { metrics, totalPoints, streakDays } = useBondStore();
+ const { devices, getStats, initialize: initDevices } = useDevicesStore();
+ const { getStats: getDietStats, initialize: initDiet } = useDietStore();
+ const { initialize: initRecords } = useRecordsStore();
+ const { getScore, initialize: initHealth } = useHealthStore();
+ const responsiveStyle = useResponsiveStyle();
+
const [isLoading, setIsLoading] = useState(true);
- const [isRefreshing, setIsRefreshing] = useState(false);
- const [pullDistance, setPullDistance] = useState(0);
- const [isPulling, setIsPulling] = useState(false);
- const touchStartY = useRef(0);
- const containerRef = useRef(null);
-
- const loadData = useCallback(async () => {
- const devices = await cameraAdapterService.getDevices();
- setCameras(devices);
- setIsLoading(false);
- }, []);
+ // 初始化所有数据
useEffect(() => {
- loadData();
- }, [loadData]);
-
- const handleRefresh = useCallback(async () => {
- setIsRefreshing(true);
- await new Promise(resolve => setTimeout(resolve, 1000));
- await loadData();
- setIsRefreshing(false);
- setPullDistance(0);
- }, [loadData]);
-
- const handleTouchStart = useCallback((e: React.TouchEvent) => {
- if (containerRef.current?.scrollTop === 0) {
- touchStartY.current = e.touches[0].clientY;
- setIsPulling(true);
- }
+ const initAll = async () => {
+ await Promise.all([
+ initDevices(),
+ initDiet(),
+ initRecords(),
+ initHealth(),
+ ]);
+ setIsLoading(false);
+ };
+ initAll();
}, []);
- const handleTouchMove = useCallback((e: React.TouchEvent) => {
- if (!isPulling || isRefreshing) return;
-
- const currentY = e.touches[0].clientY;
- const diff = currentY - touchStartY.current;
-
- if (diff > 0 && containerRef.current?.scrollTop === 0) {
- const resistance = 0.5;
- const newDistance = Math.min(diff * resistance, 100);
- setPullDistance(newDistance);
- }
- }, [isPulling, isRefreshing]);
-
- const handleTouchEnd = useCallback(() => {
- setIsPulling(false);
- if (pullDistance > 60 && !isRefreshing) {
- handleRefresh();
- } else {
- setPullDistance(0);
- }
- }, [pullDistance, isRefreshing, handleRefresh]);
-
- const upcomingReminders = currentPetId ? getUpcomingReminders(currentPetId, 3) : [];
- const recentRecords = currentPetId ? getFilteredRecords(currentPetId).slice(0, 3) : [];
- const onlineCameras = cameras.filter(c => c.status === 'online');
+ // 获取统计数据 - 使用默认值防止 undefined
+ const deviceStats = getStats() || { total: 0, online: 0, offline: 0, warning: 0 };
+ const dietStats = getDietStats('pet-1', 'day') || { calories: 0, foodAmount: 0, waterAmount: 0, meals: 0 };
+ const healthScoreData = getScore('pet-1') || { overall: 0, items: [] };
- const quickActions: QuickAction[] = [
- {
- icon: Video,
- label: '实时监控',
- description: '查看毛孩动态',
- color: '#3B82F6',
- bgGradient: 'from-blue-500 via-blue-600 to-cyan-500',
- page: 'camera-monitor',
- badge: onlineCameras.length > 0 ? `${onlineCameras.length}台` : undefined,
- badgeColor: 'bg-blue-500',
- isPrimary: true,
- },
- {
- icon: Bot,
- label: 'AI健康顾问',
- description: '智能问诊咨询',
- color: '#8B5CF6',
- bgGradient: 'from-purple-500 via-violet-500 to-fuchsia-500',
- page: 'ai-consultant',
- badge: 'AI',
- badgeColor: 'bg-gradient-to-r from-purple-500 to-pink-500',
- isPrimary: true,
+ // 功能网格 - 分类展示
+ const featureCategories: Record = {
+ health: {
+ title: '健康管理',
+ items: [
+ {
+ id: 'ai-consultant',
+ icon: Stethoscope,
+ label: 'AI问诊',
+ description: '智能诊断',
+ color: '#3B82F6',
+ bgColor: '#EFF6FF',
+ page: 'ai-consultant',
+ category: 'health',
+ badge: '在线',
+ badgeColor: '#34C759',
+ isReal: true,
+ },
+ {
+ id: 'health-report',
+ icon: FileText,
+ label: '健康报告',
+ description: '今日生成',
+ color: '#10B981',
+ bgColor: '#ECFDF5',
+ page: 'health-report',
+ category: 'health',
+ badge: healthScoreData.overall + '分',
+ badgeColor: '#10B981',
+ isReal: true,
+ },
+ {
+ id: 'health-records',
+ icon: Heart,
+ label: '健康记录',
+ description: '详细追踪',
+ color: '#EF4444',
+ bgColor: '#FEE2E2',
+ page: 'health-records',
+ category: 'health',
+ isReal: true,
+ },
+ {
+ id: 'reminders',
+ icon: Bell,
+ label: '健康提醒',
+ description: '定时提醒',
+ color: '#8B5CF6',
+ bgColor: '#F5F3FF',
+ page: 'reminders',
+ category: 'health',
+ isReal: true,
+ },
+ ],
},
- {
- icon: MessageCircle,
- label: '情绪翻译',
- description: '读懂宠物心声',
- color: '#F97316',
- bgGradient: 'from-orange-500 via-red-400 to-rose-500',
- page: 'translator',
- badge: '95%+',
- badgeColor: 'bg-orange-500',
- isPrimary: true,
+ monitor: {
+ title: '智能监控',
+ items: [
+ {
+ id: 'camera-monitor',
+ icon: Camera,
+ label: '实时监控',
+ description: '查看宠物',
+ color: '#F59E0B',
+ bgColor: '#FFFBEB',
+ page: 'camera-monitor',
+ category: 'monitor',
+ badge: (deviceStats.online || 0) + '在线',
+ badgeColor: '#34C759',
+ isReal: true,
+ },
+ {
+ id: 'devices',
+ icon: Activity,
+ label: '设备管理',
+ description: '智能设备',
+ color: '#06B6D4',
+ bgColor: '#ECFEFF',
+ page: 'devices',
+ category: 'monitor',
+ badge: (deviceStats.total || 0).toString(),
+ badgeColor: '#06B6D4',
+ isReal: true,
+ },
+ {
+ id: 'diet-data',
+ icon: Flame,
+ label: '饮食数据',
+ description: '营养分析',
+ color: '#EC4899',
+ bgColor: '#FCE7F3',
+ page: 'diet-data',
+ category: 'monitor',
+ badge: dietStats.totalMeals + '次',
+ badgeColor: '#EC4899',
+ isReal: true,
+ },
+ {
+ id: 'activity',
+ icon: TrendingUp,
+ label: '活动追踪',
+ description: '运动记录',
+ color: '#14B8A6',
+ bgColor: '#F0FDFA',
+ page: 'advanced-health',
+ category: 'monitor',
+ isReal: true,
+ },
+ ],
},
- {
- icon: FileText,
- label: '健康记录',
- description: '追踪健康状况',
- color: '#10B981',
- bgGradient: 'from-emerald-500 via-teal-500 to-cyan-500',
- page: 'health-records',
- isPrimary: false,
+ care: {
+ title: '日常护理',
+ items: [
+ {
+ id: 'records',
+ icon: Calendar,
+ label: '日常记录',
+ description: '时间线',
+ color: '#F97316',
+ bgColor: '#FFF7ED',
+ page: 'records',
+ category: 'care',
+ isReal: true,
+ },
+ {
+ id: 'pet-profile',
+ icon: PawPrint,
+ label: '宠物档案',
+ description: '详细信息',
+ color: '#A855F7',
+ bgColor: '#FAF5FF',
+ page: 'pet-profile',
+ category: 'care',
+ isReal: true,
+ },
+ {
+ id: 'translator',
+ icon: MessageCircle,
+ label: '宠物翻译',
+ description: '理解心声',
+ color: '#6366F1',
+ bgColor: '#EEF2FF',
+ page: 'translator',
+ category: 'care',
+ isReal: true,
+ },
+ {
+ id: 'diet-advice',
+ icon: Utensils,
+ label: '饮食建议',
+ description: '科学喂养',
+ color: '#84CC16',
+ bgColor: '#F7FEE7',
+ page: 'diet-advice',
+ category: 'care',
+ isReal: true,
+ },
+ ],
},
- {
- icon: BookOpen,
- label: '健康手册',
- description: '专业养宠知识',
- color: '#6366F1',
- bgGradient: 'from-indigo-500 via-purple-500 to-blue-500',
- page: 'health-manual',
- badge: '60+篇',
- badgeColor: 'bg-indigo-500',
- isPrimary: false,
+ tools: {
+ title: '实用工具',
+ items: [
+ {
+ id: 'medical',
+ icon: Shield,
+ label: '医疗记录',
+ description: '就诊历史',
+ color: '#DC2626',
+ bgColor: '#FEF2F2',
+ page: 'medical',
+ category: 'tools',
+ isReal: true,
+ },
+ {
+ id: 'training',
+ icon: BookOpen,
+ label: '训练指南',
+ description: '行为训练',
+ color: '#0EA5E9',
+ bgColor: '#F0F9FF',
+ page: 'training',
+ category: 'tools',
+ isReal: true,
+ },
+ {
+ id: 'insurance',
+ icon: Shield,
+ label: '宠物保险',
+ description: '保障计划',
+ color: '#7C3AED',
+ bgColor: '#F5F3FF',
+ page: 'insurance',
+ category: 'tools',
+ isReal: true,
+ },
+ {
+ id: 'services',
+ icon: Eye,
+ label: '周边服务',
+ description: '宠物服务',
+ color: '#F59E0B',
+ bgColor: '#FFFBEB',
+ page: 'services',
+ category: 'tools',
+ isReal: true,
+ },
+ ],
},
- {
- icon: Calendar,
- label: '智能提醒',
- description: '重要日程管理',
- color: '#F59E0B',
- bgGradient: 'from-amber-500 via-yellow-500 to-orange-500',
- page: 'reminders',
- badge: upcomingReminders.length > 0 ? `${upcomingReminders.length}条` : undefined,
- badgeColor: 'bg-amber-500',
- isPrimary: false,
- },
- ];
-
- const getEmotionEmoji = (emotion: string) => {
- const map: Record = {
- happy: '😸',
- curious: '🤔',
- anxious: '😰',
- angry: '😾',
- needs: '🥺',
- relaxed: '😌',
- excited: '🤩',
- sleepy: '😴',
- calm: '🧘',
- };
- return map[emotion] || '😐';
};
- const getEmotionLabel = (emotion: string) => {
- const map: Record = {
- happy: '开心',
- curious: '好奇',
- anxious: '焦虑',
- angry: '生气',
- needs: '需要关注',
- relaxed: '放松',
- excited: '兴奋',
- sleepy: '困倦',
- calm: '平静',
- };
- return map[emotion] || '未知';
+ // 功能点击动画
+ const handleFeatureClick = (feature: FeatureItem, element: HTMLElement) => {
+ const animator = createAnimation(element);
+ animator.press();
+
+ setTimeout(() => {
+ animator.release();
+ onNavigate(feature.page);
+ }, AnimationConfig.duration.fast);
};
- const handleCardClick = useCallback((page: string) => {
- onNavigate(page);
- }, [onNavigate]);
+ // 获取宠物头像
+ const getPetAvatar = (pet: typeof pets[0]) => {
+ if (pet.avatar) return pet.avatar;
+ return pet.type === 'dog'
+ ? 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=cute%20golden%20retriever%20dog%20portrait&image_size=square'
+ : 'https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=cute%20orange%20cat%20portrait&image_size=square';
+ };
- const _refreshProgress = Math.min(pullDistance / 60, 1);
- const refreshRotation = pullDistance * 2;
+ // 加载状态
+ if (isLoading) {
+ return (
+
+ );
+ }
return (
-
-
-
20 || isRefreshing ? 'opacity-100 scale-100' : 'opacity-0 scale-75'
- }`}
- style={{
- top: isRefreshing ? 10 : -40 + pullDistance * 0.5,
+
+ {/* 顶部宠物信息区 */}
+
+ {/* 宠物信息卡片 */}
+
+
+
+

+
+
+
+
+
+
+ {currentPet?.name || 'JOJO'}
+
+
+ {currentPet?.gender === 'male' ? '♂' : '♀'}
+
+
+
+ {currentPet?.breed || '柯基犬'} · {currentPet?.age || 2}岁
+
+
+
+ 活力充沛
+
+
+
+
+
+ {/* 健康评分卡片 */}
+
-
-
+
+
+
+
+
+
+
健康评分
+
+ {healthScoreData.overall}
+
+
+
+
-
- {isRefreshing ? '刷新中...' : pullDistance > 60 ? '释放刷新' : '下拉刷新'}
-
+
-
- {/* ColorOS 16 液态玻璃背景渐变 */}
-
-
- {/* 液态玻璃纹理背景 */}
-
-
- {/* 液态玻璃浮动光斑 */}
-
-
-
-
-
-
-
-
-
-
-
-
- 爪爪连心❤️
-
-
温暖守护 · 陪伴成长
-
-