topfans/frontend/pages/square/components/CreationGrid.vue

506 lines
12 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="creation-grid">
<view class="recommend-tag">
<text class="recommend-text">猜你喜欢</text>
</view>
<view class="creation-list">
<view class="col-left">
<view
v-for="item in col1"
:key="item.id"
class="creation-card"
@click="handleCardClick(item)"
>
<image
class="creation-image"
:src="item.cover_image"
mode="widthFix"
></image>
<view class="like-badge">
<view class="like-icon-wrapper">
<image
class="like-icon"
:src="item.is_liked ? '/static/icon/heart-icon.png' : '/static/icon/heart-icon-false.png'"
mode="aspectFit"
></image>
<text class="like-count">{{ formatCount(item.like_count) }}</text>
</view>
</view>
<view
class="wf-like-wave wf-like-wave-outer"
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
/>
<view
class="wf-like-wave wf-like-wave-inner"
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
/>
<view class="creation-info">
<view class="creation-meta">
<view class="creator-info">
<text class="creator-name">{{ item.creator_name }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="col-right">
<view
v-for="item in col2"
:key="item.id"
class="creation-card"
@click="handleCardClick(item)"
>
<image
class="creation-image"
:src="item.cover_image"
mode="widthFix"
></image>
<view class="like-badge">
<view class="like-icon-wrapper">
<image
class="like-icon"
:src="item.is_liked ? '/static/icon/heart-icon.png' : '/static/icon/heart-icon-false.png'"
mode="aspectFit"
></image>
<text class="like-count">{{ formatCount(item.like_count) }}</text>
</view>
</view>
<view
class="wf-like-wave wf-like-wave-outer"
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
/>
<view
class="wf-like-wave wf-like-wave-inner"
:class="{ 'wf-like-wave-active': likingMap[item.id] }"
/>
<view class="creation-info">
<view class="creation-meta">
<view class="creator-info">
<text class="creator-name">
{{ item.creator_name }}
</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view v-if="isLoading" class="loading-more">
<text class="loading-text">加载中...</text>
</view>
<view v-if="noMore && creationList.length > 0" class="no-more">
<text class="no-more-text">没有更多了</text>
</view>
</view>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import { getInspirationFlowApi } from "@/utils/api.js";
const props = defineProps({
screenWidth: { type: Number, default: 375 },
screenHeight: { type: Number, default: 812 },
bannerBottom: { type: Number, default: 200 },
useMockData: { type: Boolean, default: false },
category: { type: String, default: "" },
isActive: { type: Boolean, default: true },
});
const emit = defineEmits(["cardClick", "scroll"]);
const creationList = ref([]);
const col1 = ref([]);
const col2 = ref([]);
const cursor = ref("");
const isLoading = ref(false);
const noMore = ref(false);
const likingMap = ref({});
let isComponentMounted = 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 loadUsers = async () => {
if (!isComponentMounted) return Promise.resolve();
cursor.value = "";
isLoading.value = true;
noMore.value = false;
try {
const res = await getInspirationFlowApi({
limit: 20,
type: props.category,
cursor: "",
});
if (!isComponentMounted) return;
if (res.code === 200 && res.data?.items && res.data.items.length > 0) {
const items = res.data.items;
cursor.value = res.data.cursor || "";
creationList.value = items.map((item) => {
return {
id: item.asset_id,
certificate_id: item.asset_id,
cover_image: item.cover_url || "",
creator_avatar: item.owner_avatar || "",
creator_name: item.owner_nickname || item.name || "",
like_count: item.likes || item.like_count || 0,
is_liked: item.is_liked || false,
};
});
// 分成两列奇数放col1偶数放col2
col1.value = creationList.value.filter((_, i) => i % 2 === 0);
col2.value = creationList.value.filter((_, i) => i % 2 === 1);
} else {
noMore.value = true;
}
} catch (e) {
console.error("[CreationGrid] 加载数据失败", e?.message ?? e);
} finally {
isLoading.value = false;
}
};
const loadMore = async () => {
if (!isComponentMounted || isLoading.value || noMore.value) return;
isLoading.value = true;
try {
const res = await getInspirationFlowApi({
limit: 20,
type: props.category,
cursor: cursor.value,
});
if (!isComponentMounted) return;
if (res.code === 200 && res.data?.items && res.data.items.length > 0) {
const items = res.data.items;
cursor.value = res.data.cursor || "";
const newItems = items.map((item) => {
return {
id: item.asset_id,
certificate_id: item.asset_id,
cover_image: item.cover_url || "",
creator_avatar: item.owner_avatar || "",
creator_name: item.owner_nickname || item.name || "",
like_count: item.likes || item.like_count || 0,
is_liked: item.is_liked || false,
};
});
creationList.value = [...creationList.value, ...newItems];
// 重新分成两列
col1.value = creationList.value.filter((_, i) => i % 2 === 0);
col2.value = creationList.value.filter((_, i) => i % 2 === 1);
} else {
noMore.value = true;
}
} catch (e) {
console.error("[CreationGrid] 加载更多失败", e?.message ?? e);
} finally {
isLoading.value = false;
}
};
watch(
() => props.category,
() => {
loadUsers();
},
);
watch(
() => props.isActive,
(active) => {
if (active && creationList.value.length === 0) {
loadUsers();
}
},
);
onMounted(() => {
isComponentMounted = true;
// 监听点赞事件,实时更新点赞数
uni.$on("assetLiked", ({ asset_id, data }) => {
// 触发动画
likingMap.value = { ...likingMap.value, [asset_id]: true };
setTimeout(() => {
likingMap.value = { ...likingMap.value, [asset_id]: false };
}, 600);
const idx = creationList.value.findIndex((c) => c.id === asset_id);
if (idx !== -1) {
if (data && typeof data.new_like_count === "number") {
creationList.value[idx].like_count = data.new_like_count;
}
if (data && typeof data.is_liked === "boolean") {
creationList.value[idx].is_liked = data.is_liked;
}
creationList.value = [...creationList.value];
}
});
loadUsers();
});
onUnmounted(() => {
isComponentMounted = false;
uni.$off("assetLiked");
});
defineExpose({ loadMore });
</script>
<style scoped>
.creation-grid {
padding: 0 24rpx;
padding-top: 52rpx;
padding-bottom: 88rpx;
border-top-left-radius: 72rpx;
border-top-right-radius: 24rpx;
border-bottom-right-radius: 24rpx;
border-bottom-left-radius: 24rpx;
opacity: 0.78;
background: linear-gradient(
180deg,
rgba(148, 204, 153, 0.32) 0%,
rgba(185, 176, 220, 0.32) 29.33%,
rgba(207, 160, 227, 0.32) 56.25%,
rgba(129, 227, 240, 0.32) 77.88%,
rgba(220, 196, 155, 0.32) 100%
);
backdrop-filter: blur(11px);
position: relative;
}
.recommend-tag {
position: absolute;
top: -24rpx;
left: 4rpx;
width: 250rpx;
height: 56rpx;
border-top-left-radius: 44rpx;
border-top-right-radius: 8rpx;
border-bottom-right-radius: 44rpx;
border-bottom-left-radius: 4rpx;
background: linear-gradient(
90deg,
rgba(96, 210, 236, 0.73) 0%,
rgba(107, 60, 216, 0.4964) 51.44%,
rgba(226, 137, 236, 0.73) 100%
);
box-shadow: 2px 2px 4px 0px #d9262640;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.recommend-text {
font-size: 24rpx;
color: #fff;
text-shadow: 0px 2px 8px #000000ba;
font-weight: 600;
line-height: 100%;
letter-spacing: 0%;
}
.creation-list {
overflow: hidden;
}
.col-left,
.col-right {
width: 48%;
}
.col-left {
float: left;
}
.col-right {
float: right;
}
.creation-card {
width: 100%;
margin-bottom: 24rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 20rpx;
overflow: hidden;
backdrop-filter: blur(10rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
position: relative;
}
.creation-image {
width: 100%;
display: block;
box-shadow: 3px 3px 4.5px 2px #00000026;
backdrop-filter: blur(0px);
}
.like-badge {
position: absolute;
top: 0;
left: 0;
width: 122rpx;
height: 140rpx;
opacity: 1;
border-top-left-radius: 7px;
border-bottom-right-radius: 21.5px;
background: linear-gradient(
177.83deg,
rgba(83, 244, 211, 0.2) 2.52%,
rgba(15, 9, 0, 0) 69.07%
);
backdrop-filter: blur(0px);
z-index: 5;
}
.like-icon-wrapper {
display: flex;
align-items: center;
padding: 8rpx;
}
.like-badge .like-icon {
width: 38rpx;
height: 38rpx;
margin-right: 6rpx;
}
.like-badge .like-count {
font-size: 32rpx;
font-weight: 400;
line-height: 100%;
letter-spacing: 0%;
color: #fffabd;
text-shadow:
-1px 1px 4px #ce0909d6,
0px 0px 10px #fffabd;
}
.wf-like-wave {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0;
z-index: 1;
}
.wf-like-wave-outer {
background: radial-gradient(
circle,
rgba(255, 107, 107, 0.8) 0%,
transparent 70%
);
}
.wf-like-wave-inner {
background: radial-gradient(
circle,
rgba(255, 184, 0, 0.6) 0%,
transparent 70%
);
}
.wf-like-wave-active {
animation: likeWave 0.6s ease-out forwards;
}
@keyframes likeWave {
0% {
opacity: 0.9;
transform: scale(0.8);
}
100% {
opacity: 0;
transform: scale(1.5);
}
}
.creation-info {
position: absolute;
width: 100%;
height: 148rpx;
bottom: 0;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
opacity: 1;
background: linear-gradient(
177.83deg,
rgba(235, 228, 219, 0) 20.38%,
rgba(138, 135, 131, 0.228804) 45.67%,
rgba(255, 231, 231, 0.446775) 62.04%,
rgba(255, 255, 255, 0.710766) 83.52%,
#ffffff 98.2%
);
backdrop-filter: blur(0px);
}
.creation-meta {
position: absolute;
bottom: 8rpx;
left: 40rpx;
}
.creator-info {
display: flex;
align-items: center;
}
.creator-name {
font-weight: 400;
font-size: 24rpx;
line-height: 100%;
letter-spacing: 0%;
color: #554545;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.like-info {
display: flex;
align-items: center;
}
.loading-more,
.no-more {
width: 100%;
text-align: center;
padding: 32rpx 0;
}
.loading-text,
.no-more-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
</style>