Add comprehensive app update checking functionality with: - New VersionCheckContext for managing update detection and notifications - VersionUpdateModal UI component for presenting update information - Version service API integration with platform-specific update URLs - Version check menu item in personal settings with manual/automatic checking Enhance internationalization across workout features: - Complete workout type translations for English and Chinese - Localized workout detail modal with proper date/time formatting - Locale-aware date formatting in fitness rings detail - Workout notification improvements with deep linking to specific workout details Improve UI/UX with better chart rendering, sizing fixes, and enhanced navigation flow. Update app version to 1.1.3 and include app version in API headers for better tracking.
272 lines
8.5 KiB
TypeScript
272 lines
8.5 KiB
TypeScript
import { getWorkoutTypeDisplayName, WorkoutData } from '@/utils/health';
|
||
import { logger } from '@/utils/logger';
|
||
import { getNotificationEnabled } from '@/utils/userPreferences';
|
||
import {
|
||
getWorkoutNotificationEnabled,
|
||
isNotificationTimeAllowed,
|
||
isWorkoutTypeEnabled
|
||
} from '@/utils/workoutPreferences';
|
||
import { notificationService, NotificationTypes } from './notifications';
|
||
import { getWorkoutDetailMetrics } from './workoutDetail';
|
||
|
||
interface WorkoutEncouragementMessage {
|
||
title: string;
|
||
body: string;
|
||
data: Record<string, any>;
|
||
}
|
||
|
||
export async function analyzeWorkoutAndSendNotification(workout: WorkoutData): Promise<void> {
|
||
try {
|
||
// 检查用户是否启用了通用通知
|
||
const notificationsEnabled = await getNotificationEnabled();
|
||
if (!notificationsEnabled) {
|
||
logger.info('用户已禁用通知,跳过锻炼结束通知');
|
||
return;
|
||
}
|
||
|
||
// 检查用户是否启用了锻炼通知
|
||
const workoutNotificationsEnabled = await getWorkoutNotificationEnabled();
|
||
if (!workoutNotificationsEnabled) {
|
||
logger.info('用户已禁用锻炼通知,跳过锻炼结束通知');
|
||
return;
|
||
}
|
||
|
||
// 检查时间限制(避免深夜打扰)
|
||
const timeAllowed = await isNotificationTimeAllowed();
|
||
if (!timeAllowed) {
|
||
logger.info('当前时间不适合发送通知,跳过锻炼结束通知');
|
||
return;
|
||
}
|
||
|
||
// 检查特定锻炼类型是否启用了通知
|
||
const workoutTypeEnabled = await isWorkoutTypeEnabled(workout.workoutActivityTypeString || '');
|
||
if (!workoutTypeEnabled) {
|
||
logger.info('该锻炼类型已禁用通知,跳过锻炼结束通知:', workout.workoutActivityTypeString);
|
||
return;
|
||
}
|
||
|
||
// 获取详细的锻炼指标
|
||
const workoutMetrics = await getWorkoutDetailMetrics(workout);
|
||
|
||
// 生成个性化鼓励消息
|
||
const message = generateEncouragementMessage(workout, workoutMetrics);
|
||
|
||
// 发送通知
|
||
await notificationService.sendImmediateNotification({
|
||
title: message.title,
|
||
body: message.body,
|
||
data: {
|
||
type: NotificationTypes.WORKOUT_COMPLETION,
|
||
workoutId: workout.id,
|
||
workoutType: workout.workoutActivityTypeString,
|
||
duration: workout.duration,
|
||
calories: workout.totalEnergyBurned,
|
||
...message.data
|
||
},
|
||
sound: true,
|
||
priority: 'high'
|
||
});
|
||
|
||
logger.info('锻炼结束通知已发送:', message.title);
|
||
} catch (error) {
|
||
console.error('发送锻炼结束通知失败:', error);
|
||
}
|
||
}
|
||
|
||
interface WorkoutMessageConfig {
|
||
emoji: string;
|
||
titleTemplate: string;
|
||
bodyTemplate: (params: {
|
||
workoutType: string;
|
||
durationMinutes: number;
|
||
calories: number;
|
||
distanceKm?: string;
|
||
averageHeartRate?: number;
|
||
mets?: number;
|
||
}) => string;
|
||
encouragement: string;
|
||
dataExtractor?: (workout: WorkoutData, metrics: any) => Record<string, any>;
|
||
}
|
||
|
||
const WORKOUT_MESSAGES: Record<string, WorkoutMessageConfig> = {
|
||
running: {
|
||
emoji: '🏃♂️',
|
||
titleTemplate: '跑步完成!',
|
||
bodyTemplate: ({ durationMinutes, calories, averageHeartRate }) => {
|
||
let body = `您完成了${durationMinutes}分钟的跑步`;
|
||
if (calories > 0) {
|
||
body += `,消耗${calories}千卡`;
|
||
}
|
||
if (averageHeartRate) {
|
||
body += `(平均心率${averageHeartRate}次/分)`;
|
||
}
|
||
return body + '!';
|
||
},
|
||
encouragement: '坚持就是胜利!💪',
|
||
dataExtractor: (workout, metrics) => ({
|
||
heartRateContext: metrics.averageHeartRate ? 'provided' : 'none'
|
||
})
|
||
},
|
||
cycling: {
|
||
emoji: '🚴♂️',
|
||
titleTemplate: '骑行完成!',
|
||
bodyTemplate: ({ durationMinutes, calories, distanceKm }) => {
|
||
let body = `${durationMinutes}分钟骑行`;
|
||
if (distanceKm) {
|
||
body += `,行程${distanceKm}公里`;
|
||
}
|
||
if (calories > 0) {
|
||
body += `,消耗${calories}千卡`;
|
||
}
|
||
return body + '!';
|
||
},
|
||
encouragement: '追寻风的自由!🌟'
|
||
},
|
||
swimming: {
|
||
emoji: '🏊♂️',
|
||
titleTemplate: '游泳完成!',
|
||
bodyTemplate: ({ durationMinutes }) => {
|
||
return `水中${durationMinutes}分钟的锻炼完成!`;
|
||
},
|
||
encouragement: '全身肌肉都得到了锻炼!💦'
|
||
},
|
||
yoga: {
|
||
emoji: '🧘♀️',
|
||
titleTemplate: '瑜伽完成!',
|
||
bodyTemplate: ({ durationMinutes }) => {
|
||
return `${durationMinutes}分钟的瑜伽练习`;
|
||
},
|
||
encouragement: '身心合一,平静致远!🌸'
|
||
},
|
||
functionalstrengthtraining: {
|
||
emoji: '💪',
|
||
titleTemplate: '力量训练完成!',
|
||
bodyTemplate: ({ durationMinutes, calories, mets }) => {
|
||
let body = `${durationMinutes}分钟力量训练`;
|
||
if (calories > 0) {
|
||
body += `,消耗${calories}千卡`;
|
||
}
|
||
if (mets && mets > 6) {
|
||
body += '(高强度)';
|
||
}
|
||
return body + '!';
|
||
},
|
||
encouragement: '肌肉正在变得更强壮!🔥',
|
||
dataExtractor: (workout, metrics) => ({
|
||
strengthLevel: metrics?.mets && metrics.mets > 6 ? 'high' : 'moderate'
|
||
})
|
||
},
|
||
traditionalstrengthtraining: {
|
||
emoji: '💪',
|
||
titleTemplate: '力量训练完成!',
|
||
bodyTemplate: ({ durationMinutes, calories, mets }) => {
|
||
let body = `${durationMinutes}分钟力量训练`;
|
||
if (calories > 0) {
|
||
body += `,消耗${calories}千卡`;
|
||
}
|
||
if (mets && mets > 6) {
|
||
body += '(高强度)';
|
||
}
|
||
return body + '!';
|
||
},
|
||
encouragement: '肌肉正在变得更强壮!🔥',
|
||
dataExtractor: (workout, metrics) => ({
|
||
strengthLevel: metrics?.mets && metrics.mets > 6 ? 'high' : 'moderate'
|
||
})
|
||
},
|
||
highintensityintervaltraining: {
|
||
emoji: '🔥',
|
||
titleTemplate: 'HIIT训练完成!',
|
||
bodyTemplate: ({ durationMinutes, calories }) => {
|
||
let body = `${durationMinutes}分钟高强度间歇训练`;
|
||
if (calories > 0) {
|
||
body += `,消耗${calories}千卡`;
|
||
}
|
||
return body + '!';
|
||
},
|
||
encouragement: '心肺功能显著提升!⚡',
|
||
dataExtractor: (workout, metrics) => ({
|
||
hiitCompleted: true
|
||
})
|
||
}
|
||
};
|
||
|
||
function getWorkoutMessage(workoutTypeString?: string): WorkoutMessageConfig | null {
|
||
if (!workoutTypeString) return null;
|
||
|
||
const normalizedType = workoutTypeString.toLowerCase();
|
||
if (normalizedType.includes('strength')) {
|
||
return WORKOUT_MESSAGES.traditionalstrengthtraining;
|
||
}
|
||
|
||
return WORKOUT_MESSAGES[normalizedType] || null;
|
||
}
|
||
|
||
function generateEncouragementMessage(
|
||
workout: WorkoutData,
|
||
metrics: any
|
||
): WorkoutEncouragementMessage {
|
||
if (!workout) {
|
||
return {
|
||
title: '锻炼完成!',
|
||
body: '恭喜您完成锻炼!',
|
||
data: {}
|
||
};
|
||
}
|
||
|
||
const workoutType = getWorkoutTypeDisplayName(workout.workoutActivityTypeString) || '锻炼';
|
||
const durationMinutes = workout.duration ? Math.round(workout.duration / 60) : 0;
|
||
const calories = workout.totalEnergyBurned ? Math.round(workout.totalEnergyBurned) : 0;
|
||
const distanceKm = workout.totalDistance && workout.totalDistance > 0
|
||
? (workout.totalDistance / 1000).toFixed(2)
|
||
: undefined;
|
||
|
||
const messageConfig = getWorkoutMessage(workout.workoutActivityTypeString);
|
||
let title = '锻炼完成!';
|
||
let body = '';
|
||
const data: Record<string, any> = {};
|
||
|
||
if (messageConfig) {
|
||
title = `${messageConfig.emoji} ${messageConfig.titleTemplate}`;
|
||
body = messageConfig.bodyTemplate({
|
||
workoutType,
|
||
durationMinutes,
|
||
calories,
|
||
distanceKm,
|
||
averageHeartRate: metrics?.averageHeartRate,
|
||
mets: metrics?.mets
|
||
});
|
||
body += messageConfig.encouragement;
|
||
|
||
if (messageConfig.dataExtractor) {
|
||
Object.assign(data, messageConfig.dataExtractor(workout, metrics));
|
||
}
|
||
} else {
|
||
body = `${workoutType} ${durationMinutes}分钟完成!`;
|
||
if (calories > 0) {
|
||
body += `消耗${calories}千卡热量。`;
|
||
}
|
||
body += '坚持运动,收获健康!🌟';
|
||
}
|
||
|
||
if (metrics?.heartRateZones && Array.isArray(metrics.heartRateZones) && metrics.heartRateZones.length > 0) {
|
||
try {
|
||
const dominantZone = metrics.heartRateZones.reduce((prev: any, current: any) =>
|
||
current?.durationMinutes > prev?.durationMinutes ? current : prev
|
||
);
|
||
|
||
if (dominantZone?.durationMinutes > 5) {
|
||
data.heartRateZone = dominantZone.key;
|
||
data.heartRateZoneLabel = dominantZone.label;
|
||
}
|
||
} catch (error) {
|
||
console.warn('心率区间分析失败:', error);
|
||
}
|
||
}
|
||
|
||
if (metrics?.mets) {
|
||
data.intensity = metrics.mets < 3 ? 'low' : metrics.mets < 6 ? 'moderate' : 'high';
|
||
}
|
||
|
||
return { title, body, data };
|
||
} |