feat(frontend): 全面前端更新、资源添加与问题修复

- 新增并更新多个静态图标和图片资源
- 修复多个页面的返回图标路径和格式问题
- 简化用户模块的手机号缓存逻辑,移除冗余的脱敏手机号回退方案
- 更新本地开发环境的API基础请求地址
- 优化底部导航栏,支持自定义图标尺寸和旋转效果
- 重做主题横幅的进度展示样式与布局
- 为工艺选择页面添加路由映射和更完善的跳转逻辑
- 为用户个人作品页面新增倒计时计时器和收益领取功能
- 升级藏品详情页面:优化布局、新增用户头像展示、链上数据切换功能,解决代码合并冲突
- 重构用户个人中心页面:更新UI样式、新增敏感信息显示切换开关,替换为新的引导弹窗
This commit is contained in:
liulong 2026-05-15 23:51:30 +08:00
commit d64d5d8c43
18 changed files with 920 additions and 455 deletions

View File

@ -129,7 +129,7 @@
</view> </view>
<!-- 详情内容 --> <!-- 详情内容 -->
<scroll-view v-else scroll-y class="content-scroll"> <scroll-view v-else scroll-y class="content-scroll" :show-scrollbar="false">
<view class="content-wrapper"> <view class="content-wrapper">
<!-- 藏品卡片区域 --> <!-- 藏品卡片区域 -->
<view class="card-section"> <view class="card-section">
@ -171,8 +171,9 @@
<!-- 点赞 + 收益 + 倒计时行 --> <!-- 点赞 + 收益 + 倒计时行 -->
<view class="card-meta-row"> <view class="card-meta-row">
<!-- 点赞 --> <!-- 点赞 -->
<view class="like-area" @tap="handleLike"> <view v-if="assetData.display_status === 1" class="like-area" @tap="handleLike">
<image :src="isLiked ? '/static/icon/like-after.png' : '/static/icon/like-before.png'" class="like-icon" mode="aspectFit"></image> <image :src="isLiked ? '/static/icon/like-after.png' : '/static/icon/like-before.png'"
class="like-icon" mode="aspectFit"></image>
<text class="like-num">{{ likeCount }}</text> <text class="like-num">{{ likeCount }}</text>
</view> </view>
@ -183,7 +184,7 @@
</view> </view>
<!-- 倒计时如有 --> <!-- 倒计时如有 -->
<view v-if="showCountdown" class="countdown-area"> <view v-if="assetData.display_status === 1 && countdownText" class="countdown-area">
<view class="countdown-pill"> <view class="countdown-pill">
<text class="countdown-val">{{ countdownText }}</text> <text class="countdown-val">{{ countdownText }}</text>
</view> </view>
@ -193,10 +194,19 @@
<!-- 信息行创作者 + 铸造时间 + 来源 --> <!-- 信息行创作者 + 铸造时间 + 来源 -->
<view class="info-row"> <view class="info-row">
<view class="info-col">
<view class="info-item">
<text class="info-label">来源</text>
<text class="info-value">{{ assetData.source || '铸造' }}</text>
</view>
</view>
<view class="info-col"> <view class="info-col">
<view class="info-item"> <view class="info-item">
<text class="info-label">创作者</text> <text class="info-label">创作者</text>
<text class="info-value">{{ assetData.owner_nickname || '未知' }}</text> <view class="info-nickname">
<image v-if="userAvatarUrl" class="info-avatar" :src="userAvatarUrl" mode="aspectFill"></image>
<text class="info-value">{{ assetData.owner_nickname || '未知' }}</text>
</view>
</view> </view>
</view> </view>
<view class="info-col"> <view class="info-col">
@ -205,27 +215,16 @@
<text class="info-value">{{ formattedAcquireTime }}</text> <text class="info-value">{{ formattedAcquireTime }}</text>
</view> </view>
</view> </view>
<view class="info-col">
<view class="info-item">
<text class="info-label">来源</text>
<text class="info-value">{{ assetData.source || '铸造' }}</text>
</view>
</view>
</view> </view>
<!-- 创作者信息模块 --> <!-- 创作者信息模块 -->
<view class="creator-section" @tap="showLikeUsersModal = true"> <!-- <view v-if="likeCount > 0" class="creator-section" @tap="showLikeUsersModal = true"> -->
<!-- 点赞用户头像区域 --> <!-- 点赞用户头像区域 -->
<view class="liked-users-area"> <!-- <view class="liked-users-area">
<view <view v-for="(user, index) in likedUsers" :key="index" class="liked-user-avatar" :style="{
v-for="(user, index) in likedUsers" transform: `translate(${user.ellipseX || 0}rpx, ${user.ellipseY || 0}rpx) scale(${user.size || 1})`,
:key="index" zIndex: index < 2 ? index + 1 : (index < 4 ? index + 3 : index + 1)
class="liked-user-avatar" }">
:style="{
transform: `translate(${user.ellipseX || 0}rpx, ${user.ellipseY || 0}rpx) scale(${user.size || 1})`,
zIndex: index < 2 ? index + 1 : (index < 4 ? index + 3 : index + 1)
}"
>
<image class="liked-user-image" :src="user.avatar" mode="aspectFill"></image> <image class="liked-user-image" :src="user.avatar" mode="aspectFill"></image>
</view> </view>
</view> </view>
@ -233,10 +232,24 @@
<text class="creator-title">TA们最近为你点过赞</text> <text class="creator-title">TA们最近为你点过赞</text>
<view class="creator-card"> <view class="creator-card">
<text class="creator-tip">点击查看</text> <text class="creator-tip">点击查看</text>
<image class="creator-preview" src="/static/rank/activity-support-icon/tubiao.png" mode="aspectFill"></image> <image class="creator-preview" src="/static/rank/activity-support-icon/tubiao.png"
mode="aspectFill"></image>
</view>
</view> -->
<!-- </view> -->
<!-- 完整链上哈希显示遮罩层 -->
<view v-if="showTxHash" class="txhash-mask" @tap="showTxHash = false">
<view class="txhash-popup" @tap.stop>
<view class="txhash-popup-header">
<text class="txhash-popup-title">链上哈希</text>
<view class="txhash-popup-close" @tap="showTxHash = false">
<image class="close-icon" src="/static/icon/hide.png" mode="aspectFit"></image>
</view> </view>
</view> </view>
<text class="txhash-popup-content" selectable @longpress="copyHash">{{ displayTxHash }}</text>
</view> </view>
</view>
<!-- 链上数据 --> <!-- 链上数据 -->
<view class="chain-section"> <view class="chain-section">
@ -257,18 +270,19 @@
<view class="chain-row"> <view class="chain-row">
<text class="chain-label">区块链编号</text> <text class="chain-label">区块链编号</text>
<view class="chain-value-wrap"> <view class="chain-value-wrap">
<text class="chain-value">{{ showBlockNumber ? (assetData.block_number || '未知') : hiddenBlockNumber }}</text> <text class="chain-value">{{ showBlockNumber ? (assetData.block_number || '未知') :
hiddenBlockNumber }}</text>
<view class="toggle-btn" @tap="showBlockNumber = !showBlockNumber"> <view class="toggle-btn" @tap="showBlockNumber = !showBlockNumber">
<text class="toggle-icon">{{ showBlockNumber ? '👁' : '👁‍🗨' }}</text> <image class="toggle-icon" :src="showBlockNumber ? '/static/icon/show.png' : '/static/icon/hide.png'" mode="aspectFit"></image>
</view> </view>
</view> </view>
</view> </view>
<view class="chain-row"> <view class="chain-row">
<text class="chain-label">交易哈希</text> <text class="chain-label">链上哈希</text>
<view class="chain-value-wrap"> <view class="chain-value-wrap">
<text class="chain-value chain-hash" @longpress="copyHash">{{ showTxHash ? displayTxHash : hiddenTxHash }}</text> <text class="chain-value chain-hash" @longpress="copyHash">{{ hiddenTxHash }}</text>
<view class="toggle-btn" @tap="showTxHash = !showTxHash"> <view class="toggle-btn" @tap="showTxHash = !showTxHash">
<text class="toggle-icon">{{ showTxHash ? '👁' : '👁‍🗨' }}</text> <image class="toggle-icon" :src="showTxHash ? '/static/icon/show.png' : '/static/icon/hide.png'" mode="aspectFit"></image>
</view> </view>
</view> </view>
</view> </view>
@ -278,30 +292,19 @@
</scroll-view> </scroll-view>
<!-- 点赞用户弹窗 --> <!-- 点赞用户弹窗 -->
<LikeUsersModal <LikeUsersModal :visible="showLikeUsersModal" :tabs="['今日', '历史']" @close="showLikeUsersModal = false"
:visible="showLikeUsersModal" @tab-change="handleLikeUsersTabChange">
:tabs="['今日', '历史']"
@close="showLikeUsersModal = false"
@tab-change="handleLikeUsersTabChange"
>
<template #content> <template #content>
<view class="like-users-list"> <view class="like-users-list">
<view v-for="(user, index) in displayedLikeUsers" :key="index" class="like-user-item" > <view v-for="(user, index) in displayedLikeUsers" :key="index" class="like-user-item">
<image class="like-user-avatar" :src="user.avatar" mode="aspectFill"></image> <image class="like-user-avatar" :src="user.avatar" mode="aspectFill"></image>
<view class="like-user-info"> <view class="like-user-info">
<text class="like-user-name">{{ user.nickname }}</text> <text class="like-user-name">{{ user.nickname }}</text>
</view> </view>
<!-- 拜访按钮 --> <!-- 拜访按钮 -->
<view <view class="visit-button" @tap="handleVisit">
class="visit-button" <image class="visit-icon" src="/static/square/dianjibaifang.png" mode="aspectFit" lazy-load>
@tap="handleVisit" </image>
>
<image
class="visit-icon"
src="/static/square/dianjibaifang.png"
mode="aspectFit"
lazy-load
></image>
<text class="visit-text">点击拜访</text> <text class="visit-text">点击拜访</text>
</view> </view>
</view> </view>
@ -391,6 +394,22 @@ const isLiked = ref(false);
const likeCount = ref(0); const likeCount = ref(0);
const liking = ref(false); const liking = ref(false);
//
const userAvatarUrl = ref('');
//
const loadCurrentUser = () => {
try {
const userStr = uni.getStorageSync('user');
if (userStr) {
const userInfo = JSON.parse(userStr);
userAvatarUrl.value = userInfo?.avatar_url || '';
}
} catch (e) {
console.error('解析用户信息失败:', e);
}
};
// 6 // 6
// avatar: , ellipseX: X, ellipseY: Y, size: // avatar: , ellipseX: X, ellipseY: Y, size:
const likedUsers = ref([ const likedUsers = ref([
@ -398,8 +417,8 @@ const likedUsers = ref([
{ avatar: '/static/sucai/image-02.png', ellipseX: 40, ellipseY: -40, size: 0.85 }, { avatar: '/static/sucai/image-02.png', ellipseX: 40, ellipseY: -40, size: 0.85 },
{ avatar: '/static/sucai/image-03.png', ellipseX: -96, ellipseY: 0, size: 0.9 }, { avatar: '/static/sucai/image-03.png', ellipseX: -96, ellipseY: 0, size: 0.9 },
{ avatar: '/static/sucai/image-04.png', ellipseX: 120, ellipseY: -40, size: 0.9 }, { avatar: '/static/sucai/image-04.png', ellipseX: 120, ellipseY: -40, size: 0.9 },
{ avatar: '/static/sucai/image-05.png', ellipseX: 64, ellipseY: 32}, { avatar: '/static/sucai/image-05.png', ellipseX: 64, ellipseY: 32 },
{ avatar: '/static/sucai/image-06.png', ellipseX: -16, ellipseY: 32 ,size:1.15 } { avatar: '/static/sucai/image-06.png', ellipseX: -16, ellipseY: 32, size: 1.15 }
]); ]);
// ---- ---- // ---- ----
@ -486,15 +505,48 @@ const showBlockNumber = ref(false);
const showTxHash = ref(false); const showTxHash = ref(false);
// //
const remainSeconds = ref(0);
let countdownTimer = null; let countdownTimer = null;
const showCountdown = computed(() => remainSeconds.value > 0); //
const countdowns = ref({});
//
const calculateRemainingTime = (item) => {
if (item.remainSeconds !== undefined) {
if (item.remainSeconds <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(item.remainSeconds / 3600);
const minutes = Math.floor((item.remainSeconds % 3600) / 60);
const seconds = Math.floor(item.remainSeconds % 60);
return { hours, minutes, seconds, expired: false };
}
if (item.exhibition_expire_at) {
const now = Date.now();
const expireTime = new Date(item.exhibition_expire_at).getTime();
const remaining = expireTime - now;
if (remaining <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
return { hours, minutes, seconds, expired: false };
}
return { hours: 0, minutes: 0, seconds: 0, expired: true };
};
//
const updateCountdowns = () => {
countdowns.value['currentAsset'] = calculateRemainingTime({ exhibition_expire_at: assetData.value.exhibition_expire_at });
};
const countdownText = computed(() => { const countdownText = computed(() => {
const h = String(Math.floor(remainSeconds.value / 3600)).padStart(2, '0'); const countdown = countdowns.value['currentAsset'];
const m = String(Math.floor((remainSeconds.value % 3600) / 60)).padStart(2, '0'); if (!countdown || countdown.expired) return '';
const s = String(Math.floor(remainSeconds.value % 60)).padStart(2, '0'); const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`; return `${h}:${m}:${s}`;
}); });
@ -512,10 +564,7 @@ const formattedAcquireTime = computed(() => {
// //
const displayTxHash = computed(() => { const displayTxHash = computed(() => {
const hash = assetData.value.tx_hash; return assetData.value.tx_hash || '未知';
if (!hash) return '未知';
if (hash.length <= 20) return hash;
return `${hash.substring(0, 10)}...${hash.substring(hash.length - 8)}`;
}); });
// 6 + 6* // 6 + 6*
@ -578,6 +627,7 @@ const loadData = async () => {
isLiked.value = res.data.asset.is_liked || res.data.is_liked || false; isLiked.value = res.data.asset.is_liked || res.data.is_liked || false;
likeCount.value = asset.like_count || 0; likeCount.value = asset.like_count || 0;
<<<<<<< HEAD
// //
if (asset.material_relations || asset.materials) { if (asset.material_relations || asset.materials) {
loadStickersForAsset(asset.material_relations || asset.materials) loadStickersForAsset(asset.material_relations || asset.materials)
@ -585,6 +635,9 @@ const loadData = async () => {
if (asset.remain_time > 0) { if (asset.remain_time > 0) {
remainSeconds.value = asset.remain_time; remainSeconds.value = asset.remain_time;
=======
if (asset.exhibition_expire_at) {
>>>>>>> ebe57bc078a35ed4c13ccf966c8f7b5af6327c61
startCountdown(); startCountdown();
} }
console.log(res.data) console.log(res.data)
@ -627,13 +680,10 @@ const loadData = async () => {
// //
const startCountdown = () => { const startCountdown = () => {
updateCountdowns(); // 1
if (countdownTimer) clearInterval(countdownTimer); if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = setInterval(() => { countdownTimer = setInterval(() => {
if (remainSeconds.value > 0) { updateCountdowns();
remainSeconds.value--;
} else {
clearInterval(countdownTimer);
}
}, 1000); }, 1000);
}; };
@ -734,11 +784,15 @@ onLoad((options) => {
assetIdParam.value = options?.asset_id || ''; assetIdParam.value = options?.asset_id || '';
orderIdParam.value = options?.order_id || ''; orderIdParam.value = options?.order_id || '';
fromParam.value = options?.from || ''; fromParam.value = options?.from || '';
<<<<<<< HEAD
studioKindParam.value = options?.studio_kind || ''; studioKindParam.value = options?.studio_kind || '';
craftConfirmMode.value = fromParam.value === 'craft_confirm'; craftConfirmMode.value = fromParam.value === 'craft_confirm';
if (craftConfirmMode.value) { if (craftConfirmMode.value) {
loadCraftConfirm(); loadCraftConfirm();
} }
=======
loadCurrentUser();
>>>>>>> ebe57bc078a35ed4c13ccf966c8f7b5af6327c61
}); });
onShow(() => { onShow(() => {
@ -965,10 +1019,12 @@ onUnmounted(() => {
} }
.content-wrapper { .content-wrapper {
min-height: 90%;
padding: 104rpx 40rpx 80rpx; padding: 104rpx 40rpx 80rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: space-between;
gap: 0; gap: 0;
} }
@ -1069,14 +1125,15 @@ onUnmounted(() => {
0 4rpx 12rpx rgba(255, 143, 158, 0.2), 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);
padding: 10rpx 28rpx; padding: 10rpx 28rpx;
backdrop-filter: blur(12rpx);
} }
.like-area:active { .like-area:active {
opacity: 0.75; opacity: 0.75;
} }
.like-icon, .heart-icon, .crystal-icon { .like-icon,
.heart-icon,
.crystal-icon {
width: 44rpx; width: 44rpx;
height: 44rpx; height: 44rpx;
} }
@ -1084,9 +1141,10 @@ onUnmounted(() => {
.like-num { .like-num {
font-size: 32rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
color: #e6e6e6; color: #fff;
font-family: 'yt', sans-serif; font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
} }
/* 收益 */ /* 收益 */
@ -1108,22 +1166,28 @@ onUnmounted(() => {
.earnings-text { .earnings-text {
font-size: 32rpx; font-size: 32rpx;
font-weight: bold; font-weight: bold;
color: #e6e6e6; color: #fff;
font-family: 'yt', sans-serif; font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
} }
/* 倒计时 */ /* 倒计时 */
.countdown-area { .countdown-area {
display: flex; display: flex;
align-items: center; align-items: center;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
padding: 10rpx 28rpx;
} }
.countdown-pill { .countdown-pill {
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%); font-size: 32rpx;
border-radius: 999rpx; font-weight: bold;
padding: 10rpx 24rpx; color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4); font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
} }
.countdown-val { .countdown-val {
@ -1132,6 +1196,7 @@ onUnmounted(() => {
color: #fff; color: #fff;
font-family: 'yt', sans-serif; font-family: 'yt', sans-serif;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
} }
/* 信息行(持有人 + 铸造时间) */ /* 信息行(持有人 + 铸造时间) */
@ -1158,8 +1223,8 @@ onUnmounted(() => {
display: flex; display: flex;
gap: 8rpx; gap: 8rpx;
width: 416rpx; width: 416rpx;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.info-label { .info-label {
@ -1176,6 +1241,20 @@ onUnmounted(() => {
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.7); text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.7);
} }
.info-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
margin-left: 8rpx;
}
.info-nickname {
display: flex;
align-items: center;
gap: 8rpx;
border-radius: 24rpx;
}
/* 点赞用户头像区域 */ /* 点赞用户头像区域 */
.liked-users-area { .liked-users-area {
position: relative; position: relative;
@ -1195,7 +1274,7 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
} }
.liked-user-image{ .liked-user-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@ -1264,7 +1343,6 @@ onUnmounted(() => {
padding: 30rpx 16rpx; padding: 30rpx 16rpx;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
gap: 24rpx; gap: 24rpx;
} }
@ -1297,7 +1375,6 @@ onUnmounted(() => {
font-size: 28rpx; font-size: 28rpx;
color: #ffffff; color: #ffffff;
font-family: 'yt', sans-serif; font-family: 'yt', sans-serif;
text-align: right;
flex: 1; flex: 1;
word-break: break-all; word-break: break-all;
display: flex display: flex
@ -1315,13 +1392,70 @@ onUnmounted(() => {
} }
.toggle-icon { .toggle-icon {
font-size: 28rpx; width: 32rpx;
height: 32rpx;
} }
.chain-hash { .chain-hash {
color: rgba(255, 255, 255, 0.75); color: rgba(255, 255, 255, 0.75);
} }
/* 链上哈希遮罩层 */
.txhash-mask {
position: fixed;
left: 0;
right: 0;
bottom: 24rpx;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
padding: 0 64rpx;
}
.txhash-popup {
border-radius: 24rpx;
padding: 32rpx;
background: url('/static/rank/activity-support-icon/beijingkuang.png') no-repeat center center;
background-size: 105% 120%;
}
.txhash-popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.txhash-popup-title {
font-size: 32rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
}
.txhash-popup-close {
padding: 8rpx;
}
.close-icon {
width: 40rpx;
height: 40rpx;
}
.txhash-popup-content {
display: block;
font-size: 26rpx;
color: #fff;
font-family: 'yt', sans-serif;
word-break: break-all;
line-height: 1.6;
background-color: rgba(0, 0, 0, 0.2);
padding: 20rpx;
border-radius: 12rpx;
}
/* 点赞用户列表 */ /* 点赞用户列表 */
.like-users-list { .like-users-list {
display: flex; display: flex;
@ -1334,7 +1468,7 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding:0 16rpx; padding: 0 16rpx;
background: linear-gradient(to bottom right, background: linear-gradient(to bottom right,
#F0E4B1 0%, #F0E4B1 0%,
#F08399 50%, #F08399 50%,
@ -1388,11 +1522,8 @@ onUnmounted(() => {
margin-bottom: 8rpx; margin-bottom: 8rpx;
/* transform: scale(1.2); */ /* transform: scale(1.2); */
} }
.visit-text{
color: #FFFFFF;
font-size:16rpx;
}
<<<<<<< HEAD
.craft-confirm-scroll { .craft-confirm-scroll {
flex: 1; flex: 1;
height: calc(100vh - 120rpx); height: calc(100vh - 120rpx);
@ -1460,4 +1591,10 @@ onUnmounted(() => {
opacity: 0.65; opacity: 0.65;
} }
=======
.visit-text {
color: #FFFFFF;
font-size: 16rpx;
}
>>>>>>> ebe57bc078a35ed4c13ccf966c8f7b5af6327c61
</style> </style>

View File

@ -6,13 +6,12 @@
<view class="cards-container"> <view class="cards-container">
<view v-for="(card, index) in cardList" :key="index" class="card-item" <view v-for="(card, index) in cardList" :key="index" class="card-item"
:class="{ 'card-selected': selectedIndex === index }" :style="getCardStyle(index)"> :class="{ 'card-selected': selectedIndex === index }" :style="getCardStyle(index)">
<view <view class="card-frame"
class="card-frame"
:class="{ 'no-border': card.comingSoon, 'card-frame--tappable': !card.comingSoon }" :class="{ 'no-border': card.comingSoon, 'card-frame--tappable': !card.comingSoon }"
@tap.stop="onCardFrameTap(index)" @tap.stop="onCardFrameTap(index)">
>
<image class="card-image" :src="card.image" mode="aspectFill" /> <image class="card-image" :src="card.image" mode="aspectFill" />
<image v-if="card.comingSoon" class="coming-soon-badge" src="/static/castlove/jinqingqidai.png" mode="aspectFit" /> <image v-if="card.comingSoon" class="coming-soon-badge" src="/static/castlove/jinqingqidai.png"
mode="aspectFit" />
</view> </view>
</view> </view>
</view> </view>
@ -21,7 +20,8 @@
<view class="text-panel"> <view class="text-panel">
<!-- 向上按钮 --> <!-- 向上按钮 -->
<view class="arrow-btn arrow-up" @click="scrollUp"> <view class="arrow-btn arrow-up" @click="scrollUp">
<image class="arrow-icon" src="/static/castlove/jiantou.png" mode="aspectFit" style="transform: rotate(180deg);" /> <image class="arrow-icon" src="/static/castlove/jiantou.png" mode="aspectFit"
style="transform: rotate(180deg);" />
</view> </view>
<!-- 文字列表 --> <!-- 文字列表 -->
@ -65,7 +65,14 @@ export default {
}, },
data() { data() {
return { return {
selectedIndex: 2, // selectedIndex: 2,
// 便
cardRoutes: {
'光栅卡': '/pages/castlove/create',
'拍立得': '/pages/castlove/create',
'镭射卡': '/pages/castlove/create',
'撕拉片': '/pages/castlove/create',
},
cardList: [ cardList: [
{ name: '镭射卡', image: '/static/castlove/leisheka.png', comingSoon: false }, { name: '镭射卡', image: '/static/castlove/leisheka.png', comingSoon: false },
{ name: '拍立得', image: '/static/castlove/pailide.png', comingSoon: false }, { name: '拍立得', image: '/static/castlove/pailide.png', comingSoon: false },
@ -130,15 +137,24 @@ export default {
return; return;
} }
const pos = this.getCardStackPosition(index); const pos = this.getCardStackPosition(index);
const LENTICULAR_INDEX = 2; //
const goLenticular = //
this.selectedIndex === LENTICULAR_INDEX && if (pos === 2) {
(pos === 2 || pos === 1); if (card.name === '撕拉片') {
const targetName = goLenticular ? '光栅卡' : card.name; const route = this.cardRoutes[card.name];
if (pos === 2 || goLenticular) { if (route) {
uni.navigateTo({ uni.navigateTo({
url: `/pages/castlove/create?name=${encodeURIComponent(targetName)}`, url: '/pages/castlove/mint/tear-card',
}); });
}
} else {
const route = this.cardRoutes[card.name];
if (route) {
uni.navigateTo({
url: `${route}?name=${encodeURIComponent(card.name)}`,
});
}
}
return; return;
} }
this.selectCard(index); this.selectCard(index);
@ -164,9 +180,12 @@ export default {
uni.showToast({ title: '请选择已开放的工艺', icon: 'none' }) uni.showToast({ title: '请选择已开放的工艺', icon: 'none' })
return return
} }
uni.navigateTo({ const route = this.cardRoutes[card.name]
url: `/pages/castlove/create?name=${encodeURIComponent(card.name)}` if (route) {
}) uni.navigateTo({
url: `${route}?name=${encodeURIComponent(card.name)}`
})
}
} }
} }
} }
@ -217,6 +236,7 @@ export default {
background-image: url('/static/square/cangpinkuang1.png'); background-image: url('/static/square/cangpinkuang1.png');
background-size: cover; background-size: cover;
box-shadow: 0 0 0 rgba(0, 0, 0, 0.5); box-shadow: 0 0 0 rgba(0, 0, 0, 0.5);
&.no-border { &.no-border {
background-image: none; background-image: none;
} }

View File

@ -34,7 +34,7 @@
</view> </view>
</view> </view>
<view class="upload-box upload-box--half" @click="chooseLenticularSubject"> <view class="upload-box upload-box--half" @click="chooseLenticularSubject">
<image v-if="uploadedSubject" class="uploaded-image" :src="uploadedSubject" mode="aspectFit"></image> <image v-if="uploadedSubject" class="uploaded-image":src="uploadedSubject" mode="aspectFit"></image>
<view v-else class="upload-placeholder"> <view v-else class="upload-placeholder">
<image class="upload-icon" src="/static/icon/add.png" mode="aspectFit"></image> <image class="upload-icon" src="/static/icon/add.png" mode="aspectFit"></image>
<text class="upload-text">主体图</text> <text class="upload-text">主体图</text>

View File

@ -50,19 +50,23 @@ const navItems = [
name: '星册', name: '星册',
icon: '/static/icon/starbook.png', icon: '/static/icon/starbook.png',
angle: 107, // angle: 107, //
path: '/pages/starbook/index' path: '/pages/starbook/index',
}, },
{ {
name: '铸爱', name: '铸爱',
icon: '/static/icon/castlove.png', icon: '/static/icon/castlove.png',
angle: 90.01, // angle: 90.01, //
path: '/pages/castlove/mall' path: '/pages/castlove/mall',
rotate: '8deg'
}, },
{ {
name: '星城', name: '星城',
icon: '/static/icon/dressup.png', icon: '/static/icon/dressup.png',
angle: 73, // angle: 73, //
path: '/pages/starcity/index' path: '/pages/starcity/index',
iconWidth: '120rpx',
iconHeight: '120rpx'
}, },
{ {
name: '广场', name: '广场',
@ -107,7 +111,10 @@ const getNavIconStyle = (index) => {
return { return {
'--x': `${x}rpx`, '--x': `${x}rpx`,
'--y': `${y}rpx`, '--y': `${y}rpx`,
'--delay': `${delay}s` '--delay': `${delay}s`,
'--rotate': item.rotate || '0deg',
'--icon-width': item.iconWidth || '100rpx',
'--icon-height': item.iconHeight || '100rpx'
}; };
}; };
@ -231,12 +238,13 @@ const handleNavClick = (index) => {
} }
.nav-icon { .nav-icon {
width: 100rpx; width: var(--icon-width, 100rpx);
height: 100rpx; height: var(--icon-height, 100rpx);
margin-bottom: 8rpx; margin-bottom: 8rpx;
transition: transform 0.3s ease, filter 0.3s ease; transition: transform 0.3s ease, filter 0.3s ease;
filter: drop-shadow(0 6rpx 16rpx rgba(0, 0, 0, 0.4)) drop-shadow(0 2rpx 8rpx rgba(0, 0, 0, 0.2)); filter: drop-shadow(0 6rpx 16rpx rgba(0, 0, 0, 0.4)) drop-shadow(0 2rpx 8rpx rgba(0, 0, 0, 0.2));
position: relative; position: relative;
transform: rotate(var(--rotate));
} }
/* 底部倒影效果 */ /* 底部倒影效果 */

View File

@ -23,8 +23,7 @@
<view class="exhibition-grid"> <view class="exhibition-grid">
<view v-for="(item, index) in exhibitionWorks" :key="item.id" class="exhibition-card" <view v-for="(item, index) in exhibitionWorks" :key="item.id" class="exhibition-card"
:class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'" :class="index % 2 === 0 ? 'card-tilt-left' : 'card-tilt-right'" @tap="goToAssetDetail(item.id)">
@tap="goToAssetDetail(item.id)">
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'" <image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image> 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">
@ -36,6 +35,13 @@
<text class="card-rate-text">{{ item.like_count || 0 }}</text> <text class="card-rate-text">{{ item.like_count || 0 }}</text>
</view> </view>
</view> </view>
<!-- 倒计时背景 -->
<view class="countdown-background" :style="getCountdownBackgroundStyle()">
<!-- 倒计时文字 -->
<text class="countdown-text">
{{ formatCountdown(item.id) }}
</text>
</view>
<!-- 图片下方收益 --> <!-- 图片下方收益 -->
<view class="card-income-row" <view class="card-income-row"
:class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'"> :class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
@ -80,13 +86,14 @@
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row" <view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
@tap="goToAssetDetail(item.id)"> @tap="goToAssetDetail(item.id)">
<!-- 排名图标绝对定位在卡片左侧 --> <!-- 排名图标绝对定位在卡片左侧 -->
<image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)" mode="aspectFit"></image> <image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)"
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'" <image 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"
@ -105,7 +112,9 @@
<!-- 右侧奖励 --> <!-- 右侧奖励 -->
<view class="liked-reward"> <view class="liked-reward">
<image class="reward-token-icon" :src="item.earnings > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'" mode="aspectFit"> <image class="reward-token-icon"
:src="item.earnings > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
mode="aspectFit">
</image> </image>
<text class="reward-amount">+{{ item.reward }}</text> <text class="reward-amount">+{{ item.reward }}</text>
</view> </view>
@ -123,7 +132,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted, onUnmounted } 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, getUserProfileApi } from '@/utils/api.js';
@ -156,6 +165,68 @@ const formatScore = (score) => {
return Number(score).toLocaleString(); return Number(score).toLocaleString();
}; };
//
const calculateRemainingTime = (item) => {
if (item.remainSeconds !== undefined) {
if (item.remainSeconds <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(item.remainSeconds / 3600);
const minutes = Math.floor((item.remainSeconds % 3600) / 60);
const seconds = Math.floor(item.remainSeconds % 60);
return { hours, minutes, seconds, expired: false };
}
if (item.expire_at) {
const now = Date.now();
const expireTime = new Date(item.expire_at).getTime();
const remaining = expireTime - now;
if (remaining <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
return { hours, minutes, seconds, expired: false };
}
return { hours: 0, minutes: 0, seconds: 0, expired: true };
};
//
const updateCountdowns = () => {
exhibitionWorks.value.forEach(item => {
if (item && item.id) {
if (item.remainSeconds !== undefined && item.remainSeconds > 0) {
item.remainSeconds--;
}
countdowns.value[item.id] = calculateRemainingTime(item);
}
});
};
//
const formatCountdown = (itemId) => {
const countdown = countdowns.value[itemId];
if (!countdown || countdown.expired) return '00:00:00';
const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
};
//
const getCountdownBackgroundStyle = () => {
return {
position: 'absolute',
bottom: '20rpx',
right: '40%',
transform: 'translateX(50%)',
width: '140rpx',
height: '36rpx',
zIndex: 9
};
};
const rankIcons = [ const rankIcons = [
'/static/square/icon1.png', '/static/square/icon1.png',
'/static/square/icon2.png', '/static/square/icon2.png',
@ -165,6 +236,9 @@ const rankIcons = [
// //
const exhibitionWorks = ref([]); const exhibitionWorks = ref([]);
//
const countdowns = ref({});
// //
const likedWorks = ref([]); const likedWorks = ref([]);
@ -236,6 +310,19 @@ const loadLikedAssets = async () => {
onMounted(() => { onMounted(() => {
loadExhibitedAssets(); loadExhibitedAssets();
loadLikedAssets(); loadLikedAssets();
//
countdownTimer = setInterval(() => {
updateCountdowns();
}, 1000);
});
let countdownTimer = null;
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
}); });
</script> </script>
@ -400,8 +487,8 @@ onMounted(() => {
.card-rate-badge { .card-rate-badge {
position: absolute; position: absolute;
bottom: 16rpx; top: 16rpx;
left: 40%; left: 25%;
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
@ -476,6 +563,35 @@ onMounted(() => {
font-weight: 700; font-weight: 700;
} }
/* 倒计时背景 */
.countdown-background {
/* position: absolute;
bottom: 20rpx;
right: 30%;
transform: translateX(-50%); */
width: 140rpx;
height: 36rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
z-index: 9;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
opacity: 0.95;
}
/* 倒计时文字 */
.countdown-text {
font-size: 22rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
z-index: 10;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
display: flex;
justify-content: center;
}
.empty-exhibition { .empty-exhibition {
display: flex; display: flex;
align-items: center; align-items: center;
@ -527,7 +643,7 @@ onMounted(() => {
flex-shrink: 0; flex-shrink: 0;
/* margin-right: 8rpx; */ /* margin-right: 8rpx; */
position: relative; position: relative;
left: 32rpx; left: 32rpx;
} }
.rank-icon-1 { .rank-icon-1 {

View File

@ -11,7 +11,7 @@
<!-- <text class="nav-title">我的作品</text> --> <!-- <text class="nav-title">我的作品</text> -->
<view class="nav-placeholder"></view> <view class="nav-placeholder"></view>
<view class="nav-settings" @tap="goToSettings"> <view class="nav-settings" @tap="goToSettings">
<text class="nav-settings-text">个人设置</text> <image class="nav-settings-icon" src="/static/icon/settings.png" mode="aspectFit"></image>
</view> </view>
</view> </view>
@ -30,6 +30,12 @@
@tap="handleExhibitionCardTap(item, index)"> @tap="handleExhibitionCardTap(item, index)">
<image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'" <image class="card-image" :src="item.cover_url || '/static/nft/placeholder.png'"
mode="aspectFill"></image> mode="aspectFill"></image>
<!-- 领取收益按钮 -->
<view class="claim-reward-btn" v-if="isRewardClaimable(item.id)">
<image class="claim-crystal-icon" src="/static/square/shuijingtubiao.png" mode="aspectFit">
</image>
<view @tap.stop="handleClaimReward(item, index)" class="claim-btn-text">领取收益</view>
</view>
<image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill"> <image class="card-frame" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill">
</image> </image>
<!-- 点赞数 --> <!-- 点赞数 -->
@ -39,6 +45,14 @@
<text class="card-rate-text">{{ item.like_count || 0 }}</text> <text class="card-rate-text">{{ item.like_count || 0 }}</text>
</view> </view>
</view> </view>
<!-- 倒计时背景 -->
<view class="countdown-background" v-if="!isRewardClaimable(item.id)"
:style="getCountdownBackgroundStyle(index)">
<!-- 倒计时文字 -->
<text class="countdown-text">
{{ formatCountdown(item.id) }}
</text>
</view>
<!-- 图片下方收益 --> <!-- 图片下方收益 -->
<view class="card-income-row" <view class="card-income-row"
:class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'"> :class="index % 2 === 0 ? 'income-tilt-right' : 'income-tilt-left'">
@ -101,13 +115,14 @@
<view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row" <view v-for="(item, index) in likedWorks" :key="item.id" class="liked-row"
@tap="goToAssetDetail(item.id)"> @tap="goToAssetDetail(item.id)">
<!-- 排名图标绝对定位在卡片左侧 --> <!-- 排名图标绝对定位在卡片左侧 -->
<image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)" mode="aspectFit"></image> <image v-if="index < 3" :src="rankIcons[index]" :class="'rank-icon rank-icon-' + (index + 1)"
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'" <image 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"
@ -126,7 +141,9 @@
<!-- 右侧奖励 --> <!-- 右侧奖励 -->
<view class="liked-reward"> <view class="liked-reward">
<image class="reward-token-icon" :src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'" mode="aspectFit"> <image class="reward-token-icon"
:src="item.reward > 10 ? '/static/square/shuijingtubiao.png' : '/static/icon/crystal.png'"
mode="aspectFit">
</image> </image>
<text class="reward-amount">+{{ item.reward }}</text> <text class="reward-amount">+{{ item.reward }}</text>
</view> </view>
@ -258,6 +275,12 @@ const goToAssetDetail = (id) => {
// //
const cardTapTimers = {}; const cardTapTimers = {};
//
const handleClaimReward = (item, _index) => {
console.log('领取收益:', item);
uni.showToast({ title: '收益已领取', icon: 'success' });
};
const handleExhibitionCardTap = (item, index) => { const handleExhibitionCardTap = (item, index) => {
if (cardTapTimers[item.id]) { if (cardTapTimers[item.id]) {
// //
@ -302,9 +325,79 @@ const formatScore = (score) => {
return Number(score).toLocaleString(); return Number(score).toLocaleString();
}; };
//
const calculateRemainingTime = (item) => {
if (item.remainSeconds !== undefined) {
if (item.remainSeconds <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(item.remainSeconds / 3600);
const minutes = Math.floor((item.remainSeconds % 3600) / 60);
const seconds = Math.floor(item.remainSeconds % 60);
return { hours, minutes, seconds, expired: false };
}
if (item.expire_at) {
const now = Date.now();
const expireTime = new Date(item.expire_at).getTime();
const remaining = expireTime - now;
if (remaining <= 0) {
return { hours: 0, minutes: 0, seconds: 0, expired: true };
}
const hours = Math.floor(remaining / (60 * 60 * 1000));
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((remaining % (60 * 1000)) / 1000);
return { hours, minutes, seconds, expired: false };
}
return { hours: 0, minutes: 0, seconds: 0, expired: true };
};
//
const updateCountdowns = () => {
exhibitionWorks.value.forEach(item => {
if (item && item.id) {
if (item.remainSeconds !== undefined && item.remainSeconds > 0) {
item.remainSeconds--;
}
countdowns.value[item.id] = calculateRemainingTime(item);
}
});
};
//
const formatCountdown = (itemId) => {
const countdown = countdowns.value[itemId];
if (!countdown || countdown.expired) return '00:00:00';
const h = String(countdown.hours).padStart(2, '0');
const m = String(countdown.minutes).padStart(2, '0');
const s = String(countdown.seconds).padStart(2, '0');
return `${h}:${m}:${s}`;
};
//
const isRewardClaimable = (itemId) => {
const countdown = countdowns.value[itemId];
return countdown && countdown.expired;
};
//
const getCountdownBackgroundStyle = () => {
return {
position: 'absolute',
bottom: '20rpx',
right: '40%',
transform: 'translateX(50%)',
width: '140rpx',
height: '36rpx',
zIndex: 9
};
};
// //
const exhibitionWorks = ref([]); const exhibitionWorks = ref([]);
//
const countdowns = ref({});
// //
const likedWorks = ref([]); const likedWorks = ref([]);
@ -380,6 +473,11 @@ onMounted(() => {
loadExhibitedAssets(); loadExhibitedAssets();
loadLikedAssets(); loadLikedAssets();
//
countdownTimer = setInterval(() => {
updateCountdowns();
}, 1000);
// //
uni.$on('userInfoUpdated', () => { uni.$on('userInfoUpdated', () => {
loadExhibitedAssets(); loadExhibitedAssets();
@ -387,7 +485,12 @@ onMounted(() => {
}); });
}); });
let countdownTimer = null;
onUnmounted(() => { onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
}
uni.$off('userInfoUpdated'); uni.$off('userInfoUpdated');
}); });
@ -464,11 +567,9 @@ onShow(() => {
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4); inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
} }
.nav-settings-text { .nav-settings-icon {
font-size: 24rpx; width: 48rpx;
color: #fff; height: 48rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.8);
font-weight: 400;
} }
/* 内容区域 */ /* 内容区域 */
@ -560,13 +661,13 @@ onShow(() => {
.card-tilt-left { .card-tilt-left {
transform: rotate(-4deg) translateY(10rpx); transform: rotate(-4deg) translateY(10rpx);
margin-right: 32rpx; margin-right: 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);
} }
.card-tilt-right { .card-tilt-right {
transform: rotate(4deg) translateY(10rpx); transform: rotate(4deg) translateY(10rpx);
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);
} }
@ -598,6 +699,50 @@ onShow(() => {
z-index: 2; z-index: 2;
} }
/* 领取收益按钮 */
.claim-reward-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 88%;
height: 92%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
z-index: 10;
background: rgba(0, 0, 0, 0.6);
border-radius: 16rpx;
gap: 8rpx;
}
.claim-crystal-icon {
width: 50%;
height: 50%;
position: absolute;
top: 50%;
transform: translateY(-50%)
}
.claim-btn-text {
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%);
border-radius: 24rpx;
padding: 8rpx 20rpx;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
font-size: 22rpx;
color: #fff;
font-weight: 600;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
position: absolute;
bottom: 24rpx;
}
.card-user-tag { .card-user-tag {
position: absolute; position: absolute;
bottom: 56rpx; bottom: 56rpx;
@ -617,8 +762,8 @@ onShow(() => {
.card-rate-badge { .card-rate-badge {
position: absolute; position: absolute;
bottom: 16rpx; top: 16rpx;
left: 40%; left: 25%;
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;
align-items: center; align-items: center;
@ -646,7 +791,7 @@ onShow(() => {
z-index: 2; z-index: 2;
margin-right: -16rpx; margin-right: -16rpx;
left: 20rpx; left: 20rpx;
top: 8rpx /* top: 8rpx */
} }
.card-income-text-wrap { .card-income-text-wrap {
@ -697,6 +842,35 @@ onShow(() => {
font-weight: 700; font-weight: 700;
} }
/* 倒计时背景 */
.countdown-background {
/* position: absolute; */
/* top: 20rpx;
left: 30%; */
/* transform: translateX(-50%); */
width: 140rpx;
height: 36rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
border-radius: 999rpx;
z-index: 9;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
opacity: 0.95;
}
/* 倒计时文字 */
.countdown-text {
font-size: 22rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
z-index: 10;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
display: flex;
justify-content: center;
}
/* 空状态 */ /* 空状态 */
.empty-exhibition { .empty-exhibition {
display: flex; display: flex;
@ -931,8 +1105,9 @@ onShow(() => {
.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);
} }
/* 空状态 */ /* 空状态 */

View File

@ -7,251 +7,268 @@
<!-- 上半部分用户信息卡片 --> <!-- 上半部分用户信息卡片 -->
<view class="top-section"> <view class="top-section">
<!-- 蒙层 --> <!-- 蒙层 -->
<view class="background-overlay"></view> <view class="background-overlay"></view>
<!-- Header组件 --> <!-- Header组件 -->
<image class="close-icon-img" src="/static/starbookcontent/tuichu.png" mode="aspectFit" @tap="goBack"></image> <image class="close-icon-img" src="/static/starbookcontent/tuichu.png" mode="aspectFit" @tap="goBack">
</image>
<!-- 用户信息卡片 --> <!-- 用户信息卡片 -->
<view class="user-info-card"> <view class="user-info-card">
<!-- 上半部分头像和用户信息左右布局 --> <!-- 上半部分头像和用户信息左右布局 -->
<view class="user-main-info"> <view class="user-main-info">
<view @tap="handleAvatarClick" class="avatar-container"> <view @tap="handleAvatarClick" class="avatar-container">
<Avatar :key="avatarKey" :userId="uid" :size="260" :borderWidth="6" :showLevel="true" <Avatar :key="avatarKey" :userId="uid" :size="260" :borderWidth="6" :showLevel="true"
:level="fanLevel" :avatarUrl="userAvatarUrl" /> :level="fanLevel" :avatarUrl="userAvatarUrl" />
</view>
<view class="user-text-info">
<!-- 昵称 -->
<view class="info-row">
<text style="font-size: 48rpx;text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);" class="info-value">{{ nickname }}</text>
</view> </view>
<!-- DID --> <view class="user-text-info">
<view class="info-row"> <!-- 昵称 -->
<text class="info-label">DID</text> <view class="info-row">
<text class="info-value uid-value">{{ uid }}</text> <text style="font-size: 48rpx;text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);"
<view class="info-btn" @tap="copyUid">查看</view> class="info-value">{{ nickname }}</text>
</view> </view>
<!-- 链上地址 --> <!-- DID -->
<view class="info-row"> <view class="info-row">
<text class="info-label">链上地址</text> <text class="info-label">DID</text>
<text class="info-value address-value">{{ displayAddress }}</text> <view class="address-row">
<view class="info-btn" @tap="copyAddress">查看</view> <text class="info-value uid-value">{{ uid }}</text>
</view> <!-- <image class="toggle-icon"
<!-- 手机号 --> :src="showBlockNumber ? '/static/icon/show.png' : '/static/icon/hide.png'"
<view class="info-row"> mode="aspectFit" @tap="copyUid"></image> -->
<text class="info-label">注册手机</text> </view>
<text class="info-value" v-if="displayMobile">{{ displayMobile }}</text> </view>
<text class="info-value" v-else>未绑定</text> <!-- 链上地址 -->
<view class="info-btn" @tap="handleViewMobile">查看</view> <view class="info-row">
<text class="info-label">链上地址</text>
<view class="address-row">
<text class="info-value address-value" @longpress="copyAddress">{{ displayAddress }}</text>
<view class="toggle-btn" @tap="showBlockNumber = !showBlockNumber">
<image class="toggle-icon" :src="showBlockNumber ? '/static/icon/show.png' : '/static/icon/hide.png'" mode="aspectFit"></image>
</view>
</view>
</view>
<!-- 手机号 -->
<view class="info-row">
<text class="info-label">注册手机</text>
<view class="address-row">
<text class="info-value" v-if="mobile">{{ displayMobile }}</text>
<view class="toggle-btn" @tap="toggleMobileDisplay">
<image class="toggle-icon" :src="showMobile ? '/static/icon/show.png' : '/static/icon/hide.png'" mode="aspectFit"></image>
</view>
</view>
</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view>
<!-- 下半部分白色背景区域 --> <!-- 下半部分白色背景区域 -->
<view class="bottom-section"> <view class="bottom-section">
<view class="content-area"> <view class="content-area">
<!-- 我的资产 --> <!-- 我的资产 -->
<view class="section-bg" style="padding-top: 0;"> <view class="section-bg" style="padding-top: 0;">
<view class="section-title">我的资产</view> <view class="section-title">我的资产</view>
<view class="assets-grid"> <view class="assets-grid">
<view class="asset-card" @tap="handleAssetClick('myWorks')"> <view class="asset-card" @tap="handleAssetClick('myWorks')">
<image class="asset-icon" src="/static/icon/dazi.png" mode="aspectFit"></image> <image class="asset-icon" src="/static/icon/dazi.png" mode="aspectFit"></image>
<text class="asset-text">个人中心</text> <text class="asset-text">个人中心</text>
<!-- <text class="arrow"></text> --> <!-- <text class="arrow"></text> -->
</view> </view>
<view class="asset-card" @tap="handleAssetClick('starbook')"> <view class="asset-card" @tap="handleAssetClick('starbook')">
<image class="asset-icon" src="/static/icon/starbook.png" mode="aspectFit"></image> <image class="asset-icon" src="/static/icon/starbook.png" mode="aspectFit"></image>
<text class="asset-text">我的星册</text> <text class="asset-text">我的星册</text>
<!-- <text class="arrow"></text> --> <!-- <text class="arrow"></text> -->
</view>
</view> </view>
</view> </view>
</view>
<!-- 服务与工具 --> <!-- 服务与工具 -->
<view class="section-bg"> <view class="section-bg">
<view class="section-title">服务与工具</view> <view class="section-title">服务与工具</view>
<view class="service-buttons-container"> <view class="service-buttons-container">
<!-- 新手指引入口 --> <!-- 新手指引入口 -->
<view class="service-button" @tap="handleGuideClick"> <view class="service-button" @tap="handleGuideClick">
<image class="service-icon" src="/static/icon/onboarding.png" mode="aspectFit"></image> <image class="service-icon" src="/static/icon/onboarding.png" mode="aspectFit"></image>
<text class="service-text">新手指引</text> <text class="service-text">新手指引</text>
<text v-if="guideClaimableCount > 0" class="guide-badge">{{ guideClaimableCount }}</text> <text v-if="guideClaimableCount > 0" class="guide-badge">{{ guideClaimableCount
</view> }}</text>
<view class="service-button" @tap="handleChangeNickname"> </view>
<image class="service-icon" src="/static/icon/edit-nickname.png" mode="aspectFit"></image> <view class="service-button" @tap="handleChangeNickname">
<text class="service-text">修改昵称</text> <image class="service-icon" src="/static/icon/edit-nickname.png" mode="aspectFit">
</view> </image>
<view class="service-button" @tap="handleChangePassword"> <text class="service-text">修改昵称</text>
<image class="service-icon" src="/static/icon/edit-password.png" mode="aspectFit"></image> </view>
<text class="service-text">修改密码</text> <view class="service-button" @tap="handleChangePassword">
</view> <image class="service-icon" src="/static/icon/edit-password.png" mode="aspectFit">
<!-- <view class="service-button" @tap="handleSwitchRole"> </image>
<text class="service-text">修改密码</text>
</view>
<!-- <view class="service-button" @tap="handleSwitchRole">
<image class="service-icon" src="/static/icon/switch-account.png" mode="aspectFit"></image> <image class="service-icon" src="/static/icon/switch-account.png" mode="aspectFit"></image>
<text class="service-text">添加身份</text> <text class="service-text">添加身份</text>
</view> --> </view> -->
<view class="service-button" @tap="handleDeleteAccount"> <view class="service-button" @tap="handleDeleteAccount">
<image class="service-icon" src="/static/icon/logout.png" mode="aspectFit"></image> <image class="service-icon" src="/static/icon/logout.png" mode="aspectFit"></image>
<text class="service-text">注销账号</text> <text class="service-text">注销账号</text>
</view>
</view>
</view>
</view>
<!-- 修改昵称弹窗 -->
<view class="nickname-modal" v-if="showNicknameModal" @tap="closeNicknameModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">修改昵称</view>
<input class="modal-input" v-model="newNickname" placeholder="请输入新昵称" maxlength="20" />
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeNicknameModal">取消</button>
<button class="modal-btn-confirm" @tap="confirmChangeNickname">确认</button>
</view>
</view>
</view>
<!-- 修改密码弹窗 -->
<view class="password-modal" v-if="showPasswordModal" @tap="closePasswordModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">修改密码</view>
<!-- 旧密码输入框 -->
<view class="modal-password-wrapper">
<input class="modal-password-input" :type="showOldPassword ? 'text' : 'password'"
v-model="oldPassword" placeholder="请输入旧密码" placeholder-class="input-placeholder" />
<view class="modal-eye-icon" @click="showOldPassword = !showOldPassword">
<text class="eye-text">{{ showOldPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<!-- 新密码输入框 -->
<view class="modal-password-wrapper">
<input class="modal-password-input" :type="showNewPassword ? 'text' : 'password'"
v-model="newPassword" placeholder="请输入新密码" placeholder-class="input-placeholder" />
<view class="modal-eye-icon" @click="showNewPassword = !showNewPassword">
<text class="eye-text">{{ showNewPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closePasswordModal">取消</button>
<button class="modal-btn-confirm" @tap="confirmChangePassword">确认</button>
</view>
</view>
</view>
<!-- 添加身份弹窗 -->
<view class="add-identity-modal" v-if="showAddIdentityModal" @tap="closeAddIdentityModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">添加身份</view>
<view class="modal-subtitle">选择您喜欢的明星成为TA的粉丝</view>
<!-- 明星选择列表 -->
<view class="star-list">
<view v-for="star in fanIdentitiesList" :key="star.star_id" class="star-item"
:class="{ 'star-item-selected': selectedStarId === star.star_id }"
@tap="selectStar(star.star_id)">
<view class="star-info">
<text class="star-name">{{ star.name }}</text>
<text class="star-tag" v-if="star.tag">{{ star.tag }}</text>
</view>
<view class="star-radio">
<view v-if="selectedStarId === star.star_id" class="star-radio-checked"></view>
</view> </view>
</view> </view>
</view> </view>
</view>
<!-- 昵称输入框 --> <!-- 修改昵称弹窗 -->
<input class="modal-input" v-model="newIdentityNickname" placeholder="请输入您的新昵称" maxlength="20" /> <view class="nickname-modal" v-if="showNicknameModal" @tap="closeNicknameModal">
<view class="modal-content" @tap.stop>
<view class="modal-buttons"> <view class="modal-title">修改昵称</view>
<button class="modal-btn-cancel" @tap="closeAddIdentityModal">取消</button> <input class="modal-input" v-model="newNickname" placeholder="请输入新昵称" maxlength="20" />
<button class="modal-btn-confirm" @tap="confirmAddIdentity">确认</button> <view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeNicknameModal">取消</button>
<button class="modal-btn-confirm" @tap="confirmChangeNickname">确认</button>
</view>
</view> </view>
</view> </view>
</view>
<!-- 身份切换下拉列表 --> <!-- 修改密码弹窗 -->
<view class="identity-dropdown-modal" v-if="showIdentityDropdown" @tap="closeIdentityDropdown"> <view class="password-modal" v-if="showPasswordModal" @tap="closePasswordModal">
<view class="identity-dropdown-content" @tap.stop> <view class="modal-content" @tap.stop>
<view class="identity-dropdown-title">切换粉丝身份</view> <view class="modal-title">修改密码</view>
<view class="identity-list"> <!-- 旧密码输入框 -->
<view v-for="identity in myFanIdentities" :key="identity.star_id" class="identity-item" :class="{ <view class="modal-password-wrapper">
'identity-item-active': identity.star_id === currentStarId, <input class="modal-password-input" :type="showOldPassword ? 'text' : 'password'"
'identity-item-disabled': identity.star_id === currentStarId v-model="oldPassword" placeholder="请输入旧密码" placeholder-class="input-placeholder" />
}" @tap="handleSwitchIdentity(identity.star_id)"> <view class="modal-eye-icon" @click="showOldPassword = !showOldPassword">
<view class="identity-info"> <text class="eye-text">{{ showOldPassword ? '👁️' : '👁️‍🗨️' }}</text>
<view class="identity-main"> </view>
<text class="identity-star-name">{{ identity.star_name }}</text> </view>
<text class="identity-star-tag">{{ identity.star_tag }}</text> <!-- 新密码输入框 -->
<view class="modal-password-wrapper">
<input class="modal-password-input" :type="showNewPassword ? 'text' : 'password'"
v-model="newPassword" placeholder="请输入新密码" placeholder-class="input-placeholder" />
<view class="modal-eye-icon" @click="showNewPassword = !showNewPassword">
<text class="eye-text">{{ showNewPassword ? '👁️' : '👁️‍🗨️' }}</text>
</view>
</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closePasswordModal">取消</button>
<button class="modal-btn-confirm" @tap="confirmChangePassword">确认</button>
</view>
</view>
</view>
<!-- 添加身份弹窗 -->
<view class="add-identity-modal" v-if="showAddIdentityModal" @tap="closeAddIdentityModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">添加身份</view>
<view class="modal-subtitle">选择您喜欢的明星成为TA的粉丝</view>
<!-- 明星选择列表 -->
<view class="star-list">
<view v-for="star in fanIdentitiesList" :key="star.star_id" class="star-item"
:class="{ 'star-item-selected': selectedStarId === star.star_id }"
@tap="selectStar(star.star_id)">
<view class="star-info">
<text class="star-name">{{ star.name }}</text>
<text class="star-tag" v-if="star.tag">{{ star.tag }}</text>
</view>
<view class="star-radio">
<view v-if="selectedStarId === star.star_id" class="star-radio-checked"></view>
</view> </view>
<text class="identity-nickname">昵称: {{ identity.nickname }}</text>
</view> </view>
<view v-if="identity.star_id === currentStarId" class="identity-current-badge"> </view>
<text class="identity-current-text">当前身份</text>
<!-- 昵称输入框 -->
<input class="modal-input" v-model="newIdentityNickname" placeholder="请输入您的新昵称"
maxlength="20" />
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeAddIdentityModal">取消</button>
<button class="modal-btn-confirm" @tap="confirmAddIdentity">确认</button>
</view>
</view>
</view>
<!-- 身份切换下拉列表 -->
<view class="identity-dropdown-modal" v-if="showIdentityDropdown" @tap="closeIdentityDropdown">
<view class="identity-dropdown-content" @tap.stop>
<view class="identity-dropdown-title">切换粉丝身份</view>
<view class="identity-list">
<view v-for="identity in myFanIdentities" :key="identity.star_id" class="identity-item"
:class="{
'identity-item-active': identity.star_id === currentStarId,
'identity-item-disabled': identity.star_id === currentStarId
}" @tap="handleSwitchIdentity(identity.star_id)">
<view class="identity-info">
<view class="identity-main">
<text class="identity-star-name">{{ identity.star_name }}</text>
<text class="identity-star-tag">{{ identity.star_tag }}</text>
</view>
<text class="identity-nickname">昵称: {{ identity.nickname }}</text>
</view>
<view v-if="identity.star_id === currentStarId" class="identity-current-badge">
<text class="identity-current-text">当前身份</text>
</view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 注销账号确认弹窗 -->
<view class="confirm-modal" v-if="showDeleteAccountModal" @tap="closeDeleteAccountModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">注销账号</view>
<view class="modal-message">注销账号后您的所有数据将被永久删除且无法恢复确定要注销账号吗</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeDeleteAccountModal">取消</button>
<button class="modal-btn-confirm danger" @tap="confirmDeleteAccount">确定注销</button>
</view>
</view>
</view>
<!-- 退出登录确认弹窗 -->
<view class="confirm-modal" v-if="showLogoutModal" @tap="closeLogoutModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">退出登录</view>
<view class="modal-message">确定要退出登录吗</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeLogoutModal">取消</button>
<button class="modal-btn-confirm" @tap="confirmLogout">确定</button>
</view>
</view>
</view>
<!-- 修改头像弹窗 -->
<view class="avatar-modal" v-if="showAvatarModal" @tap="closeAvatarModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">修改头像</view>
<!-- 头像预览 -->
<view class="avatar-preview">
<Avatar :key="avatarKey" :userId="uid" :size="200" :borderWidth="6"
:avatarUrl="userAvatarUrl" />
</view>
<!-- 上传按钮 -->
<button class="upload-avatar-btn" @tap="handleUploadAvatar" :disabled="uploadingAvatar">
{{ uploadingAvatar ? '上传中...' : '上传新头像' }}
</button>
<view class="upload-hint">支持JPGPNG格式大小不超过10MB</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeAvatarModal">取消</button>
</view>
</view>
</view>
</view> </view>
<!-- 注销账号确认弹窗 --> <!-- 退出登录按钮 -->
<view class="confirm-modal" v-if="showDeleteAccountModal" @tap="closeDeleteAccountModal"> <view class="logout-section">
<view class="modal-content" @tap.stop> <button class="logout-button" @tap="handleLogout">退出登录</button>
<view class="modal-title">注销账号</view>
<view class="modal-message">注销账号后您的所有数据将被永久删除且无法恢复确定要注销账号吗</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeDeleteAccountModal">取消</button>
<button class="modal-btn-confirm danger" @tap="confirmDeleteAccount">确定注销</button>
</view>
</view>
</view> </view>
<!-- 退出登录确认弹窗 -->
<view class="confirm-modal" v-if="showLogoutModal" @tap="closeLogoutModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">退出登录</view>
<view class="modal-message">确定要退出登录吗</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeLogoutModal">取消</button>
<button class="modal-btn-confirm" @tap="confirmLogout">确定</button>
</view>
</view>
</view>
<!-- 修改头像弹窗 -->
<view class="avatar-modal" v-if="showAvatarModal" @tap="closeAvatarModal">
<view class="modal-content" @tap.stop>
<view class="modal-title">修改头像</view>
<!-- 头像预览 -->
<view class="avatar-preview">
<Avatar :key="avatarKey" :userId="uid" :size="200" :borderWidth="6"
:avatarUrl="userAvatarUrl" />
</view>
<!-- 上传按钮 -->
<button class="upload-avatar-btn" @tap="handleUploadAvatar" :disabled="uploadingAvatar">
{{ uploadingAvatar ? '上传中...' : '上传新头像' }}
</button>
<view class="upload-hint">支持JPGPNG格式大小不超过10MB</view>
<view class="modal-buttons">
<button class="modal-btn-cancel" @tap="closeAvatarModal">取消</button>
</view>
</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-section">
<button class="logout-button" @tap="handleLogout">退出登录</button>
</view>
</scroll-view> </scroll-view>
<!-- 新手引导列表弹窗 --> <!-- 新手引导弹窗 -->
<GuideListModal :visible="showGuideListModal" @start-guide="handleStartGuide" <GuideModal :visible="showGuideModal" @close="showGuideModal = false" @updated="handleGuideUpdated" />
@claim-success="handleClaimSuccess" @close="showGuideListModal = false" />
<!-- 全局引导遮罩 --> <!-- 全局引导遮罩 -->
<GuideOverlay /> <GuideOverlay />
@ -268,7 +285,7 @@ import Avatar from '../components/Avatar.vue';
import { getUserProfileApi, deleteAccountApi, updateNicknameApi, updatePasswordApi, getFanIdentitiesApi, addFanIdentityApi, getMyFanIdentitiesApi, switchFanIdentityApi, getOssSignatureApi, updateAvatarApi } from '@/utils/api'; import { getUserProfileApi, deleteAccountApi, updateNicknameApi, updatePasswordApi, getFanIdentitiesApi, addFanIdentityApi, getMyFanIdentitiesApi, switchFanIdentityApi, getOssSignatureApi, updateAvatarApi } from '@/utils/api';
import { validateNickname } from '@/utils/validator.js'; import { validateNickname } from '@/utils/validator.js';
import { clearAvatarCache } from '@/utils/avatarCache'; import { clearAvatarCache } from '@/utils/avatarCache';
import GuideListModal from '@/components/GuideListModal.vue'; import GuideModal from '@/pages/tasks/GuideModal.vue';
import GuideOverlay from '@/components/GuideOverlay.vue'; import GuideOverlay from '@/components/GuideOverlay.vue';
import { import {
getClaimableRewardCount getClaimableRewardCount
@ -295,8 +312,13 @@ const newPassword = ref('');
const showOldPassword = ref(false); const showOldPassword = ref(false);
// //
const showGuideListModal = ref(false); const showGuideModal = ref(false);
const guideClaimableCount = computed(() => getClaimableRewardCount()); const guideClaimableCount = computed(() => getClaimableRewardCount());
//
const handleGuideUpdated = () => {
console.log('[Profile] Guide updated');
};
const showNewPassword = ref(false); const showNewPassword = ref(false);
// //
@ -325,18 +347,33 @@ const avatarKey = ref(0); // 用于强制刷新Avatar组件
// //
const mobile = ref(''); const mobile = ref('');
// // /
const showBlockNumber = ref(false);
const showMobile = ref(false);
// showMobile
const displayMobile = computed(() => { const displayMobile = computed(() => {
return mobile.value || ''; console.log('displayMobile computed, mobile:', mobile.value, 'showMobile:', showMobile.value);
if (!mobile.value) return '';
// showMobiletruefalse
if (showMobile.value) {
return mobile.value;
}
// 34
const phone = mobile.value;
return phone.length === 11 ? phone.slice(0, 3) + '****' + phone.slice(-4) : phone;
}); });
// //
const displayAddress = computed(() => { const displayAddress = computed(() => {
const address = blockchainAddress.value; const address = blockchainAddress.value;
console.log('displayAddress computed, address:', address, 'showBlockNumber:', showBlockNumber.value);
// //
if (!address) return '0xabcd...123c'; if (!address) return '0xabcd...123c';
// showBlockNumbertruefalse
if (showBlockNumber.value) return address;
if (address.length <= 20) return address; if (address.length <= 20) return address;
// 108 // 44
return address.substring(0, 4) + '...' + address.substring(address.length - 4); return address.substring(0, 4) + '...' + address.substring(address.length - 4);
}); });
@ -392,7 +429,7 @@ const fetchUserInfo = async (forceRefresh = false) => {
fanTag.value = userForCache.fan_identity?.name || ''; fanTag.value = userForCache.fan_identity?.name || '';
blockchainAddress.value = userForCache.blockchain_address || ''; blockchainAddress.value = userForCache.blockchain_address || '';
userAvatarUrl.value = userForCache.avatar_url || ''; userAvatarUrl.value = userForCache.avatar_url || '';
mobile.value = uni.getStorageSync('login_mobile') || ''; mobile.value = userForCache.mobile || uni.getStorageSync('login_mobile') || '';
} }
} catch (error) { } catch (error) {
console.error('获取用户信息失败:', error); console.error('获取用户信息失败:', error);
@ -408,7 +445,7 @@ const fetchUserInfo = async (forceRefresh = false) => {
fanTag.value = cachedUser.fan_identity?.tag || ''; fanTag.value = cachedUser.fan_identity?.tag || '';
blockchainAddress.value = cachedUser.blockchain_address || ''; blockchainAddress.value = cachedUser.blockchain_address || '';
userAvatarUrl.value = cachedUser.avatar_url || ''; userAvatarUrl.value = cachedUser.avatar_url || '';
mobile.value = uni.getStorageSync('login_mobile') || ''; mobile.value = cachedUser.mobile || uni.getStorageSync('login_mobile') || '';
} catch (e) { } catch (e) {
console.error('解析缓存用户信息失败:', e); console.error('解析缓存用户信息失败:', e);
} }
@ -423,6 +460,12 @@ const fetchUserInfo = async (forceRefresh = false) => {
} }
}; };
//
const toggleMobileDisplay = () => {
console.log('toggleMobileDisplay called, showMobile:', showMobile.value);
showMobile.value = !showMobile.value;
};
// UID // UID
const copyUid = () => { const copyUid = () => {
if (!uid.value) { if (!uid.value) {
@ -445,19 +488,6 @@ const handleViewNickname = () => {
}); });
}; };
//
const handleViewMobile = () => {
if (!displayMobile.value) {
uni.showToast({ title: '未绑定手机号', icon: 'none' });
return;
}
uni.setClipboardData({
data: displayMobile.value,
success: () => uni.showToast({ title: '已复制手机号', icon: 'success' }),
fail: () => uni.showToast({ title: '复制失败', icon: 'none' })
});
};
// //
const copyAddress = () => { const copyAddress = () => {
if (!blockchainAddress.value) { if (!blockchainAddress.value) {
@ -917,50 +947,7 @@ const handleDeleteAccount = () => {
// //
const handleGuideClick = () => { const handleGuideClick = () => {
showGuideListModal.value = true; showGuideModal.value = true;
};
//
const handleStartGuide = (params) => {
// { key, fromStep, targetPage } key
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('[Profile] handleStartGuide:', { key, fromStep, targetPage });
showGuideListModal.value = false;
//
if (key === 'square_home') {
// 使 targetPage
const navigateUrl = targetPage || '/pages/square/square';
uni.navigateTo({
url: navigateUrl,
success: () => {
//
setTimeout(() => {
if (fromStep === 0) {
store.dispatch("guide/startGuideFromBeginning", key);
} else {
store.dispatch("guide/resumeGuide", key);
}
}, 500);
}
});
} else {
//
if (fromStep === 0) {
store.dispatch("guide/startGuideFromBeginning", key);
} else {
store.dispatch("guide/resumeGuide", key);
}
}
};
//
const handleClaimSuccess = (reward) => {
console.log("[Profile] 领取奖励成功:", reward);
}; };
// //
@ -1309,12 +1296,12 @@ onShow(() => {
display: none; display: none;
} }
.close-icon-img{ .close-icon-img {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
position: relative; position: relative;
top: 32rpx; top: 32rpx;
left: 32rpx; left: 32rpx;
} }
.user-info-card { .user-info-card {
@ -1353,8 +1340,8 @@ onShow(() => {
gap: 8rpx; gap: 8rpx;
margin-left: 8rpx; margin-left: 8rpx;
background-image: url(/static/rank/activity-support-icon/beijingkuang.png); background-image: url(/static/rank/activity-support-icon/beijingkuang.png);
background-size: 100% 110%; background-size: 100% 110%;
background-position: center; background-position: center;
padding: 24rpx 24rpx 24rpx 240rpx; padding: 24rpx 24rpx 24rpx 240rpx;
min-width: 356rpx; min-width: 356rpx;
} }
@ -1371,6 +1358,11 @@ onShow(() => {
min-width: 112rpx; min-width: 112rpx;
} }
.address-row {
display: flex;
align-items: center;
}
.info-value { .info-value {
font-size: 24rpx; font-size: 24rpx;
color: #ffffff; color: #ffffff;
@ -1378,15 +1370,18 @@ onShow(() => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
min-width: 200rpx;
} }
.uid-value { .uid-value {}
max-width: 200rpx;
.toggle-icon {
width: 32rpx;
height: 32rpx;
} }
.address-value { .address-value {}
max-width: 160rpx;
}
.info-btn { .info-btn {
font-size: 20rpx; font-size: 20rpx;
@ -1521,7 +1516,7 @@ onShow(() => {
.asset-card { .asset-card {
background-image: url(/static/rank/activity-support-icon/beijingkuang.png); background-image: url(/static/rank/activity-support-icon/beijingkuang.png);
background-size: 100% 100%; background-size: 100% 125%;
background-position: center; background-position: center;
border-radius: 20rpx; border-radius: 20rpx;
padding: 0 16rpx; padding: 0 16rpx;
@ -1619,8 +1614,10 @@ onShow(() => {
.logout-section { .logout-section {
padding: 10rpx 30rpx; padding: 10rpx 30rpx;
padding-bottom: calc(40rpx + constant(safe-area-inset-bottom)); /* iOS 11.0 */ padding-bottom: calc(40rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */ /* iOS 11.0 */
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
/* iOS 11.2+ */
display: flex; display: flex;
position: relative; position: relative;
} }
@ -1686,7 +1683,10 @@ onShow(() => {
.modal-content { .modal-content {
width: 80%; width: 80%;
max-width: 600rpx; max-width: 600rpx;
background: #ffffff; /* background: #ffffff; */
background-image: url('/static/starbookcontent/beijing.png');
background-size: cover;
background-position: center bottom;
border-radius: 30rpx; border-radius: 30rpx;
padding: 60rpx 40rpx; padding: 60rpx 40rpx;
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,13 +1,7 @@
<template> <template>
<view class="theme-banner"> <view class="theme-banner">
<!-- 背景图 - 使用懒加载 --> <!-- 背景图 - 使用懒加载 -->
<image <image v-if="bannerImage" :src="bannerImage" class="banner-bg" mode="aspectFill" :lazy-load="true" />
v-if="bannerImage"
:src="bannerImage"
class="banner-bg"
mode="aspectFill"
:lazy-load="true"
/>
<!-- 内容层 --> <!-- 内容层 -->
<view class="banner-content" @tap="handleBannerClick"> <view class="banner-content" @tap="handleBannerClick">
@ -16,7 +10,7 @@
<view class="title-section"> <view class="title-section">
<view class="subtitle-row"> <view class="subtitle-row">
<text class="subtitle-text">{{'「'+ title +'」' }}</text> <text class="subtitle-text">{{ '「' + title + '」' }}</text>
<!-- 如果确实需要过时数据警告可以保留否则可隐藏 --> <!-- 如果确实需要过时数据警告可以保留否则可隐藏 -->
<view v-if="isStaleData" class="stale-indicator"> <view v-if="isStaleData" class="stale-indicator">
@ -28,7 +22,7 @@
<!-- 下半部分右下角数字 --> <!-- 下半部分右下角数字 -->
<view class="footer-section"> <view class="footer-section">
<text class="progress-text">{{ formattedCurrent }} / {{ formattedTarget }}</text> <text class="progress-text">当前进度<text class="current-value">{{ formattedCurrent }}</text> / {{ formattedTarget }}</text>
</view> </view>
</view> </view>
@ -178,13 +172,26 @@ const progressPercent = computed(() => {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: flex-end; align-items: flex-end;
} }
.progress-text { .progress-text {
font-size: 32rpx; font-size: 24rpx;
color: #fff; color: #fff;
text-shadow: 0 0 8rpx rgba(255, 255, 255, 0.6); text-shadow: 0 0 8rpx rgba(255, 255, 255, 0.6);
font-family: 'yt', sans-serif; font-family: 'yt', sans-serif;
background-image: url('@/static/rank/activity-support-icon/beijingkuang.png');
background-size: cover;
background-position: center;
padding: 0.8rpx 24rpx;
border-radius: 40rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.6);
}
.current-value {
color: #FFD700;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.6);
} }
.progress-bar { .progress-bar {

View File

@ -17,7 +17,7 @@
<view class="modal-header"> <view class="modal-header">
<!-- 返回按钮 --> <!-- 返回按钮 -->
<view class="back-button" @click="handleClose"> <view class="back-button" @click="handleClose">
<image class="back-icon" src="/static/icon/back.png" mode="aspectFit" /> <image class="back-icon" src="/static/starbookcontent/tuichu.png" mode="aspectFit" />
</view> </view>
<!-- 标题 --> <!-- 标题 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -96,8 +96,8 @@ const actions = {
// 缓存用户信息 // 缓存用户信息
const user = res.data.user const user = res.data.user
// 缓存登录手机号(优先使用后端返回的脱敏手机号) // 缓存登录手机号
const loginMobile = user.mobile_masked || mobile const loginMobile = mobile
uni.setStorageSync('login_mobile', loginMobile) uni.setStorageSync('login_mobile', loginMobile)
uni.setStorageSync('user', JSON.stringify(user)) uni.setStorageSync('user', JSON.stringify(user))
@ -131,8 +131,8 @@ const actions = {
// 缓存用户信息 // 缓存用户信息
const user = res.data.user const user = res.data.user
// 缓存登录手机号(优先使用后端返回的脱敏手机号) // 缓存登录手机号
const loginMobile = user.mobile_masked || mobile const loginMobile = mobile
uni.setStorageSync('login_mobile', loginMobile) uni.setStorageSync('login_mobile', loginMobile)
uni.setStorageSync('user', JSON.stringify(user)) uni.setStorageSync('user', JSON.stringify(user))

View File

@ -3,19 +3,19 @@
// 不需要手动注释! // 不需要手动注释!
// #ifdef H5 // #ifdef H5
// const baseURL = 'http://localhost:8080' // H5 开发用本机 const baseURL = 'http://192.168.110.60:8080' // H5 开发用本机
const baseURL = 'http://101.132.250.62:8080' // H5 开发用本机 // const baseURL = 'http://101.132.250.62:8080' // H5 开发用本机
// #endif // #endif
// #ifdef APP-PLUS // #ifdef APP-PLUS
// 开发调试手机和电脑同一WiFi时用这个改成你电脑IP // 开发调试手机和电脑同一WiFi时用这个改成你电脑IP
// 上线后:改成实际服务器地址 // 上线后:改成实际服务器地址
// const baseURL = 'http://192.168.110.60:8080' const baseURL = 'http://192.168.110.60:8080'
// #endif // #endif
// 服务器地址(正式上线用) // 服务器地址(正式上线用)
// #ifdef APP-PLUS // #ifdef APP-PLUS
const baseURL = 'http://101.132.250.62:8080' // const baseURL = 'http://101.132.250.62:8080'
// #endif // #endif
// 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false // 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false
@ -468,7 +468,9 @@ export function getBatchOssPresignedUrlsApi(files, expires = 3600, type = 'asset
* @returns {Promise<{ imageUrl: string, orderId?: string, data: object }>} * @returns {Promise<{ imageUrl: string, orderId?: string, data: object }>}
*/ */
export function uploadLocalFileToOss(filePath, options = {}) { export function uploadLocalFileToOss(filePath, options = {}) {
const { type = 'asset', orderId = '', fileName } = options const {
type = 'asset', orderId = '', fileName
} = options
const objectName = fileName || `${Date.now()}.jpg` const objectName = fileName || `${Date.now()}.jpg`
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getOssSignatureApi(type, orderId) getOssSignatureApi(type, orderId)