diff --git a/client/app.json b/client/app.json index 00ddbaf..6c4afd0 100644 --- a/client/app.json +++ b/client/app.json @@ -1,11 +1,11 @@ { "expo": { - "name": "Hey Mama", - "slug": "hey-mama", + "name": "client", + "slug": "client", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", - "scheme": "heymama", + "scheme": "client", "userInterfaceStyle": "automatic", "newArchEnabled": true, "splash": { @@ -15,7 +15,7 @@ }, "ios": { "supportsTablet": true, - "bundleIdentifier": "com.heymama.app" + "bundleIdentifier": "com.anonymous.client" }, "android": { "adaptiveIcon": { diff --git a/client/app/(app)/home.tsx b/client/app/(app)/home.tsx index 10c1fb8..9699192 100644 --- a/client/app/(app)/home.tsx +++ b/client/app/(app)/home.tsx @@ -1,5 +1,5 @@ import { useEffect, useLayoutEffect, useMemo, useState, useCallback, useRef } from 'react'; -import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated } from 'react-native'; +import { StyleSheet, View, Dimensions, Text, Pressable, PanResponder, Animated as RNAnimated, ImageBackground } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useNavigation, useFocusEffect } from 'expo-router'; import Animated, { @@ -39,6 +39,40 @@ import LikeIcon from '@/assets/images/icon/like_icon.svg'; const { height: SCREEN_HEIGHT } = Dimensions.get('window'); +// 预定义风景图列表 +const NATURE_IMAGES = [ + require('@/assets/theme/nature/1.png'), + require('@/assets/theme/nature/2.png'), + require('@/assets/theme/nature/3.png'), + require('@/assets/theme/nature/4.png'), + require('@/assets/theme/nature/5.png'), + require('@/assets/theme/nature/6.png'), + require('@/assets/theme/nature/7.png'), + require('@/assets/theme/nature/8.png'), + require('@/assets/theme/nature/9.png'), + require('@/assets/theme/nature/10.png'), + require('@/assets/theme/nature/11.png'), + require('@/assets/theme/nature/12.png'), + require('@/assets/theme/nature/13.png'), + require('@/assets/theme/nature/14.png'), + require('@/assets/theme/nature/15.png'), + require('@/assets/theme/nature/17.png'), + require('@/assets/theme/nature/18.png'), + require('@/assets/theme/nature/19.png'), + require('@/assets/theme/nature/20.png'), + require('@/assets/theme/nature/22.png'), +]; + +// 预定义颜色列表 +const THEME_COLORS = [ + '#F7D9BF', + '#CBF2D8', + '#F5CDDE', + '#F2ECCB', + '#E2CBF2', + '#CBD9F2', +]; + export default function HomeScreen() { const { t } = useTranslation(); const navigation = useNavigation(); @@ -126,12 +160,26 @@ export default function HomeScreen() { }; }, []); - const backgroundColor = themeMode === 'color' ? '#F3D0E1' : '#F4D6C2'; + const backgroundColor = useMemo(() => { + if (themeMode === 'color') { + const colorIndex = Math.floor(index / 10) % THEME_COLORS.length; + return THEME_COLORS[colorIndex]; + } + return '#F4D6C2'; // 风景模式下的默认底色(图片加载前显示) + }, [themeMode, index]); + + // 计算当前应该显示的风景图索引(滑动 10 次切换一张) + const natureImageIndex = useMemo(() => { + return Math.floor(index / 10) % NATURE_IMAGES.length; + }, [index]); + + const currentNatureImage = NATURE_IMAGES[natureImageIndex]; useLayoutEffect(() => { navigation.setOptions({ headerShadowVisible: false, - headerStyle: { backgroundColor }, + headerStyle: { backgroundColor: themeMode === 'scenery' ? 'transparent' : backgroundColor }, + headerTransparent: themeMode === 'scenery', headerRight: () => ( ), }); - }, [backgroundColor, navigation, t]); + }, [backgroundColor, themeMode, navigation, t]); const textAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translateY.value }], @@ -241,10 +289,11 @@ export default function HomeScreen() { // 2. 保存到收藏夹,包含当前背景信息 await addFavorite({ - id: typeof currentContentId === 'number' ? String(currentContentId) : (item as any).id, + favId: String(Date.now()), // 生成唯一 ID + id: item.id, date: dateStr, themeMode: themeMode, - background: backgroundColor, // 目前存储的是颜色值 + background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor, }); // 3. 爱心缩放动画 @@ -267,8 +316,15 @@ export default function HomeScreen() { return ( - - {item.text} + {themeMode === 'scenery' && ( + + )} + + {item.text} @@ -281,9 +337,13 @@ export default function HomeScreen() { style={styles.reactionInner} > {likeFilled ? ( - + ) : ( - + )} @@ -342,9 +402,15 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, card: { - paddingHorizontal: 30, - alignItems: 'center', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 30, + zIndex: 5, // 降低层级,防止遮挡底部按钮 }, text: { fontSize: 22, @@ -353,6 +419,16 @@ const styles = StyleSheet.create({ fontWeight: '700', textAlign: 'center', }, + sceneryCard: { + // 风景模式下稍微收窄文案宽度,增加呼吸感 + paddingHorizontal: 50, + }, + sceneryText: { + color: '#FFFFFF', + textShadowColor: 'rgba(0, 0, 0, 0.5)', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 4, + }, actions: { position: 'absolute', bottom: SCREEN_HEIGHT * 0.16, @@ -360,6 +436,7 @@ const styles = StyleSheet.create({ right: 0, flexDirection: 'row', justifyContent: 'center', + zIndex: 20, // 提升层级,确保在最顶层可点击 }, reactionButton: { alignItems: 'center', diff --git a/client/app/(onboarding)/onboarding.tsx b/client/app/(onboarding)/onboarding.tsx index 5104f72..42bbfe8 100644 --- a/client/app/(onboarding)/onboarding.tsx +++ b/client/app/(onboarding)/onboarding.tsx @@ -140,14 +140,13 @@ export default function OnboardingScreen() { } }; - const onSkip = () => { + const onSkip = async () => { // 跳过整个 Onboarding:仍生成一个“全跳过”的最小画像,保证下游可用 const scoringProfile = buildUserProfileFromQuestionnaire({}); - void setUserProfileScoring(scoringProfile); + await setUserProfileScoring(scoringProfile); // 标记已完成,避免下次启动再次进入 Onboarding - void setOnboardingCompleted(true); - + await setOnboardingCompleted(true); router.replace('/(app)/home'); }; diff --git a/client/app/index.tsx b/client/app/index.tsx index 45f2198..869780e 100644 --- a/client/app/index.tsx +++ b/client/app/index.tsx @@ -1,9 +1,8 @@ import { useEffect } from 'react'; import { ActivityIndicator, StyleSheet, View } from 'react-native'; import { useRouter } from 'expo-router'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { getOnboardingCompleted, getConsentAccepted, setOnboardingCompleted, setConsentAccepted } from '@/src/storage/appStorage'; +import { getOnboardingCompleted, getConsentAccepted } from '@/src/storage/appStorage'; /** * 启动分发:根据 consent 和 onboarding 状态跳转 @@ -14,9 +13,6 @@ export default function Index() { useEffect(() => { let cancelled = false; (async () => { - // 注意:不要在启动时无条件清空存储,否则 Onboarding/画像等数据无法持久化。 - // 如需调试重置,请在开发期手动清空或自行加调试开关。 - // 1. 检查是否同意协议 const consentAccepted = await getConsentAccepted(); if (cancelled) return; @@ -26,9 +22,17 @@ export default function Index() { return; } - // 2. 检查 Onboarding + // 2. 检查 Onboarding 是否已完成 const completed = await getOnboardingCompleted(); - router.replace(completed ? '/(app)/home' : '/(onboarding)/onboarding'); + if (cancelled) return; + + if (completed) { + // 如果已经完成过流程,直接进 Home + router.replace('/(app)/home'); + } else { + // 如果是首次进入(或未完成流程),进入 Onboarding + router.replace('/(onboarding)/onboarding'); + } })(); return () => { cancelled = true; @@ -45,4 +49,3 @@ export default function Index() { const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, }); - diff --git a/client/assets/theme/nature/1.png b/client/assets/theme/nature/1.png new file mode 100644 index 0000000..65e98a2 Binary files /dev/null and b/client/assets/theme/nature/1.png differ diff --git a/client/assets/theme/nature/10.png b/client/assets/theme/nature/10.png new file mode 100644 index 0000000..a460796 Binary files /dev/null and b/client/assets/theme/nature/10.png differ diff --git a/client/assets/theme/nature/11.png b/client/assets/theme/nature/11.png new file mode 100644 index 0000000..1a66767 Binary files /dev/null and b/client/assets/theme/nature/11.png differ diff --git a/client/assets/theme/nature/12.png b/client/assets/theme/nature/12.png new file mode 100644 index 0000000..f32f373 Binary files /dev/null and b/client/assets/theme/nature/12.png differ diff --git a/client/assets/theme/nature/13.png b/client/assets/theme/nature/13.png new file mode 100644 index 0000000..b4613cf Binary files /dev/null and b/client/assets/theme/nature/13.png differ diff --git a/client/assets/theme/nature/14.png b/client/assets/theme/nature/14.png new file mode 100644 index 0000000..a8e1de6 Binary files /dev/null and b/client/assets/theme/nature/14.png differ diff --git a/client/assets/theme/nature/15.png b/client/assets/theme/nature/15.png new file mode 100644 index 0000000..d84c28f Binary files /dev/null and b/client/assets/theme/nature/15.png differ diff --git a/client/assets/theme/nature/17.png b/client/assets/theme/nature/17.png new file mode 100644 index 0000000..86c003f Binary files /dev/null and b/client/assets/theme/nature/17.png differ diff --git a/client/assets/theme/nature/18.png b/client/assets/theme/nature/18.png new file mode 100644 index 0000000..e945639 Binary files /dev/null and b/client/assets/theme/nature/18.png differ diff --git a/client/assets/theme/nature/19.png b/client/assets/theme/nature/19.png new file mode 100644 index 0000000..a3bda43 Binary files /dev/null and b/client/assets/theme/nature/19.png differ diff --git a/client/assets/theme/nature/2.png b/client/assets/theme/nature/2.png new file mode 100644 index 0000000..21bac6a Binary files /dev/null and b/client/assets/theme/nature/2.png differ diff --git a/client/assets/theme/nature/20.png b/client/assets/theme/nature/20.png new file mode 100644 index 0000000..59be818 Binary files /dev/null and b/client/assets/theme/nature/20.png differ diff --git a/client/assets/theme/nature/22.png b/client/assets/theme/nature/22.png new file mode 100644 index 0000000..7e9c2a9 Binary files /dev/null and b/client/assets/theme/nature/22.png differ diff --git a/client/assets/theme/nature/3.png b/client/assets/theme/nature/3.png new file mode 100644 index 0000000..4a1f8ab Binary files /dev/null and b/client/assets/theme/nature/3.png differ diff --git a/client/assets/theme/nature/4.png b/client/assets/theme/nature/4.png new file mode 100644 index 0000000..9837d09 Binary files /dev/null and b/client/assets/theme/nature/4.png differ diff --git a/client/assets/theme/nature/5.png b/client/assets/theme/nature/5.png new file mode 100644 index 0000000..f7d8cb1 Binary files /dev/null and b/client/assets/theme/nature/5.png differ diff --git a/client/assets/theme/nature/6.png b/client/assets/theme/nature/6.png new file mode 100644 index 0000000..457e4a3 Binary files /dev/null and b/client/assets/theme/nature/6.png differ diff --git a/client/assets/theme/nature/7.png b/client/assets/theme/nature/7.png new file mode 100644 index 0000000..ed2cd1d Binary files /dev/null and b/client/assets/theme/nature/7.png differ diff --git a/client/assets/theme/nature/8.png b/client/assets/theme/nature/8.png new file mode 100644 index 0000000..8e6e7b9 Binary files /dev/null and b/client/assets/theme/nature/8.png differ diff --git a/client/assets/theme/nature/9.png b/client/assets/theme/nature/9.png new file mode 100644 index 0000000..9d47eb0 Binary files /dev/null and b/client/assets/theme/nature/9.png differ diff --git a/client/components/home/.ProfileModal.tsx.swp b/client/components/home/.ProfileModal.tsx.swp deleted file mode 100644 index 9e07e56..0000000 Binary files a/client/components/home/.ProfileModal.tsx.swp and /dev/null differ diff --git a/client/components/home/ProfileModal.tsx b/client/components/home/ProfileModal.tsx index e657e3f..9381112 100644 --- a/client/components/home/ProfileModal.tsx +++ b/client/components/home/ProfileModal.tsx @@ -50,6 +50,29 @@ type Props = { type Page = 'root' | 'favorites' | 'dailyReminder' | 'widget' | 'language' | 'widgetHowTo'; type NavDirection = 'forward' | 'back'; +const NATURE_IMAGES = [ + require('@/assets/theme/nature/1.png'), + require('@/assets/theme/nature/2.png'), + require('@/assets/theme/nature/3.png'), + require('@/assets/theme/nature/4.png'), + require('@/assets/theme/nature/5.png'), + require('@/assets/theme/nature/6.png'), + require('@/assets/theme/nature/7.png'), + require('@/assets/theme/nature/8.png'), + require('@/assets/theme/nature/9.png'), + require('@/assets/theme/nature/10.png'), + require('@/assets/theme/nature/11.png'), + require('@/assets/theme/nature/12.png'), + require('@/assets/theme/nature/13.png'), + require('@/assets/theme/nature/14.png'), + require('@/assets/theme/nature/15.png'), + require('@/assets/theme/nature/17.png'), + require('@/assets/theme/nature/18.png'), + require('@/assets/theme/nature/19.png'), + require('@/assets/theme/nature/20.png'), + require('@/assets/theme/nature/22.png'), +]; + export default function ProfileModal({ visible, name: propName, onClose }: Props) { const { t } = useTranslation(); @@ -260,11 +283,11 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) { setFavorites(list); } - async function handleRemove(id: string) { + async function handleRemove(favId: string) { // 1. 调用存储层移除收藏 - await removeFavorite(id); + await removeFavorite(favId); // 2. 更新本地状态 - setFavorites(prev => prev.filter(item => item.id !== id)); + setFavorites(prev => prev.filter(item => item.favId !== favId)); } return ( @@ -274,7 +297,7 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) { ) : ( it.id} + keyExtractor={(it) => it.favId} contentContainerStyle={styles.favList} showsVerticalScrollIndicator={false} renderItem={({ item }) => ( @@ -290,11 +313,30 @@ function FavoritesPage({ visible, page }: { visible: boolean; page: Page }) { - {item.text} + {item.themeMode === 'scenery' ? ( + + + + ) : null} + + {item.text} + handleRemove(item.id)} + onPress={() => handleRemove(item.favId)} style={styles.favRemoveBtn} hitSlop={10} > @@ -765,6 +807,7 @@ const styles = StyleSheet.create({ position: 'relative', borderWidth: 1, borderColor: 'rgba(119, 47, 0, 0.05)', + overflow: 'hidden', }, favThumbText: { fontSize: 15, diff --git a/client/components/ui/.SheetModal.tsx.swp b/client/components/ui/.SheetModal.tsx.swp deleted file mode 100644 index e0c938d..0000000 Binary files a/client/components/ui/.SheetModal.tsx.swp and /dev/null differ diff --git a/client/ios/情绪小组件/EmotionWidget.swift b/client/ios/情绪小组件/EmotionWidget.swift index 510cc41..3376cfa 100644 --- a/client/ios/情绪小组件/EmotionWidget.swift +++ b/client/ios/情绪小组件/EmotionWidget.swift @@ -1,54 +1,28 @@ import WidgetKit import SwiftUI -// V2:纯色背景 + 随机文案小组件(Small/Medium/Large + 点击跳转 Home) +// V1:写死文案的小组件(Small/Medium/Large + 点击跳转 Home) struct EmotionProvider: TimelineProvider { - private let quotes = [ - "你已经很努力了,今天也值得被温柔对待。", - "轻轻呼吸,感受当下的每一刻。", - "所有的压力,都会在深呼吸中慢慢消散。", - "给生活一点留白,给自己一点温柔。", - "不要走得太快,等一等落下的灵魂。", - "世界虽嘈杂,但你可以拥有一颗宁静的心。", - "每一个瞬间,都是生命最好的安排。", - "抱抱自己,辛苦了,亲爱的。", - "慢一点也没关系,只要你在前行。", - "今天,你对自己微笑了吗?", - "愿你历经山河,仍觉得人间值得。", - "心简单,世界就简单;心平顺,生活就平顺。", - "即使生活偶尔晦暗,你也要成为自己的光。", - "别让琐事挤走生活的快乐,别让压力消磨奋斗的激情。" - ] - func placeholder(in context: Context) -> EmotionEntry { - EmotionEntry(date: Date(), text: quotes[0]) + EmotionEntry(date: Date()) } func getSnapshot(in context: Context, completion: @escaping (EmotionEntry) -> ()) { - let entry = EmotionEntry(date: Date(), text: quotes.randomElement() ?? quotes[0]) - completion(entry) + completion(EmotionEntry(date: Date())) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - var entries: [EmotionEntry] = [] - let currentDate = Date() - - // 生成未来 24 小时的 6 个条目,每 4 小时更换一次随机文案 - for hourOffset in 0..<6 { - let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset * 4, to: currentDate)! - let entry = EmotionEntry(date: entryDate, text: quotes.randomElement() ?? quotes[0]) - entries.append(entry) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) + // V1:内容写死,不做数据更新;给一个较长的刷新间隔(系统仍可能自行调度) + let entry = EmotionEntry(date: Date()) + let nextUpdate = Calendar.current.date(byAdding: .day, value: 7, to: Date()) + ?? Date().addingTimeInterval(60 * 60 * 24 * 7) + completion(Timeline(entries: [entry], policy: .after(nextUpdate))) } } struct EmotionEntry: TimelineEntry { let date: Date - let text: String } struct EmotionWidgetView: View { @@ -56,37 +30,160 @@ struct EmotionWidgetView: View { @Environment(\.widgetFamily) var family private let title = "正念" + private let text = "你已经很努力了,今天也值得被温柔对待。" private let deepLink = URL(string: "client:///(app)/home") - - // 背景色 #F7D9BF - private let backgroundColor = Color(red: 247/255, green: 217/255, blue: 191/255) - // 文本颜色(深咖色,适合搭配浅橘色背景) - private let textColor = Color(red: 74/255, green: 52/255, blue: 40/255) var body: some View { - VStack(alignment: .center, spacing: 0) { - Spacer(minLength: 0) - - Text(entry.text) - .font(.system(size: family == .systemSmall ? 17 : 20, weight: .medium)) - .foregroundColor(textColor) - .lineSpacing(6) - .multilineTextAlignment(.center) - .minimumScaleFactor(0.7) - .fixedSize(horizontal: false, vertical: true) - - Spacer(minLength: 0) - - if family != .systemSmall { - Text("Hey Mama") - .font(.system(size: 10, weight: .semibold)) - .foregroundColor(textColor.opacity(0.3)) - .padding(.bottom, 4) - } + switch family { + case .systemSmall: + smallView() + case .systemMedium: + mediumView() + case .systemLarge: + largeView() + default: + smallView() + } + } + + // 统一的“卡片背景”风格(iOS 15 兼容) + private func cardBackground(colors: [Color]) -> some View { + ZStack { + LinearGradient( + colors: colors, + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + // 轻微光斑,增加层次 + RadialGradient( + gradient: Gradient(colors: [Color.white.opacity(0.16), Color.white.opacity(0.0)]), + center: .topTrailing, + startRadius: 10, + endRadius: 180 + ) + } + .overlay( + RoundedRectangle(cornerRadius: 18, style: .continuous) + .stroke(Color.white.opacity(0.14), lineWidth: 1) + ) + .cornerRadius(18) + } + + private func chip(_ text: String) -> some View { + Text(text) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(Color.white.opacity(0.9)) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(Color.white.opacity(0.14)) + .cornerRadius(999) + } + + private func smallView() -> some View { + ZStack { + cardBackground(colors: [ + Color(red: 0.06, green: 0.08, blue: 0.12), + Color(red: 0.13, green: 0.16, blue: 0.22), + ]) + + VStack(alignment: .leading, spacing: 10) { + HStack { + chip(title) + Spacer(minLength: 0) + } + + Text(text) + .font(.system(size: 15, weight: .semibold)) + .foregroundColor(Color.white.opacity(0.92)) + .lineSpacing(2) + .lineLimit(4) + + Spacer(minLength: 0) + + Text("点我回到 App") + .font(.system(size: 11, weight: .medium)) + .foregroundColor(Color.white.opacity(0.65)) + } + .padding(14) + } + .widgetURL(deepLink) + } + + private func mediumView() -> some View { + ZStack { + cardBackground(colors: [ + Color(red: 0.06, green: 0.08, blue: 0.12), + Color(red: 0.09, green: 0.11, blue: 0.17), + ]) + + HStack(alignment: .top, spacing: 14) { + VStack(alignment: .leading, spacing: 10) { + chip(title) + Text(text) + .font(.system(size: 17, weight: .semibold)) + .foregroundColor(Color.white.opacity(0.92)) + .lineSpacing(3) + .lineLimit(5) + + Spacer(minLength: 0) + + Text("轻轻呼吸,回到当下") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(Color.white.opacity(0.7)) + } + + // 右侧装饰区:让版面更饱满 + VStack(alignment: .trailing, spacing: 8) { + Text(entry.date, style: .time) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(Color.white.opacity(0.8)) + Spacer(minLength: 0) + Text("今日") + .font(.system(size: 28, weight: .bold)) + .foregroundColor(Color.white.opacity(0.12)) + } + } + .padding(16) + } + .widgetURL(deepLink) + } + + private func largeView() -> some View { + ZStack { + cardBackground(colors: [ + Color(red: 0.06, green: 0.08, blue: 0.12), + Color(red: 0.14, green: 0.18, blue: 0.28), + ]) + + VStack(alignment: .leading, spacing: 14) { + HStack { + chip(title) + Spacer(minLength: 0) + Text(entry.date, style: .time) + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(Color.white.opacity(0.78)) + } + + Text(text) + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(Color.white.opacity(0.92)) + .lineSpacing(4) + .lineLimit(8) + + Spacer(minLength: 0) + + HStack { + Text("点我回到 Home") + .font(.system(size: 12, weight: .medium)) + .foregroundColor(Color.white.opacity(0.7)) + Spacer(minLength: 0) + Text("🌿") + .font(.system(size: 18)) + .opacity(0.9) + } + } + .padding(18) } - .padding(family == .systemSmall ? 16 : 24) - .frame(maxWidth: .infinity, maxHeight: .infinity) // 强制撑开容器 - .background(backgroundColor) // 将背景色直接应用到容器上 .widgetURL(deepLink) } } @@ -97,13 +194,7 @@ struct EmotionWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: EmotionProvider()) { entry in - if #available(iOS 17.0, *) { - EmotionWidgetView(entry: entry) - .containerBackground(Color(red: 247/255, green: 217/255, blue: 191/255), for: .widget) - } else { - EmotionWidgetView(entry: entry) - .background(Color(red: 247/255, green: 217/255, blue: 191/255)) - } + EmotionWidgetView(entry: entry) } .configurationDisplayName("情绪小组件") .description("一段温柔提醒,陪你回到当下。") diff --git a/client/src/storage/appStorage.ts b/client/src/storage/appStorage.ts index c30c995..a148837 100644 --- a/client/src/storage/appStorage.ts +++ b/client/src/storage/appStorage.ts @@ -105,6 +105,7 @@ export async function setReaction(contentId: string, reaction: Reaction): Promis } export type FavoriteItem = { + favId: string; // 唯一标识,支持重复点赞同一文案 id: string; date: string; themeMode: ThemeMode; @@ -117,15 +118,15 @@ export async function getFavorites(): Promise { export async function addFavorite(item: FavoriteItem): Promise { const list = await getFavorites(); - if (list.some(i => i.id === item.id)) return; + // 允许重复点赞,不再根据 id 去重 const newList = [item, ...list]; console.log('Adding to favorites, new list size:', newList.length); await setJson(KEY_FAVORITES_ITEMS, newList); } -export async function removeFavorite(contentId: string): Promise { +export async function removeFavorite(favId: string): Promise { const list = await getFavorites(); - const next = list.filter(item => item.id !== contentId); + const next = list.filter(item => item.favId !== favId); await setJson(KEY_FAVORITES_ITEMS, next); }