style:修改前端样式

This commit is contained in:
zheng020 2026-06-01 15:06:35 +08:00
parent 3a2575c78e
commit 60db94e377
11 changed files with 1375 additions and 694 deletions

File diff suppressed because it is too large Load Diff

View File

@ -22,39 +22,76 @@
</view> </view>
<view class="exhibition-grid"> <view class="exhibition-grid">
<view v-for="(item, index) in exhibitionWorks" :key="item.id" class="exhibition-card" <!-- 左边展位 (slot_index=1) -->
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'" @tap="goToAssetDetail(item.id)"> <view v-if="exhibitionAtSlot[0]" class="exhibition-card card-tilt-left"
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'" @tap="goToAssetDetail(exhibitionAtSlot[0].id)">
mode="aspectFill"></image> <LenticularCard v-if="exhibitionAtSlot[0].is_lenticular" class="card-lenticular"
:layers="getLenticularLayers(exhibitionAtSlot[0].id)"
:transforms="getLenticularTransforms(exhibitionAtSlot[0].id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[0].id, x, y)" />
<image v-else class="card-image"
:src="exhibitionAtSlot[0].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
</image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"> <image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image> </image>
<!-- 点赞数 --> <!-- 点赞数 -->
<view class="card-rate-badge"> <view class="card-rate-badge">
<image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image> <image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<view class="card-rate-text-wrap"> <view class="card-rate-text-wrap">
<text class="card-rate-text">{{ item.like_count || 0 }}</text> <text class="card-rate-text">{{ exhibitionAtSlot[0].like_count || 0 }}</text>
</view> </view>
</view> </view>
<!-- 倒计时背景 -->
<view class="countdown-background" :style="getCountdownBackgroundStyle()"> <view class="countdown-background" :style="getCountdownBackgroundStyle()">
<!-- 倒计时文字 --> <text class="countdown-text">{{ formatCountdown(exhibitionAtSlot[0].id) }}</text>
<text class="countdown-text">
{{ formatCountdown(item.id) }}
</text>
</view> </view>
<!-- 图片下方收益 --> <view class="card-income-row income-tilt-right">
<view class="card-income-row"
:class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
<image class="topfans-icon" src="/static/icon/crystal.png" mode="aspectFit"></image> <image class="topfans-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
<view class="card-income-text-wrap"> <view class="card-income-text-wrap">
<text class="card-income-text">{{ item.earnings || 0 }}/</text> <text class="card-income-text">{{ exhibitionAtSlot[0].earnings || 0 }}/</text>
</view> </view>
</view> </view>
</view> </view>
<view v-else class="empty-card empty-card-left">
<image class="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
</view>
<!-- 空状态占位 --> <!-- 右边展位 (slot_index=2) -->
<view v-if="exhibitionWorks.length === 0" class="empty-exhibition"> <view v-if="exhibitionAtSlot[1]" class="exhibition-card card-tilt-right"
<text class="empty-text">该用户暂无在展作品</text> @tap="goToAssetDetail(exhibitionAtSlot[1].id)">
<LenticularCard v-if="exhibitionAtSlot[1].is_lenticular" class="card-lenticular"
:layers="getLenticularLayers(exhibitionAtSlot[1].id)"
:transforms="getLenticularTransforms(exhibitionAtSlot[1].id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLenticularSimulate(exhibitionAtSlot[1].id, x, y)" />
<image v-else class="card-image"
:src="exhibitionAtSlot[1].cover_url || '/static/nft/placeholder.png'" mode="aspectFill">
</image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
<!-- 点赞数 -->
<view class="card-rate-badge">
<image class="heart-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<view class="card-rate-text-wrap">
<text class="card-rate-text">{{ exhibitionAtSlot[1].like_count || 0 }}</text>
</view>
</view>
<view class="countdown-background" :style="getCountdownBackgroundStyle()">
<text class="countdown-text">{{ formatCountdown(exhibitionAtSlot[1].id) }}</text>
</view>
<view class="card-income-row income-tilt-left">
<image class="topfans-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
<view class="card-income-text-wrap">
<text class="card-income-text">{{ exhibitionAtSlot[1].earnings || 0 }}/</text>
</view>
</view>
</view>
<view v-else class="empty-card empty-card-right">
<image class="empty-cover" src="/static/nft/placeholder.png" mode="aspectFill"></image>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image>
</view> </view>
</view> </view>
</view> </view>
@ -68,18 +105,6 @@
</image> </image>
<text class="section-label-text">当前点赞作品</text> <text class="section-label-text">当前点赞作品</text>
</view> </view>
<!-- <view class="section-label" :class="{ 'tab-active': likedTab === 'today' }"
@tap="switchLikedTab('today')">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill">
</image>
<text class="section-label-text">今日点赞作品</text>
</view>
<view class="section-label" :class="{ 'tab-active': likedTab === 'week' }"
@tap="switchLikedTab('week')">
<image class="section-label-bg" src="/static/nft/dingbutubiao_liang.png" mode="aspectFill">
</image>
<text class="section-label-text">本周点赞作品</text>
</view> -->
</view> </view>
<scroll-view class="liked-list" scroll-y="true" :show-scrollbar="false"> <scroll-view class="liked-list" scroll-y="true" :show-scrollbar="false">
@ -90,11 +115,15 @@
mode="aspectFit"></image> mode="aspectFit"></image>
<!-- 卡片主体 --> <!-- 卡片主体 -->
<view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''"> <view class="liked-item" :class="index === 0 ? 'liked-item-first' : ''">
<!-- 作品封面 --> <!-- 作品封面 -->
<view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''"> <view class="liked-cover-wrap" :class="index === 0 ? 'liked-cover-wrap-first' : ''">
<image class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'" <LenticularCard v-if="item.is_lenticular" class="liked-lenticular"
:layers="getLikedLenticularLayers(item.id)"
:transforms="getLikedLenticularTransforms(item.id)" :gyro-source="gyroSourceLabel"
:skip-built-in-touch="true" :shimmer-mid-opacity="0.16"
@simulate="(x, y) => onLikedLenticularSimulate(item.id, x, y)" />
<image v-else class="liked-cover" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image> mode="aspectFill"></image>
<image class="liked-cover-frame" src="/static/square/cangpinkuang1.png" <image class="liked-cover-frame" src="/static/square/cangpinkuang1.png"
mode="aspectFill"></image> mode="aspectFill"></image>
@ -116,7 +145,7 @@
:src="item.earnings > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'" :src="item.earnings > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
mode="aspectFit"> mode="aspectFit">
</image> </image>
<text class="reward-amount">+{{ item.reward }}</text> <text class="reward-amount">{{ item.reward }}</text>
</view> </view>
</view> </view>
</view> </view>
@ -132,9 +161,13 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app'; import { onLoad } from '@dcloudio/uni-app';
import { getUserGalleriesApi, getUserLikedAssetsApi, getUserProfileApi } from '@/utils/api.js'; import { getUserGalleriesApi, getUserLikedAssetsApi, getAssetMaterialsApi } from '@/utils/api.js';
import LenticularCard from '@/components/lenticular/LenticularCard.vue';
import { useLenticularCraftTiltPreview } from '@/composables/useLenticularCraftTiltPreview.js';
import { buildLenticularLayers, buildLenticularLayersTwo } from '@/utils/castloveMintForm.js';
import { LenticularEngine, DEFAULT_PHYSICS } from '@/utils/lenticular-engine.js';
const userId = ref(''); const userId = ref('');
const nickname = ref(''); const nickname = ref('');
@ -226,7 +259,6 @@ const getCountdownBackgroundStyle = () => {
}; };
}; };
const rankIcons = [ const rankIcons = [
'/static/square/icon1.png', '/static/square/icon1.png',
'/static/square/icon2.png', '/static/square/icon2.png',
@ -245,11 +277,269 @@ const likedWorks = ref([]);
// : current-, today-, week- // : current-, today-, week-
const likedTab = ref('current'); const likedTab = ref('current');
//
// transforms asset id
const lenticularTransformsMap = ref({});
const lenticularLayersByAsset = ref({});
const activeLenticularId = ref(null);
const gyroSourceLabel = ref('device');
//
const likedLenticularLayersByAsset = ref({});
const likedLenticularTransformsMap = ref({});
// 使
const lenticularPhysics = ref(null);
const lenticularEngine = ref(null);
let lenticularRafId = null;
// 使 useLenticularPreview
const lenticularLayersRef = ref([]);
function getLenticularLayers(assetId) {
return lenticularLayersByAsset.value[assetId] || [];
}
function getLenticularTransforms(assetId) {
return lenticularTransformsMap.value[assetId] || {};
}
//
function getLikedLenticularLayers(assetId) {
return likedLenticularLayersByAsset.value[assetId] || [];
}
function getLikedLenticularTransforms(assetId) {
return likedLenticularTransformsMap.value[assetId] || {};
}
function onLikedLenticularSimulate(assetId, x, y) {
simulateLikedLenticularTilt(assetId, x, y);
}
function simulateLikedLenticularTilt(assetId, x, y) {
if (!lenticularEngine.value) return;
const layers = likedLenticularLayersByAsset.value[assetId];
if (!layers || layers.length === 0) return;
lenticularEngine.value.setLayers(layers);
const renderState = lenticularEngine.value.feedSimulatedTilt(x, y ? y : 0);
const transforms = {};
for (const l of layers) {
const rs = renderState.layerOffsets.get(l.id);
const op = renderState.layerOpacities.get(l.id);
transforms[l.id] = {
x: rs ? rs.x : 0,
y: rs ? rs.y : 0,
opacity: op != null ? op : l.opacity,
};
}
likedLenticularTransformsMap.value = { ...likedLenticularTransformsMap.value, [assetId]: transforms };
}
async function loadLikedLenticularLayersForAsset(assetId) {
const item = likedWorks.value.find(w => w.id === assetId);
if (!item) return;
try {
const materialsRes = await getAssetMaterialsApi(assetId);
if (materialsRes.code === 200 && materialsRes.data) {
const materialsList = Array.isArray(materialsRes.data) ? materialsRes.data : materialsRes.data.materials || [];
let subjectUrl = item.cover_url;
let bgUrl = '';
for (const mat of materialsList) {
if (mat.material_type === 'main' && mat.material_url_signed) {
subjectUrl = mat.material_url_signed;
}
if (mat.material_type === 'bg' && mat.material_url_signed) {
bgUrl = mat.material_url_signed;
}
}
if (bgUrl) {
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayersTwo(bgUrl, subjectUrl);
} else {
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayers(subjectUrl);
}
initLikedTransformsForAsset(assetId);
} else {
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
initLikedTransformsForAsset(assetId);
}
} catch (e) {
console.error('[hisWorks] 获取点赞作品素材列表失败:', e);
likedLenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
initLikedTransformsForAsset(assetId);
}
}
function initLikedTransformsForAsset(assetId) {
const layers = likedLenticularLayersByAsset.value[assetId];
if (!layers || layers.length === 0) return;
const transforms = {};
for (const l of layers) {
transforms[l.id] = { x: 0, y: 0, opacity: l.opacity || 1 };
}
likedLenticularTransformsMap.value = { ...likedLenticularTransformsMap.value, [assetId]: transforms };
}
async function loadLenticularLayersForAsset(assetId) {
const item = exhibitionWorks.value.find(w => w.id === assetId);
if (!item) return;
try {
const materialsRes = await getAssetMaterialsApi(assetId);
if (materialsRes.code === 200 && materialsRes.data) {
const materialsList = Array.isArray(materialsRes.data) ? materialsRes.data : materialsRes.data.materials || [];
let subjectUrl = item.cover_url;
let bgUrl = '';
for (const mat of materialsList) {
if (mat.material_type === 'main' && mat.material_url_signed) {
subjectUrl = mat.material_url_signed;
}
if (mat.material_type === 'bg' && mat.material_url_signed) {
bgUrl = mat.material_url_signed;
}
}
if (bgUrl) {
lenticularLayersByAsset.value[assetId] = buildLenticularLayersTwo(bgUrl, subjectUrl);
} else {
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(subjectUrl);
}
initTransformsForAsset(assetId);
} else {
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
initTransformsForAsset(assetId);
}
} catch (e) {
console.error('[hisWorks] 获取素材列表失败:', e);
lenticularLayersByAsset.value[assetId] = buildLenticularLayers(item.cover_url);
}
}
//
function onLenticularSimulate(assetId, x, y) {
simulateLenticularTilt(assetId, x, y);
}
//
function simulateLenticularTilt(assetId, x, y) {
if (!lenticularEngine.value) return;
const layers = lenticularLayersByAsset.value[assetId];
if (!layers || layers.length === 0) return;
//
lenticularEngine.value.setLayers(layers);
//
const renderState = lenticularEngine.value.feedSimulatedTilt(x, y ? y : 0);
const transforms = {};
for (const l of layers) {
const rs = renderState.layerOffsets.get(l.id);
const op = renderState.layerOpacities.get(l.id);
transforms[l.id] = {
x: rs ? rs.x : 0,
y: rs ? rs.y : 0,
opacity: op != null ? op : l.opacity,
};
}
// Vue
lenticularTransformsMap.value = { ...lenticularTransformsMap.value, [assetId]: transforms };
}
//
function initLenticularEngine() {
if (lenticularEngine.value) return;
const physics = { ...DEFAULT_PHYSICS, parallaxDepth: 18 };
physics.gyroSimEnabled = false;
lenticularPhysics.value = physics;
lenticularEngine.value = new LenticularEngine(physics);
}
// transforms
function initTransformsForAsset(assetId) {
const layers = lenticularLayersByAsset.value[assetId];
if (!layers || layers.length === 0) return;
const transforms = {};
for (const l of layers) {
transforms[l.id] = { x: 0, y: 0, opacity: l.opacity || 1 };
}
lenticularTransformsMap.value = { ...lenticularTransformsMap.value, [assetId]: transforms };
}
// transforms
function startLenticularRenderLoop() {
if (lenticularRafId !== null) return;
const tick = () => {
// transforms
for (const assetId of Object.keys(lenticularLayersByAsset.value)) {
const layers = lenticularLayersByAsset.value[assetId];
if (!layers || layers.length === 0) continue;
lenticularEngine.value.setLayers(layers);
const renderState = lenticularEngine.value.feedSimulatedTilt(0, 0);
const transforms = {};
for (const l of layers) {
const rs = renderState.layerOffsets.get(l.id);
const op = renderState.layerOpacities.get(l.id);
transforms[l.id] = {
x: rs ? rs.x : 0,
y: rs ? rs.y : 0,
opacity: op != null ? op : l.opacity,
};
}
lenticularTransformsMap.value = { ...lenticularTransformsMap.value, [assetId]: transforms };
}
//
for (const assetId of Object.keys(likedLenticularLayersByAsset.value)) {
const layers = likedLenticularLayersByAsset.value[assetId];
if (!layers || layers.length === 0) continue;
lenticularEngine.value.setLayers(layers);
const renderState = lenticularEngine.value.feedSimulatedTilt(0, 0);
const transforms = {};
for (const l of layers) {
const rs = renderState.layerOffsets.get(l.id);
const op = renderState.layerOpacities.get(l.id);
transforms[l.id] = {
x: rs ? rs.x : 0,
y: rs ? rs.y : 0,
opacity: op != null ? op : l.opacity,
};
}
likedLenticularTransformsMap.value = { ...likedLenticularTransformsMap.value, [assetId]: transforms };
}
lenticularRafId = requestAnimationFrame(tick);
};
lenticularRafId = requestAnimationFrame(tick);
}
function stopLenticularRenderLoop() {
if (lenticularRafId !== null) {
cancelAnimationFrame(lenticularRafId);
lenticularRafId = null;
}
}
// slot 1=, slot 2=
const exhibitionAtSlot = computed(() => {
const slots = [null, null];
for (const item of exhibitionWorks.value) {
const pos = (item.slot_index ?? 0) - 1;
if (pos >= 0 && pos < 2) {
slots[pos] = item;
}
}
return slots;
});
// //
const switchLikedTab = async (tab) => { const switchLikedTab = async (tab) => {
if (likedTab.value === tab) return; if (likedTab.value === tab) return;
likedTab.value = tab; likedTab.value = tab;
likedWorks.value = []; likedWorks.value = [];
//
likedLenticularLayersByAsset.value = {};
likedLenticularTransformsMap.value = {};
await loadLikedAssets(); await loadLikedAssets();
}; };
@ -267,7 +557,17 @@ const loadExhibitedAssets = async () => {
like_count: slot.asset.like_count, like_count: slot.asset.like_count,
earnings: slot.asset.earnings || 0, earnings: slot.asset.earnings || 0,
name: slot.asset.name, name: slot.asset.name,
})); slot_index: slot.slot_index ?? 0,
is_lenticular: slot.asset.is_lenticular ?? false,
}))
.sort((a, b) => (a.slot_index ?? 0) - (b.slot_index ?? 0));
//
for (const item of exhibitionWorks.value) {
if (item.is_lenticular) {
loadLenticularLayersForAsset(item.id);
}
}
} }
} catch (err) { } catch (err) {
console.error('加载展出作品失败:', err); console.error('加载展出作品失败:', err);
@ -297,10 +597,18 @@ const loadLikedAssets = async () => {
like_count: item.like_count, like_count: item.like_count,
earnings: item.earnings, earnings: item.earnings,
name: item.name, name: item.name,
is_lenticular: item.is_lenticular ?? false,
status_text: index < 3 ? '排名进榜' : '潜力待挖', status_text: index < 3 ? '排名进榜' : '潜力待挖',
score: item.like_count, score: item.like_count,
reward: Math.floor(item.earnings || 0), reward: Math.floor(item.earnings || 0),
})); }));
//
for (const item of likedWorks.value) {
if (item.is_lenticular) {
loadLikedLenticularLayersForAsset(item.id);
}
}
} }
} catch (err) { } catch (err) {
console.error('加载点赞作品失败:', err); console.error('加载点赞作品失败:', err);
@ -308,6 +616,8 @@ const loadLikedAssets = async () => {
}; };
onMounted(() => { onMounted(() => {
initLenticularEngine();
startLenticularRenderLoop();
loadExhibitedAssets(); loadExhibitedAssets();
loadLikedAssets(); loadLikedAssets();
@ -323,6 +633,7 @@ onUnmounted(() => {
if (countdownTimer) { if (countdownTimer) {
clearInterval(countdownTimer); clearInterval(countdownTimer);
} }
stopLenticularRenderLoop();
}); });
</script> </script>
@ -332,6 +643,7 @@ onUnmounted(() => {
position: relative; position: relative;
} }
/* 背景图片 */
.bg-image { .bg-image {
position: fixed; position: fixed;
top: 0; top: 0;
@ -341,13 +653,14 @@ onUnmounted(() => {
z-index: 0; z-index: 0;
} }
/* 导航栏 */
.nav-bar { .nav-bar {
position: relative; position: relative;
z-index: 10; z-index: 10;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 80rpx 32rpx 16rpx; padding: 96rpx 32rpx 16rpx;
} }
.nav-back { .nav-back {
@ -365,28 +678,39 @@ onUnmounted(() => {
} }
.nav-title { .nav-title {
font-size: 48rpx; font-size: 36rpx;
font-weight: 700; font-weight: 700;
color: #fff; color: #5a2d82;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.9);
letter-spacing: 2rpx; letter-spacing: 2rpx;
} }
.nav-placeholder { .nav-placeholder {
width: 80rpx; width: 64rpx;
} }
/* 内容区域 */
.scroll-content { .scroll-content {
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.section-block { .section-block {
/* background: rgb(249 159 192 / 45%);
border-radius: 48rpx; */
padding: 16rpx; padding: 16rpx;
} }
/* 区块 */
.section-1 {
margin-bottom: 8rpx;
}
/* 点赞标签容器 */
.liked-tabs {
display: flex;
gap: 16rpx;
margin-bottom: 16rpx;
}
/* 区块标签 */
.section-label { .section-label {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@ -406,12 +730,6 @@ onUnmounted(() => {
opacity: 1; opacity: 1;
} }
.liked-tabs {
display: flex;
gap: 16rpx;
margin-bottom: 16rpx;
}
.section-label-bg { .section-label-bg {
position: absolute; position: absolute;
top: 0; top: 0;
@ -430,6 +748,7 @@ onUnmounted(() => {
padding: 0 28rpx; padding: 0 28rpx;
} }
/* 在展作品网格 */
.exhibition-grid { .exhibition-grid {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -454,6 +773,7 @@ onUnmounted(() => {
.card-tilt-right { .card-tilt-right {
transform: rotate(4deg) translateY(10rpx); transform: rotate(4deg) translateY(10rpx);
margin-left: 32rpx;
border-radius: 32rpx; border-radius: 32rpx;
box-shadow: 16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9); box-shadow: 16rpx 16rpx 16rpx rgba(229, 76, 93, 0.9);
} }
@ -486,6 +806,18 @@ onUnmounted(() => {
z-index: 2; z-index: 2;
} }
.card-lenticular {
width: 88%;
height: 93%;
left: 5%;
top: 4%;
border-radius: 24rpx;
transform-origin: center center;
position: relative;
z-index: 3;
overflow: hidden;
}
.card-rate-badge { .card-rate-badge {
position: absolute; position: absolute;
top: 16rpx; top: 16rpx;
@ -498,6 +830,7 @@ onUnmounted(() => {
z-index: 9; z-index: 9;
} }
/* 图片下方收益 */
.card-income-row { .card-income-row {
position: absolute; position: absolute;
bottom: -88rpx; bottom: -88rpx;
@ -516,7 +849,6 @@ onUnmounted(() => {
z-index: 2; z-index: 2;
margin-right: -16rpx; margin-right: -16rpx;
left: 20rpx; left: 20rpx;
top: 8rpx;
} }
.card-income-text-wrap { .card-income-text-wrap {
@ -527,7 +859,8 @@ onUnmounted(() => {
#B94E73 100%); #B94E73 100%);
border-radius: 999rpx; border-radius: 999rpx;
padding: 8rpx 20rpx 8rpx 40rpx; padding: 8rpx 20rpx 8rpx 40rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 143, 158, 0.2), box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4); inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
display: flex; display: flex;
align-items: center; align-items: center;
@ -541,6 +874,7 @@ onUnmounted(() => {
text-align: center; text-align: center;
} }
.heart-icon { .heart-icon {
width: 28rpx; width: 28rpx;
height: 28rpx; height: 28rpx;
@ -553,7 +887,8 @@ onUnmounted(() => {
#B94E73 100%); #B94E73 100%);
border-radius: 999rpx; border-radius: 999rpx;
padding: 2rpx 16rpx; padding: 2rpx 16rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 143, 158, 0.2), box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4); inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
display: flex; display: flex;
} }
@ -566,10 +901,6 @@ onUnmounted(() => {
/* 倒计时背景 */ /* 倒计时背景 */
.countdown-background { .countdown-background {
/* position: absolute;
bottom: 20rpx;
right: 30%;
transform: translateX(-50%); */
width: 140rpx; width: 140rpx;
height: 36rpx; height: 36rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%); background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
@ -593,11 +924,32 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
} }
.empty-exhibition { /* 空状态 */
display: flex; .empty-card {
align-items: center; width: 248rpx;
justify-content: center; height: 380rpx;
padding: 80rpx 0; border-radius: 20rpx;
overflow: visible;
position: relative;
margin: 0 32rpx;
}
.empty-card-left {
transform: rotate(-4deg) translateY(10rpx);
}
.empty-card-right {
transform: rotate(4deg) translateY(10rpx);
}
.empty-cover {
width: 88%;
height: 92%;
border-radius: 80rpx;
position: relative;
z-index: 3;
padding: 16rpx;
opacity: 0.5;
} }
.empty-text { .empty-text {
@ -605,6 +957,7 @@ onUnmounted(() => {
color: #b09cc0; color: #b09cc0;
} }
/* 当前点赞列表 */
.liked-list { .liked-list {
max-height: 732rpx; max-height: 732rpx;
} }
@ -620,13 +973,12 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
background: #ffffff50; background: #ffffff50;
border-radius: 32rpx; border-radius: 48rpx;
padding: 24rpx 20rpx; padding: 24rpx 20rpx;
gap: 16rpx;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
width: 80%; width: 80%;
padding-left: 13%; padding-left: 13%;
justify-content: space-between;
} }
.liked-item-first { .liked-item-first {
@ -642,7 +994,6 @@ onUnmounted(() => {
/* 排名图标 - 排名越靠前越大 */ /* 排名图标 - 排名越靠前越大 */
.rank-icon { .rank-icon {
flex-shrink: 0; flex-shrink: 0;
/* margin-right: 8rpx; */
position: relative; position: relative;
left: 32rpx; left: 32rpx;
} }
@ -662,12 +1013,12 @@ onUnmounted(() => {
height: 88rpx; height: 88rpx;
} }
/* 作品封面 */
.liked-cover-wrap { .liked-cover-wrap {
width: 88rpx; width: 88rpx;
height: 88rpx; height: 88rpx;
flex-shrink: 0; flex-shrink: 0;
margin-left: -18rpx; margin-left: -18rpx;
margin-right: 48rpx;
position: relative; position: relative;
} }
@ -682,6 +1033,17 @@ onUnmounted(() => {
padding: 0.25rem; padding: 0.25rem;
} }
.liked-lenticular {
width: 90%;
height: 90%;
border-radius: 24rpx;
transform: rotate(-22deg);
transform-origin: center center;
position: relative;
z-index: 3;
overflow: hidden;
}
.liked-cover-frame { .liked-cover-frame {
position: absolute; position: absolute;
top: 0; top: 0;
@ -693,12 +1055,12 @@ onUnmounted(() => {
transform-origin: center center; transform-origin: center center;
} }
/* 作品信息 */
.liked-info { .liked-info {
flex: 1;
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; margin: 0 16rpx 0 32rpx;
} }
.liked-status { .liked-status {
@ -731,26 +1093,31 @@ onUnmounted(() => {
margin-top: 4rpx; margin-top: 4rpx;
} }
/* 右侧奖励 */
.liked-reward { .liked-reward {
min-width: 136rpx;
padding: 8rpx 16rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 999rpx;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center;
gap: 8rpx;
flex-shrink: 0;
} }
.reward-token-icon { .reward-token-icon {
width: 56rpx; width: 56rpx;
height: 56rpx; height: 56rpx;
margin-right: 8rpx;
} }
.reward-amount { .reward-amount {
font-size: 28rpx; font-size: 28rpx;
color: #c060e0; color: #fff;
font-weight: 700; font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.7);
} }
/* 空状态 */
.empty-liked { .empty-liked {
padding: 60rpx 0; padding: 60rpx 0;
display: flex; display: flex;

View File

@ -1448,6 +1448,7 @@ onShow(() => {
box-sizing: border-box; box-sizing: border-box;
width: 80%; width: 80%;
padding-left: 13%; padding-left: 13%;
justify-content: space-between;
} }
.liked-item-first { .liked-item-first {

View File

@ -7,7 +7,7 @@
<!-- 骨架屏 --> <!-- 骨架屏 -->
<view v-if="loading" class="grid-skeleton"> <view v-if="loading" class="grid-skeleton">
<view v-for="i in 12" :key="i" class="skeleton-card"> <view v-for="i in 11" :key="i" class="skeleton-card">
<view class="skeleton-image"></view> <view class="skeleton-image"></view>
<view class="skeleton-info"> <view class="skeleton-info">
<view class="skeleton-avatar"></view> <view class="skeleton-avatar"></view>
@ -18,137 +18,203 @@
<!-- 内容网格 --> <!-- 内容网格 -->
<view v-else class="items-grid"> <view v-else class="items-grid">
<view v-for="(item, index) in items" :key="item.id || index" class="grid-card" @click="handleCardClick(item)"> <view
v-for="(item, index) in items"
:key="item.id || index"
class="grid-card"
@click="handleCardClick(item)"
>
<!-- 点赞动效波纹 --> <!-- 点赞动效波纹 -->
<view class="wf-like-wave wf-like-wave-outer" :class="{ 'wf-like-wave-active': likingMap[item.id] }" /> <view
<view class="wf-like-wave wf-like-wave-inner" :class="{ 'wf-like-wave-active': likingMap[item.id] }" /> 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="card-bottom"> <view class="card-bottom" :class="`card-bottom-${index + 1}`">
<image class="card-image" :src="item.cover_url || item.cover_image || ''" mode="aspectFill"></image> <image
<view class="like-badge"> class="card-image"
:src="item.cover_url || item.cover_image || ''"
mode="aspectFill"
></image>
<view class="like-badge" :class="`like-badge-${index + 1}`">
<view class="like-icon-wrapper"> <view class="like-icon-wrapper">
<image class="like-icon" <image
:src="item.is_liked ? '/static/icon/heart-icon.png' : '/static/icon/heart-icon-false.png'" class="like-icon"
mode="aspectFit"> :src="
item.is_liked
? '/static/icon/heart-icon.png'
: '/static/icon/heart-icon-false.png'
"
mode="aspectFit"
>
</image> </image>
<text class="like-count">{{ formatCount(item.like_count) }}</text> <text class="like-count">{{ formatCount(item.like_count) }}</text>
</view> </view>
</view> </view>
<!-- 用户信息 --> <!-- 用户信息 -->
<view class="card-info"> <view class="card-info" :class="`card-info-${index + 1}`">
<view class="user-info"> <view class="user-info">
<image class="user-avatar" :src="item.owner_avatar || item.creator_avatar || ''" mode="aspectFill"> <image
class="user-avatar"
:src="item.owner_avatar || item.creator_avatar || ''"
mode="aspectFill"
>
</image> </image>
<text class="user-name">{{ item.owner_nickname || item.creator_name || item.name || '' }}</text> <text class="user-name">{{
item.owner_nickname || item.creator_name || item.name || ""
}}</text>
</view> </view>
</view> </view>
<!-- 前三名专属包裹 card-bottom 的边框 -->
<view v-if="index < 3" class="card-frame">
<image
class="frame-image"
:src="TOP_FRAME_MAP[index]"
mode="scaleToFill"
></image>
</view>
</view>
<!-- 前三名专属右上角装饰图位于 grid-card 层级避免被 card-bottom overflow 裁切 -->
<view v-if="index < 3" class="corner-decoration">
<image :src="TOP_ICON_MAP[index]" mode="aspectFit"></image>
</view> </view>
<!-- Top 排名标签 --> <!-- Top 排名标签 -->
<view class="top-badge"> <view class="top-badge" :class="`top-badge-${index + 1}`">
<view
v-if="index < 3"
class="corner-decoration top-corner-decoration"
>
<image :src="TOP_ICON_MAP[index]" mode="aspectFit"></image>
</view>
<view class="badge-rank">TOP {{ index + 1 }}</view> <view class="badge-rank">TOP {{ index + 1 }}</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue' import { ref, watch, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import { getHotRankingApi } from '@/utils/api.js' import { getHotRankingApi } from "@/utils/api.js";
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: '在线榜单' default: "在线榜单",
}, },
dimension: { dimension: {
type: String, type: String,
default: 'displaying' default: "displaying",
} },
}) });
const emit = defineEmits(['cardClick']) const emit = defineEmits(["cardClick"]);
const items = ref([]) const items = ref([]);
const loading = ref(false) const loading = ref(false);
const likingMap = ref({}) const likingMap = ref({});
//
const TOP_FRAME_MAP = {
0: "/static/square/top/TOP1biankuang.png",
1: "/static/square/top/TOP2biankuang.png",
2: "/static/square/top/TOP3biankuangpng.png",
};
const TOP_ICON_MAP = {
0: "/static/square/top/TOP1icon.png",
1: "/static/square/top/TOP2icon.png",
2: "/static/square/top/TOP3icon.png",
};
// dimension // dimension
watch(() => props.dimension, () => { watch(
loadData() () => props.dimension,
}) () => {
loadData();
},
);
// //
const formatCount = (count) => { const formatCount = (count) => {
if (!count) return '0' if (!count) return "0";
if (count >= 10000) return (count / 10000).toFixed(1) + 'w' if (count >= 10000) return (count / 10000).toFixed(1) + "w";
if (count >= 1000) return (count / 1000).toFixed(1) + 'k' if (count >= 1000) return (count / 1000).toFixed(1) + "k";
return count.toString() return count.toString();
} };
const handleCardClick = (item) => { const handleCardClick = (item) => {
emit('cardClick', item) emit("cardClick", item);
} };
// //
const onAssetLiked = ({ asset_id, data }) => { const onAssetLiked = ({ asset_id, data }) => {
const index = items.value.findIndex(item => (item.asset_id || item.id) === asset_id) const index = items.value.findIndex(
(item) => (item.asset_id || item.id) === asset_id,
);
if (index !== -1) { if (index !== -1) {
const updatedItems = [...items.value] const updatedItems = [...items.value];
updatedItems[index] = { updatedItems[index] = {
...updatedItems[index], ...updatedItems[index],
is_liked: data?.is_liked ?? true, is_liked: data?.is_liked ?? true,
like_count: data?.new_like_count ?? (updatedItems[index].like_count || 0) + 1 like_count:
} data?.new_like_count ?? (updatedItems[index].like_count || 0) + 1,
items.value = updatedItems };
items.value = updatedItems;
// //
likingMap.value = { ...likingMap.value, [asset_id]: true } likingMap.value = { ...likingMap.value, [asset_id]: true };
setTimeout(() => { setTimeout(() => {
likingMap.value = { ...likingMap.value, [asset_id]: false } likingMap.value = { ...likingMap.value, [asset_id]: false };
}, 600) }, 600);
} }
} };
// //
const loadData = async () => { const loadData = async () => {
loading.value = true loading.value = true;
try { try {
const res = await getHotRankingApi(props.dimension, null, 1, 12) const res = await getHotRankingApi(props.dimension, null, 1, 11);
if (res.code === 200 && res.data?.items) { if (res.code === 200 && res.data?.items) {
items.value = res.data.items.map(item => ({ items.value = res.data.items.map((item) => ({
...item, ...item,
id: item.id || item.asset_id id: item.id || item.asset_id,
})) }));
} }
} catch (e) { } catch (e) {
console.error('[HotCategoryBlock] 加载数据失败', e?.message ?? e) console.error("[HotCategoryBlock] 加载数据失败", e?.message ?? e);
} finally { } finally {
loading.value = false loading.value = false;
} }
} };
onMounted(() => { onMounted(() => {
uni.$on("assetLiked", onAssetLiked);
uni.$on('assetLiked', onAssetLiked) });
})
onShow(()=>{ onShow(() => {
loadData() loadData();
}) });
onUnmounted(() => { onUnmounted(() => {
uni.$off('assetLiked', onAssetLiked) uni.$off("assetLiked", onAssetLiked);
}) });
</script> </script>
<style scoped> <style scoped>
.hot-category-block { .hot-category-block {
padding: 14rpx 14rpx 0; padding: 19rpx 9.5rpx;
border-radius: 24rpx; border-radius: 24rpx;
opacity: 0.8; opacity: 0.8;
background: linear-gradient(161.28deg, rgba(255, 90, 93, 0.2) 16.63%, rgba(76, 237, 255, 0.2) 48.19%, rgba(255, 122, 124, 0.2) 83.71%); background: linear-gradient(
161.28deg,
rgba(255, 90, 93, 0.2) 16.63%,
rgba(76, 237, 255, 0.2) 48.19%,
rgba(255, 122, 124, 0.2) 83.71%
);
backdrop-filter: blur(9.300000190734863px); backdrop-filter: blur(9.300000190734863px);
position: relative; position: relative;
} }
@ -163,8 +229,13 @@ onUnmounted(() => {
border-top-right-radius: 8rpx; border-top-right-radius: 8rpx;
border-bottom-right-radius: 44rpx; border-bottom-right-radius: 44rpx;
border-bottom-left-radius: 4rpx; border-bottom-left-radius: 4rpx;
background: linear-gradient(90deg, rgba(255, 0, 4, 0.73) -3.96%, rgba(254, 141, 103, 0.73) 57.95%, rgba(252, 228, 75, 0.73) 97%); background: linear-gradient(
box-shadow: 2px 2px 4px 0px #D9262640; 90deg,
rgba(255, 0, 4, 0.73) -3.96%,
rgba(254, 141, 103, 0.73) 57.95%,
rgba(252, 228, 75, 0.73) 97%
);
box-shadow: 2px 2px 4px 0px #d9262640;
backdrop-filter: blur(7.599999904632568px); backdrop-filter: blur(7.599999904632568px);
display: flex; display: flex;
align-items: center; align-items: center;
@ -196,14 +267,28 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
} }
/* 骨架屏第一排3个大图 */
.skeleton-card:nth-child(-n + 3) {
width: calc(33.333% - 12rpx);
}
.skeleton-image { .skeleton-image {
width: 100%; width: 100%;
height: 320rpx; height: 192rpx;
background: linear-gradient(90deg, #3a3a4a 25%, #4a4a5a 50%, #3a3a4a 75%); background: linear-gradient(90deg, #3a3a4a 25%, #4a4a5a 50%, #3a3a4a 75%);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
} }
/* 骨架屏:第一排图片更高 */
.skeleton-card:nth-child(-n + 3) .skeleton-image {
height: 236rpx;
border-top-left-radius: 8px;
border-top-right-radius: 24px;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 21px;
}
.skeleton-info { .skeleton-info {
display: flex; display: flex;
align-items: center; align-items: center;
@ -240,22 +325,73 @@ onUnmounted(() => {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
} }
.grid-card { .grid-card {
width: calc(25% - 12rpx); width: calc(25% - 12rpx);
border-radius: 16rpx; border-radius: 16rpx;
overflow: hidden; /* overflow: hidden; */
position: relative; position: relative;
} }
/* 第一排3个大图突出显示 */
.grid-card:nth-child(-n + 3) {
width: calc(33.333% - 12rpx);
}
.grid-card:nth-child(-n + 3) .card-image {
height: 236rpx;
box-shadow: 3px 3px 4.5px 2px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(0px);
padding: 8rpx;
box-sizing: border-box;
}
.card-image { .card-image {
width: 100%; width: 100%;
height: 192rpx; height: 192rpx;
display: block; display: block;
} }
/* 前三名专属:包裹 card-bottom 的边框 */
.card-frame {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 4;
pointer-events: none;
}
.frame-image {
width: 100%;
height: 100%;
display: block;
}
.corner-decoration {
position: absolute;
top: -16rpx;
right: -16rpx;
width: 64rpx;
height: 64rpx;
z-index: 6;
pointer-events: none;
transform: rotate(60deg);
}
.corner-decoration.top-corner-decoration {
left: -24rpx;
right: 0;
}
.corner-decoration image {
width: 100%;
height: 100%;
display: block;
}
/* 底部信息模块 - 独立模块,有背景色和圆角 */ /* 底部信息模块 - 独立模块,有背景色和圆角 */
.card-bottom { .card-bottom {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
@ -264,42 +400,77 @@ onUnmounted(() => {
position: relative; position: relative;
} }
.card-bottom-1,
.card-bottom-2,
.card-bottom-3 {
border-radius: 28rpx;
}
/* Top 排名标签 */ /* Top 排名标签 */
.top-badge { .top-badge {
width: 80rpx; width: 80rpx;
height: 32rpx; height: 32rpx;
border-radius: 16rpx; border-radius: 16rpx;
margin: 16rpx auto; margin: 16rpx auto;
background: linear-gradient(93.1deg, rgba(224, 180, 247, 0.71) -12.06%, rgba(178, 246, 204, 0.71) 52.09%, rgba(98, 178, 244, 0.71) 163.5%); background: linear-gradient(
93.1deg,
rgba(224, 180, 247, 0.71) -12.06%,
rgba(178, 246, 204, 0.71) 52.09%,
rgba(98, 178, 244, 0.71) 163.5%
);
backdrop-filter: blur(11.699999809265137px); backdrop-filter: blur(11.699999809265137px);
overflow: hidden; /* overflow: hidden; */
}
.top-badge-1,
.top-badge-2,
.top-badge-3 {
padding-left: 24rpx;
} }
.badge-rank { .badge-rank {
width: 80rpx; width: 80rpx;
height: 32rpx; height: 32rpx;
color: #FFFABD; color: #fffabd;
font-size: 18rpx; font-size: 18rpx;
font-weight: 600; font-weight: 600;
border-radius: 16rpx; border-radius: 16rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-shadow: -1px 1px 4px #CE0909D6; text-shadow: -1px 1px 4px #ce0909d6;
;
} }
/* 用户信息 */ /* 用户信息 */
.card-info { .card-info {
width: 100%;
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
padding: 0 16rpx 16rpx; padding: 8rpx;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
position: absolute;
bottom: 0;
background: linear-gradient(
177.83deg,
rgba(235, 228, 219, 0) 11.38%,
rgba(138, 135, 131, 0.4) 23.67%,
rgba(255, 231, 231, 0.6) 43.04%,
rgba(255, 255, 255, 0.9) 67.52%,
#ffffff 98.2%
);
backdrop-filter: blur(0px);
}
.card-info-1,
.card-info-2,
.card-info-3 {
padding-bottom: 16rpx;
} }
.user-info { .user-info {
display: flex; display: flex;
align-items: center; align-items: flex-end;
} }
.user-avatar { .user-avatar {
@ -310,8 +481,9 @@ onUnmounted(() => {
} }
.user-name { .user-name {
font-size: 22rpx; font-size: 18rpx;
color: #fff; font-weight: 400;
color: #554545;
max-width: 120rpx; max-width: 120rpx;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -327,12 +499,22 @@ onUnmounted(() => {
opacity: 1; opacity: 1;
border-top-left-radius: 7px; border-top-left-radius: 7px;
border-bottom-right-radius: 21.5px; 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%); background: linear-gradient(
177.83deg,
rgba(83, 244, 211, 0.2) 2.52%,
rgba(15, 9, 0, 0) 69.07%
);
backdrop-filter: blur(0px); backdrop-filter: blur(0px);
z-index: 5; z-index: 5;
} }
.like-badge-1,
.like-badge-2,
.like-badge-3 {
padding: 10rpx 0 0 10rpx;
}
.like-icon-wrapper { .like-icon-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
@ -369,15 +551,19 @@ onUnmounted(() => {
} }
.wf-like-wave-outer { .wf-like-wave-outer {
background: radial-gradient(circle, background: radial-gradient(
rgba(255, 107, 107, 0.8) 0%, circle,
transparent 70%); rgba(255, 107, 107, 0.8) 0%,
transparent 70%
);
} }
.wf-like-wave-inner { .wf-like-wave-inner {
background: radial-gradient(circle, background: radial-gradient(
rgba(255, 184, 0, 0.6) 0%, circle,
transparent 70%); rgba(255, 184, 0, 0.6) 0%,
transparent 70%
);
} }
.wf-like-wave-active { .wf-like-wave-active {

View File

@ -1,39 +1,76 @@
<template> <template>
<view class="square-container"> <view class="square-container">
<!-- 背景图片 --> <!-- 背景图片 -->
<image class="bg-wrapper" src="/static/square/squearbj1.png" mode="aspectFill"></image> <image
class="bg-wrapper"
src="/static/square/squearbj1.png"
mode="aspectFill"
></image>
<!-- Header组件 --> <!-- Header组件 -->
<Header :showGuideIcon="true" :showTaskIcon="true" :showStarActivityIcon="true" backIconColor="#e6e6e6" /> <Header
:showGuideIcon="true"
:showTaskIcon="true"
:showStarActivityIcon="true"
backIconColor="#e6e6e6"
/>
<!-- 蒙层 - 导航栏展开时显示 --> <!-- 蒙层 - 导航栏展开时显示 -->
<view v-if="navExpanded" class="nav-mask" @click="navExpanded = false"></view> <view
v-if="navExpanded"
class="nav-mask"
@click="navExpanded = false"
></view>
<!-- 排行榜弹窗 --> <!-- 排行榜弹窗 -->
<RankingModal :visible="showRankingModal" :parent-active="true" :star-id="currentStarId" <RankingModal
@update:visible="handleRankingModalClose" @visit="handleRankingVisit" /> :visible="showRankingModal"
:parent-active="true"
:star-id="currentStarId"
@update:visible="handleRankingModalClose"
@visit="handleRankingVisit"
/>
<!-- 底部导航栏 --> <!-- 底部导航栏 -->
<BottomNav :activeTab="4" :isExpanded="navExpanded" @update:activeTab="handleTabChange" <BottomNav
@update:isExpanded="navExpanded = $event" /> :activeTab="4"
:isExpanded="navExpanded"
@update:activeTab="handleTabChange"
@update:isExpanded="navExpanded = $event"
/>
<!-- 全局引导遮罩 --> <!-- 全局引导遮罩 -->
<GuideOverlay /> <GuideOverlay />
<!-- 内容区域 --> <!-- 内容区域 -->
<scroll-view class="content-wrapper" scroll-y :show-scrollbar="false" :bounce="false" <scroll-view
@scrolltolower="handleScrollToLower"> class="content-wrapper"
scroll-y
:show-scrollbar="false"
:bounce="false"
@scrolltolower="handleScrollToLower"
>
<!-- 区域一顶部运营轮播图 --> <!-- 区域一顶部运营轮播图 -->
<view class="banner-section"> <view class="banner-section">
<BannerCarousel :bannerActivities="bannerActivities" @activityClick="handleActivityClick" <BannerCarousel
@top3Click="showRankingModal = true" /> :bannerActivities="bannerActivities"
@activityClick="handleActivityClick"
@top3Click="showRankingModal = true"
/>
</view> </view>
<ContentTabs class="tabs" :modelValue="activeContentTab" @update:modelValue="activeContentTab = $event" /> <ContentTabs
class="tabs"
:modelValue="activeContentTab"
@update:modelValue="activeContentTab = $event"
/>
<!-- 在线榜单区块 --> <!-- 在线榜单区块 -->
<view class="hot-category-wrapper"> <view class="hot-category-wrapper">
<HotCategoryBlock :dimension="activeContentTab" @cardClick="handleCardClick" /> <HotCategoryBlock
:dimension="activeContentTab"
@cardClick="handleCardClick"
/>
<view class="hot-more-btn" @click="goToHotCategoryMore"> <view class="hot-more-btn" @click="goToHotCategoryMore">
<text class="hot-more-text">查看更多</text> <text class="hot-more-text">查看更多</text>
</view> </view>
@ -41,99 +78,129 @@
<!-- 区域二主Tab标签区星卡/吧唧/海报 --> <!-- 区域二主Tab标签区星卡/吧唧/海报 -->
<view class="main-tab-section"> <view class="main-tab-section">
<view v-for="(tab, index) in mainTabs" :key="index" class="tab-item" @click="handleMainTabClick(tab)"> <view
<image class="tab-icon" :src="tab.icon" mode="aspectFit" v-for="(tab, index) in mainTabs"
:style="{ width: tab.width + 'rpx', height: tab.height + 'rpx', borderRadius: tab.type === 'badge' ? '50%' : '0', transform: 'rotate(' + tab.rotate + 'deg)' }"> :key="index"
class="tab-item"
@click="handleMainTabClick(tab)"
>
<image
class="tab-icon"
:src="tab.icon"
mode="aspectFit"
:style="{
width: tab.width + 'rpx',
height: tab.height + 'rpx',
borderRadius: tab.type === 'badge' ? '50%' : '0',
transform: 'rotate(' + tab.rotate + 'deg)',
}"
>
</image> </image>
<text class="tab-name">{{ tab.name }}</text> <text class="tab-name">{{ tab.name }}</text>
</view> </view>
</view> </view>
<!-- 区域三分类标签区 --> <!-- 区域三分类标签区 -->
<view id="category-section" class="category-section" :class="{ fixed: isFixed }"> <view
id="category-section"
class="category-section"
:class="{ fixed: isFixed }"
>
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false"> <scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
<view v-for="(category, index) in categories" :key="index" class="category-item" <view
:class="{ active: activeCategoryTab === category.value }" @click="handleCategoryChange(category.value)"> v-for="(category, index) in categories"
:key="index"
class="category-item"
:class="{ active: activeCategoryTab === category.value }"
@click="handleCategoryChange(category.value)"
>
<text class="category-text">{{ category.label }}</text> <text class="category-text">{{ category.label }}</text>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<!-- 区域四创作网格列表 --> <!-- 区域四创作网格列表 -->
<CreationGrid :screenWidth="screenWidth" :screenHeight="screenHeight" :bannerBottom="bannerBottomPx" <CreationGrid
:category="activeCategoryTab" :isActive="isActive" @cardClick="handleCardClick" ref="creationGridRef" /> :screenWidth="screenWidth"
:screenHeight="screenHeight"
:bannerBottom="bannerBottomPx"
:category="activeCategoryTab"
:isActive="isActive"
@cardClick="handleCardClick"
ref="creationGridRef"
/>
</scroll-view> </scroll-view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue' import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { onLoad, onShow, onHide } from '@dcloudio/uni-app' import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import { useStore } from 'vuex' import { useStore } from "vuex";
import Header from '../components/Header.vue' import Header from "../components/Header.vue";
import BottomNav from '../components/BottomNav.vue' import BottomNav from "../components/BottomNav.vue";
import GuideOverlay from '@/components/GuideOverlay.vue' import GuideOverlay from "@/components/GuideOverlay.vue";
import RankingModal from '../components/RankingModal.vue' import RankingModal from "../components/RankingModal.vue";
import BannerCarousel from './components/BannerCarousel.vue' import BannerCarousel from "./components/BannerCarousel.vue";
import ContentTabs from './components/ContentTabs.vue' import ContentTabs from "./components/ContentTabs.vue";
import HotCategoryBlock from './components/HotCategoryBlock.vue' import HotCategoryBlock from "./components/HotCategoryBlock.vue";
import CreationGrid from './components/CreationGrid.vue' import CreationGrid from "./components/CreationGrid.vue";
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js' // import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { useBanner } from './composables/useBanner.js' import { useBanner } from "./composables/useBanner.js";
import { doubleTapLike } from '@/utils/likeHelper.js' import { doubleTapLike } from "@/utils/likeHelper.js";
// ========== Store & User Info ========== // ========== Store & User Info ==========
const store = useStore() const store = useStore();
// const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname || '') // const currentUserNickname = computed(() => store.state.user?.userInfo?.nickname || '')
const currentStarId = ref(uni.getStorageSync('star_id') || null) const currentStarId = ref(uni.getStorageSync("star_id") || null);
// ========== UI State ========== // ========== UI State ==========
const activeContentTab = ref('displaying') const activeContentTab = ref("displaying");
const activeCategoryTab = ref('hot') const activeCategoryTab = ref("hot");
const navExpanded = ref(false) const navExpanded = ref(false);
const showRankingModal = ref(false) const showRankingModal = ref(false);
const isActive = ref(true) const isActive = ref(true);
const isFixed = ref(false) const isFixed = ref(false);
const creationGridRef = ref(null) const creationGridRef = ref(null);
const cardTapTimers = {} const cardTapTimers = {};
const likingMap = ref({}) const likingMap = ref({});
// Tab // Tab
const mainTabs = ref([ const mainTabs = ref([
{ {
name: '星卡', name: "星卡",
type: 'star_card', type: "star_card",
icon: '/static/square/xingka.png', icon: "/static/square/xingka.png",
width: 96, width: 96,
height: 96, height: 96,
rotate: -15 rotate: -15,
}, },
{ {
name: '吧唧', name: "吧唧",
type: 'badge', type: "badge",
icon: '/static/square/baji.png', icon: "/static/square/baji.png",
width: 100, width: 100,
height: 100, height: 100,
rotate: 0 rotate: 0,
}, },
{ {
name: '海报', name: "海报",
type: 'poster', type: "poster",
icon: '/static/square/haibao.png', icon: "/static/square/haibao.png",
width: 100, width: 100,
height: 108, height: 108,
rotate: -15 rotate: -15,
} },
]); ]);
// ========== ========== // ========== ==========
const categories = ref([ const categories = ref([
{ label: '热门作品', value: 'hot' }, { label: "热门作品", value: "hot" },
{ label: '最新作品', value: 'latest' }, { label: "最新作品", value: "latest" },
{ label: '星卡', value: 'star_card' }, { label: "星卡", value: "star_card" },
{ label: '吧唧', value: 'badge' }, { label: "吧唧", value: "badge" },
{ label: '海报', value: 'poster' } { label: "海报", value: "poster" },
]) ]);
// ========== Watch activeContentTab ========== // ========== Watch activeContentTab ==========
// watch(activeContentTab, (newTab) => { // watch(activeContentTab, (newTab) => {
@ -144,17 +211,16 @@ const categories = ref([
// }) // })
// ========== Screen Info ========== // ========== Screen Info ==========
const screenWidth = ref(375) const screenWidth = ref(375);
const screenHeight = ref(812) const screenHeight = ref(812);
// ========== Composables ========== // ========== Composables ==========
const { const { bannerActivities, loadBannerActivities } = useBanner();
bannerActivities,
loadBannerActivities,
} = useBanner()
// banner(216+360rpx) + tab(16+80rpx) + (8rpx) 680rpx // banner(216+360rpx) + tab(16+80rpx) + (8rpx) 680rpx
const bannerBottomPx = computed(() => Math.round(screenWidth.value / 750 * 715)) const bannerBottomPx = computed(() =>
Math.round((screenWidth.value / 750) * 715),
);
// ========== Handlers ========== // ========== Handlers ==========
@ -164,7 +230,6 @@ const handleCardClick = (card) => {
clearTimeout(cardTapTimers[card.id]); clearTimeout(cardTapTimers[card.id]);
delete cardTapTimers[card.id]; delete cardTapTimers[card.id];
// //
likingMap.value = { ...likingMap.value, [card.id]: true }; likingMap.value = { ...likingMap.value, [card.id]: true };
setTimeout(() => { setTimeout(() => {
@ -173,7 +238,7 @@ const handleCardClick = (card) => {
doubleTapLike(card.id, card.exhibition_id || 0, (success) => { doubleTapLike(card.id, card.exhibition_id || 0, (success) => {
if (success) { if (success) {
uni.showToast({ title: '点赞成功', icon: 'success' }) uni.showToast({ title: "点赞成功", icon: "success" });
} }
}); });
} else { } else {
@ -181,96 +246,103 @@ const handleCardClick = (card) => {
if (card.id) { if (card.id) {
cardTapTimers[card.id] = setTimeout(() => { cardTapTimers[card.id] = setTimeout(() => {
delete cardTapTimers[card.id]; delete cardTapTimers[card.id];
uni.navigateTo({ url: `/pages/asset-detail/asset-detail?asset_id=${card.id}` }); uni.navigateTo({
url: `/pages/asset-detail/asset-detail?asset_id=${card.id}`,
});
}, 300); }, 300);
} }
} }
} };
const handleScrollToLower = () => { const handleScrollToLower = () => {
if (creationGridRef.value) { if (creationGridRef.value) {
creationGridRef.value.loadMore() creationGridRef.value.loadMore();
} }
} };
const handleActivityClick = (item) => { const handleActivityClick = (item) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/support-activity/index?id=${item.id}`, url: `/pages/support-activity/index?id=${item.id}`,
}) });
} };
const handleRankingVisit = ({ userId, nickname }) => { const handleRankingVisit = ({ userId, nickname }) => {
showRankingModal.value = false showRankingModal.value = false;
uni.navigateTo({ uni.navigateTo({
url: `/pages/profile/hisWorks?userId=${userId}&nickname=${encodeURIComponent(nickname)}` url: `/pages/profile/hisWorks?userId=${userId}&nickname=${encodeURIComponent(nickname)}`,
}); });
} };
const handleRankingModalClose = (visible) => { const handleRankingModalClose = (visible) => {
showRankingModal.value = visible showRankingModal.value = visible;
if (!visible && store.state.guide.componentMode) { if (!visible && store.state.guide.componentMode) {
uni.$emit('guide:closeComponent') uni.$emit("guide:closeComponent");
} }
} };
const goToHotCategoryMore = () => { const goToHotCategoryMore = () => {
const title = activeContentTab.value === 'displaying' ? '日榜' : activeContentTab.value === 'week' ? '周榜' : '月榜' const title =
activeContentTab.value === "displaying"
? "日榜"
: activeContentTab.value === "week"
? "周榜"
: "月榜";
uni.navigateTo({ uni.navigateTo({
url: `/pages/square/hot-category-more?dimension=${activeContentTab.value}&title=${encodeURIComponent(title)}` url: `/pages/square/hot-category-more?dimension=${activeContentTab.value}&title=${encodeURIComponent(title)}`,
}) });
} };
const handleTabChange = (newTab) => { const handleTabChange = (newTab) => {
if (newTab === 4) { if (newTab === 4) {
navExpanded.value = false navExpanded.value = false;
return return;
} }
const routes = [ const routes = [
'/pages/ai-dazi/index', "/pages/ai-dazi/index",
'/pages/starbook/index', "/pages/starbook/index",
'/pages/castlove/mall', "/pages/castlove/mall",
'/pages/starcity/index', "/pages/starcity/index",
'/pages/square/square' "/pages/square/square",
] ];
if (newTab >= 0 && newTab < routes.length) { if (newTab >= 0 && newTab < routes.length) {
uni.navigateTo({ uni.navigateTo({
url: routes[newTab] url: routes[newTab],
}) });
} }
} };
const handleCategoryChange = (value) => { const handleCategoryChange = (value) => {
if (activeCategoryTab.value === value) return if (activeCategoryTab.value === value) return;
activeCategoryTab.value = value activeCategoryTab.value = value;
} };
// ========== Tile Change Callback ========== // ========== Tile Change Callback ==========
const handleTileChange = () => { } const handleTileChange = () => {};
// ========== Reset Square ========== // ========== Reset Square ==========
const resetSquare = async () => { } const resetSquare = async () => {};
// ========== Lifecycle ========== // ========== Lifecycle ==========
onMounted(() => { onMounted(() => {
const info = uni.getSystemInfoSync() const info = uni.getSystemInfoSync();
screenWidth.value = info.windowWidth screenWidth.value = info.windowWidth;
screenHeight.value = info.windowHeight screenHeight.value = info.windowHeight;
resetSquare() resetSquare();
loadBannerActivities() loadBannerActivities();
}) });
onShow(() => { onShow(() => {
isActive.value = true isActive.value = true;
activeContentTab.value = 'displaying' activeContentTab.value = "displaying";
activeCategoryTab.value = 'hot' activeCategoryTab.value = "hot";
}) });
onHide(() => { onHide(() => {
isActive.value = false isActive.value = false;
}) });
// onLoad((options) => { // onLoad((options) => {
// if (options && 'guide_debug' in options) { // if (options && 'guide_debug' in options) {
@ -298,15 +370,15 @@ onHide(() => {
// }) // })
// //
uni.$on('guide:openComponent', (componentName) => { uni.$on("guide:openComponent", (componentName) => {
if (componentName === 'RankingModal') { if (componentName === "RankingModal") {
showRankingModal.value = true showRankingModal.value = true;
} }
}) });
onUnmounted(() => { onUnmounted(() => {
uni.$off('guide:openComponent') uni.$off("guide:openComponent");
}) });
</script> </script>
<style scoped> <style scoped>
@ -361,7 +433,11 @@ onUnmounted(() => {
justify-content: space-around; justify-content: space-around;
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;
background: linear-gradient(135deg, rgba(240, 228, 177, 0.3), rgba(240, 131, 153, 0.3)); background: linear-gradient(
135deg,
rgba(240, 228, 177, 0.3),
rgba(240, 131, 153, 0.3)
);
border-radius: 24rpx; border-radius: 24rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.2); box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.2);
transition: all 0.3s; transition: all 0.3s;
@ -416,7 +492,7 @@ onUnmounted(() => {
} }
.category-item.active { .category-item.active {
background: linear-gradient(135deg, #F0E4B1, #F08399); background: linear-gradient(135deg, #f0e4b1, #f08399);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.4); box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.4);
} }
@ -446,8 +522,15 @@ onUnmounted(() => {
border-radius: 20rpx; border-radius: 20rpx;
opacity: 0.66; opacity: 0.66;
padding: 8rpx 20rpx; padding: 8rpx 20rpx;
background: linear-gradient(90deg, rgba(255, 222, 8, 0.61) -17.54%, rgba(255, 0, 25, 0.61) 116.67%); background: linear-gradient(
box-shadow: 2px 2px 4px 0px #F2151578; 90deg,
rgba(255, 222, 8, 0.61) -17.54%,
rgba(255, 0, 25, 0.61) 116.67%
);
box-shadow: 2px 2px 4px 0px #f2151578;
display: flex;
justify-content: center;
align-items: center;
} }
.hot-more-text { .hot-more-text {

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB