style:修改主页样式和个人页样式

This commit is contained in:
zheng020 2026-05-28 17:46:00 +08:00
parent c61482df10
commit 59fdac5bc2
10 changed files with 695 additions and 347 deletions

View File

@ -2,10 +2,13 @@
## 一、页面结构
### 完整页面布局4个分类区块纵向堆叠
```
┌──────────────────────────────────────┐
热门推荐
BannerCarousel
├──────────────────────────────────────┤
│ 热门推荐 │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ ← 行1 │
│ └────┘ └────┘ └────┘ └────┘ │
@ -13,6 +16,52 @@
│ │ │ │ │ │ │ │ │ ← 行2 │
│ └────┘ └────┘ └────┘ └────┘ │
│ 🔄 刷新 查看更多 ›│
├──────────────────────────────────────┤
│ 热门星卡 │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ ← 行1 │
│ └────┘ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ ← 行2 │
│ └────┘ └────┘ └────┘ └────┘ │
│ 🔄 刷新 查看更多 ›│
├──────────────────────────────────────┤
│ 热门吧唧 │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ ← 行1 │
│ └────┘ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ ← 行2 │
│ └────┘ └────┘ └────┘ └────┘ │
│ 🔄 刷新 查看更多 ›│
├──────────────────────────────────────┤
│ 热门海报 │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ ← 行1 │
│ └────┘ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ ← 行2 │
│ └────┘ └────┘ └────┘ └────┘ │
│ 🔄 刷新 查看更多 ›│
├──────────────────────────────────────┤
│ CreationGrid原有组件不变
│ [热门作品] [最新作品] [星卡] [吧唧]... │
└──────────────────────────────────────┘
```
### 单个 HotCategoryBlock 内部结构
```
┌──────────────────────────────────────┐
│ 热门推荐 │ ← block-title
├──────────────────────────────────────┤
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ │
│ └────┘ └────┘ └────┘ └────┘ │ ← block-grid (4×2)
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ │ │ │ │ │ │ │ │
│ └────┘ └────┘ └────┘ └────┘ │
│ 🔄 刷新 查看更多 ›│ ← block-actions
└──────────────────────────────────────┘
```

View File

@ -11,10 +11,6 @@ const app = new Vue({
app.$mount()
// #endif
// #ifdef VUE3 && H5
import './pages/castlove/create-laser-upload.css'
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
import store from './store'

View File

@ -5,83 +5,85 @@
<text class="back-icon" :style="{ color: backIconColor }"></text>
</view>
<!-- 右侧图标区域 -->
<view class="right-icons">
<!-- 任务图标 -->
<view v-if="showTaskIcon" class="daily-task-group" @click="handleTaskClick">
<!-- 1. 上层任务图标悬浮在上面 -->
<view class="task-icon-box">
<image class="task-icon-img" src="/static/icon/task.png" mode="aspectFit"></image>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</view>
<!-- 2. 下层文字背景块 -->
<view class="task-text-box">
<text class="task-text-label">每日任务</text>
</view>
<!-- 左侧水晶余额 -->
<view class="crystal-balance-new" @click="handleAvatarClick">
<!-- 1. 左侧钻石图标层级最高盖住右侧背景 -->
<view class="crystal-icon-box">
<image class="crystal-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
</view>
<!-- 新手引导 -->
<!-- <view v-if="showGuideIcon" class="daily-task-group" @click="handleGuideClick">
1. 上层新手引导悬浮在上面
<view class="task-icon-box">
<image class="task-icon-img" src="/static/icon/onboarding-bg.png" mode="aspectFit"></image>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
<!-- 2. 右侧容器用于包裹背景和文字 -->
<view class="crystal-info-container">
<image class="crystal-bg-img" src="/static/square/shuijingzhanshikuang.png" mode="aspectFill">
</image>
<!-- 上层文字内容 -->
<view class="crystal-text-layer">
<text class="balance-number">{{ exhibitionRevenue }}</text>
</view>
2. 下层文字背景块
<view class="task-text-box">
<text class="task-text-label">新手引导</text>
</view>
</view> -->
<!-- 收益文字 -->
<!-- <view class="crystal-bg-layer">
<text class="balance-income">收益 {{ hourlyEarnings }}/</text>
</view> -->
</view>
</view>
<!-- 星援活动列表 -->
<view v-if="showStarActivityIcon" class="star-activity-list">
<view v-for="activity in starActivities" :key="activity.id" class="daily-task-group"
@click="handleActivityClick(activity)">
<!-- 上层活动图标 -->
<!-- 右侧图标区域 -->
<view class="right-icons">
<view class="left-items">
<!-- 任务图标 -->
<view v-if="showTaskIcon" class="daily-task-group" @click="handleTaskClick">
<!-- 1. 上层任务图标悬浮在上面 -->
<view class="task-icon-box">
<image class="task-icon-img" :src="activity.icon || '/static/icon/bus-icon.png'"
mode="aspectFit"></image>
<image class="task-icon-img" src="/static/icon/task.png" mode="aspectFit"></image>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</view>
<!-- 下层文字背景块 -->
<!-- 2. 下层文字背景块 -->
<view class="task-text-box">
<text class="task-text-label">{{ activity.theme || '星援活动' }}</text>
<text class="task-text-label">每日任务</text>
</view>
</view>
<!-- 新手引导 -->
<!-- <view v-if="showGuideIcon" class="daily-task-group" @click="handleGuideClick">
1. 上层新手引导悬浮在上面
<view class="task-icon-box">
<image class="task-icon-img" src="/static/icon/onboarding-bg.png" mode="aspectFit"></image>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</view>
2. 下层文字背景块
<view class="task-text-box">
<text class="task-text-label">新手引导</text>
</view>
</view> -->
<!-- 星援活动列表 -->
<view v-if="showStarActivityIcon" class="star-activity-list">
<view v-for="activity in starActivities" :key="activity.id" class="daily-task-group"
@click="handleActivityClick(activity)">
<!-- 上层活动图标 -->
<view class="task-icon-box">
<image class="task-icon-img" :src="activity.icon || '/static/icon/bus-icon.png'"
mode="aspectFit"></image>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</view>
<!-- 下层文字背景块 -->
<view class="task-text-box">
<text class="task-text-label">{{
activity.theme || "星援活动"
}}</text>
</view>
</view>
</view>
</view>
<!-- 顶粉水晶余额 -->
<view class="crystal-balance-new" @click="handleAvatarClick">
<!-- 1. 左侧钻石图标层级最高盖住右侧背景 -->
<view class="crystal-icon-box">
<image class="crystal-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
</view>
<!-- 2. 右侧容器用于包裹背景和文字 -->
<view class="crystal-info-container">
<image class="crystal-bg-img" src="/static/square/shuijingzhanshikuang.png" mode="aspectFill">
</image>
<!-- 上层文字内容 -->
<view class="crystal-text-layer">
<text class="balance-number">{{ exhibitionRevenue }}</text>
</view>
<!-- 收益文字 -->
<view class="crystal-bg-layer">
<text class="balance-income">收益 {{ hourlyEarnings }}/</text>
</view>
</view>
</view>
<!-- 用户头像 -->
<view class="icon-item avatar-icon" @click="handleAvatarClick">
<Avatar :key="avatarKey" :userId="userUid" :avatarUrl="userAvatarUrl" :size="96" :borderWidth="2" />
<Avatar :key="avatarKey" :userId="userUid" :avatarUrl="userAvatarUrl" :size="68" :borderWidth="2" />
</view>
</view>
@ -94,15 +96,15 @@
</template>
<script setup>
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { useStore } from 'vuex';
import Avatar from './Avatar.vue';
import DailyTasks from '@/pages/tasks/daily-tasks.vue';
import GuideModal from '@/pages/tasks/GuideModal.vue';
import { useBanner } from '@/pages/square/composables/useBanner.js';
import { reportEvent } from '@/utils/task-api.js';
import { getEarningsSummaryApi } from '@/utils/api.js';
import { computed, ref, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { useStore } from "vuex";
import Avatar from "./Avatar.vue";
import DailyTasks from "@/pages/tasks/daily-tasks.vue";
import GuideModal from "@/pages/tasks/GuideModal.vue";
import { useBanner } from "@/pages/square/composables/useBanner.js";
import { reportEvent } from "@/utils/task-api.js";
import { getEarningsSummaryApi } from "@/utils/api.js";
// square useBanner
const { bannerActivities, loadBannerActivities } = useBanner();
@ -111,28 +113,28 @@ const { bannerActivities, loadBannerActivities } = useBanner();
const props = defineProps({
showBack: {
type: Boolean,
default: false
default: false,
},
backUrl: {
type: String,
default: ''
default: "",
},
backIconColor: {
type: String,
default: '#000000'
default: "#000000",
},
showTaskIcon: {
type: Boolean,
default: true
default: true,
},
showGuideIcon: {
type: Boolean,
default: true
default: true,
},
showStarActivityIcon: {
type: Boolean,
default: true
}
default: true,
},
});
const store = useStore();
@ -150,24 +152,24 @@ const showGuideModal = ref(false);
//
const loadUserInfo = () => {
try {
const userStr = uni.getStorageSync('user');
const userStr = uni.getStorageSync("user");
if (userStr) {
userInfo.value = JSON.parse(userStr);
}
} catch (e) {
console.error('解析用户信息失败:', e);
console.error("解析用户信息失败:", e);
userInfo.value = null;
}
};
// UID
const userUid = computed(() => {
return userInfo.value?.uid || '';
return userInfo.value?.uid || "";
});
// URL
const userAvatarUrl = computed(() => {
return userInfo.value?.avatar_url || '';
return userInfo.value?.avatar_url || "";
});
// //
@ -190,7 +192,7 @@ const loadEarningsSummary = async () => {
hourlyEarnings.value = data.total_hourly_earnings || 0;
}
} catch (e) {
console.error('获取收益汇总失败:', e);
console.error("获取收益汇总失败:", e);
}
};
@ -213,7 +215,10 @@ const handleUserInfoUpdate = () => {
//
const handleBalanceUpdate = (data) => {
if (userInfo.value) {
userInfo.value = { ...userInfo.value, crystal_balance: data.crystal_balance };
userInfo.value = {
...userInfo.value,
crystal_balance: data.crystal_balance,
};
exhibitionRevenue.value = data.crystal_balance || 0;
} else {
loadUserInfo();
@ -223,43 +228,51 @@ const handleBalanceUpdate = (data) => {
//
function checkAndReportDailyLogin() {
// 5
const now = new Date()
const currentHour = now.getHours()
if (currentHour < 5) return // 5
const now = new Date();
const currentHour = now.getHours();
if (currentHour < 5) return; // 5
const today = now.toISOString().split('T')[0]
const starId = uni.getStorageSync('star_id')
if (!starId) return
const today = now.toISOString().split("T")[0];
const starId = uni.getStorageSync("star_id");
if (!starId) return;
// ID
const userStr = uni.getStorageSync('user')
let userId = null
const userStr = uni.getStorageSync("user");
let userId = null;
if (userStr) {
try {
const user = JSON.parse(userStr)
userId = user?.uid || null
const user = JSON.parse(userStr);
userId = user?.uid || null;
} catch (e) { }
}
if (!userId) return
if (!userId) return;
//
const dailyLoginKey = `daily_login_completed_${today}_${userId}_${starId}`
const dailyLoginKey = `daily_login_completed_${today}_${userId}_${starId}`;
if (!uni.getStorageSync(dailyLoginKey)) {
reportEvent('daily_login', starId).then(() => {
//
uni.setStorageSync(dailyLoginKey, true)
const yesterday = new Date(now.getTime() - 86400000).toISOString().split('T')[0]
uni.removeStorageSync(`daily_login_completed_${yesterday}_${userId}_${starId}`)
}).catch(err => {
console.error('上报登录事件失败:', err)
})
reportEvent("daily_login", starId)
.then(() => {
//
uni.setStorageSync(dailyLoginKey, true);
const yesterday = new Date(now.getTime() - 86400000)
.toISOString()
.split("T")[0];
uni.removeStorageSync(
`daily_login_completed_${yesterday}_${userId}_${starId}`,
);
})
.catch((err) => {
console.error("上报登录事件失败:", err);
});
}
}
// - bannerActivities expired
const starActivities = computed(() => {
// return bannerActivities.value
return bannerActivities.value.filter(activity => activity.status !== 'expired');
return bannerActivities.value.filter(
(activity) => activity.status !== "expired",
);
});
//
@ -267,9 +280,9 @@ onMounted(async () => {
await loadUserInfo();
await loadBannerActivities();
// await loadEarningsSummary();
uni.$on('avatarUpdated', handleAvatarUpdate);
uni.$on('userInfoUpdated', handleUserInfoUpdate);
uni.$on('balanceUpdated', handleBalanceUpdate);
uni.$on("avatarUpdated", handleAvatarUpdate);
uni.$on("userInfoUpdated", handleUserInfoUpdate);
uni.$on("balanceUpdated", handleBalanceUpdate);
// 5
checkAndReportDailyLogin();
@ -282,16 +295,16 @@ onShow(() => {
//
onUnmounted(() => {
uni.$off('avatarUpdated', handleAvatarUpdate);
uni.$off('userInfoUpdated', handleUserInfoUpdate);
uni.$off('balanceUpdated', handleBalanceUpdate);
uni.$off("avatarUpdated", handleAvatarUpdate);
uni.$off("userInfoUpdated", handleUserInfoUpdate);
uni.$off("balanceUpdated", handleBalanceUpdate);
});
//
const handleBack = () => {
if (props.backUrl) {
uni.navigateTo({
url: props.backUrl
url: props.backUrl,
});
} else {
//
@ -302,7 +315,7 @@ const handleBack = () => {
} else {
// square
uni.reLaunch({
url: '/pages/square/square'
url: "/pages/square/square",
});
}
}
@ -316,7 +329,7 @@ const handleTaskClick = () => {
const handleActivityClick = (activity) => {
if (activity) {
uni.navigateTo({
url: `/pages/support-activity/index?id=${activity.id}`
url: `/pages/support-activity/index?id=${activity.id}`,
});
}
};
@ -329,22 +342,22 @@ const handleGuideClick = () => {
//
const handleGuideUpdated = () => {
//
console.log('[Header] Guide updated');
console.log("[Header] Guide updated");
};
//
const handleStartGuide = (params) => {
const key = typeof params === 'string' ? params : (params?.key || '');
const fromStep = typeof params === 'object' ? (params.fromStep ?? 0) : 0;
const targetPage = typeof params === 'object' ? (params.targetPage || '') : '';
const key = typeof params === "string" ? params : params?.key || "";
const fromStep = typeof params === "object" ? (params.fromStep ?? 0) : 0;
const targetPage = typeof params === "object" ? params.targetPage || "" : "";
console.log('[Header] handleStartGuide:', { key, fromStep, targetPage });
console.log("[Header] handleStartGuide:", { key, fromStep, targetPage });
showGuideListModal.value = false;
//
if (key === 'square_home') {
const navigateUrl = targetPage || '/pages/square/square';
if (key === "square_home") {
const navigateUrl = targetPage || "/pages/square/square";
uni.navigateTo({
url: navigateUrl,
success: () => {
@ -355,7 +368,7 @@ const handleStartGuide = (params) => {
store.dispatch("guide/resumeGuide", key);
}
}, 500);
}
},
});
} else {
if (fromStep === 0) {
@ -383,20 +396,20 @@ const handleAvatarClick = () => {
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
//
if (currentPage.route === 'pages/profile/myWorks') {
if (currentPage.route === "pages/profile/myWorks") {
//
return;
}
}
uni.navigateTo({
url: '/pages/profile/myWorks'
url: "/pages/profile/myWorks",
});
};
//
defineExpose({
showTaskModal,
closeTaskModal
closeTaskModal,
});
</script>
@ -438,32 +451,43 @@ defineExpose({
margin-left: auto;
}
.left-items {
display: flex;
align-items: center;
justify-content: flex-start;
}
.star-activity-list {
display: flex;
align-items: center;
gap: 8rpx;
width: 68rpx;
/* 控制整体宽度,根据图标大小调整 */
height: 156rpx;
/* 预估高度,包含图标和文字块 */
}
.daily-task-group {
position: relative;
/* 必须是相对定位,作为子元素的定位基准 */
width: 100rpx;
width: 68rpx;
/* 控制整体宽度,根据图标大小调整 */
height: 120rpx;
height: 156rpx;
/* 预估高度,包含图标和文字块 */
margin-right: 24rpx;
}
/* --- 上层图标样式 --- */
.task-icon-box {
position: absolute;
top: 8rpx;
top: 40rpx;
/* 固定在顶部 */
left: 50%;
transform: translateX(-50%);
/* 水平居中 */
width: 88rpx;
width: 34rpx;
/* 图标宽度 */
height: 88rpx;
height: 40rpx;
/* 图标高度 */
z-index: 10;
/* 确保图标在文字块上面 */
@ -490,12 +514,13 @@ defineExpose({
/* 关键:调整这个值,让文字块往上顶,与图标产生重叠 */
left: 50%;
transform: translateX(-50%);
width: 88rpx;
height: 80rpx;
background-image: url('/static/square/gerenzhongxincangpinkuang.png');
width: 60rpx;
height: 60rpx;
background-image: url("/static/square/gerenzhongxincangpinkuang.png");
background-size: 100% 100%;
background-repeat: no-repeat;
border-radius: 20rpx;
box-shadow: 0px 4px 4px 0px #ee262654;
display: flex;
align-items: center;
justify-content: center;
@ -504,11 +529,13 @@ defineExpose({
}
.task-text-label {
font-size: 16rpx;
color: #fff;
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.9);
font-weight: 500;
font-size: 12rpx;
line-height: 100%;
letter-spacing: 0%;
color: #fff9e7;
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.84);
margin-top: 32rpx;
}
/* 水晶余额组件 */
@ -526,13 +553,14 @@ defineExpose({
/* --- 左侧钻石图标 --- */
.crystal-icon-box {
position: absolute;
left: 0;
bottom: 10rpx;
left: 8rpx;
bottom: 24rpx;
/* 与右侧底部平齐 */
z-index: 10;
/* 最高层级:盖住右侧背景 */
width: 100rpx;
height: 100rpx;
width: 68rpx;
height: 60rpx;
transform: rotate(-15deg);
opacity: 1;
}
.crystal-icon {
@ -543,12 +571,14 @@ defineExpose({
/* --- 右侧容器 --- */
.crystal-info-container {
position: relative;
height: 56rpx;
width: 80rpx;
border-radius: 16rpx;
bottom: 4rpx;
padding-right: 28rpx;
padding-left: 56rpx;
height: 34rpx;
width: 120rpx;
opacity: 1;
border-radius: 20rpx;
border-width: 1px;
border: 1px solid #F3A68A40;
box-shadow: 2px 2px 4.1px 0px #F936365C;
}
.crystal-bg-img {
@ -567,14 +597,18 @@ defineExpose({
display: flex;
flex-direction: column;
align-items: center;
margin-top: 8rpx;
border-radius: 120rpx;
opacity: 0.75;
background: linear-gradient(270deg, rgba(168, 17, 255, 0.83) 0%, rgba(30, 180, 217, 0.470865) 81.01%, rgba(101, 10, 153, 0) 178.07%);
}
.balance-number {
font-size: 24rpx;
font-weight: bold;
color: #FFB800;
font-family: 'yt', sans-serif;
color: #ffb800;
font-family: "yt", sans-serif;
text-shadow:
0 0 10rpx rgba(255, 184, 0, 0.8),
0 2rpx 4rpx rgba(0, 0, 0, 0.5);
@ -586,14 +620,14 @@ defineExpose({
.balance-income {
font-size: 18rpx;
color: #FFFFFF;
color: #ffffff;
margin-bottom: 2rpx;
margin-left: 28rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.9);
}
/* --- 下层:渐变背景 --- */
.crystal-bg-layer {
/* .crystal-bg-layer {
display: flex;
align-items: center;
justify-content: center;
@ -605,9 +639,9 @@ defineExpose({
z-index: 1;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E7399 100%);
#f0e4b1 0%,
#f08399 50%,
#b94e7399 100%);
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
@ -615,15 +649,13 @@ defineExpose({
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
border-radius: 10rpx;
}
} */
.icon-item {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-left: 32rpx;
margin-top: 16rpx;
}
.task-icon {

View File

@ -2,18 +2,22 @@
<view class="profile-container">
<!-- 背景图片 - 固定在容器上 -->
<image class="background-image" src="/static/square/squearbj.png" mode="aspectFill"></image>
<scroll-view class="profile-scroll" scroll-y :show-scrollbar="false" :enable-back-to-top="true">
<!-- 可替换背景装饰层 -->
<view class="replaceable-bg-layer">
<image class="replaceable-bg-overlay-image" :src="replaceableBgOverlay" mode="aspectFill"></image>
</view>
<!-- Header组件 -->
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
</view>
<view class="profile-scroll">
<!-- 右上角模块 -->
<view class="top-right-module">
<text class="level-label">LV:</text>
<text class="level-value">{{ fanLevel }}</text>
</view>
<!-- 上半部分用户信息卡片 -->
<view class="top-section">
<!-- 蒙层 -->
<view class="background-overlay"></view>
<!-- Header组件 -->
<view class="nav-back" @tap="goBack">
<text class="nav-back-icon"></text>
</view>
<!-- 用户信息卡片 -->
<view class="user-info-card">
<!-- 上半部分头像和用户信息左右布局 -->
@ -43,7 +47,7 @@
<text class="info-label">链上地址</text>
<view class="address-row">
<text class="info-value address-value" @longpress="copyAddress">{{ displayAddress
}}</text>
}}</text>
<view class="toggle-btn" @tap="showBlockNumber = !showBlockNumber">
<image class="toggle-icon"
:src="showBlockNumber ? '/static/icon/show.png' : '/static/icon/hide.png'"
@ -270,7 +274,7 @@
<view class="logout-section">
<button class="logout-button" @tap="handleLogout">退出登录</button>
</view>
</scroll-view>
</view>
<!-- 新手引导弹窗 -->
<GuideModal :visible="showGuideModal" @close="showGuideModal = false" @updated="handleGuideUpdated" />
@ -356,6 +360,15 @@ const mobile = ref('');
const showBlockNumber = ref(false);
const showMobile = ref(false);
//
const replaceableBgOverlay = ref('/static/sucai/image-06.png');
//
const updateBgOverlay = (newOverlayImage) => {
replaceableBgOverlay.value = newOverlayImage;
};
// showMobile
const displayMobile = computed(() => {
console.log('displayMobile computed, mobile:', mobile.value, 'showMobile:', showMobile.value);
@ -1263,10 +1276,18 @@ onShow(() => {
}
.profile-scroll {
width: 100%;
height: 100%;
position: relative;
z-index: 1;
width: 616rpx;
height: 955.2rpx;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
border-radius: 86rpx;
background: linear-gradient(133.78deg, rgba(154, 146, 255, 0.19) 9.69%, rgba(255, 202, 229, 0.19) 43.91%, rgba(255, 250, 253, 0.1881) 76.13%, rgba(63, 63, 76, 0.19) 91.61%);
backdrop-filter: blur(35.70000076293945px);
box-shadow: 2px 4px 1.8px 0px #D629291F;
}
/* 隐藏滚动条 */
@ -1276,6 +1297,35 @@ onShow(() => {
height: 0;
}
/* 右上角模块 */
.top-right-module {
position: fixed;
width: 313.6rpx;
height: 73.6rpx;
top: -16rpx;
right: 0;
border-top-right-radius: 35.5rpx;
border-top-left-radius: 7rpx;
border-bottom-left-radius: 35.5rpx;
border-bottom-right-radius: 6rpx;
background: linear-gradient(135deg, #9E90FF 0%, #FFCAD3 100%);
z-index: 4;
display: flex;
align-items: center;
justify-content: center;
}
.level-label {
color: #fff;
font-size: 24rpx;
}
.level-value {
color: #fff;
font-size: 28rpx;
font-weight: bold;
}
/* 上半部分:用户信息区域 */
.top-section {
position: relative;
@ -1298,12 +1348,26 @@ onShow(() => {
/* filter: blur(20rpx); */
}
.replaceable-bg-layer {
position: fixed;
width: 100%;
height: 80vh;
z-index: 1;
pointer-events: none;
}
.replaceable-bg-overlay-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.background-overlay {
display: none;
}
.nav-back {
position: relative;
position: fixed;
top: 96rpx;
left: 32rpx;
width: 80rpx;
@ -1313,6 +1377,7 @@ onShow(() => {
justify-content: center;
/* background: rgba(255,255,255,0.5);
border-radius: 50%; */
z-index: 4;
}
.nav-back-icon {

View File

@ -1,23 +1,88 @@
<template>
<view class="creation-grid">
<view v-for="(item, index) in creationList" :key="item.id" class="creation-card" @click="handleCardClick(item)">
<image class="creation-image" :src="item.cover_image" mode="aspectFill"></image>
<!-- 光波动画层 - 外层 -->
<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-id">
<text class="id-text">#{{ item.certificate_id }}</text>
</view>
<view class="creation-meta">
<view class="creator-info">
<image class="creator-avatar" :src="item.creator_avatar" mode="aspectFill"></image>
<text class="creator-name">{{ item.creator_name }}</text>
<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="like-info">
<image class="like-icon" src="/static/icon/heart-icon.png" mode="aspectFit"></image>
<text class="like-count">{{ formatCount(item.like_count) }}</text>
<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>
@ -33,154 +98,248 @@
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { getInspirationFlowApi } from '@/utils/api.js'
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: '' },
category: { type: String, default: "" },
isActive: { type: Boolean, default: true },
})
});
const emit = defineEmits(['cardClick', 'scroll'])
const emit = defineEmits(["cardClick", "scroll"]);
const creationList = ref([])
const cursor = ref('')
const isLoading = ref(false)
const noMore = ref(false)
const likingMap = ref({})
let isComponentMounted = false
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()
}
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)
}
emit("cardClick", item);
};
const loadUsers = async () => {
if (!isComponentMounted) return Promise.resolve()
if (!isComponentMounted) return Promise.resolve();
cursor.value = ''
isLoading.value = true
noMore.value = false
cursor.value = "";
isLoading.value = true;
noMore.value = false;
try {
const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: '' })
if (!isComponentMounted) return
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 || ''
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 || '',
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,
};
});
// col1col2
col1.value = creationList.value.filter((_, i) => i % 2 === 0);
col2.value = creationList.value.filter((_, i) => i % 2 === 1);
} else {
noMore.value = true
noMore.value = true;
}
} catch (e) {
console.error('[CreationGrid] 加载数据失败', e?.message ?? e)
console.error("[CreationGrid] 加载数据失败", e?.message ?? e);
} finally {
isLoading.value = false
isLoading.value = false;
}
}
};
const loadMore = async () => {
if (!isComponentMounted || isLoading.value || noMore.value) return
if (!isComponentMounted || isLoading.value || noMore.value) return;
isLoading.value = true
isLoading.value = true;
try {
const res = await getInspirationFlowApi({ limit: 20, type: props.category, cursor: cursor.value })
if (!isComponentMounted) return
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 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 || '',
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]
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
noMore.value = true;
}
} catch (e) {
console.error('[CreationGrid] 加载更多失败', e?.message ?? e)
console.error("[CreationGrid] 加载更多失败", e?.message ?? e);
} finally {
isLoading.value = false
isLoading.value = false;
}
}
};
watch(() => props.category, () => {
loadUsers()
})
watch(
() => props.category,
() => {
loadUsers();
},
);
watch(() => props.isActive, (active) => {
if (active && creationList.value.length === 0) {
loadUsers()
}
})
watch(
() => props.isActive,
(active) => {
if (active && creationList.value.length === 0) {
loadUsers();
}
},
);
onMounted(() => {
isComponentMounted = true
isComponentMounted = true;
//
uni.$on('assetLiked', ({ asset_id, data }) => {
uni.$on("assetLiked", ({ asset_id, data }) => {
//
likingMap.value = { ...likingMap.value, [asset_id]: true }
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 && data && typeof data.new_like_count === 'number') {
creationList.value[idx].like_count = data.new_like_count
creationList.value = [...creationList.value]
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()
})
});
loadUsers();
});
onUnmounted(() => {
isComponentMounted = false
uni.$off('assetLiked')
})
isComponentMounted = false;
uni.$off("assetLiked");
});
defineExpose({ loadMore })
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;
flex-wrap: wrap;
justify-content: space-between;
padding: 24rpx;
padding-bottom: 120rpx;
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: 48%;
width: 100%;
margin-bottom: 24rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 20rpx;
@ -192,27 +351,77 @@ defineExpose({ loadMore })
.creation-image {
width: 100%;
height: 400rpx;
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: 400rpx;
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%);
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%);
background: radial-gradient(
circle,
rgba(255, 184, 0, 0.6) 0%,
transparent 70%
);
}
.wf-like-wave-active {
@ -224,6 +433,7 @@ defineExpose({ loadMore })
opacity: 0.9;
transform: scale(0.8);
}
100% {
opacity: 0;
transform: scale(1.5);
@ -231,24 +441,29 @@ defineExpose({ loadMore })
}
.creation-info {
padding: 16rpx;
}
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%
);
.creation-id {
margin-bottom: 12rpx;
}
.id-text {
font-size: 22rpx;
color: #FFB800;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
backdrop-filter: blur(0px);
}
.creation-meta {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 8rpx;
left: 40rpx;
}
.creator-info {
@ -256,16 +471,18 @@ defineExpose({ loadMore })
align-items: center;
}
.creator-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 8rpx;
}
.creator-name {
font-size: 22rpx;
color: #fff;
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 {
@ -273,17 +490,6 @@ defineExpose({ loadMore })
align-items: center;
}
.like-icon {
width: 24rpx;
height: 24rpx;
margin-right: 4rpx;
}
.like-count {
font-size: 22rpx;
color: #fff;
}
.loading-more,
.no-more {
width: 100%;
@ -296,4 +502,4 @@ defineExpose({ loadMore })
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
</style>
</style>

View File

@ -56,7 +56,7 @@
</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">
<view
v-for="(category, index) in categories"
@ -68,7 +68,7 @@
<text class="category-text">{{ category.label }}</text>
</view>
</scroll-view>
</view>
</view> -->
<!-- 热门分类区块 -->
<view
@ -111,14 +111,14 @@ import BannerCarousel from './components/BannerCarousel.vue'
import HotCategoryBlock from './components/HotCategoryBlock.vue'
import CreationGrid from './components/CreationGrid.vue'
import { getHotInspirationFlowBatchApi } from '@/utils/api.js'
import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
// import { clearSubStepProgress, shouldShowGuideStartModal } from '@/utils/guideConfig.js'
import { useBanner } from './composables/useBanner.js'
import { doubleTapLike } from '@/utils/likeHelper.js'
import { USE_MOCK_DATA } from './config/mockData.js'
// import { USE_MOCK_DATA } from './config/mockData.js'
// ========== Store & User Info ==========
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)
// ========== UI State ==========
@ -289,30 +289,30 @@ onHide(() => {
isActive.value = false
})
onLoad((options) => {
if (options && 'guide_debug' in options) {
const debugValue = options.guide_debug
const isDebug = debugValue === '1' || debugValue === 'true'
if (isDebug) {
uni.setStorageSync('guide_debug_mode', true)
uni.setStorageSync('is_new_user', true)
console.log('[Guide] 调试模式已开启')
} else {
uni.setStorageSync('guide_debug_mode', false)
uni.removeStorageSync('is_new_user')
console.log('[Guide] 调试模式已关闭')
}
}
// onLoad((options) => {
// if (options && 'guide_debug' in options) {
// const debugValue = options.guide_debug
// const isDebug = debugValue === '1' || debugValue === 'true'
// if (isDebug) {
// uni.setStorageSync('guide_debug_mode', true)
// uni.setStorageSync('is_new_user', true)
// console.log('[Guide] ')
// } else {
// uni.setStorageSync('guide_debug_mode', false)
// uni.removeStorageSync('is_new_user')
// console.log('[Guide] ')
// }
// }
if (options && options.guide_key) {
console.log('[Guide] 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step)
store.dispatch('guide/resumeGuide', options.guide_key).then(res => {
console.log('[Guide] resumeGuide 结果:', res)
}).catch(err => {
console.error('[Guide] resumeGuide 失败:', err)
})
}
})
// if (options && options.guide_key) {
// console.log('[Guide] , guide_key:', options.guide_key, 'guide_step:', options.guide_step)
// store.dispatch('guide/resumeGuide', options.guide_key).then(res => {
// console.log('[Guide] resumeGuide :', res)
// }).catch(err => {
// console.error('[Guide] resumeGuide :', err)
// })
// }
// })
//
uni.$on('guide:openComponent', (componentName) => {
@ -350,7 +350,7 @@ onUnmounted(() => {
position: relative;
z-index: 1;
width: 100%;
height: calc(100vh - 64rpx);
height: 100vh;
/* margin-top: 160rpx; */
padding: 240rpx 24rpx 0;
box-sizing: border-box;

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB