小组件换行文案更新

This commit is contained in:
吕新雨
2026-02-10 13:35:32 +08:00
29 changed files with 751 additions and 208 deletions

View File

@@ -55,6 +55,7 @@ import { getBootId } from '@/src/utils/bootSession';
import { advanceSuixinState, buildInitialSuixinState, NEUTRAL_THEME_COLORS } from '@/src/features/suixinTheme';
import { wrapText } from '@/src/features/textWrap';
import { defaultMeasureWidthImpl } from '@/src/features/textWrap/measure';
import { ensureDailyWidgetRecoUpToDate } from '@/src/modules/dailyWidgetReco';
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
@@ -265,11 +266,24 @@ export default function HomeScreen() {
maxLines: 3,
overflowMode: 'CLIP',
lineMode: 'AUTO',
configVersion: 'v1',
// Home采用“更偏好语气停顿/更好看”的排版风格微调(不影响算法默认 v1
configVersion: 'v1-home',
debug: __DEV__,
fontSpec,
contextProfile: `APP|${Platform.OS}|home|${lang}`,
measureWidthImpl: defaultMeasureWidthImpl,
scoringOverrides:
lang === 'TC'
? {
// 更偏好在逗号/句号等处断行(即便宽度允许也不一定要塞满)
weights: { R_PUNCT_BREAK: 180 },
// 让“理想行宽”更短,避免宽屏下过度延后断行
idealWidthRatio: { APP: 0.82 },
// 更宽容短行(尤其是第一行在标点处停顿)
minPreferredRatio: 0.45,
shortLastLineRatio: 0.45,
}
: undefined,
});
if (cancelled) return;
@@ -377,6 +391,11 @@ export default function HomeScreen() {
setIndex(0);
fetchNewFeed();
}
// Widget前台辅助刷新尽力而为
// - 写入 App Group 的 dailyReco 缓存
// - 生成 wrapped_text_by_family供 Widget 直接渲染
ensureDailyWidgetRecoUpToDate({ reason: 'home_focus' }).catch(() => {});
})();
return () => {
cancelled = true;
@@ -444,13 +463,41 @@ export default function HomeScreen() {
});
}, [busy, index, currentFeed.length, isFetching, fetchNewFeed, translateY, opacity]);
// 切换到上一条文案的统一动画逻辑(下滑触发)
const triggerPrevContent = useCallback(() => {
if (busy) return;
setBusy(true);
// 1. 当前文案向下移动并消失
translateY.value = withTiming(40, { duration: 300, easing: Easing.out(Easing.quad) });
opacity.value = withTiming(0, { duration: 300 }, (finished) => {
if (finished) {
// 2. 切换数据索引(循环回退)
const nextIndex = index - 1 < 0 ? Math.max(0, currentFeed.length - 1) : index - 1;
runOnJS(setIndex)(nextIndex);
runOnJS(setLikeFilled)(false);
// 3. 准备上一条文案:先瞬移到上方 40pt
translateY.value = -40;
// 4. 上一条文案向下移动到原位并显现
translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) });
opacity.value = withTiming(1, { duration: 400 }, (finished) => {
if (finished) {
runOnJS(setBusy)(false);
}
});
}
});
}, [busy, index, currentFeed.length, translateY, opacity]);
const lastTapRef = useRef<number>(0);
// 使用 Ref 解决 PanResponder 闭包陷阱,确保手势回调能拿到最新的 state 和 function
const handlersRef = useRef({ onPressLike, triggerNextContent });
const handlersRef = useRef({ onPressLike, triggerNextContent, triggerPrevContent });
useEffect(() => {
handlersRef.current = { onPressLike, triggerNextContent };
}, [onPressLike, triggerNextContent]);
handlersRef.current = { onPressLike, triggerNextContent, triggerPrevContent };
}, [onPressLike, triggerNextContent, triggerPrevContent]);
// 使用系统自带的 PanResponder 代替第三方手势库
const panResponder = useRef(
@@ -475,9 +522,11 @@ export default function HomeScreen() {
}
lastTapRef.current = now;
// 2. 上滑逻辑判定
// 2. 上滑/下滑逻辑判定
if (gestureState.dy < -50) { // 上滑超过 50pt
runOnJS(handlersRef.current.triggerNextContent)();
} else if (gestureState.dy > 50) { // 下滑超过 50pt
runOnJS(handlersRef.current.triggerPrevContent)();
}
},
})
@@ -549,13 +598,13 @@ export default function HomeScreen() {
onPress={() => setThemeOpen(true)}
accessibilityLabel={t('home.theme')}
>
<ThemeIcon width={18} height={18} />
<ThemeIcon width={20} height={20} />
</CircleIconButton>
<CircleIconButton
onPress={() => setProfileOpen(true)}
accessibilityLabel={t('home.profile')}
>
<MyIcon width={18} height={18} />
<MyIcon width={20} height={20} />
</CircleIconButton>
</View>
@@ -577,15 +626,16 @@ export default function HomeScreen() {
onPress={onPressLike}
accessibilityRole="button"
accessibilityLabel={t('home.like')}
hitSlop={20}
// 稍微增大可点击区域,提升单手操作成功率
hitSlop={24}
style={styles.reactionInner}
>
{likeFilled ? (
<LikeFilledIcon width={35} height={36} color="#EA6969" />
<LikeFilledIcon width={40} height={41} color="#EA6969" />
) : (
<LikeIcon
width={35}
height={36}
width={40}
height={41}
color={themeMode === 'scenery' ? '#FFFFFF' : '#5E2A28'}
/>
)}
@@ -615,7 +665,8 @@ function CircleIconButton({
return (
<Pressable
onPress={onPress}
hitSlop={10}
// 稍微增大可点击区域,提升易用性
hitSlop={14}
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
style={styles.circleBtn}
@@ -640,9 +691,9 @@ const styles = StyleSheet.create({
zIndex: 30,
},
circleBtn: {
width: 34,
height: 34,
borderRadius: 17,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.75)',
alignItems: 'center',
justifyContent: 'center',

View File

@@ -175,18 +175,17 @@ export default function OnboardingScreen() {
}
};
const onSkip = async () => {
// 跳过整个 Onboarding仍生成一个“全跳过”的最小画像保证下游可用
const scoringProfile = buildUserProfileFromQuestionnaire({});
await setUserProfileScoring(scoringProfile);
// 同步到 App Group供 iOS Widget 使用(失败不阻塞)
await syncWidgetConfig();
await syncWidgetUserProfileFromScoring(scoringProfile);
// 标记已完成,避免下次启动再次进入 Onboarding
await setOnboardingCompleted(true);
router.replace('/(app)/home');
/** 只跳過當前這一步(不填/不選當前題,進入下一步) */
const handleSkipCurrentStep = () => {
if (currentStep.type === 'name') {
onNext();
} else if (currentStep.type === 'selection') {
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
onNext();
} else if (currentStep.type === 'reminder') {
setReminderTimes(0);
onFinish();
}
};
// 题目为多选:点击切换选中状态
@@ -210,9 +209,10 @@ export default function OnboardingScreen() {
title={currentTitle}
currentStep={stepIndex}
totalSteps={STEPS.length - 1}
onSkip={onSkip}
onSkip={handleSkipCurrentStep}
onBack={onBack}
showBackButton={stepIndex > 0}
userName={name}
>
{currentStep.type === 'name' && (
<NameInputStep
@@ -233,7 +233,7 @@ export default function OnboardingScreen() {
{currentStep.type === 'reminder' && (
<ReminderStep
value={reminderTimes}
value={Math.max(1, reminderTimes)}
onChange={setReminderTimes}
onFinish={onFinish}
onSkip={() => {

View File

@@ -8,6 +8,7 @@ import { setConsentAccepted, getConsentAccepted } from '../../src/storage/appSto
import { fetchLegalLinks } from '@/src/services/legalApi';
import { getOnboardingCompleted } from '@/src/storage/appStorage';
import { API_BASE_URL } from '@/src/constants/env';
import { isTraditionalChineseLocaleTag } from '@/src/i18n/locale';
// 导入 SVG 组件
import FlowersBg from '../../assets/images/index/flowers_endbg.svg';
@@ -15,10 +16,30 @@ import WelcomeBtn from '../../assets/images/index/welcome_btn.svg';
const { width, height } = Dimensions.get('window');
// 繁中開屏 consent 文案:寫死在元件內,避免 Metro/iOS bundle 快取導致永遠顯示舊文案。
// 若需修改,請改這裡並同步 client/src/i18n/locales/zh-TW.json 的 consent 區塊。
const ZH_TW_CONSENT = {
title: '我們知道,',
subtitle: '當媽媽很不容易。',
subtitleSecondary: '這裡給你一些溫柔的肯定與提醒',
};
export default function SplashScreen() {
const router = useRouter();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const [showConsent, setShowConsent] = useState(false);
// 繁中時強制使用上方常數(含 zh-TW / zh-Hant / zh-Hant-TW其餘用 i18n
const isZhTW = isTraditionalChineseLocaleTag(i18n.language || '');
const title = isZhTW ? ZH_TW_CONSENT.title : t('consent.title');
const subtitle = isZhTW ? ZH_TW_CONSENT.subtitle : t('consent.subtitle');
const subtitleSecondary = isZhTW ? ZH_TW_CONSENT.subtitleSecondary : t('consent.subtitleSecondary');
useEffect(() => {
if (typeof __DEV__ !== 'undefined' && __DEV__ && showConsent) {
console.log('[i18n consent] language=', i18n.language, 'title=', title, 'subtitle=', subtitle);
}
}, [showConsent, i18n.language, title, subtitle]);
const [links, setLinks] = useState<{ privacy?: string; terms?: string }>({});
const [linksLoading, setLinksLoading] = useState(false);
const mountedRef = useRef(true);
@@ -126,13 +147,16 @@ export default function SplashScreen() {
/>
</View>
{/* 文案内容 */}
{/* 文案内容:主標題兩行 + 可選二級標題(字號更小、顏色更淺);繁中為元件內常數,其餘用 i18n */}
<View style={[styles.contentContainer, { position: 'absolute', top: contentTop }]}>
<Text style={styles.titleText}>
{t('consent.title')}
{title}
{'\n'}
{t('consent.subtitle')}
{subtitle}
</Text>
{subtitleSecondary ? (
<Text style={styles.consentSubtitleSecondary}>{subtitleSecondary}</Text>
) : null}
</View>
<SafeAreaView style={styles.bottomContainer} edges={['bottom']}>
@@ -213,6 +237,14 @@ const styles = StyleSheet.create({
fontWeight: '600',
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
},
consentSubtitleSecondary: {
marginTop: 12,
fontSize: 16,
lineHeight: 22,
color: 'rgba(119, 47, 0, 0.6)',
textAlign: 'center',
fontFamily: Platform.OS === 'ios' ? 'STIX Two Text' : 'serif',
},
bottomContainer: {
position: 'absolute',
bottom: 60,