topfans/frontend/pages/square/components/HotCategoryBlock.vue
zheng020 de4b376a87 fix: HotCategoryBlock grid layout to 4 columns
Change card width from 48% to calc(25% - 12rpx) to achieve
4 columns × 2 rows layout as specified in design docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:46:08 +08:00

329 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="hot-category-block">
<!-- 标题 -->
<view class="block-title">{{ title }}</view>
<!-- 骨架屏 -->
<view v-if="loading" class="grid-skeleton">
<view v-for="i in 8" :key="i" class="skeleton-card">
<view class="skeleton-image"></view>
<view class="skeleton-info">
<view class="skeleton-avatar"></view>
<view class="skeleton-name"></view>
</view>
</view>
</view>
<!-- 内容网格 -->
<view v-else class="items-grid">
<view
v-for="(item, index) in items"
:key="item.id || index"
class="grid-card"
@click="handleCardClick(item)"
>
<image class="card-image" :src="item.cover_url || item.cover_image || ''" mode="aspectFill"></image>
<!-- 底部渐变遮罩 -->
<view class="card-gradient"></view>
<!-- 用户信息 -->
<view class="card-info">
<view class="user-info">
<image class="user-avatar" :src="item.owner_avatar || item.creator_avatar || ''" mode="aspectFill"></image>
<text class="user-name">{{ item.owner_nickname || item.creator_name || item.name || '' }}</text>
</view>
<view class="like-info">
<image class="like-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<text class="like-count">{{ formatCount(item.likes || item.like_count || 0) }}</text>
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="block-actions">
<view class="action-left">
<view class="refresh-btn" @click="handleRefresh">
<image class="refresh-icon" :class="{ spinning: refreshing }" src="/static/icon/refresh-icon.png" mode="aspectFit"></image>
<text class="action-text">刷新</text>
</view>
</view>
<view class="action-right">
<text class="action-text">查看更多</text>
<text class="arrow"></text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { getHotInspirationFlowApi } from '@/utils/api.js'
const props = defineProps({
categoryType: {
type: String,
default: 'hot_recommend'
},
title: {
type: String,
default: '热门推荐'
}
})
const emit = defineEmits(['cardClick'])
const items = ref([])
const loading = ref(false)
const refreshing = ref(false)
// 格式化数量
const formatCount = (count) => {
if (!count) return '0'
if (count >= 10000) return (count / 10000).toFixed(1) + 'w'
if (count >= 1000) return (count / 1000).toFixed(1) + 'k'
return count.toString()
}
// 点击卡片
const handleCardClick = (item) => {
emit('cardClick', item)
}
// 刷新数据
const handleRefresh = async () => {
if (refreshing.value) return
refreshing.value = true
try {
const res = await getHotInspirationFlowApi(props.categoryType)
if (res.code === 200 && res.data?.items) {
items.value = res.data.items
}
} catch (e) {
console.error('[HotCategoryBlock] 刷新数据失败', e?.message ?? e)
} finally {
refreshing.value = false
}
}
// 设置数据
const setItems = (newItems) => {
items.value = newItems || []
}
// 设置加载状态
const setLoading = (status) => {
loading.value = status
}
defineExpose({
setItems,
setLoading,
handleRefresh
})
</script>
<style scoped>
.hot-category-block {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10rpx);
border-radius: 24rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.block-title {
font-size: 32rpx;
font-weight: 600;
color: #fff;
margin-bottom: 24rpx;
}
/* 骨架屏 */
.grid-skeleton {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.skeleton-card {
width: calc(25% - 12rpx);
margin-bottom: 16rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
overflow: hidden;
}
.skeleton-image {
width: 100%;
height: 320rpx;
background: linear-gradient(90deg, #3a3a4a 25%, #4a4a5a 50%, #3a3a4a 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-info {
display: flex;
align-items: center;
padding: 16rpx;
}
.skeleton-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #3a3a4a;
margin-right: 8rpx;
}
.skeleton-name {
width: 100rpx;
height: 24rpx;
background: #3a3a4a;
border-radius: 8rpx;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* 内容网格 */
.items-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.grid-card {
width: calc(25% - 12rpx);
margin-bottom: 16rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 16rpx;
overflow: hidden;
position: relative;
}
.card-image {
width: 100%;
height: 320rpx;
}
.card-gradient {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 100%);
}
.card-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
box-sizing: border-box;
}
.user-info {
display: flex;
align-items: center;
}
.user-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.user-name {
font-size: 22rpx;
color: #fff;
max-width: 120rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.like-info {
display: flex;
align-items: center;
}
.like-icon {
width: 24rpx;
height: 24rpx;
margin-right: 4rpx;
}
.like-count {
font-size: 22rpx;
color: #fff;
}
/* 操作按钮 */
.block-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.1);
}
.action-left {
display: flex;
align-items: center;
}
.action-right {
display: flex;
align-items: center;
}
.refresh-btn {
display: flex;
align-items: center;
}
.refresh-icon {
width: 28rpx;
height: 28rpx;
margin-right: 8rpx;
}
.refresh-icon.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.action-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
.arrow {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
margin-left: 4rpx;
}
</style>