2000 lines
47 KiB
Vue
2000 lines
47 KiB
Vue
<template>
|
||
<view class="profile-container">
|
||
<!-- 背景图片 - 固定在容器上 -->
|
||
<image class="background-image" src="/static/background/starbook.png" mode="aspectFill"></image>
|
||
|
||
<scroll-view class="profile-scroll" scroll-y :show-scrollbar="false" :enable-back-to-top="true">
|
||
<!-- 上半部分:用户信息卡片 -->
|
||
<view class="top-section">
|
||
|
||
<!-- 蒙层 -->
|
||
<view class="background-overlay"></view>
|
||
|
||
<!-- Header组件 -->
|
||
<Header :showBack="true" :showGuideIcon="false" :showTaskIcon="false" :showStarActivityIcon="false" />
|
||
|
||
<!-- 用户信息卡片 -->
|
||
<view class="user-info-card">
|
||
<!-- 上半部分:头像和用户信息左右布局 -->
|
||
<view class="user-main-info">
|
||
<view @tap="handleAvatarClick" class="avatar-container">
|
||
<Avatar :key="avatarKey" :userId="uid" :size="180" :borderWidth="6" :showLevel="true"
|
||
:level="fanLevel" :avatarUrl="userAvatarUrl" />
|
||
</view>
|
||
<view class="user-text-info">
|
||
<!-- 第1行:等级 + 昵称 + 粉丝标签 -->
|
||
<view class="name-wrapper">
|
||
<!-- <text class="level-badge">LV {{ fanLevel }}</text> -->
|
||
<text class="user-name">{{ nickname }}</text>
|
||
<view class="fan-tag-badge" v-if="fanTag" @tap="handleFanTagClick">
|
||
<text class="fan-tag-text">{{ fanTag }}</text>
|
||
<image class="fan-tag-arrow" src="/static/icon/dropdown-arrow.png" mode="aspectFit">
|
||
</image>
|
||
</view>
|
||
</view>
|
||
<!-- 第2行:DID -->
|
||
<text class="user-did">数字身份DID: {{ uid }}</text>
|
||
<!-- 第3行:区块链地址 -->
|
||
<view class="blockchain-address-wrapper">
|
||
<text class="blockchain-address-text">链上地址: {{ displayAddress }}</text>
|
||
<view class="copy-icon" @tap="copyAddress">
|
||
<text class="copy-icon-text">📋</text>
|
||
</view>
|
||
</view>
|
||
<!-- 第4行:手机号 -->
|
||
<text class="user-mobile" v-if="displayMobile">{{ displayMobile }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 下半部分:白色背景区域 -->
|
||
<view class="bottom-section">
|
||
<view class="content-area">
|
||
<!-- 我的资产 -->
|
||
<view class="section-bg" style="padding-top: 0;">
|
||
<view class="section-title">我的资产</view>
|
||
<view class="assets-grid">
|
||
<view class="asset-card" @tap="handleAssetClick('exhibition')">
|
||
<image class="asset-icon" src="/static/icon/square.png" mode="aspectFit"></image>
|
||
<text class="asset-text">我的展馆</text>
|
||
<!-- <text class="arrow">›</text> -->
|
||
</view>
|
||
<view class="asset-card" @tap="handleAssetClick('starbook')">
|
||
<image class="asset-icon" src="/static/icon/starbook.png" mode="aspectFit"></image>
|
||
<text class="asset-text">我的星册</text>
|
||
<!-- <text class="arrow">›</text> -->
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 服务与工具 -->
|
||
<view class="section-bg">
|
||
<view class="section-title">服务与工具</view>
|
||
<view class="service-buttons-container">
|
||
<!-- 新手指引入口 -->
|
||
<view class="service-button" @tap="handleGuideClick">
|
||
<image class="service-icon" src="/static/icon/onboarding.png" mode="aspectFit"></image>
|
||
<text class="service-text">新手指引</text>
|
||
<text v-if="guideClaimableCount > 0" class="guide-badge">{{ guideClaimableCount }}</text>
|
||
</view>
|
||
<view class="service-button" @tap="handleChangeNickname">
|
||
<image class="service-icon" src="/static/icon/edit-nickname.png" mode="aspectFit"></image>
|
||
<text class="service-text">修改昵称</text>
|
||
</view>
|
||
<view class="service-button" @tap="handleChangePassword">
|
||
<image class="service-icon" src="/static/icon/edit-password.png" mode="aspectFit"></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>
|
||
<text class="service-text">添加身份</text>
|
||
</view>
|
||
<view class="service-button" @tap="handleDeleteAccount">
|
||
<image class="service-icon" src="/static/icon/logout.png" mode="aspectFit"></image>
|
||
<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>
|
||
|
||
<!-- 昵称输入框 -->
|
||
<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 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">支持JPG、PNG格式,大小不超过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>
|
||
|
||
<!-- 新手引导列表弹窗 -->
|
||
<GuideListModal :visible="showGuideListModal" @start-guide="handleStartGuide"
|
||
@claim-success="handleClaimSuccess" @close="showGuideListModal = false" />
|
||
|
||
<!-- 全局引导遮罩 -->
|
||
<GuideOverlay />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue';
|
||
import { onShow, onLoad } from '@dcloudio/uni-app';
|
||
import { useStore } from 'vuex';
|
||
import { onReady } from "@dcloudio/uni-app";
|
||
import Header from '../components/Header.vue';
|
||
import Avatar from '../components/Avatar.vue';
|
||
import { getUserProfileApi, deleteAccountApi, updateNicknameApi, updatePasswordApi, getFanIdentitiesApi, addFanIdentityApi, getMyFanIdentitiesApi, switchFanIdentityApi, getOssSignatureApi, updateAvatarApi, getOssPresignedUrlApi } from '@/utils/api';
|
||
import { validateNickname } from '@/utils/validator.js';
|
||
import { clearAvatarCache, downloadAndCacheAvatar } from '@/utils/avatarCache';
|
||
import GuideListModal from '@/components/GuideListModal.vue';
|
||
import GuideOverlay from '@/components/GuideOverlay.vue';
|
||
import {
|
||
getClaimableRewardCount
|
||
} from '@/utils/guideConfig.js';
|
||
|
||
const store = useStore();
|
||
|
||
// 用户信息
|
||
const nickname = ref('');
|
||
const uid = ref('');
|
||
const blockchainAddress = ref('');
|
||
const fanLevel = ref(0);
|
||
const fanTag = ref('');
|
||
const loading = ref(false);
|
||
|
||
// 修改昵称弹窗
|
||
const showNicknameModal = ref(false);
|
||
const newNickname = ref('');
|
||
|
||
// 修改密码弹窗
|
||
const showPasswordModal = ref(false);
|
||
const oldPassword = ref('');
|
||
const newPassword = ref('');
|
||
const showOldPassword = ref(false);
|
||
|
||
// 新手引导
|
||
const showGuideListModal = ref(false);
|
||
const guideClaimableCount = computed(() => getClaimableRewardCount());
|
||
const showNewPassword = ref(false);
|
||
|
||
// 添加身份弹窗
|
||
const showAddIdentityModal = ref(false);
|
||
const fanIdentitiesList = ref([]);
|
||
const selectedStarId = ref(null);
|
||
const newIdentityNickname = ref('');
|
||
|
||
// 身份切换下拉列表
|
||
const showIdentityDropdown = ref(false);
|
||
const myFanIdentities = ref([]);
|
||
const currentStarId = ref(null);
|
||
|
||
// 注销账号确认弹窗
|
||
const showDeleteAccountModal = ref(false);
|
||
|
||
// 退出登录确认弹窗
|
||
const showLogoutModal = ref(false);
|
||
|
||
// 修改头像弹窗
|
||
const showAvatarModal = ref(false);
|
||
const userAvatarUrl = ref('');
|
||
const uploadingAvatar = ref(false);
|
||
const avatarKey = ref(0); // 用于强制刷新Avatar组件
|
||
|
||
// 手机号
|
||
const mobile = ref('');
|
||
|
||
// 显示脱敏后的手机号(后端已脱敏,直接显示)
|
||
const displayMobile = computed(() => {
|
||
return mobile.value || '';
|
||
});
|
||
|
||
// 显示截断后的区块链地址
|
||
const displayAddress = computed(() => {
|
||
const address = blockchainAddress.value;
|
||
// 如果没有地址,显示默认地址
|
||
if (!address) return '0xabcd...123c';
|
||
if (address.length <= 20) return address;
|
||
// 显示前10个字符和后8个字符
|
||
return address.substring(0, 4) + '...' + address.substring(address.length - 4);
|
||
});
|
||
|
||
// 获取用户信息
|
||
const fetchUserInfo = async (forceRefresh = false) => {
|
||
loading.value = true;
|
||
try {
|
||
// 每次进入页面都从API获取最新数据
|
||
const res = await getUserProfileApi();
|
||
|
||
if (res.code === 200 && res.data) {
|
||
const apiData = res.data;
|
||
|
||
// 读取旧缓存数据(保留某些字段)
|
||
let oldCachedUser = null;
|
||
const oldCachedUserStr = uni.getStorageSync('user');
|
||
if (oldCachedUserStr) {
|
||
try {
|
||
oldCachedUser = JSON.parse(oldCachedUserStr);
|
||
} catch (e) {
|
||
console.error('解析旧缓存失败:', e);
|
||
}
|
||
}
|
||
|
||
// 转换API数据结构为缓存格式
|
||
const userForCache = {
|
||
uid: apiData.uid,
|
||
nickname: apiData.nickname,
|
||
avatar_url: apiData.avatar_url || '',
|
||
// 从 current_identity 转换为 fan_identity
|
||
fan_identity: apiData.current_identity ? {
|
||
identity_id: apiData.current_identity.identity_id,
|
||
name: apiData.current_identity.identity_name,
|
||
tag: apiData.current_identity.tag
|
||
} : (oldCachedUser?.fan_identity || null),
|
||
fan_level: apiData.current_identity?.level || 0,
|
||
crystal_balance: apiData.current_identity?.crystal_balance || 0,
|
||
// 保留旧缓存中的其他字段(如果API没有返回)
|
||
blockchain_address: apiData.blockchain_address || oldCachedUser?.blockchain_address || '',
|
||
assets_num: apiData.assets_num !== undefined ? apiData.assets_num : (oldCachedUser?.assets_num || 0),
|
||
slot_limit: apiData.slot_limit !== undefined ? apiData.slot_limit : (oldCachedUser?.slot_limit || 3),
|
||
starbook_limit: apiData.starbook_limit !== undefined ? apiData.starbook_limit : (oldCachedUser?.starbook_limit || 3),
|
||
mobile: apiData.mobile || oldCachedUser?.mobile || ''
|
||
};
|
||
|
||
// 覆盖式更新缓存
|
||
uni.setStorageSync('user', JSON.stringify(userForCache));
|
||
|
||
// 更新显示
|
||
nickname.value = userForCache.nickname || '';
|
||
uid.value = userForCache.uid || '';
|
||
fanLevel.value = userForCache.fan_level || 0;
|
||
fanTag.value = userForCache.fan_identity?.tag || '';
|
||
blockchainAddress.value = userForCache.blockchain_address || '';
|
||
userAvatarUrl.value = userForCache.avatar_url || '';
|
||
mobile.value = uni.getStorageSync('login_mobile') || '';
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户信息失败:', error);
|
||
|
||
// 如果API调用失败,尝试从缓存读取
|
||
const cachedUserStr = uni.getStorageSync('user');
|
||
if (cachedUserStr) {
|
||
try {
|
||
const cachedUser = JSON.parse(cachedUserStr);
|
||
nickname.value = cachedUser.nickname || '';
|
||
uid.value = cachedUser.uid || '';
|
||
fanLevel.value = cachedUser.fan_level || 0;
|
||
fanTag.value = cachedUser.fan_identity?.tag || '';
|
||
blockchainAddress.value = cachedUser.blockchain_address || '';
|
||
userAvatarUrl.value = cachedUser.avatar_url || '';
|
||
mobile.value = uni.getStorageSync('login_mobile') || '';
|
||
} catch (e) {
|
||
console.error('解析缓存用户信息失败:', e);
|
||
}
|
||
}
|
||
|
||
uni.showToast({
|
||
title: error.message || '获取用户信息失败',
|
||
icon: 'none'
|
||
});
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 复制区块链地址
|
||
const copyAddress = () => {
|
||
if (!blockchainAddress.value) {
|
||
uni.showToast({
|
||
title: '地址为空',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
uni.setClipboardData({
|
||
data: blockchainAddress.value,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: '已复制到剪贴板',
|
||
icon: 'success'
|
||
});
|
||
},
|
||
fail: () => {
|
||
uni.showToast({
|
||
title: '复制失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
};
|
||
|
||
// 处理资产按钮点击
|
||
const handleAssetClick = (type) => {
|
||
if (type === 'exhibition') {
|
||
// 跳转到展馆页面
|
||
uni.navigateTo({
|
||
url: '/pages/exhibition/exhibition'
|
||
});
|
||
} else if (type === 'starbook') {
|
||
// 跳转到星册页面
|
||
uni.navigateTo({
|
||
url: '/pages/starbook/index'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理修改昵称
|
||
const handleChangeNickname = () => {
|
||
newNickname.value = nickname.value; // 初始化输入框为当前昵称
|
||
showNicknameModal.value = true;
|
||
};
|
||
|
||
// 关闭修改昵称弹窗
|
||
const closeNicknameModal = () => {
|
||
showNicknameModal.value = false;
|
||
newNickname.value = '';
|
||
};
|
||
|
||
// 确认修改昵称
|
||
const confirmChangeNickname = async () => {
|
||
const trimmedNickname = newNickname.value.trim();
|
||
|
||
// 验证昵称
|
||
const validation = validateNickname(trimmedNickname);
|
||
if (!validation.valid) {
|
||
uni.showToast({
|
||
title: validation.message,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 校验新昵称和当前昵称是否相同
|
||
if (trimmedNickname === nickname.value) {
|
||
uni.showToast({
|
||
title: '不能使用相同昵称',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '修改中...',
|
||
mask: true
|
||
});
|
||
|
||
// 调用修改昵称API
|
||
const res = await updateNicknameApi(trimmedNickname);
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 200) {
|
||
// 修改成功,强制从服务器刷新个人信息
|
||
await fetchUserInfo(true);
|
||
|
||
uni.showToast({
|
||
title: '昵称修改成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
closeNicknameModal();
|
||
} else {
|
||
// 业务错误
|
||
uni.showToast({
|
||
title: res.message || '修改失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
// 显示错误信息
|
||
uni.showToast({
|
||
title: error.message || '修改失败,请重试',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理修改密码
|
||
const handleChangePassword = () => {
|
||
oldPassword.value = '';
|
||
newPassword.value = '';
|
||
showOldPassword.value = false;
|
||
showNewPassword.value = false;
|
||
showPasswordModal.value = true;
|
||
};
|
||
|
||
// 关闭修改密码弹窗
|
||
const closePasswordModal = () => {
|
||
showPasswordModal.value = false;
|
||
oldPassword.value = '';
|
||
newPassword.value = '';
|
||
showOldPassword.value = false;
|
||
showNewPassword.value = false;
|
||
};
|
||
|
||
// 确认修改密码
|
||
const confirmChangePassword = async () => {
|
||
// 验证旧密码
|
||
if (!oldPassword.value.trim()) {
|
||
uni.showToast({
|
||
title: '请输入旧密码',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 验证新密码
|
||
if (!newPassword.value.trim()) {
|
||
uni.showToast({
|
||
title: '请输入新密码',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '修改中...',
|
||
mask: true
|
||
});
|
||
|
||
// 调用修改密码API
|
||
const res = await updatePasswordApi(oldPassword.value.trim(), newPassword.value.trim());
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 200) {
|
||
// 修改成功
|
||
uni.showToast({
|
||
title: '修改成功,请重新登录',
|
||
icon: 'success',
|
||
duration: 2000
|
||
});
|
||
|
||
// 延迟执行登出和跳转
|
||
setTimeout(() => {
|
||
// 调用store的logout方法
|
||
store.dispatch('user/logout');
|
||
|
||
// 清除所有本地缓存
|
||
uni.removeStorageSync('access_token');
|
||
uni.removeStorageSync('user');
|
||
uni.removeStorageSync('nickname');
|
||
// 清除临时注册数据(如果有)
|
||
uni.removeStorageSync('temp_register_mobile');
|
||
uni.removeStorageSync('temp_register_password');
|
||
uni.removeStorageSync('temp_register_nickname');
|
||
|
||
// 跳转到登录页
|
||
uni.reLaunch({
|
||
url: '/pages/login/login'
|
||
});
|
||
}, 2000);
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
// 处理错误,显示错误信息
|
||
const errorMessage = error.message || '修改失败,请重试';
|
||
uni.showToast({
|
||
title: errorMessage,
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理添加身份
|
||
const handleSwitchRole = async () => {
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '加载中...',
|
||
mask: true
|
||
});
|
||
|
||
// 获取可选的粉丝身份列表
|
||
const res = await getFanIdentitiesApi();
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 200 && res.data && res.data.items) {
|
||
fanIdentitiesList.value = res.data.items;
|
||
showAddIdentityModal.value = true;
|
||
} else {
|
||
uni.showToast({
|
||
title: res.message || '获取明星列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('获取明星列表失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '获取明星列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 选择明星
|
||
const selectStar = (starId) => {
|
||
selectedStarId.value = starId;
|
||
};
|
||
|
||
// 关闭添加身份弹窗
|
||
const closeAddIdentityModal = () => {
|
||
showAddIdentityModal.value = false;
|
||
selectedStarId.value = null;
|
||
newIdentityNickname.value = '';
|
||
fanIdentitiesList.value = [];
|
||
};
|
||
|
||
// 确认添加身份
|
||
const confirmAddIdentity = async () => {
|
||
// 验证是否选择了明星
|
||
if (!selectedStarId.value) {
|
||
uni.showToast({
|
||
title: '请选择您喜欢的明星',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 验证昵称
|
||
const trimmedNickname = newIdentityNickname.value.trim();
|
||
const validation = validateNickname(trimmedNickname);
|
||
if (!validation.valid) {
|
||
uni.showToast({
|
||
title: validation.message,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '添加中...',
|
||
mask: true
|
||
});
|
||
|
||
// 调用添加粉丝身份API
|
||
const res = await addFanIdentityApi(selectedStarId.value, trimmedNickname);
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 200) {
|
||
// 添加成功
|
||
uni.showToast({
|
||
title: '添加身份成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
// 关闭弹窗
|
||
closeAddIdentityModal();
|
||
|
||
// 强制刷新用户信息
|
||
await fetchUserInfo(true);
|
||
} else {
|
||
uni.showToast({
|
||
title: res.message || '添加失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('添加身份失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '添加失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理粉丝标签点击
|
||
const handleFanTagClick = async () => {
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '加载中...',
|
||
mask: true
|
||
});
|
||
|
||
// 获取用户的所有粉丝身份
|
||
const res = await getMyFanIdentitiesApi();
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 200 && res.data && res.data.items) {
|
||
myFanIdentities.value = res.data.items;
|
||
currentStarId.value = res.data.current_star_id;
|
||
showIdentityDropdown.value = true;
|
||
} else {
|
||
uni.showToast({
|
||
title: res.message || '获取身份列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('获取身份列表失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '获取身份列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 关闭身份切换下拉列表
|
||
const closeIdentityDropdown = () => {
|
||
showIdentityDropdown.value = false;
|
||
myFanIdentities.value = [];
|
||
currentStarId.value = null;
|
||
};
|
||
|
||
// 处理切换身份
|
||
const handleSwitchIdentity = async (starId) => {
|
||
// 验证不能切换到当前身份
|
||
if (starId === currentStarId.value) {
|
||
uni.showToast({
|
||
title: '当前已是该身份',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '切换中...',
|
||
mask: true
|
||
});
|
||
|
||
// 调用切换身份API
|
||
const res = await switchFanIdentityApi(starId);
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 200 && res.data) {
|
||
// 更新本地缓存的 access_token
|
||
uni.setStorageSync('access_token', res.data.access_token);
|
||
|
||
// 更新用户信息
|
||
const currentIdentity = res.data.current_identity;
|
||
if (currentIdentity) {
|
||
// 从当前身份列表中找到完整信息
|
||
const identityInfo = myFanIdentities.value.find(item => item.star_id === starId);
|
||
if (identityInfo) {
|
||
nickname.value = identityInfo.nickname;
|
||
}
|
||
fanTag.value = currentIdentity.tag;
|
||
fanLevel.value = currentIdentity.level;
|
||
}
|
||
|
||
// 更新用户缓存
|
||
const newStarId = currentIdentity ? currentIdentity.star_id : starId;
|
||
const cachedUserStr = uni.getStorageSync('user');
|
||
if (cachedUserStr) {
|
||
try {
|
||
const cachedUser = JSON.parse(cachedUserStr);
|
||
cachedUser.nickname = nickname.value;
|
||
cachedUser.fan_level = currentIdentity ? currentIdentity.level : fanLevel.value;
|
||
cachedUser.crystal_balance = currentIdentity ? currentIdentity.crystal_balance : (cachedUser.crystal_balance || 0);
|
||
cachedUser.star_id = newStarId;
|
||
cachedUser.fan_identity = {
|
||
star_id: newStarId,
|
||
identity_id: currentIdentity.identity_id,
|
||
name: currentIdentity.identity_name,
|
||
tag: currentIdentity.tag
|
||
};
|
||
uni.setStorageSync('user', JSON.stringify(cachedUser));
|
||
// 同步更新独立的 star_id key,供 square.vue / RankingModal 直接读取
|
||
uni.setStorageSync('star_id', newStarId);
|
||
store.commit('user/SET_USER_INFO', cachedUser);
|
||
} catch (e) {
|
||
console.error('更新用户缓存失败:', e);
|
||
}
|
||
}
|
||
|
||
// 显示成功提示
|
||
uni.showToast({
|
||
title: '切换身份成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
// 关闭下拉列表
|
||
closeIdentityDropdown();
|
||
|
||
// 先从服务器拉取最新用户信息(含新身份的 avatar_url)
|
||
await fetchUserInfo(true);
|
||
|
||
// 拉取完成后通知 Header 用最新缓存刷新
|
||
uni.$emit('userInfoUpdated');
|
||
uni.$emit('avatarUpdated', { avatarUrl: userAvatarUrl.value });
|
||
uni.$emit('balanceUpdated', { crystal_balance: currentIdentity ? currentIdentity.crystal_balance : 0 });
|
||
} else {
|
||
uni.showToast({
|
||
title: res.message || '切换失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('切换身份失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '切换失败,请重试',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理注销账号
|
||
const handleDeleteAccount = () => {
|
||
showDeleteAccountModal.value = true;
|
||
};
|
||
|
||
// 点击新手指引
|
||
const handleGuideClick = () => {
|
||
showGuideListModal.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);
|
||
};
|
||
|
||
// 关闭注销账号弹窗
|
||
const closeDeleteAccountModal = () => {
|
||
showDeleteAccountModal.value = false;
|
||
};
|
||
|
||
// 确认注销账号
|
||
const confirmDeleteAccount = async () => {
|
||
try {
|
||
// 关闭弹窗
|
||
closeDeleteAccountModal();
|
||
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '注销中...',
|
||
mask: true
|
||
});
|
||
|
||
// 调用注销账号接口
|
||
const result = await deleteAccountApi();
|
||
|
||
uni.hideLoading();
|
||
|
||
if (result.code === 200) {
|
||
// 调用store的logout方法
|
||
store.dispatch('user/logout');
|
||
|
||
// 清除所有本地缓存
|
||
uni.removeStorageSync('access_token');
|
||
uni.removeStorageSync('user');
|
||
uni.removeStorageSync('nickname');
|
||
// 清除临时注册数据(如果有)
|
||
uni.removeStorageSync('temp_register_mobile');
|
||
uni.removeStorageSync('temp_register_password');
|
||
uni.removeStorageSync('temp_register_nickname');
|
||
|
||
// 显示成功提示
|
||
uni.showToast({
|
||
title: '账号已注销',
|
||
icon: 'success'
|
||
});
|
||
|
||
// 延迟跳转到登录页
|
||
setTimeout(() => {
|
||
uni.reLaunch({
|
||
url: '/pages/login/login'
|
||
});
|
||
}, 1500);
|
||
} else {
|
||
uni.showToast({
|
||
title: result.message || '注销失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('注销账号失败:', error);
|
||
uni.showToast({
|
||
title: '注销失败,请稍后重试',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 处理退出登录
|
||
const handleLogout = () => {
|
||
showLogoutModal.value = true;
|
||
};
|
||
|
||
// 关闭退出登录弹窗
|
||
const closeLogoutModal = () => {
|
||
showLogoutModal.value = false;
|
||
};
|
||
|
||
// 确认退出登录
|
||
const confirmLogout = () => {
|
||
// 关闭弹窗
|
||
closeLogoutModal();
|
||
|
||
// 调用store的logout方法
|
||
store.dispatch('user/logout');
|
||
|
||
// 清除所有本地缓存
|
||
uni.removeStorageSync('access_token');
|
||
uni.removeStorageSync('user');
|
||
uni.removeStorageSync('nickname');
|
||
// 清除临时注册数据(如果有)
|
||
uni.removeStorageSync('temp_register_mobile');
|
||
uni.removeStorageSync('temp_register_password');
|
||
uni.removeStorageSync('temp_register_nickname');
|
||
|
||
// 跳转到登录页
|
||
uni.reLaunch({
|
||
url: '/pages/login/login'
|
||
});
|
||
};
|
||
|
||
// 打开头像修改弹窗
|
||
const handleAvatarClick = () => {
|
||
showAvatarModal.value = true;
|
||
};
|
||
|
||
// 关闭头像修改弹窗
|
||
const closeAvatarModal = () => {
|
||
showAvatarModal.value = false;
|
||
};
|
||
|
||
// 处理头像更新后的操作(下载缓存、更新本地存储、刷新UI)
|
||
const handleAvatarUpdateSuccess = async (newAvatarUrl) => {
|
||
try {
|
||
// 1. 获取新头像的真实URL并下载缓存
|
||
const fileName = 'avatar.png';
|
||
const urlRes = await getOssPresignedUrlApi(fileName, 3600, 'avatar');
|
||
if (urlRes.code === 200 && urlRes.data && urlRes.data.url) {
|
||
// 立即下载并缓存新头像
|
||
await downloadAndCacheAvatar(newAvatarUrl, urlRes.data.url);
|
||
}
|
||
} catch (error) {
|
||
console.error('预下载头像失败:', error);
|
||
// 不影响主流程,继续
|
||
}
|
||
|
||
// 2. 更新本地缓存
|
||
userAvatarUrl.value = newAvatarUrl;
|
||
const userStr = uni.getStorageSync('user');
|
||
if (userStr) {
|
||
const user = JSON.parse(userStr);
|
||
user.avatar_url = newAvatarUrl;
|
||
uni.setStorageSync('user', JSON.stringify(user));
|
||
}
|
||
|
||
// 3. 强制刷新Avatar组件
|
||
avatarKey.value += 1;
|
||
|
||
// 4. 触发全局事件,通知其他组件刷新头像
|
||
uni.$emit('avatarUpdated', { avatarUrl: newAvatarUrl });
|
||
|
||
// 5. 显示成功提示
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '头像修改成功',
|
||
icon: 'success'
|
||
});
|
||
|
||
uploadingAvatar.value = false;
|
||
closeAvatarModal();
|
||
};
|
||
|
||
// 上传头像
|
||
const handleUploadAvatar = () => {
|
||
// 选择图片
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: async (res) => {
|
||
const tempFilePath = res.tempFilePaths[0];
|
||
|
||
// 获取文件信息验证大小
|
||
uni.getFileInfo({
|
||
filePath: tempFilePath,
|
||
success: async (fileInfo) => {
|
||
// 验证文件大小(10MB = 10 * 1024 * 1024)
|
||
if (fileInfo.size > 10 * 1024 * 1024) {
|
||
uni.showToast({
|
||
title: '图片大小不能超过10MB',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 上传到OSS
|
||
await uploadAvatarToOss(tempFilePath);
|
||
},
|
||
fail: (error) => {
|
||
console.error('获取文件信息失败:', error);
|
||
uni.showToast({
|
||
title: '获取文件信息失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
});
|
||
},
|
||
fail: (error) => {
|
||
console.error('选择图片失败:', error);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 上传头像到OSS
|
||
const uploadAvatarToOss = async (filePath) => {
|
||
try {
|
||
uploadingAvatar.value = true;
|
||
uni.showLoading({ title: '上传中...', mask: true });
|
||
|
||
// 1. 获取OSS签名
|
||
const signRes = await getOssSignatureApi('avatar');
|
||
if (signRes.code !== 200) {
|
||
throw new Error(signRes.message || '获取签名失败');
|
||
}
|
||
|
||
// 2. 构建FormData并上传到OSS
|
||
// 注意:文件名固定为avatar.png
|
||
uni.uploadFile({
|
||
url: signRes.data.host,
|
||
filePath: filePath,
|
||
name: 'file',
|
||
formData: {
|
||
key: signRes.data.dir + 'avatar.png',
|
||
policy: signRes.data.policy,
|
||
success_action_status: '200',
|
||
'x-oss-credential': signRes.data.x_oss_credential,
|
||
'x-oss-date': signRes.data.x_oss_date,
|
||
'x-oss-security-token': signRes.data.security_token,
|
||
'x-oss-signature': signRes.data.signature,
|
||
'x-oss-signature-version': signRes.data.x_oss_signature_version
|
||
},
|
||
success: async (uploadRes) => {
|
||
try {
|
||
if (uploadRes.statusCode === 200 || uploadRes.statusCode === 204) {
|
||
// 3. 拼接完整URL
|
||
const avatarUrl = `${signRes.data.host}/${signRes.data.dir}avatar.png`;
|
||
|
||
// 4. 清除旧头像的缓存(如果有)
|
||
if (userAvatarUrl.value) {
|
||
clearAvatarCache(userAvatarUrl.value);
|
||
}
|
||
|
||
// 5. 调用后端更新头像接口
|
||
const updateRes = await updateAvatarApi(avatarUrl);
|
||
|
||
if (updateRes.code === 200) {
|
||
// 6. 处理头像更新成功后的操作
|
||
await handleAvatarUpdateSuccess(avatarUrl);
|
||
} else {
|
||
throw new Error(updateRes.message || '更新头像失败');
|
||
}
|
||
} else {
|
||
throw new Error(`上传失败,状态码: ${uploadRes.statusCode}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('处理上传结果失败:', error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '上传失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
uploadingAvatar.value = false;
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('OSS上传失败:', error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '上传失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
uploadingAvatar.value = false;
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('上传头像失败:', error);
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: error.message || '上传失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
uploadingAvatar.value = false;
|
||
}
|
||
};
|
||
|
||
onReady(() => {
|
||
// 不再自动触发引导,由用户从引导列表选择后触发
|
||
});
|
||
|
||
// 处理引导跳转参数
|
||
onLoad((options) => {
|
||
if (options && options.guide_key) {
|
||
console.log('[Guide] profile 收到引导跳转参数, guide_key:', options.guide_key, 'guide_step:', options.guide_step)
|
||
store.dispatch('guide/resumeGuide', options.guide_key).then(res => {
|
||
console.log('[Guide] profile resumeGuide 结果:', res)
|
||
}).catch(err => {
|
||
console.error('[Guide] profile resumeGuide 失败:', err)
|
||
})
|
||
}
|
||
});
|
||
|
||
onShow(() => {
|
||
// 每次页面显示时都刷新用户信息并通知 Header 更新余额
|
||
fetchUserInfo().then(() => {
|
||
const cachedUserStr = uni.getStorageSync('user');
|
||
if (cachedUserStr) {
|
||
try {
|
||
const cachedUser = JSON.parse(cachedUserStr);
|
||
uni.$emit('balanceUpdated', { crystal_balance: cachedUser.crystal_balance || 0 });
|
||
} catch (e) {
|
||
console.error('解析缓存用户信息失败:', e);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.profile-container {
|
||
position: relative;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.profile-scroll {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 隐藏滚动条 */
|
||
.profile-scroll::-webkit-scrollbar {
|
||
display: none;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
/* 上半部分:用户信息区域 */
|
||
.top-section {
|
||
position: relative;
|
||
width: 100%;
|
||
height: auto;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.background-image {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
object-fit: cover;
|
||
/* filter: blur(20rpx); */
|
||
}
|
||
|
||
.background-overlay {
|
||
display: none;
|
||
}
|
||
|
||
.user-info-card {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 90%;
|
||
max-width: 600rpx;
|
||
margin: 0 auto;
|
||
margin-top: 225rpx;
|
||
padding: 40rpx 0;
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
/* 上半部分:头像和用户信息左右布局 */
|
||
.user-main-info {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
/* gap: 30rpx; */
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* 右侧用户文本信息 */
|
||
.user-text-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
/* gap: 12rpx; */
|
||
margin-left: 30rpx;
|
||
padding-bottom: 10rpx;
|
||
}
|
||
|
||
.name-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
/* gap: 12rpx; */
|
||
flex-wrap: wrap;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.level-badge {
|
||
font-size: 24rpx;
|
||
color: #ffffff;
|
||
border-radius: 12rpx;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 32rpx;
|
||
color: #ffffff;
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.fan-tag-badge {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 16rpx;
|
||
padding: 4rpx 12rpx;
|
||
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.fan-tag-badge:active {
|
||
opacity: 0.8;
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.fan-tag-text {
|
||
font-size: 20rpx;
|
||
color: #ffffff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.fan-tag-arrow {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.user-did {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.user-mobile {
|
||
font-size: 48rpx;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.blockchain-address-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.blockchain-address-text {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.copy-icon {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.copy-icon-text {
|
||
font-size: 36rpx;
|
||
}
|
||
|
||
/* 下半部分:透明背景区域 */
|
||
.bottom-section {
|
||
position: relative;
|
||
width: 100%;
|
||
min-height: 55vh;
|
||
z-index: 3;
|
||
padding: 0 30rpx;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.content-area {
|
||
flex: 1;
|
||
}
|
||
|
||
.section-bg {
|
||
border-radius: 30rpx;
|
||
padding: 30rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 40rpx;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
font-weight: bold;
|
||
color: #fff;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.assets-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20rpx;
|
||
margin-bottom: 0;
|
||
height: 192rpx;
|
||
}
|
||
|
||
.asset-card {
|
||
background: rgba(217, 156, 180, 0.3);
|
||
|
||
border-radius: 20rpx;
|
||
padding: 24rpx 16rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
position: relative;
|
||
}
|
||
|
||
.asset-icon {
|
||
width: 128rpx;
|
||
height: 128rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.asset-text {
|
||
font-size: 32rpx;
|
||
color: #fff;
|
||
margin-bottom: 8rpx;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
|
||
}
|
||
|
||
.arrow {
|
||
position: absolute;
|
||
right: 20rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 48rpx;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 服务与工具按钮 */
|
||
.service-buttons-container {
|
||
background: rgba(217, 156, 180, 0.3);
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-start;
|
||
margin-bottom: 0;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.service-button {
|
||
border-radius: 20rpx;
|
||
width: 136rpx;
|
||
height: 160rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
padding: 16rpx;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
margin-bottom: 16rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.service-icon {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.service-icon-emoji {
|
||
font-size: 40rpx;
|
||
margin-bottom: 6rpx;
|
||
display: block;
|
||
}
|
||
|
||
.service-text {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
text-align: center;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.guide-badge {
|
||
position: absolute;
|
||
top: 10rpx;
|
||
right: 20rpx;
|
||
background: #ff6b6b;
|
||
color: #fff;
|
||
font-size: 20rpx;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.logout-section {
|
||
padding: 10rpx 30rpx;
|
||
padding-bottom: calc(40rpx + constant(safe-area-inset-bottom)); /* iOS 11.0 */
|
||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); /* iOS 11.2+ */
|
||
display: flex;
|
||
position: relative;
|
||
}
|
||
|
||
/* 使用 ::after 伪元素绘制完美的圆角横线 */
|
||
.logout-section::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: -6rpx;
|
||
left: 0;
|
||
right: 0;
|
||
height: 8rpx;
|
||
background-color: rgba(252, 252, 252, 0.6);
|
||
border-radius: 8rpx;
|
||
width: calc(100% - 60rpx);
|
||
transform: translateX(30rpx);
|
||
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.logout-button {
|
||
width: 60%;
|
||
height: 112rpx;
|
||
line-height: 112rpx;
|
||
background: rgba(217, 156, 180, 0.3);
|
||
border-radius: 40rpx;
|
||
font-size: 56rpx;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
margin-top: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
|
||
.logout-button::after {
|
||
border: none;
|
||
}
|
||
|
||
.logout-button:active {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
/* 修改昵称弹窗 */
|
||
.nickname-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 80%;
|
||
max-width: 600rpx;
|
||
background: #ffffff;
|
||
border-radius: 30rpx;
|
||
padding: 60rpx 40rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
text-align: center;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.modal-input {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 20rpx;
|
||
padding: 0 30rpx;
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
margin-bottom: 40rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.modal-buttons {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.modal-btn-cancel,
|
||
.modal-btn-confirm {
|
||
flex: 1;
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
border-radius: 44rpx;
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
border: none;
|
||
}
|
||
|
||
.modal-btn-cancel {
|
||
background: #f5f5f5;
|
||
color: #666666;
|
||
}
|
||
|
||
.modal-btn-cancel::after {
|
||
border: none;
|
||
}
|
||
|
||
.modal-btn-confirm {
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
color: #ffffff;
|
||
}
|
||
|
||
.modal-btn-confirm::after {
|
||
border: none;
|
||
}
|
||
|
||
.modal-btn-cancel:active {
|
||
background: #e0e0e0;
|
||
}
|
||
|
||
.modal-btn-confirm:active {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
/* 修改密码弹窗 */
|
||
.password-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-password-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 88rpx;
|
||
margin-bottom: 40rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 30rpx;
|
||
padding-right: 100rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.modal-password-input {
|
||
flex: 1;
|
||
height: 100%;
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
}
|
||
|
||
.modal-eye-icon {
|
||
position: absolute;
|
||
right: 30rpx;
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.eye-text {
|
||
font-size: 40rpx;
|
||
}
|
||
|
||
.input-placeholder {
|
||
color: #999999;
|
||
}
|
||
|
||
/* 确认弹窗(注销账号、退出登录) */
|
||
.confirm-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-message {
|
||
font-size: 28rpx;
|
||
color: #666666;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.modal-btn-confirm.danger {
|
||
background: #ff3b30;
|
||
}
|
||
|
||
.modal-btn-confirm.danger:active {
|
||
background: #e6342a;
|
||
}
|
||
|
||
/* 添加身份弹窗 */
|
||
.add-identity-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-subtitle {
|
||
font-size: 28rpx;
|
||
color: #666666;
|
||
text-align: center;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.star-list {
|
||
max-height: 400rpx;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
margin-bottom: 30rpx;
|
||
border-radius: 20rpx;
|
||
background: #f5f5f5;
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.star-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20rpx 30rpx;
|
||
background: #ffffff;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 10rpx;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.star-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.star-item-selected {
|
||
background: linear-gradient(165deg, rgba(240, 228, 177, 0.2) 0%, rgba(240, 131, 153, 0.2) 50%, rgba(185, 78, 115, 0.2) 90%, rgba(131, 75, 158, 0.2) 100%);
|
||
border: 2rpx solid #F08399;
|
||
}
|
||
|
||
.star-item:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.star-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.star-name {
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.star-tag {
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.star-radio {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 50%;
|
||
border: 2rpx solid #cccccc;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.star-item-selected .star-radio {
|
||
border-color: #F08399;
|
||
background: #F08399;
|
||
}
|
||
|
||
.star-radio-checked {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
border-radius: 50%;
|
||
background: #ffffff;
|
||
}
|
||
|
||
/* 身份切换下拉列表 */
|
||
.identity-dropdown-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.identity-dropdown-content {
|
||
width: 85%;
|
||
max-width: 650rpx;
|
||
max-height: 70vh;
|
||
background: #ffffff;
|
||
border-radius: 30rpx;
|
||
padding: 40rpx;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.identity-dropdown-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #333333;
|
||
text-align: center;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.identity-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.identity-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx 20rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 20rpx;
|
||
margin-bottom: 16rpx;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.identity-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.identity-item-active {
|
||
background: linear-gradient(165deg, rgba(240, 228, 177, 0.3) 0%, rgba(240, 131, 153, 0.3) 50%, rgba(185, 78, 115, 0.3) 90%, rgba(131, 75, 158, 0.3) 100%);
|
||
border: 2rpx solid #F08399;
|
||
}
|
||
|
||
.identity-item-disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.identity-item:not(.identity-item-disabled):active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.identity-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.identity-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.identity-star-name {
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.identity-star-tag {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
background: rgba(217, 156, 180, 0.3);
|
||
padding: 2rpx 10rpx;
|
||
border-radius: 10rpx;
|
||
}
|
||
|
||
.identity-nickname {
|
||
font-size: 26rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.identity-current-badge {
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
border-radius: 16rpx;
|
||
padding: 6rpx 12rpx;
|
||
}
|
||
|
||
.identity-current-text {
|
||
font-size: 22rpx;
|
||
color: #ffffff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 修改头像弹窗 */
|
||
.avatar-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.avatar-preview {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.upload-avatar-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
border-radius: 44rpx;
|
||
color: #ffffff;
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
border: none;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.upload-avatar-btn::after {
|
||
border: none;
|
||
}
|
||
|
||
.upload-avatar-btn:disabled {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.upload-hint {
|
||
font-size: 24rpx;
|
||
color: #999999;
|
||
text-align: center;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
</style>
|