hookehuyr

refactor(collect): 收藏功能改为 API 驱动模式

- 移除乐观更新逻辑,改为 API 成功后更新 UI
- useCollectOperation 返回 { success, newStatus } 对象
- ArticleCard/MaterialCard 使用 async/await 等待 API 响应
- ListItemActions 添加 @tap.stop 防止事件冒泡

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
......@@ -118,7 +118,7 @@ const props = defineProps({
/** 是否显示封面图 */
showCover: {
type: Boolean,
default: true
default: false
}
});
......@@ -172,11 +172,11 @@ const handleView = () => {
/**
* 处理收藏点击
*
* @description 调用收藏操作并通知父组件
* @description 调用收藏操作,成功后通知父组件更新状态
*/
const handleCollect = () => {
// 调用收藏操作
toggleCollect({
const handleCollect = async () => {
// 调用收藏操作 API
const result = await toggleCollect({
id: props.id,
title: props.title,
excerpt: props.excerpt,
......@@ -185,15 +185,18 @@ const handleCollect = () => {
collected: props.collected
});
// 通知父组件收藏状态改变
// API 调用成功后,通知父组件更新本地状态
if (result.success) {
emit('collectChanged', {
id: props.id,
title: props.title,
excerpt: props.excerpt,
coverUrl: props.coverUrl,
date: props.date,
collected: !props.collected // 新状态(取反)
collected: result.newStatus // ← 使用 API 返回的新状态
});
}
// 如果 API 失败,不发出事件,UI 保持原状态
};
</script>
......
......@@ -194,11 +194,11 @@ const handleView = () => {
/**
* 处理收藏点击
*
* @description 内部处理收藏逻辑,调用 useCollectOperation 并通知父组件
* @description 调用收藏操作,成功后通知父组件更新状态
*/
const handleCollect = () => {
// 调用收藏操作
toggleCollect({
const handleCollect = async () => {
// 调用收藏操作 API
const result = await toggleCollect({
id: props.id,
title: props.title,
fileName: props.fileName,
......@@ -210,7 +210,8 @@ const handleCollect = () => {
downloadUrl: props.downloadUrl
})
// 通知父组件收藏状态改变
// API 调用成功后,通知父组件更新本地状态
if (result.success) {
emit('collectChanged', {
id: props.id,
title: props.title,
......@@ -218,10 +219,12 @@ const handleCollect = () => {
fileSize: props.fileSize,
learners: props.learners,
readPeoplePercent: props.readPeoplePercent,
collected: !props.collected, // 新状态(取反)
collected: result.newStatus, // ← 使用 API 返回的新状态
extension: props.extension,
downloadUrl: props.downloadUrl
})
}
// 如果 API 失败,不发出事件,UI 保持原状态
};
</script>
......
......@@ -17,19 +17,19 @@
<template>
<view class="flex justify-end gap-[24rpx]">
<!-- 查看按钮 -->
<view v-if="viewable" class="flex items-center text-blue-600" @tap="handleView">
<view v-if="viewable" class="flex items-center text-blue-600" @tap.stop="handleView">
<IconFont name="eye" size="14" class="mr-[8rpx]" />
<text class="text-[24rpx]">查看</text>
</view>
<!-- 收藏按钮 -->
<view v-if="collectable" class="flex items-center" :class="isCollected ? 'text-red-500' : 'text-gray-400'" @tap="handleCollect">
<view v-if="collectable" class="flex items-center" :class="isCollected ? 'text-red-500' : 'text-gray-400'" @tap.stop="handleCollect">
<IconFont :name="isCollected ? 'heart-fill' : 'heart'" size="14" class="mr-[8rpx]" />
<text class="text-[24rpx]">{{ isCollected ? '已收藏' : '收藏' }}</text>
</view>
<!-- 删除按钮 -->
<view v-if="deletable" class="flex items-center text-red-500" @tap="handleDelete">
<view v-if="deletable" class="flex items-center text-red-500" @tap.stop="handleDelete">
<IconFont name="del" size="14" class="mr-[8rpx]" />
<text class="text-[24rpx]">删除</text>
</view>
......
/**
* 收藏操作 Composable
*
* @description 统一的收藏/取消收藏逻辑,支持乐观更新和错误回滚
* @description 统一的收藏/取消收藏逻辑
* @author Claude Code
* @created 2026-02-05
* @updated 2026-02-27 - 改为事件驱动模式,移除乐观更新
*/
import { addAPI, delAPI } from '../api/favorite.js'
......@@ -13,7 +14,7 @@ import eventBus, { Events } from '@/utils/eventBus'
/**
* 使用收藏操作
*
* @description 提供统一的收藏/取消收藏功能,包含乐观更新和错误处理
* @description 提供统一的收藏/取消收藏功能
* @param {Object} options - 配置选项
* @param {Function} [options.onSuccess] - 成功回调
* @param {Function} [options.onError] - 错误回调
......@@ -22,10 +23,11 @@ import eventBus, { Events } from '@/utils/eventBus'
* @example
* const { toggleCollect } = useCollectOperation()
*
* // 在组件中使用
* const item = ref({ id: 123, collected: false })
*
* await toggleCollect(item, '收藏成功', '已取消收藏')
* const result = await toggleCollect({ id: 123, collected: false })
* if (result.success) {
* // 更新本地状态
* item.collected = result.newStatus
* }
*/
export function useCollectOperation(options = {}) {
const { onSuccess, onError } = options
......@@ -34,20 +36,19 @@ export function useCollectOperation(options = {}) {
* 切换收藏状态
* @description 统一的收藏/取消收藏操作
* @param {Object} item - 资料项(必须包含 collected 和 id/meta_id 字段)
* @param {string} successMsg - 成功提示文案
* @param {string} successMsg - 成功提示文案(可选)
* @param {string} [errorMsg='操作失败'] - 失败提示文案
* @returns {Promise<boolean>} 操作是否成功
* @returns {Promise<Object>} 操作结果 { success: boolean, newStatus: boolean }
*/
const toggleCollect = async (item, successMsg, errorMsg = '操作失败') => {
try {
// 乐观更新 UI
// 计算目标状态(注意:这里不修改原对象)
const newCollectStatus = !item.collected
item.collected = newCollectStatus
// 获取 meta_id(优先使用 meta_id,其次使用 id)
const metaId = item.meta_id || item.id
// 调用 API
try {
// 调用 API(根据当前状态决定是添加还是删除)
const res = newCollectStatus
? await addAPI({ meta_id: metaId }) // 添加收藏
: await delAPI({ meta_id: metaId }) // 取消收藏
......@@ -70,10 +71,10 @@ export function useCollectOperation(options = {}) {
// 调用成功回调
onSuccess?.(item, newCollectStatus)
return true
// 返回成功结果和新状态,由调用方决定如何更新 UI
return { success: true, newStatus: newCollectStatus }
} else {
// API 调用失败,回滚 UI 状态
item.collected = !newCollectStatus
// API 调用失败
Taro.showToast({
title: res.msg || errorMsg,
icon: 'none',
......@@ -83,11 +84,11 @@ export function useCollectOperation(options = {}) {
// 调用错误回调
onError?.(item, res.msg)
return false
// 返回失败结果,状态不变
return { success: false, newStatus: item.collected }
}
} catch (err) {
// 发生错误,回滚 UI 状态
item.collected = !item.collected
// 发生错误
console.error('[useCollectOperation] 收藏操作失败:', err)
Taro.showToast({
title: '网络错误,请重试',
......@@ -98,7 +99,8 @@ export function useCollectOperation(options = {}) {
// 调用错误回调
onError?.(item, err.message)
return false
// 返回失败结果,状态不变
return { success: false, newStatus: item.collected }
}
}
......