feat: 增强通知功能及用户体验
- 在 Bootstrapper 组件中新增通知服务初始化逻辑,注册每日午餐提醒 - 在 CoachScreen 中优化欢迎消息生成逻辑,整合用户配置文件数据 - 更新 GoalsScreen 组件,优化目标创建时的通知设置逻辑 - 在 NotificationTest 组件中添加调试通知状态功能,提升开发便利性 - 新增 NutritionNotificationHelpers 中的午餐提醒功能,支持每日提醒设置 - 更新相关文档,详细描述新功能和使用方法
This commit is contained in:
@@ -32,6 +32,8 @@ import { useCosUpload } from '@/hooks/useCosUpload';
|
|||||||
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
import { deleteConversation, getConversationDetail, listConversations, type AiConversationListItem } from '@/services/aiCoach';
|
||||||
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||||
import { api, getAuthToken, postTextStream } from '@/services/api';
|
import { api, getAuthToken, postTextStream } from '@/services/api';
|
||||||
|
import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
|
import { generateWelcomeMessage, hasRecordedMoodToday } from '@/utils/welcomeMessage';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { ActionSheet } from '../../components/ui/ActionSheet';
|
import { ActionSheet } from '../../components/ui/ActionSheet';
|
||||||
@@ -170,109 +172,18 @@ export default function CoachScreen() {
|
|||||||
const userProfile = useAppSelector((s) => s.user?.profile);
|
const userProfile = useAppSelector((s) => s.user?.profile);
|
||||||
const { upload } = useCosUpload();
|
const { upload } = useCosUpload();
|
||||||
|
|
||||||
// 生成个性化欢迎消息
|
// 获取今日心情记录
|
||||||
const generateWelcomeMessage = useCallback(() => {
|
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||||
const hour = new Date().getHours();
|
const todayMoodRecord = useAppSelector(selectLatestMoodRecordByDate(today));
|
||||||
const name = userProfile?.name || '朋友';
|
const hasRecordedMoodTodayValue = hasRecordedMoodToday(todayMoodRecord?.checkinDate);
|
||||||
const botName = (params?.name || 'Seal').toString();
|
|
||||||
|
|
||||||
// 时段问候
|
// 转换用户配置文件数据类型以匹配欢迎消息函数的需求
|
||||||
let timeGreeting = '';
|
const transformedUserProfile = userProfile ? {
|
||||||
if (hour >= 5 && hour < 9) {
|
name: userProfile.name,
|
||||||
timeGreeting = '早上好';
|
weight: userProfile.weight ? Number(userProfile.weight) : undefined,
|
||||||
} else if (hour >= 9 && hour < 12) {
|
height: userProfile.height ? Number(userProfile.height) : undefined,
|
||||||
timeGreeting = '上午好';
|
pilatesPurposes: userProfile.pilatesPurposes
|
||||||
} else if (hour >= 12 && hour < 14) {
|
} : undefined;
|
||||||
timeGreeting = '中午好';
|
|
||||||
} else if (hour >= 14 && hour < 18) {
|
|
||||||
timeGreeting = '下午好';
|
|
||||||
} else if (hour >= 18 && hour < 22) {
|
|
||||||
timeGreeting = '晚上好';
|
|
||||||
} else {
|
|
||||||
timeGreeting = '夜深了';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 欢迎消息模板
|
|
||||||
const welcomeMessages = [
|
|
||||||
{
|
|
||||||
condition: () => hour >= 5 && hour < 9,
|
|
||||||
messages: [
|
|
||||||
`${timeGreeting},${name}!🐳 我是你的小海豹${botName},新的一天开始啦!让我们一起游向健康的目标吧~`,
|
|
||||||
`${timeGreeting}!🌅 早晨的阳光真好呢,我是${botName},你的专属小海豹健康伙伴!要不要先制定今天的健康计划呢?`,
|
|
||||||
`${timeGreeting},${name}!🐋 小海豹${botName}来报到啦!今天想从哪个方面开始我们的健康之旅呢?营养、运动还是生活管理,我都可以帮你哦~`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: () => hour >= 9 && hour < 12,
|
|
||||||
messages: [
|
|
||||||
`${timeGreeting},${name}!🐳 上午是身体最活跃的时候呢,小海豹${botName}在这里为你加油!有什么健康目标需要我帮你规划吗?`,
|
|
||||||
`${timeGreeting}!☀️ 工作忙碌的上午,别忘了给身体一些关爱哦~我是你的小海豹${botName},随时准备为你提供营养建议和运动指导!`,
|
|
||||||
`${timeGreeting},${name}!🐋 作为你的小海豹伙伴${botName},我想说:每一个健康的选择都在让我们的身体更棒呢!今天想从哪个方面开始呢?`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: () => hour >= 12 && hour < 14,
|
|
||||||
messages: [
|
|
||||||
`${timeGreeting},${name}!🍽️ 午餐时间到啦!小海豹${botName}提醒你,合理的营养搭配能让下午充满能量哦~`,
|
|
||||||
`${timeGreeting}!🌊 忙碌的上午结束了,该给身体补充能量啦!我是你的小海豹${botName},无论是饮食调整还是运动安排,都可以找我商量哦~`,
|
|
||||||
`${timeGreeting},${name}!🐳 午间时光,小海豹${botName}建议你关注饮食均衡,也要适度放松一下呢~`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: () => hour >= 14 && hour < 18,
|
|
||||||
messages: [
|
|
||||||
`${timeGreeting},${name}!🌊 下午是运动的黄金时段呢!小海豹${botName}可以为你制定个性化的健身计划,让我们一起游向更好的身材吧~`,
|
|
||||||
`${timeGreeting}!🐋 午后时光,正是关注健康的好时机!我是你的小海豹${botName},从营养到运动,我都能为你提供贴心指导哦~`,
|
|
||||||
`${timeGreeting},${name}!🐳 下午时光,身心健康同样重要呢!作为你的小海豹${botName},我在这里支持你的每一个健康目标~`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: () => hour >= 18 && hour < 22,
|
|
||||||
messages: [
|
|
||||||
`${timeGreeting},${name}!🌙 忙碌了一天,现在是放松身心的好时候呢!小海豹${botName}可以为你提供放松建议和恢复方案哦~`,
|
|
||||||
`${timeGreeting}!🌊 夜幕降临,这是一天中最适合总结的时刻!我是你的小海豹${botName},让我们一起回顾今天的健康表现,规划明天的目标吧~`,
|
|
||||||
`${timeGreeting},${name}!🐳 晚间时光属于你自己,也是关爱身体的珍贵时间!作为你的小海豹${botName},我想陪你聊聊如何更好地管理健康生活呢~`
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: () => hour >= 22 || hour < 5,
|
|
||||||
messages: [
|
|
||||||
`${timeGreeting},${name}!🌙 优质睡眠是健康的基石呢!小海豹${botName}提醒你,如果需要睡眠优化建议,随时可以问我哦~`,
|
|
||||||
`夜深了,${name}。🌊 充足的睡眠对身体恢复很重要呢!我是你的小海豹${botName},有什么关于睡眠健康的问题都可以咨询我~`,
|
|
||||||
`夜深了,愿你能拥有甜甜的睡眠。🐳 我是你的小海豹${botName},明天我们继续在健康管理的海洋里同行。晚安,${name}!`
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 特殊情况的消息
|
|
||||||
const specialMessages = [
|
|
||||||
{
|
|
||||||
condition: () => !userProfile?.weight && !userProfile?.height,
|
|
||||||
message: `你好,${name}!🐳 我是你的小海豹${botName}!我注意到你还没有完善健康档案呢,不如先聊聊你的健康目标和身体状况,这样小海豹就能为你制定更贴心的健康方案啦~`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: () => userProfile && (!userProfile.pilatesPurposes || userProfile.pilatesPurposes.length === 0),
|
|
||||||
message: `${timeGreeting},${name}!🐋 作为你的小海豹${botName},我想更好地了解你的健康需求呢!告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 检查特殊情况
|
|
||||||
for (const special of specialMessages) {
|
|
||||||
if (special.condition()) {
|
|
||||||
return special.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据时间选择合适的消息组
|
|
||||||
const timeGroup = welcomeMessages.find(group => group.condition());
|
|
||||||
if (timeGroup) {
|
|
||||||
const messages = timeGroup.messages;
|
|
||||||
return messages[Math.floor(Math.random() * messages.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认消息
|
|
||||||
return `你好,我是你的小海豹${botName}!🐳 可以向我咨询营养摄入、身材管理、健身锻炼、生活管理等各方面的健康问题哦~`;
|
|
||||||
}, [userProfile, params?.name]);
|
|
||||||
|
|
||||||
const chips = useMemo(() => [
|
const chips = useMemo(() => [
|
||||||
// { key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
|
// { key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
|
||||||
@@ -317,15 +228,35 @@ export default function CoachScreen() {
|
|||||||
const timer = setTimeout(scrollToEnd, 100);
|
const timer = setTimeout(scrollToEnd, 100);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
// 使用 ref 存储最新值以避免依赖项导致的死循环
|
||||||
|
const latestValuesRef = useRef({
|
||||||
|
transformedUserProfile,
|
||||||
|
paramsName: params?.name || 'Seal',
|
||||||
|
hasRecordedMoodTodayValue
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新 ref 的值
|
||||||
|
useEffect(() => {
|
||||||
|
latestValuesRef.current = {
|
||||||
|
transformedUserProfile,
|
||||||
|
paramsName: params?.name || 'Seal',
|
||||||
|
hasRecordedMoodTodayValue
|
||||||
|
};
|
||||||
|
}, [transformedUserProfile, params?.name, hasRecordedMoodTodayValue]);
|
||||||
|
|
||||||
// 初始化欢迎消息
|
// 初始化欢迎消息
|
||||||
const initializeWelcomeMessage = useCallback(() => {
|
const initializeWelcomeMessage = useCallback(() => {
|
||||||
|
const { transformedUserProfile, paramsName, hasRecordedMoodTodayValue } = latestValuesRef.current;
|
||||||
const welcomeMessage: ChatMessage = {
|
const welcomeMessage: ChatMessage = {
|
||||||
id: 'm_welcome',
|
id: 'm_welcome',
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: generateWelcomeMessage(),
|
content: generateWelcomeMessage({
|
||||||
|
userProfile: transformedUserProfile,
|
||||||
|
hasRecordedMoodToday: hasRecordedMoodTodayValue
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
setMessages([welcomeMessage]);
|
setMessages([welcomeMessage]);
|
||||||
}, [generateWelcomeMessage]);
|
}, []); // 空依赖项,通过 ref 获取最新值
|
||||||
|
|
||||||
// 启动页面时尝试恢复当次应用会话缓存
|
// 启动页面时尝试恢复当次应用会话缓存
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -417,7 +348,7 @@ export default function CoachScreen() {
|
|||||||
showSub = Keyboard.addListener('keyboardWillChangeFrame', (e: any) => {
|
showSub = Keyboard.addListener('keyboardWillChangeFrame', (e: any) => {
|
||||||
try {
|
try {
|
||||||
if (e?.endCoordinates?.height) {
|
if (e?.endCoordinates?.height) {
|
||||||
const height = Math.max(0, e.endCoordinates.height);
|
const height = Math.max(0, e.endCoordinates.height - 100);
|
||||||
setKeyboardOffset(height);
|
setKeyboardOffset(height);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -570,7 +501,14 @@ export default function CoachScreen() {
|
|||||||
attachments: undefined,
|
attachments: undefined,
|
||||||
}));
|
}));
|
||||||
setConversationId(detail.conversationId);
|
setConversationId(detail.conversationId);
|
||||||
setMessages(mapped.length ? mapped : [{ id: 'm_welcome', role: 'assistant', content: generateWelcomeMessage() }]);
|
setMessages(mapped.length ? mapped : [{
|
||||||
|
id: 'm_welcome',
|
||||||
|
role: 'assistant',
|
||||||
|
content: generateWelcomeMessage({
|
||||||
|
userProfile: transformedUserProfile,
|
||||||
|
hasRecordedMoodToday: hasRecordedMoodTodayValue
|
||||||
|
})
|
||||||
|
}]);
|
||||||
setHistoryVisible(false);
|
setHistoryVisible(false);
|
||||||
setTimeout(scrollToEnd, 0);
|
setTimeout(scrollToEnd, 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -587,7 +525,14 @@ export default function CoachScreen() {
|
|||||||
await deleteConversation(id);
|
await deleteConversation(id);
|
||||||
if (conversationId === id) {
|
if (conversationId === id) {
|
||||||
setConversationId(undefined);
|
setConversationId(undefined);
|
||||||
setMessages([{ id: 'm_welcome', role: 'assistant', content: generateWelcomeMessage() }]);
|
setMessages([{
|
||||||
|
id: 'm_welcome',
|
||||||
|
role: 'assistant',
|
||||||
|
content: generateWelcomeMessage({
|
||||||
|
userProfile: transformedUserProfile,
|
||||||
|
hasRecordedMoodToday: hasRecordedMoodTodayValue
|
||||||
|
})
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
await refreshHistory(historyPage);
|
await refreshHistory(historyPage);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1856,7 +1801,7 @@ export default function CoachScreen() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={styles.headerLeft}>
|
<View style={styles.headerLeft}>
|
||||||
<Text style={[styles.headerTitle, { color: theme.text }]}>{botName}</Text>
|
<Text style={[styles.headerTitle, { color: theme.text }]}>海豹管家</Text>
|
||||||
|
|
||||||
{/* 使用次数显示 */}
|
{/* 使用次数显示 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|||||||
@@ -118,10 +118,6 @@ export default function GoalsScreen() {
|
|||||||
|
|
||||||
// 处理错误提示
|
// 处理错误提示
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('tasksError', tasksError);
|
|
||||||
console.log('createError', createError);
|
|
||||||
console.log('completeError', completeError);
|
|
||||||
console.log('skipError', skipError);
|
|
||||||
|
|
||||||
if (tasksError) {
|
if (tasksError) {
|
||||||
Alert.alert('错误', tasksError);
|
Alert.alert('错误', tasksError);
|
||||||
@@ -154,10 +150,11 @@ export default function GoalsScreen() {
|
|||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
|
|
||||||
// 获取用户名
|
// 获取用户名
|
||||||
const userName = userProfile?.name || '小海豹';
|
const userName = userProfile?.name || '主人';
|
||||||
|
|
||||||
// 创建目标成功后,设置定时推送
|
// 创建目标成功后,设置定时推送
|
||||||
try {
|
try {
|
||||||
|
if (goalData.hasReminder) {
|
||||||
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
|
const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications(
|
||||||
{
|
{
|
||||||
title: goalData.title,
|
title: goalData.title,
|
||||||
@@ -170,8 +167,9 @@ export default function GoalsScreen() {
|
|||||||
},
|
},
|
||||||
userName
|
userName
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds);
|
console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (notificationError) {
|
} catch (notificationError) {
|
||||||
console.error('创建目标定时推送失败:', notificationError);
|
console.error('创建目标定时推送失败:', notificationError);
|
||||||
// 通知创建失败不影响目标创建的成功
|
// 通知创建失败不影响目标创建的成功
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import 'react-native-reanimated';
|
|||||||
|
|
||||||
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
||||||
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
|
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||||
import { backgroundTaskManager } from '@/services/backgroundTaskManager';
|
import { backgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||||
|
import { notificationService } from '@/services/notifications';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
|
import { NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RNExitApp from 'react-native-exit-app';
|
import RNExitApp from 'react-native-exit-app';
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ import { Provider } from 'react-redux';
|
|||||||
|
|
||||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { privacyAgreed } = useAppSelector((state) => state.user);
|
const { privacyAgreed, profile } = useAppSelector((state) => state.user);
|
||||||
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
|
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
|
||||||
const [userDataLoaded, setUserDataLoaded] = React.useState(false);
|
const [userDataLoaded, setUserDataLoaded] = React.useState(false);
|
||||||
|
|
||||||
@@ -34,15 +35,30 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
const initializeBackgroundTasks = async () => {
|
const initializeBackgroundTasks = async () => {
|
||||||
try {
|
try {
|
||||||
await backgroundTaskManager.initialize();
|
await backgroundTaskManager.initialize();
|
||||||
|
|
||||||
console.log('后台任务管理器初始化成功');
|
console.log('后台任务管理器初始化成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('后台任务管理器初始化失败:', error);
|
console.error('后台任务管理器初始化失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initializeNotifications = async () => {
|
||||||
|
try {
|
||||||
|
// 初始化通知服务
|
||||||
|
await notificationService.initialize();
|
||||||
|
|
||||||
|
// 只有在用户数据加载完成后且用户名存在时才注册午餐提醒
|
||||||
|
if (userDataLoaded && profile?.name) {
|
||||||
|
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name);
|
||||||
|
console.log('通知服务初始化成功,午餐提醒已注册');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('通知服务初始化失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
loadUserData();
|
loadUserData();
|
||||||
initializeBackgroundTasks();
|
initializeBackgroundTasks();
|
||||||
|
initializeNotifications();
|
||||||
// 冷启动时清空 AI 教练会话缓存
|
// 冷启动时清空 AI 教练会话缓存
|
||||||
clearAiCoachSessionCache();
|
clearAiCoachSessionCache();
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@@ -54,6 +70,23 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [userDataLoaded, privacyAgreed]);
|
}, [userDataLoaded, privacyAgreed]);
|
||||||
|
|
||||||
|
// 当用户数据加载完成且用户名存在时,注册午餐提醒
|
||||||
|
React.useEffect(() => {
|
||||||
|
const registerLunchReminder = async () => {
|
||||||
|
if (userDataLoaded && profile?.name) {
|
||||||
|
try {
|
||||||
|
await notificationService.initialize();
|
||||||
|
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name);
|
||||||
|
console.log('午餐提醒已注册');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册午餐提醒失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerLunchReminder();
|
||||||
|
}, [userDataLoaded, profile?.name]);
|
||||||
|
|
||||||
const handlePrivacyAgree = () => {
|
const handlePrivacyAgree = () => {
|
||||||
dispatch(setPrivacyAgreed());
|
dispatch(setPrivacyAgreed());
|
||||||
setShowPrivacyModal(false);
|
setShowPrivacyModal(false);
|
||||||
@@ -76,7 +109,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const colorScheme = useColorScheme();
|
|
||||||
const [loaded] = useFonts({
|
const [loaded] = useFonts({
|
||||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const NotificationTest: React.FC = () => {
|
|||||||
sendWorkoutReminder,
|
sendWorkoutReminder,
|
||||||
sendGoalAchievement,
|
sendGoalAchievement,
|
||||||
sendMoodCheckinReminder,
|
sendMoodCheckinReminder,
|
||||||
|
debugNotificationStatus,
|
||||||
} = useNotifications();
|
} = useNotifications();
|
||||||
|
|
||||||
const [scheduledNotifications, setScheduledNotifications] = useState<any[]>([]);
|
const [scheduledNotifications, setScheduledNotifications] = useState<any[]>([]);
|
||||||
@@ -132,6 +133,15 @@ export const NotificationTest: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDebugNotificationStatus = async () => {
|
||||||
|
try {
|
||||||
|
await debugNotificationStatus();
|
||||||
|
Alert.alert('调试完成', '请查看控制台输出');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '调试失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
<ScrollView showsVerticalScrollIndicator={false}>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
@@ -203,6 +213,13 @@ export const NotificationTest: React.FC = () => {
|
|||||||
<ThemedText style={styles.buttonText}>获取已安排通知</ThemedText>
|
<ThemedText style={styles.buttonText}>获取已安排通知</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.debugButton]}
|
||||||
|
onPress={handleDebugNotificationStatus}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>调试通知状态</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.dangerButton]}
|
style={[styles.button, styles.dangerButton]}
|
||||||
onPress={handleCancelAllNotifications}
|
onPress={handleCancelAllNotifications}
|
||||||
@@ -267,6 +284,9 @@ const styles = StyleSheet.create({
|
|||||||
dangerButton: {
|
dangerButton: {
|
||||||
backgroundColor: '#FF3B30',
|
backgroundColor: '#FF3B30',
|
||||||
},
|
},
|
||||||
|
debugButton: {
|
||||||
|
backgroundColor: '#FF9500',
|
||||||
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface UseNotificationsReturn {
|
|||||||
sendWorkoutReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
sendWorkoutReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
||||||
sendGoalAchievement: (title: string, body: string) => Promise<string>;
|
sendGoalAchievement: (title: string, body: string) => Promise<string>;
|
||||||
sendMoodCheckinReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
sendMoodCheckinReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
||||||
|
debugNotificationStatus: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotifications = (): UseNotificationsReturn => {
|
export const useNotifications = (): UseNotificationsReturn => {
|
||||||
@@ -134,6 +135,10 @@ export const useNotifications = (): UseNotificationsReturn => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const debugNotificationStatus = useCallback(async () => {
|
||||||
|
return notificationService.debugNotificationStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 组件挂载时自动初始化
|
// 组件挂载时自动初始化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialize();
|
initialize();
|
||||||
@@ -153,5 +158,6 @@ export const useNotifications = (): UseNotificationsReturn => {
|
|||||||
sendWorkoutReminder,
|
sendWorkoutReminder,
|
||||||
sendGoalAchievement,
|
sendGoalAchievement,
|
||||||
sendMoodCheckinReminder,
|
sendMoodCheckinReminder,
|
||||||
|
debugNotificationStatus,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { Platform } from 'react-native';
|
|
||||||
|
|
||||||
// 配置通知处理方式
|
// 配置通知处理方式
|
||||||
Notifications.setNotificationHandler({
|
Notifications.setNotificationHandler({
|
||||||
@@ -15,17 +14,50 @@ Notifications.setNotificationHandler({
|
|||||||
export interface NotificationData {
|
export interface NotificationData {
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
data?: Record<string, any>;
|
data?: Record<string, unknown>;
|
||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
priority?: 'default' | 'normal' | 'high';
|
priority?: 'default' | 'normal' | 'high';
|
||||||
vibrate?: number[];
|
vibrate?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DateTrigger {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.DATE;
|
||||||
|
date: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RepeatingTrigger {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL;
|
||||||
|
seconds: number;
|
||||||
|
repeats?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DailyTrigger {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.DAILY;
|
||||||
|
hour: number;
|
||||||
|
minute: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WeeklyTrigger {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.WEEKLY;
|
||||||
|
hour: number;
|
||||||
|
minute: number;
|
||||||
|
weekday: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonthlyTrigger {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.MONTHLY;
|
||||||
|
hour: number;
|
||||||
|
minute: number;
|
||||||
|
day: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalendarTrigger = DailyTrigger | WeeklyTrigger | MonthlyTrigger;
|
||||||
|
|
||||||
export class NotificationService {
|
export class NotificationService {
|
||||||
private static instance: NotificationService;
|
private static instance: NotificationService;
|
||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() { }
|
||||||
|
|
||||||
public static getInstance(): NotificationService {
|
public static getInstance(): NotificationService {
|
||||||
if (!NotificationService.instance) {
|
if (!NotificationService.instance) {
|
||||||
@@ -56,16 +88,20 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取推送令牌(用于远程推送,本地推送不需要)
|
// 获取推送令牌(用于远程推送,本地推送不需要)
|
||||||
if (Platform.OS !== 'web') {
|
// if (Platform.OS !== 'web') {
|
||||||
const token = await Notifications.getExpoPushTokenAsync({
|
// const token = await Notifications.getExpoPushTokenAsync({
|
||||||
projectId: 'your-project-id', // 需要替换为实际的Expo项目ID
|
// projectId: 'your-project-id', // 需要替换为实际的Expo项目ID
|
||||||
});
|
// });
|
||||||
console.log('推送令牌:', token.data);
|
// console.log('推送令牌:', token.data);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 设置通知监听器
|
// 设置通知监听器
|
||||||
this.setupNotificationListeners();
|
this.setupNotificationListeners();
|
||||||
|
|
||||||
|
// 检查已存在的通知
|
||||||
|
const existingNotifications = await this.getAllScheduledNotifications();
|
||||||
|
console.log('已存在的通知数量:', existingNotifications.length);
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
console.log('推送通知服务初始化成功');
|
console.log('推送通知服务初始化成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -111,6 +147,11 @@ export class NotificationService {
|
|||||||
// 处理目标提醒通知
|
// 处理目标提醒通知
|
||||||
console.log('用户点击了目标提醒通知', data);
|
console.log('用户点击了目标提醒通知', data);
|
||||||
// 这里可以添加导航到目标页面的逻辑
|
// 这里可以添加导航到目标页面的逻辑
|
||||||
|
} else if (data?.type === 'lunch_reminder') {
|
||||||
|
// 处理午餐提醒通知
|
||||||
|
console.log('用户点击了午餐提醒通知', data);
|
||||||
|
// 这里可以添加导航到午餐记录页面的逻辑
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +208,9 @@ export class NotificationService {
|
|||||||
vibrate: notification.vibrate,
|
vibrate: notification.vibrate,
|
||||||
},
|
},
|
||||||
trigger: {
|
trigger: {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.DATE,
|
||||||
date: date.getTime(),
|
date: date.getTime(),
|
||||||
} as any,
|
} as DateTrigger,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('定时通知已安排,ID:', notificationId);
|
console.log('定时通知已安排,ID:', notificationId);
|
||||||
@@ -219,9 +261,10 @@ export class NotificationService {
|
|||||||
vibrate: notification.vibrate,
|
vibrate: notification.vibrate,
|
||||||
},
|
},
|
||||||
trigger: {
|
trigger: {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
|
||||||
seconds: totalSeconds,
|
seconds: totalSeconds,
|
||||||
repeats: true,
|
repeats: true,
|
||||||
} as any,
|
} as RepeatingTrigger,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('重复通知已安排,ID:', notificationId);
|
console.log('重复通知已安排,ID:', notificationId);
|
||||||
@@ -238,7 +281,7 @@ export class NotificationService {
|
|||||||
async scheduleCalendarRepeatingNotification(
|
async scheduleCalendarRepeatingNotification(
|
||||||
notification: NotificationData,
|
notification: NotificationData,
|
||||||
options: {
|
options: {
|
||||||
type: 'daily' | 'weekly' | 'monthly';
|
type: Notifications.SchedulableTriggerInputTypes;
|
||||||
hour: number;
|
hour: number;
|
||||||
minute: number;
|
minute: number;
|
||||||
weekdays?: number[]; // 0-6,0为周日,仅用于weekly类型
|
weekdays?: number[]; // 0-6,0为周日,仅用于weekly类型
|
||||||
@@ -246,38 +289,38 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
let trigger: any;
|
let trigger: CalendarTrigger;
|
||||||
|
|
||||||
switch (options.type) {
|
switch (options.type) {
|
||||||
case 'daily':
|
case Notifications.SchedulableTriggerInputTypes.DAILY:
|
||||||
trigger = {
|
trigger = {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
||||||
hour: options.hour,
|
hour: options.hour,
|
||||||
minute: options.minute,
|
minute: options.minute,
|
||||||
repeats: true,
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'weekly':
|
case Notifications.SchedulableTriggerInputTypes.WEEKLY:
|
||||||
if (options.weekdays && options.weekdays.length > 0) {
|
if (options.weekdays && options.weekdays.length > 0) {
|
||||||
trigger = {
|
trigger = {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
|
||||||
hour: options.hour,
|
hour: options.hour,
|
||||||
minute: options.minute,
|
minute: options.minute,
|
||||||
weekday: options.weekdays[0], // Expo只支持单个weekday
|
weekday: options.weekdays[0], // Expo只支持单个weekday
|
||||||
repeats: true,
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
trigger = {
|
trigger = {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
||||||
hour: options.hour,
|
hour: options.hour,
|
||||||
minute: options.minute,
|
minute: options.minute,
|
||||||
repeats: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'monthly':
|
case Notifications.SchedulableTriggerInputTypes.MONTHLY:
|
||||||
trigger = {
|
trigger = {
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
|
||||||
hour: options.hour,
|
hour: options.hour,
|
||||||
minute: options.minute,
|
minute: options.minute,
|
||||||
day: options.dayOfMonth || 1,
|
day: options.dayOfMonth || 1,
|
||||||
repeats: true,
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -336,6 +379,7 @@ export class NotificationService {
|
|||||||
async getAllScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
|
async getAllScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
|
||||||
try {
|
try {
|
||||||
const notifications = await Notifications.getAllScheduledNotificationsAsync();
|
const notifications = await Notifications.getAllScheduledNotificationsAsync();
|
||||||
|
|
||||||
return notifications;
|
return notifications;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取已安排通知失败:', error);
|
console.error('获取已安排通知失败:', error);
|
||||||
@@ -343,6 +387,7 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取通知权限状态
|
* 获取通知权限状态
|
||||||
*/
|
*/
|
||||||
@@ -380,6 +425,7 @@ export const NotificationTypes = {
|
|||||||
MOOD_CHECKIN: 'mood_checkin',
|
MOOD_CHECKIN: 'mood_checkin',
|
||||||
NUTRITION_REMINDER: 'nutrition_reminder',
|
NUTRITION_REMINDER: 'nutrition_reminder',
|
||||||
PROGRESS_UPDATE: 'progress_update',
|
PROGRESS_UPDATE: 'progress_update',
|
||||||
|
LUNCH_REMINDER: 'lunch_reminder',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 便捷方法
|
// 便捷方法
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
import { NotificationData, notificationService } from '../services/notifications';
|
import { NotificationData, notificationService } from '../services/notifications';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,7 +160,7 @@ export class GoalNotificationHelpers {
|
|||||||
const dailyId = await notificationService.scheduleCalendarRepeatingNotification(
|
const dailyId = await notificationService.scheduleCalendarRepeatingNotification(
|
||||||
notification,
|
notification,
|
||||||
{
|
{
|
||||||
type: 'daily',
|
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
||||||
hour: hours,
|
hour: hours,
|
||||||
minute: minutes,
|
minute: minutes,
|
||||||
}
|
}
|
||||||
@@ -175,7 +176,7 @@ export class GoalNotificationHelpers {
|
|||||||
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
|
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
|
||||||
notification,
|
notification,
|
||||||
{
|
{
|
||||||
type: 'weekly',
|
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
|
||||||
hour: hours,
|
hour: hours,
|
||||||
minute: minutes,
|
minute: minutes,
|
||||||
weekdays: [weekday],
|
weekdays: [weekday],
|
||||||
@@ -189,7 +190,7 @@ export class GoalNotificationHelpers {
|
|||||||
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
|
const weeklyId = await notificationService.scheduleCalendarRepeatingNotification(
|
||||||
notification,
|
notification,
|
||||||
{
|
{
|
||||||
type: 'weekly',
|
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
|
||||||
hour: hours,
|
hour: hours,
|
||||||
minute: minutes,
|
minute: minutes,
|
||||||
}
|
}
|
||||||
@@ -206,7 +207,7 @@ export class GoalNotificationHelpers {
|
|||||||
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
|
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
|
||||||
notification,
|
notification,
|
||||||
{
|
{
|
||||||
type: 'monthly',
|
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
|
||||||
hour: hours,
|
hour: hours,
|
||||||
minute: minutes,
|
minute: minutes,
|
||||||
dayOfMonth: dayOfMonth,
|
dayOfMonth: dayOfMonth,
|
||||||
@@ -220,7 +221,7 @@ export class GoalNotificationHelpers {
|
|||||||
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
|
const monthlyId = await notificationService.scheduleCalendarRepeatingNotification(
|
||||||
notification,
|
notification,
|
||||||
{
|
{
|
||||||
type: 'monthly',
|
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
|
||||||
hour: hours,
|
hour: hours,
|
||||||
minute: minutes,
|
minute: minutes,
|
||||||
dayOfMonth: 1,
|
dayOfMonth: 1,
|
||||||
@@ -323,6 +324,96 @@ export class NutritionNotificationHelpers {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排每日午餐提醒
|
||||||
|
* @param userName 用户名
|
||||||
|
* @param hour 小时 (默认12点)
|
||||||
|
* @param minute 分钟 (默认0分)
|
||||||
|
* @returns 通知ID
|
||||||
|
*/
|
||||||
|
static async scheduleDailyLunchReminder(
|
||||||
|
userName: string,
|
||||||
|
hour: number = 12,
|
||||||
|
minute: number = 0
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
// 检查是否已经存在午餐提醒
|
||||||
|
const existingNotifications = await notificationService.getAllScheduledNotifications();
|
||||||
|
|
||||||
|
|
||||||
|
console.log('existingNotifications', existingNotifications);
|
||||||
|
const existingLunchReminder = existingNotifications.find(
|
||||||
|
notification =>
|
||||||
|
notification.content.data?.type === 'lunch_reminder' &&
|
||||||
|
notification.content.data?.isDailyReminder === true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingLunchReminder) {
|
||||||
|
console.log('午餐提醒已存在,跳过重复注册:', existingLunchReminder.identifier);
|
||||||
|
return existingLunchReminder.identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建午餐提醒通知
|
||||||
|
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: '午餐记录提醒',
|
||||||
|
body: `${userName},记得记录今天的午餐情况哦!`,
|
||||||
|
data: {
|
||||||
|
type: 'lunch_reminder',
|
||||||
|
isDailyReminder: true,
|
||||||
|
meal: '午餐'
|
||||||
|
},
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
||||||
|
hour: hour,
|
||||||
|
minute: minute,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('每日午餐提醒已安排,ID:', notificationId);
|
||||||
|
return notificationId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('安排每日午餐提醒失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送午餐记录提醒
|
||||||
|
*/
|
||||||
|
static async sendLunchReminder(userName: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '午餐记录提醒',
|
||||||
|
body: `${userName},记得记录今天的午餐情况哦!`,
|
||||||
|
data: { type: 'lunch_reminder', meal: '午餐' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消午餐提醒
|
||||||
|
*/
|
||||||
|
static async cancelLunchReminder(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const notifications = await notificationService.getAllScheduledNotifications();
|
||||||
|
|
||||||
|
for (const notification of notifications) {
|
||||||
|
if (notification.content.data?.type === 'lunch_reminder' &&
|
||||||
|
notification.content.data?.isDailyReminder === true) {
|
||||||
|
await notificationService.cancelNotification(notification.identifier);
|
||||||
|
console.log('已取消午餐提醒:', notification.identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消午餐提醒失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安排营养记录提醒
|
* 安排营养记录提醒
|
||||||
*/
|
*/
|
||||||
@@ -491,5 +582,12 @@ export const NotificationTemplates = {
|
|||||||
sound: true,
|
sound: true,
|
||||||
priority: 'normal' as const,
|
priority: 'normal' as const,
|
||||||
}),
|
}),
|
||||||
|
lunch: (userName: string) => ({
|
||||||
|
title: '午餐记录提醒',
|
||||||
|
body: `${userName},记得记录今天的午餐情况哦!`,
|
||||||
|
data: { type: 'lunch_reminder', meal: '午餐' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal' as const,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
142
utils/welcomeMessage.ts
Normal file
142
utils/welcomeMessage.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
// 用户配置文件类型
|
||||||
|
type UserProfile = {
|
||||||
|
name?: string;
|
||||||
|
weight?: number;
|
||||||
|
height?: number;
|
||||||
|
pilatesPurposes?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 参数类型
|
||||||
|
type GenerateWelcomeMessageParams = {
|
||||||
|
userProfile?: UserProfile;
|
||||||
|
hasRecordedMoodToday?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成个性化欢迎消息
|
||||||
|
* @param params 参数对象,包含用户配置和今日是否已记录心情
|
||||||
|
* @returns 个性化欢迎消息字符串
|
||||||
|
*/
|
||||||
|
export function generateWelcomeMessage(params: GenerateWelcomeMessageParams): string {
|
||||||
|
const { userProfile, hasRecordedMoodToday = false } = params;
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
const name = userProfile?.name || '朋友';
|
||||||
|
|
||||||
|
// 时段问候
|
||||||
|
let timeGreeting = '';
|
||||||
|
if (hour >= 5 && hour < 9) {
|
||||||
|
timeGreeting = '早上好';
|
||||||
|
} else if (hour >= 9 && hour < 12) {
|
||||||
|
timeGreeting = '上午好';
|
||||||
|
} else if (hour >= 12 && hour < 14) {
|
||||||
|
timeGreeting = '中午好';
|
||||||
|
} else if (hour >= 14 && hour < 18) {
|
||||||
|
timeGreeting = '下午好';
|
||||||
|
} else if (hour >= 18 && hour < 22) {
|
||||||
|
timeGreeting = '晚上好';
|
||||||
|
} else {
|
||||||
|
timeGreeting = '夜深了';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 欢迎消息模板
|
||||||
|
const welcomeMessages = [
|
||||||
|
{
|
||||||
|
condition: () => hour >= 5 && hour < 9,
|
||||||
|
messages: [
|
||||||
|
`${timeGreeting},${name}!🐳 我是你的小海豹,新的一天开始啦!让我们一起游向健康的目标吧~`,
|
||||||
|
`${timeGreeting}!🌅 早晨的阳光真好呢,我是你的专属小海豹健康伙伴!要不要先制定今天的健康计划呢?`,
|
||||||
|
`${timeGreeting},${name}!🐋 小海豹来报到啦!今天想从哪个方面开始我们的健康之旅呢?营养、运动还是生活管理,我都可以帮你哦~`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: () => hour >= 9 && hour < 12,
|
||||||
|
messages: [
|
||||||
|
`${timeGreeting},${name}!🐳 上午是身体最活跃的时候呢,小海豹在这里为你加油!有什么健康目标需要我帮你规划吗?`,
|
||||||
|
`${timeGreeting}!☀️ 工作忙碌的上午,别忘了给身体一些关爱哦~我是你的小海豹,随时准备为你提供营养建议和运动指导!`,
|
||||||
|
`${timeGreeting},${name}!🐋 作为你的小海豹伙伴,我想说:每一个健康的选择都在让我们的身体更棒呢!今天想从哪个方面开始呢?`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: () => hour >= 12 && hour < 14,
|
||||||
|
messages: [
|
||||||
|
`${timeGreeting},${name}!🍽️ 午餐时间到啦!小海豹提醒你,合理的营养搭配能让下午充满能量哦~`,
|
||||||
|
`${timeGreeting}!🌊 忙碌的上午结束了,该给身体补充能量啦!我是你的小海豹,无论是饮食调整还是运动安排,都可以找我商量哦~`,
|
||||||
|
`${timeGreeting},${name}!🐳 午间时光,小海豹建议你关注饮食均衡,也要适度放松一下呢~`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: () => hour >= 14 && hour < 18,
|
||||||
|
messages: [
|
||||||
|
`${timeGreeting},${name}!🌊 下午是运动的黄金时段呢!小海豹可以为你制定个性化的健身计划,让我们一起游向更好的身材吧~`,
|
||||||
|
`${timeGreeting}!🐋 午后时光,正是关注健康的好时机!我是你的小海豹,从营养到运动,我都能为你提供贴心指导哦~`,
|
||||||
|
`${timeGreeting},${name}!🐳 下午时光,身心健康同样重要呢!作为你的小海豹,我在这里支持你的每一个健康目标~`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: () => hour >= 18 && hour < 22,
|
||||||
|
messages: [
|
||||||
|
`${timeGreeting},${name}!🌙 忙碌了一天,现在是放松身心的好时候呢!小海豹可以为你提供放松建议和恢复方案哦~`,
|
||||||
|
`${timeGreeting}!🌊 夜幕降临,这是一天中最适合总结的时刻!我是你的小海豹,让我们一起回顾今天的健康表现,规划明天的目标吧~`,
|
||||||
|
`${timeGreeting},${name}!🐳 晚间时光属于你自己,也是关爱身体的珍贵时间!作为你的小海豹,我想陪你聊聊如何更好地管理健康生活呢~`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: () => hour >= 22 || hour < 5,
|
||||||
|
messages: [
|
||||||
|
`${timeGreeting},${name}!🌙 优质睡眠是健康的基石呢!小海豹提醒你,如果需要睡眠优化建议,随时可以问我哦~`,
|
||||||
|
`夜深了,${name}。🌊 充足的睡眠对身体恢复很重要呢!我是你的小海豹,有什么关于睡眠健康的问题都可以咨询我~`,
|
||||||
|
`夜深了,愿你能拥有甜甜的睡眠。🐳 我是你的小海豹,明天我们继续在健康管理的海洋里同行。晚安,${name}!`
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 特殊情况的消息
|
||||||
|
const specialMessages = [
|
||||||
|
{
|
||||||
|
condition: () => !userProfile?.weight && !userProfile?.height,
|
||||||
|
message: `你好,${name}!🐳 我是你的小海豹!我注意到你还没有完善健康档案呢,不如先聊聊你的健康目标和身体状况,这样小海豹就能为你制定更贴心的健康方案啦~`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
condition: () => userProfile && (!userProfile.pilatesPurposes || userProfile.pilatesPurposes.length === 0),
|
||||||
|
message: `${timeGreeting},${name}!🐋 作为你的小海豹,我想更好地了解你的健康需求呢!告诉我你希望在营养摄入、身材管理、健身锻炼或生活管理方面实现什么目标吧~`
|
||||||
|
},
|
||||||
|
// 新增:晚上20点后且未记录心情的特殊提示
|
||||||
|
{
|
||||||
|
condition: () => hour >= 20 && !hasRecordedMoodToday,
|
||||||
|
message: `${timeGreeting},${name}!🌙 夜深了,小海豹注意到你今天还没有记录心情呢。记录心情有助于更好地了解自己的情绪变化,对健康管理很有帮助哦~要不要现在记录一下今天的心情?`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 检查特殊情况
|
||||||
|
for (const special of specialMessages) {
|
||||||
|
if (special.condition()) {
|
||||||
|
return special.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据时间选择合适的消息组
|
||||||
|
const timeGroup = welcomeMessages.find(group => group.condition());
|
||||||
|
if (timeGroup) {
|
||||||
|
const messages = timeGroup.messages;
|
||||||
|
return messages[Math.floor(Math.random() * messages.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认消息
|
||||||
|
return `你好,我是你的小海豹!🐳 可以向我咨询营养摄入、身材管理、健身锻炼、生活管理等各方面的健康问题哦~`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户今天是否已经记录了心情
|
||||||
|
* @param lastMoodDate 上次记录心情的日期
|
||||||
|
* @returns 今天是否已记录心情
|
||||||
|
*/
|
||||||
|
export function hasRecordedMoodToday(lastMoodDate?: string | Date): boolean {
|
||||||
|
if (!lastMoodDate) return false;
|
||||||
|
|
||||||
|
const today = dayjs().format('YYYY-MM-DD');
|
||||||
|
const lastMoodDay = dayjs(lastMoodDate).format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
return today === lastMoodDay;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user