feat(product-detail): 实现商品详情页的收藏/取消收藏功能,包括:

1. 引入收藏相关API和图标组件
2. 添加收藏状态管理和加载状态
3. 实现收藏状态拉取和切换逻辑
4. 新增收藏按钮样式和交互
5. 处理未登录跳转登录的逻辑
This commit is contained in:
sparksfly 2026-08-04 16:06:40 +08:00
parent 54875823e9
commit f8f326c889
2 changed files with 2762 additions and 1 deletions

2682
db/snack_mall.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,10 +10,11 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { ShoppingCart, Plus, Minus } from '@element-plus/icons-vue' import { ShoppingCart, Plus, Minus, Star, StarFilled } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/modules/user' import { useUserStore } from '@/stores/modules/user'
import { getProductDetailApi } from '@/api/product' import { getProductDetailApi } from '@/api/product'
import { addToCartApi } from '@/api/cart' import { addToCartApi } from '@/api/cart'
import { addFavoriteApi, cancelFavoriteByProductIdApi, checkFavoriteApi } from '@/api/favorite'
import type { ProductDetail, ProductSku } from '@/types/product' import type { ProductDetail, ProductSku } from '@/types/product'
import AppHeader from '@/components/layout/AppHeader.vue' import AppHeader from '@/components/layout/AppHeader.vue'
import AppFooter from '@/components/layout/AppFooter.vue' import AppFooter from '@/components/layout/AppFooter.vue'
@ -31,6 +32,10 @@ const selectedSku = ref<ProductSku | null>(null)
const quantity = ref(1) const quantity = ref(1)
const addingToCart = ref(false) const addingToCart = ref(false)
// ==================== ====================
const favorited = ref(false)
const favoriteToggling = ref(false)
// ==================== ==================== // ==================== ====================
const allImages = computed(() => { const allImages = computed(() => {
if (!product.value) return [] if (!product.value) return []
@ -113,6 +118,39 @@ async function handleAddToCart() {
} }
} }
async function loadFavoriteState() {
if (!product.value || !userStore.isLoggedIn) return
try {
favorited.value = await checkFavoriteApi(product.value.id)
} catch {
/* 未登录 / 接口异常时保持未收藏 */
}
}
async function handleToggleFavorite() {
if (!product.value) return
if (!userStore.isLoggedIn) {
router.push(`/login?redirect=${encodeURIComponent(route.fullPath)}`)
return
}
favoriteToggling.value = true
try {
if (favorited.value) {
await cancelFavoriteByProductIdApi(product.value.id)
favorited.value = false
ElMessage.success('已取消收藏')
} else {
await addFavoriteApi(product.value.id)
favorited.value = true
ElMessage.success('收藏成功,可在个人中心查看')
}
} catch {
/* 拦截器已提示 */
} finally {
favoriteToggling.value = false
}
}
// ==================== ==================== // ==================== ====================
onMounted(async () => { onMounted(async () => {
const id = Number(route.params.id) const id = Number(route.params.id)
@ -127,6 +165,7 @@ onMounted(async () => {
if (product.value?.skuList?.length) { if (product.value?.skuList?.length) {
selectedSku.value = product.value.skuList[0] selectedSku.value = product.value.skuList[0]
} }
loadFavoriteState()
} catch { } catch {
ElMessage.error('商品不存在或已下架') ElMessage.error('商品不存在或已下架')
} finally { } finally {
@ -292,6 +331,16 @@ onMounted(async () => {
> >
加入购物车 加入购物车
</el-button> </el-button>
<el-button
size="large"
class="fav-btn"
:class="{ active: favorited }"
:icon="favorited ? StarFilled : Star"
:loading="favoriteToggling"
@click="handleToggleFavorite"
>
{{ favorited ? '已收藏' : '收藏' }}
</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -701,6 +750,36 @@ onMounted(async () => {
} }
} }
.fav-btn {
min-width: 120px;
height: 52px;
font-size: $font-size-md;
font-weight: 600;
border-radius: $radius-full;
border: 2px solid $color-border-dark;
background: $bg-surface;
color: $color-text-regular;
box-shadow: 0 4px 0 0 $color-border-dark;
transition: transform 0.2s, box-shadow 0.2s, color 0.2s, background 0.2s, border-color 0.2s;
&:hover {
color: $color-primary;
border-color: $color-primary;
transform: translateY(-1px);
}
&:active {
box-shadow: 0 1px 0 0 $color-border-dark;
transform: translateY(3px);
}
&.active {
background: $color-primary-light;
border-color: $color-primary;
color: $color-primary;
}
}
// ==================== ==================== // ==================== ====================
.detail-bottom { .detail-bottom {
margin-top: 56px; margin-top: 56px;