fix:重复点击
This commit is contained in:
@@ -113,6 +113,18 @@ export default function HomeScreen() {
|
||||
const [cardWidth, setCardWidth] = useState<number | null>(null);
|
||||
const [wrappedText, setWrappedText] = useState<string>('');
|
||||
const wrapLogRef = useRef<{ key: string } | null>(null);
|
||||
const busyRef = useRef(false);
|
||||
const indexRef = useRef(0);
|
||||
const currentFeedRef = useRef<FeedItem[]>([]);
|
||||
const likedIdsRef = useRef<Set<string>>(new Set());
|
||||
const likeInFlightRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
busyRef.current = busy;
|
||||
}, [busy]);
|
||||
useEffect(() => {
|
||||
indexRef.current = index;
|
||||
}, [index]);
|
||||
|
||||
// 解决语言切换时重复触发拉取/清空导致“文案不停跳动”的问题:
|
||||
// 用 ref 持有最新状态,避免 useCallback 依赖 feedItems/isFetching 造成函数 identity 变化 → effect 重复执行
|
||||
@@ -183,6 +195,9 @@ export default function HomeScreen() {
|
||||
text: t(item.textKey)
|
||||
}));
|
||||
}, [feedItems, t]);
|
||||
useEffect(() => {
|
||||
currentFeedRef.current = currentFeed;
|
||||
}, [currentFeed]);
|
||||
|
||||
const item = useMemo(() => {
|
||||
const data = currentFeed[index % currentFeed.length];
|
||||
@@ -430,24 +445,60 @@ export default function HomeScreen() {
|
||||
transform: [{ scale: likeScale.value }],
|
||||
}));
|
||||
|
||||
const setBusySafe = useCallback((next: boolean) => {
|
||||
busyRef.current = next;
|
||||
setBusy(next);
|
||||
}, []);
|
||||
|
||||
const setLikeInFlight = useCallback((next: boolean) => {
|
||||
likeInFlightRef.current = next;
|
||||
}, []);
|
||||
|
||||
const syncLikeFilledByIndex = useCallback((nextIndex: number) => {
|
||||
const list = currentFeedRef.current;
|
||||
const len = list.length;
|
||||
if (!len) {
|
||||
setLikeFilled(false);
|
||||
return;
|
||||
}
|
||||
const safe = ((nextIndex % len) + len) % len;
|
||||
const nextId = String(list[safe]?.content_id);
|
||||
setLikeFilled(likedIdsRef.current.has(nextId));
|
||||
}, []);
|
||||
|
||||
const applyIndexChange = useCallback((nextIndex: number) => {
|
||||
indexRef.current = nextIndex;
|
||||
setIndex(nextIndex);
|
||||
syncLikeFilledByIndex(nextIndex);
|
||||
}, [syncLikeFilledByIndex]);
|
||||
|
||||
const maybeFetchNewFeedIfNeeded = useCallback((nextIndex: number) => {
|
||||
const len = currentFeedRef.current.length;
|
||||
if (!len) return;
|
||||
// 当接近当前列表末尾时(例如还剩 5 条)提前拉取
|
||||
if (nextIndex + 5 >= len && !isFetchingRef.current) {
|
||||
fetchNewFeed();
|
||||
}
|
||||
}, [fetchNewFeed]);
|
||||
|
||||
// 切换到下一条文案的统一动画逻辑
|
||||
const triggerNextContent = useCallback(() => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
if (busyRef.current) return;
|
||||
setBusySafe(true);
|
||||
|
||||
// 注意:不要在 Reanimated worklet 回调里读取 React ref(例如 indexRef/currentFeedRef),会导致值不更新或异常
|
||||
const nextIndex = indexRef.current + 1;
|
||||
|
||||
// 1. 当前文案向上移动并消失
|
||||
translateY.value = withTiming(-40, { duration: 300, easing: Easing.out(Easing.quad) });
|
||||
opacity.value = withTiming(0, { duration: 300 }, (finished) => {
|
||||
if (finished) {
|
||||
// 2. 切换数据索引
|
||||
runOnJS(setIndex)(index + 1);
|
||||
runOnJS(setLikeFilled)(false);
|
||||
runOnJS(applyIndexChange)(nextIndex);
|
||||
runOnJS(advanceSuixinOnNextContent)();
|
||||
|
||||
// 检查是否需要拉取新文案(当接近当前列表末尾时,例如还剩 5 条)
|
||||
if (index + 5 >= currentFeed.length && !isFetching) {
|
||||
runOnJS(fetchNewFeed)();
|
||||
}
|
||||
// 检查是否需要拉取新文案(注意:不要把匿名函数塞进 runOnJS,可能导致原生崩溃)
|
||||
runOnJS(maybeFetchNewFeedIfNeeded)(nextIndex);
|
||||
|
||||
// 3. 准备下一条文案:先瞬移到下方 40pt
|
||||
translateY.value = 40;
|
||||
@@ -456,26 +507,29 @@ export default function HomeScreen() {
|
||||
translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) });
|
||||
opacity.value = withTiming(1, { duration: 400 }, (finished) => {
|
||||
if (finished) {
|
||||
runOnJS(setBusy)(false);
|
||||
runOnJS(setBusySafe)(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [busy, index, currentFeed.length, isFetching, fetchNewFeed, translateY, opacity]);
|
||||
}, [applyIndexChange, setBusySafe, translateY, opacity, advanceSuixinOnNextContent, maybeFetchNewFeedIfNeeded]);
|
||||
|
||||
// 切换到上一条文案的统一动画逻辑(下滑触发)
|
||||
const triggerPrevContent = useCallback(() => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
if (busyRef.current) return;
|
||||
setBusySafe(true);
|
||||
|
||||
// 注意:同上,不要在 worklet 里读取 React ref
|
||||
const len = currentFeedRef.current.length;
|
||||
const raw = indexRef.current - 1;
|
||||
const nextIndex = len ? ((raw % len) + len) % len : Math.max(0, raw);
|
||||
|
||||
// 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);
|
||||
runOnJS(applyIndexChange)(nextIndex);
|
||||
|
||||
// 3. 准备上一条文案:先瞬移到上方 40pt
|
||||
translateY.value = -40;
|
||||
@@ -484,12 +538,12 @@ export default function HomeScreen() {
|
||||
translateY.value = withTiming(0, { duration: 400, easing: Easing.out(Easing.back(1)) });
|
||||
opacity.value = withTiming(1, { duration: 400 }, (finished) => {
|
||||
if (finished) {
|
||||
runOnJS(setBusy)(false);
|
||||
runOnJS(setBusySafe)(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [busy, index, currentFeed.length, translateY, opacity]);
|
||||
}, [applyIndexChange, setBusySafe, translateY, opacity]);
|
||||
|
||||
const lastTapRef = useRef<number>(0);
|
||||
|
||||
@@ -533,7 +587,21 @@ export default function HomeScreen() {
|
||||
).current;
|
||||
|
||||
async function onPressLike() {
|
||||
if (busy) return;
|
||||
if (busyRef.current) return;
|
||||
if (likeInFlightRef.current) return;
|
||||
|
||||
// 已经喜欢过:不重复写入收藏,直接当作“下一条”
|
||||
if (likeFilled || likedIdsRef.current.has(item.id)) {
|
||||
triggerNextContent();
|
||||
return;
|
||||
}
|
||||
|
||||
likeInFlightRef.current = true;
|
||||
|
||||
const likedItemId = item.id;
|
||||
const likedItemText = item.text;
|
||||
// 先记下“已喜欢”,保证回退时能恢复点亮状态(即便异步保存稍后才完成)
|
||||
likedIdsRef.current.add(likedItemId);
|
||||
setLikeFilled(true);
|
||||
|
||||
// 1. 获取当前日期
|
||||
@@ -543,24 +611,33 @@ export default function HomeScreen() {
|
||||
// 2. 保存到收藏夹,包含当前背景信息
|
||||
const favItem = {
|
||||
favId: String(Date.now()), // 生成唯一 ID
|
||||
id: item.id,
|
||||
text: item.text,
|
||||
id: likedItemId,
|
||||
text: likedItemText,
|
||||
date: dateStr,
|
||||
themeMode: themeMode,
|
||||
background: themeMode === 'scenery' ? String(natureImageIndex) : backgroundColor,
|
||||
};
|
||||
console.log('Home: Triggering addFavorite', JSON.stringify(favItem));
|
||||
await addFavorite(favItem);
|
||||
try {
|
||||
await addFavorite(favItem);
|
||||
} catch (error) {
|
||||
console.error('Home: addFavorite 失败', error);
|
||||
}
|
||||
|
||||
// 3. 记录到后端 Reaction(喜欢)
|
||||
console.log('Home: Triggering setReaction', item.id);
|
||||
await setReaction(item.id, 'like');
|
||||
try {
|
||||
await setReaction(item.id, 'like');
|
||||
} catch (error) {
|
||||
console.error('Home: setReaction 失败', error);
|
||||
}
|
||||
|
||||
// 4. 爱心缩放动画
|
||||
likeScale.value = withSequence(
|
||||
withTiming(0.8, { duration: 100 }),
|
||||
withTiming(1.2, { duration: 150 }),
|
||||
withTiming(1, { duration: 100 }, (finished) => {
|
||||
runOnJS(setLikeInFlight)(false);
|
||||
if (finished) {
|
||||
console.log('Home: Like animation finished, triggering next content');
|
||||
runOnJS(triggerNextContent)();
|
||||
|
||||
Reference in New Issue
Block a user