2815 lines
70 KiB
Vue
2815 lines
70 KiB
Vue
<template>
|
||
<view class="profile-container">
|
||
<!-- 背景图片 - 固定在容器上 -->
|
||
<image
|
||
class="background-image"
|
||
src="/static/square/squearbj.png"
|
||
mode="aspectFill"
|
||
></image>
|
||
<!-- 可替换背景装饰层 -->
|
||
<view class="replaceable-bg-layer">
|
||
<image
|
||
class="replaceable-bg-overlay-image"
|
||
:src="replaceableBgOverlay"
|
||
mode="aspectFill"
|
||
></image>
|
||
<!-- 底部滤镜层 -->
|
||
<view class="replaceable-bg-filter"></view>
|
||
</view>
|
||
<!-- Header组件 -->
|
||
<view class="nav-back" @tap="goBack">
|
||
<text class="nav-back-icon">←</text>
|
||
</view>
|
||
<view class="profile-scroll">
|
||
<!-- 右上角模块 -->
|
||
<view class="top-right-module">
|
||
<view class="level-box">
|
||
<text class="level-label">LV</text>
|
||
<text class="level-value">{{ fanLevel }}</text>
|
||
</view>
|
||
</view>
|
||
<!-- 上半部分:用户信息卡片 -->
|
||
<view class="top-section">
|
||
<!-- 用户信息卡片 -->
|
||
<view class="user-info-card">
|
||
<!-- 上半部分:头像和用户信息左右布局 -->
|
||
<view class="user-main-info">
|
||
<view @tap="handleAvatarClick" class="avatar-container">
|
||
<Avatar
|
||
:key="avatarKey"
|
||
:userId="uid"
|
||
:size="182.4"
|
||
:borderWidth="6"
|
||
:showLevel="false"
|
||
:level="fanLevel"
|
||
:avatarUrl="userAvatarUrl"
|
||
/>
|
||
</view>
|
||
<view class="user-text-info">
|
||
<!-- 昵称 -->
|
||
<view class="info-row">
|
||
<text
|
||
style="
|
||
font-size: 30rpx;
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||
color: #fff9e7;
|
||
min-width: 128rpx;
|
||
"
|
||
class="info-value"
|
||
>{{ nickname }}</text
|
||
>
|
||
<view class="edit-btn" @tap="handleChangeNickname">
|
||
<image
|
||
class="edit-nickname-btn"
|
||
src="/static/icon/edit-nickname.png"
|
||
mode="aspectFit"
|
||
></image>
|
||
<view class="edit-nickname-text">
|
||
<text class="edit-nickname-placeholder">修改昵称 </text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="info-row-wrapper">
|
||
<!-- DID -->
|
||
<view class="info-row">
|
||
<text class="info-label">DID</text>
|
||
<view class="address-row">
|
||
<text class="info-value uid-value">{{ uid }}</text>
|
||
<!-- <image class="toggle-icon"
|
||
:src="showBlockNumber ? '/static/icon/show.png' : '/static/icon/hide.png'"
|
||
mode="aspectFit" @tap="copyUid"></image> -->
|
||
</view>
|
||
</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 class="bottom-section">
|
||
<view class="content-area">
|
||
<!-- 我的资产 -->
|
||
<view class="section-bg">
|
||
<view class="section-title">我的资产</view>
|
||
<view class="assets-grid">
|
||
<view class="asset-card" @tap="handleAssetClick('myWorks')">
|
||
<image
|
||
class="asset-icon"
|
||
src="/static/icon/dazi.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="handleShowAppIntro">
|
||
<image
|
||
class="service-icon"
|
||
src="/static/icon/edit-password.png"
|
||
mode="aspectFit"
|
||
>
|
||
</image>
|
||
<text class="service-text">APP介绍</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="handleShowFeedback">
|
||
<image
|
||
class="service-icon"
|
||
src="/static/icon/logout.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>
|
||
|
||
<!-- 修改密码弹窗(Plan A: 旧密码 + 短信验证码双保险) -->
|
||
<view
|
||
class="password-modal"
|
||
v-if="showPasswordModal"
|
||
@tap="closePasswordModal"
|
||
>
|
||
<view class="modal-content" @tap.stop>
|
||
<view class="modal-title">修改密码</view>
|
||
|
||
<!-- 1. 手机号(默认空,要求用户输入完整 11 位;校验必须等于当前登录账号的 mobile,否则拦截) -->
|
||
<view class="modal-row">
|
||
<input
|
||
class="modal-input sms-mobile-input"
|
||
type="number"
|
||
maxlength="11"
|
||
v-model="smsMobile"
|
||
placeholder="请输入完整手机号"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 2. 短信验证码 + 获取验证码按钮(同行) -->
|
||
<view class="modal-row">
|
||
<input
|
||
class="modal-input sms-code-input"
|
||
type="number"
|
||
maxlength="6"
|
||
v-model="smsCode"
|
||
placeholder="请输入短信验证码"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
<button
|
||
class="btn-get-code"
|
||
:disabled="codeCountdown > 0 || sendingCode"
|
||
@tap="handleSendCode"
|
||
>
|
||
{{
|
||
codeCountdown > 0 ? `${codeCountdown}s 后重发` : "获取验证码"
|
||
}}
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 3. 旧密码 -->
|
||
<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>
|
||
|
||
<!-- 4. 新密码 -->
|
||
<view class="modal-password-wrapper">
|
||
<input
|
||
class="modal-password-input"
|
||
:type="showNewPassword ? 'text' : 'password'"
|
||
v-model="newPassword"
|
||
placeholder="请输入新密码(至少6位)"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
<view
|
||
class="modal-eye-icon"
|
||
@click="showNewPassword = !showNewPassword"
|
||
>
|
||
<text class="eye-text">{{
|
||
showNewPassword ? "👁️" : "👁️🗨️"
|
||
}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 5. 确认新密码 -->
|
||
<view class="modal-password-wrapper">
|
||
<input
|
||
class="modal-password-input"
|
||
:type="showConfirmPassword ? 'text' : 'password'"
|
||
v-model="confirmPassword"
|
||
placeholder="请再次输入新密码"
|
||
placeholder-class="input-placeholder"
|
||
/>
|
||
<view
|
||
class="modal-eye-icon"
|
||
@click="showConfirmPassword = !showConfirmPassword"
|
||
>
|
||
<text class="eye-text">{{
|
||
showConfirmPassword ? "👁️" : "👁️🗨️"
|
||
}}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal-buttons">
|
||
<button class="modal-btn-cancel" @tap="closePasswordModal">
|
||
取消
|
||
</button>
|
||
<button
|
||
class="modal-btn-confirm"
|
||
@tap="confirmChangePassword"
|
||
:disabled="changingPassword"
|
||
>
|
||
{{ changingPassword ? "修改中..." : "确认" }}
|
||
</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>
|
||
<!-- 退出登录按钮 -->
|
||
<view class="logout-section" @tap="handleLogout">
|
||
<text class="logout-button">退出登录</text>
|
||
</view>
|
||
|
||
<!-- 新手引导弹窗 -->
|
||
<GuideModal
|
||
:visible="showGuideModal"
|
||
@close="showGuideModal = false"
|
||
@updated="handleGuideUpdated"
|
||
/>
|
||
|
||
<!-- 意见反馈弹窗 -->
|
||
<FeedbackModal
|
||
:visible="showFeedbackModal"
|
||
@close="showFeedbackModal = false"
|
||
@submit="handleFeedbackSubmit"
|
||
/>
|
||
|
||
<!-- 全局引导遮罩 -->
|
||
<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,
|
||
sendCodeApi,
|
||
verifyCodeApi,
|
||
} from "@/utils/api";
|
||
import { validateNickname, validatePassword } from "@/utils/validator.js";
|
||
import { clearAvatarCache } from "@/utils/avatarCache";
|
||
import GuideModal from "@/pages/tasks/GuideModal.vue";
|
||
import GuideOverlay from "@/components/GuideOverlay.vue";
|
||
import FeedbackModal from "../components/FeedbackModal.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("");
|
||
|
||
// 修改密码弹窗(Plan A: 旧密码 + 短信验证码双保险)
|
||
const showPasswordModal = ref(false);
|
||
const oldPassword = ref("");
|
||
const newPassword = ref("");
|
||
const confirmPassword = ref("");
|
||
const showOldPassword = ref(false);
|
||
const showNewPassword = ref(false);
|
||
const showConfirmPassword = ref(false);
|
||
// 短信相关
|
||
const smsMobile = ref(""); // 弹窗打开时不预填,要求用户主动输入完整手机号;校验时与 mobile.value 比对,不允许发到其他号
|
||
const smsCode = ref("");
|
||
const sendingCode = ref(false);
|
||
const codeCountdown = ref(0);
|
||
const codeCountdownTimer = ref(null);
|
||
// 防重
|
||
const changingPassword = ref(false);
|
||
|
||
// 新手引导
|
||
const showGuideModal = ref(false);
|
||
const guideClaimableCount = computed(() => getClaimableRewardCount());
|
||
|
||
// 引导更新回调
|
||
const handleGuideUpdated = () => {
|
||
console.log("[Profile] Guide updated");
|
||
};
|
||
|
||
// 意见反馈弹窗
|
||
const showFeedbackModal = ref(false);
|
||
const handleShowFeedback = () => {
|
||
showFeedbackModal.value = true;
|
||
};
|
||
const handleFeedbackSubmit = (payload) => {
|
||
console.log("[Profile] feedback submitted:", payload);
|
||
};
|
||
|
||
// 添加身份弹窗
|
||
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 showBlockNumber = ref(false);
|
||
const showMobile = ref(false);
|
||
|
||
// 可替换背景叠加层图片(类似微信更换主页背景)
|
||
const replaceableBgOverlay = ref("/static/background/profilebj.png");
|
||
|
||
// 更新背景叠加层方法
|
||
const updateBgOverlay = (newOverlayImage) => {
|
||
replaceableBgOverlay.value = newOverlayImage;
|
||
};
|
||
|
||
// 显示手机号(根据showMobile状态显示脱敏或未脱敏)
|
||
const displayMobile = computed(() => {
|
||
console.log(
|
||
"displayMobile computed, mobile:",
|
||
mobile.value,
|
||
"showMobile:",
|
||
showMobile.value,
|
||
);
|
||
if (!mobile.value) return "";
|
||
// showMobile为true时显示完整号码,false时显示脱敏号码
|
||
if (showMobile.value) {
|
||
return mobile.value;
|
||
}
|
||
// 脱敏显示:显示前3位和后4位
|
||
const phone = mobile.value;
|
||
return phone.length === 11
|
||
? phone.slice(0, 3) + "****" + phone.slice(-4)
|
||
: phone;
|
||
});
|
||
|
||
// 显示截断后的区块链地址
|
||
const displayAddress = computed(() => {
|
||
const address = blockchainAddress.value;
|
||
console.log(
|
||
"displayAddress computed, address:",
|
||
address,
|
||
"showBlockNumber:",
|
||
showBlockNumber.value,
|
||
);
|
||
// 如果没有地址,显示默认地址
|
||
if (!address) return "0xabcd...123c";
|
||
// showBlockNumber为true时显示完整地址,false时显示截断地址
|
||
if (showBlockNumber.value) return address;
|
||
if (address.length <= 20) return address;
|
||
// 显示前4个字符和后4个字符
|
||
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 === 0 && 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?.name || "";
|
||
blockchainAddress.value = userForCache.blockchain_address || "";
|
||
userAvatarUrl.value = userForCache.avatar_url || "";
|
||
mobile.value =
|
||
userForCache.mobile || 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 =
|
||
cachedUser.mobile || uni.getStorageSync("login_mobile") || "";
|
||
} catch (e) {
|
||
console.error("解析缓存用户信息失败:", e);
|
||
}
|
||
}
|
||
|
||
uni.showToast({
|
||
title: error.message || "获取用户信息失败",
|
||
icon: "none",
|
||
});
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 切换手机号显示
|
||
const toggleMobileDisplay = () => {
|
||
console.log("toggleMobileDisplay called, showMobile:", showMobile.value);
|
||
showMobile.value = !showMobile.value;
|
||
};
|
||
|
||
// 复制UID
|
||
const copyUid = () => {
|
||
if (!uid.value) {
|
||
uni.showToast({ title: "UID为空", icon: "none" });
|
||
return;
|
||
}
|
||
uni.setClipboardData({
|
||
data: uid.value,
|
||
success: () => uni.showToast({ title: "已复制UID", icon: "success" }),
|
||
fail: () => uni.showToast({ title: "复制失败", icon: "none" }),
|
||
});
|
||
};
|
||
|
||
// 查看昵称(复制)
|
||
const handleViewNickname = () => {
|
||
uni.setClipboardData({
|
||
data: nickname.value,
|
||
success: () => uni.showToast({ title: "已复制昵称", icon: "success" }),
|
||
fail: () => uni.showToast({ title: "复制失败", icon: "none" }),
|
||
});
|
||
};
|
||
|
||
// 复制区块链地址
|
||
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 === "myWorks") {
|
||
// 跳转到展馆页面
|
||
uni.navigateTo({
|
||
url: "/pages/profile/myWorks",
|
||
});
|
||
} 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 === 0) {
|
||
// 修改成功,强制从服务器刷新个人信息
|
||
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 = "";
|
||
confirmPassword.value = "";
|
||
smsMobile.value = ""; // 默认不预填,要求用户主动输入完整手机号(校验时与 mobile.value 比对)
|
||
smsCode.value = "";
|
||
showOldPassword.value = false;
|
||
showNewPassword.value = false;
|
||
showConfirmPassword.value = false;
|
||
codeCountdown.value = 0;
|
||
if (codeCountdownTimer.value) {
|
||
clearInterval(codeCountdownTimer.value);
|
||
codeCountdownTimer.value = null;
|
||
}
|
||
showPasswordModal.value = true;
|
||
};
|
||
|
||
// 关闭修改密码弹窗(清空所有,包括倒计时 interval)
|
||
const closePasswordModal = () => {
|
||
showPasswordModal.value = false;
|
||
oldPassword.value = "";
|
||
newPassword.value = "";
|
||
confirmPassword.value = "";
|
||
smsMobile.value = "";
|
||
smsCode.value = "";
|
||
showOldPassword.value = false;
|
||
showNewPassword.value = false;
|
||
showConfirmPassword.value = false;
|
||
codeCountdown.value = 0;
|
||
if (codeCountdownTimer.value) {
|
||
clearInterval(codeCountdownTimer.value);
|
||
codeCountdownTimer.value = null;
|
||
}
|
||
};
|
||
|
||
// 发送验证码
|
||
const handleSendCode = async () => {
|
||
if (sendingCode.value || codeCountdown.value > 0) return;
|
||
const m = (smsMobile.value || "").replace(/\s/g, "");
|
||
// 校验 1:必须是合法 11 位手机号
|
||
if (!/^1\d{10}$/.test(m)) {
|
||
return uni.showToast({ title: "请输入有效的11位手机号", icon: "none" });
|
||
}
|
||
// 校验 2:必须等于当前登录账号绑定的手机号(防止给其他号乱发)
|
||
if (mobile.value && m !== mobile.value) {
|
||
return uni.showToast({
|
||
title: "只能发送到当前账号绑定的手机号",
|
||
icon: "none",
|
||
});
|
||
}
|
||
try {
|
||
sendingCode.value = true;
|
||
const res = await sendCodeApi(m, "password");
|
||
if (res.code === 0) {
|
||
uni.showToast({ title: "验证码已发送", icon: "success" });
|
||
codeCountdown.value = 60;
|
||
// 先清理可能存在的旧 timer
|
||
if (codeCountdownTimer.value) clearInterval(codeCountdownTimer.value);
|
||
codeCountdownTimer.value = setInterval(() => {
|
||
codeCountdown.value--;
|
||
if (codeCountdown.value <= 0) {
|
||
clearInterval(codeCountdownTimer.value);
|
||
codeCountdownTimer.value = null;
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
uni.showToast({ title: res.message || "发送失败", icon: "none" });
|
||
}
|
||
} catch (e) {
|
||
uni.showToast({ title: e.message || "发送失败", icon: "none" });
|
||
} finally {
|
||
sendingCode.value = false;
|
||
}
|
||
};
|
||
|
||
// 确认修改密码(Plan A: 5 项本地校验 + 短信验证 + 改密)
|
||
const confirmChangePassword = async () => {
|
||
// 本地校验
|
||
if (!smsCode.value.match(/^\d{6}$/)) {
|
||
return uni.showToast({ title: "请输入6位短信验证码", icon: "none" });
|
||
}
|
||
if (!oldPassword.value.trim()) {
|
||
return uni.showToast({ title: "请输入旧密码", icon: "none" });
|
||
}
|
||
if (oldPassword.value === newPassword.value) {
|
||
return uni.showToast({ title: "新密码不能与旧密码相同", icon: "none" });
|
||
}
|
||
const v = validatePassword(newPassword.value);
|
||
if (!v.valid) return uni.showToast({ title: v.message, icon: "none" });
|
||
if (newPassword.value !== confirmPassword.value) {
|
||
return uni.showToast({ title: "两次输入的新密码不一致", icon: "none" });
|
||
}
|
||
|
||
if (changingPassword.value) return;
|
||
changingPassword.value = true;
|
||
uni.showLoading({ title: "修改中...", mask: true });
|
||
|
||
try {
|
||
// Step 1: verify SMS code → verify_token(用输入框的 mobile,与 sendCode 保持一致)
|
||
const m = (smsMobile.value || "").replace(/\s/g, "");
|
||
// 校验:必须等于当前登录账号绑定的手机号
|
||
if (mobile.value && m !== mobile.value) {
|
||
throw new Error("请使用当前账号绑定的手机号");
|
||
}
|
||
const vRes = await verifyCodeApi(m, smsCode.value, "password");
|
||
if (vRes.code !== 0 || !vRes.data?.verify_token) {
|
||
throw new Error(vRes.message || "短信验证码错误");
|
||
}
|
||
const verifyToken = vRes.data.verify_token;
|
||
|
||
// Step 2: update password
|
||
const res = await updatePasswordApi(
|
||
oldPassword.value.trim(),
|
||
newPassword.value,
|
||
verifyToken,
|
||
);
|
||
uni.hideLoading();
|
||
|
||
// 业务码 200 走这里;400/500 已被拦截器 reject 跳到 catch
|
||
if (res.code === 0) {
|
||
uni.showToast({
|
||
title: "修改成功,请重新登录",
|
||
icon: "success",
|
||
duration: 2000,
|
||
});
|
||
setTimeout(() => {
|
||
// 改密后清空所有登录态 + 注册临时数据
|
||
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 (e) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: e.message || "修改失败,请重试", icon: "none" });
|
||
changingPassword.value = false;
|
||
}
|
||
};
|
||
|
||
// 处理 APP 介绍(补 BUG #2: 原 @tap 缺失)
|
||
const handleShowAppIntro = () => {
|
||
uni.showModal({
|
||
title: "APP介绍",
|
||
content:
|
||
"顶粉APP是一款粉丝专属应用,集成了身份管理、藏品展示、社交互动等功能。\n(更多介绍功能开发中...)",
|
||
showCancel: false,
|
||
confirmText: "我知道了",
|
||
});
|
||
};
|
||
|
||
// 处理添加身份
|
||
const handleSwitchRole = async () => {
|
||
try {
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: "加载中...",
|
||
mask: true,
|
||
});
|
||
|
||
// 获取可选的粉丝身份列表
|
||
const res = await getFanIdentitiesApi();
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.code === 0 && 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 === 0) {
|
||
// 添加成功
|
||
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 === 0 && 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 === 0 && 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 = () => {
|
||
showGuideModal.value = true;
|
||
};
|
||
|
||
// 关闭注销账号弹窗
|
||
const closeDeleteAccountModal = () => {
|
||
showDeleteAccountModal.value = false;
|
||
};
|
||
|
||
// 确认注销账号
|
||
const confirmDeleteAccount = async () => {
|
||
try {
|
||
// 关闭弹窗
|
||
closeDeleteAccountModal();
|
||
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: "注销中...",
|
||
mask: true,
|
||
});
|
||
|
||
// 调用注销账号接口
|
||
const result = await deleteAccountApi();
|
||
|
||
uni.hideLoading();
|
||
|
||
if (result.code === 0) {
|
||
// 调用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 goBack = () => {
|
||
// 获取页面栈
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 1) {
|
||
// 有上一页,执行返回
|
||
uni.navigateBack();
|
||
} else {
|
||
// 没有上一页,跳转到square页面
|
||
uni.reLaunch({
|
||
url: "/pages/square/square",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 打开头像修改弹窗
|
||
const handleAvatarClick = () => {
|
||
showAvatarModal.value = true;
|
||
};
|
||
|
||
// 关闭头像修改弹窗
|
||
const closeAvatarModal = () => {
|
||
showAvatarModal.value = false;
|
||
};
|
||
|
||
// 处理头像更新后的操作(更新本地存储、刷新UI)
|
||
const handleAvatarUpdateSuccess = async (newAvatarUrl) => {
|
||
// 后端签名接口返回的 key 每次都唯一(avatar_<uuid>.png),
|
||
// URL 字符串天然不同,<image> 会重新拉取,无需再追加 ?t= 兜底
|
||
// 1. 更新本地缓存
|
||
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 });
|
||
console.log(filePath);
|
||
// 1. 获取OSS签名
|
||
const signRes = await getOssSignatureApi("avatar");
|
||
if (signRes.code !== 0) {
|
||
throw new Error(signRes.message || "获取签名失败");
|
||
}
|
||
console.log(signRes);
|
||
// 2. 构建FormData并上传到OSS
|
||
// 直接用后端返回的 key(每次唯一),避免覆盖原图、并发竞态、webview 缓存命中老图
|
||
const ossKey = signRes.data.key;
|
||
uni.uploadFile({
|
||
url: signRes.data.host,
|
||
filePath: filePath,
|
||
name: "file",
|
||
formData: {
|
||
key: ossKey,
|
||
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(用后端返回的 key,每次唯一)
|
||
const avatarUrl = `${signRes.data.host}/${ossKey}`;
|
||
|
||
// 4. 清除旧头像的缓存(如果有)
|
||
if (userAvatarUrl.value) {
|
||
clearAvatarCache(userAvatarUrl.value);
|
||
}
|
||
console.log(avatarUrl);
|
||
// 5. 调用后端更新头像接口
|
||
const updateRes = await updateAvatarApi(avatarUrl);
|
||
console.log("updateAvatarApi 返回:", updateRes); // 添加这行
|
||
if (updateRes.code === 0) {
|
||
// 6. 处理头像更新成功后的操作
|
||
await handleAvatarUpdateSuccess(avatarUrl);
|
||
console.log("updateAvatarApi 返回:", 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 lang="scss">
|
||
.profile-container {
|
||
position: relative;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.profile-scroll {
|
||
width: 616rpx;
|
||
height: 955.2rpx;
|
||
position: relative;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 2;
|
||
border-radius: 86rpx;
|
||
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
122.57deg,
|
||
rgba(154, 146, 255, 0.19) 9.69%,
|
||
rgba(255, 202, 229, 0.19) 43.91%,
|
||
rgba(255, 250, 253, 0.188) 76.13%,
|
||
rgba(63, 63, 76, 0.19) 91.61%
|
||
);
|
||
box-shadow: 2px 4px 1.8px 0 rgba(214, 41, 41, 0.12);
|
||
filter: blur(17.850000381469727px);
|
||
}
|
||
|
||
&::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: 86rpx;
|
||
border-top: 3.2rpx solid rgba(255, 255, 255, 0.8);
|
||
background-image:
|
||
linear-gradient(
|
||
to bottom,
|
||
rgba(255, 255, 255, 0.8) 0%,
|
||
rgba(255, 255, 255, 0.8) 60%,
|
||
rgba(255, 202, 229, 0) 100%
|
||
),
|
||
linear-gradient(
|
||
to bottom,
|
||
rgba(255, 255, 255, 0.8) 0%,
|
||
rgba(255, 255, 255, 0.8) 60%,
|
||
rgba(255, 202, 229, 0) 100%
|
||
);
|
||
background-position:
|
||
0 0,
|
||
100% 0;
|
||
background-size:
|
||
3.2rpx 65%,
|
||
3.2rpx 65%;
|
||
background-repeat: no-repeat;
|
||
pointer-events: none;
|
||
z-index: 3;
|
||
}
|
||
}
|
||
|
||
/* 隐藏滚动条 */
|
||
.profile-scroll::-webkit-scrollbar {
|
||
display: none;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
/* 右上角模块 */
|
||
.top-right-module {
|
||
position: fixed;
|
||
width: 313.6rpx;
|
||
height: 73.6rpx;
|
||
top: -16rpx;
|
||
right: 0;
|
||
border-top-right-radius: 35.5rpx;
|
||
border-top-left-radius: 7rpx;
|
||
border-bottom-left-radius: 35.5rpx;
|
||
border-bottom-right-radius: 6rpx;
|
||
background: linear-gradient(135deg, #9e90ff 0%, #ffcad3 100%);
|
||
z-index: 4;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.level-box {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.level-label {
|
||
color: #fffabd;
|
||
font-size: 24rpx;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.level-value {
|
||
color: #fff;
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
color: #fffabd;
|
||
text-shadow: -1px 1px 4px #ce090984;
|
||
}
|
||
|
||
/* 上半部分:用户信息区域 */
|
||
.top-section {
|
||
position: relative;
|
||
width: 100%;
|
||
height: auto;
|
||
overflow: hidden;
|
||
margin-bottom: 64rpx;
|
||
}
|
||
|
||
.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); */
|
||
}
|
||
|
||
.replaceable-bg-layer {
|
||
position: absolute;
|
||
width: 100%;
|
||
/* height: 80vh; */
|
||
z-index: 1;
|
||
// pointer-events: none;
|
||
aspect-ratio: 81 / 107;
|
||
top: -256rpx;
|
||
}
|
||
.replaceable-bg-overlay-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.replaceable-bg-filter {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 64rpx;
|
||
mask-image: linear-gradient(to bottom, #000 0%, #000 60%, transparent 80%);
|
||
-webkit-mask-image: linear-gradient(
|
||
to bottom,
|
||
#000 0%,
|
||
#000 60%,
|
||
transparent 80%
|
||
);
|
||
// pointer-events: none;
|
||
}
|
||
|
||
.background-overlay {
|
||
display: none;
|
||
}
|
||
|
||
.nav-back {
|
||
position: fixed;
|
||
top: 96rpx;
|
||
left: 32rpx;
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
/* background: rgba(255,255,255,0.5);
|
||
border-radius: 50%; */
|
||
z-index: 4;
|
||
}
|
||
|
||
.nav-back-icon {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
}
|
||
|
||
.close-icon-img {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
position: relative;
|
||
top: 96rpx;
|
||
left: 32rpx;
|
||
}
|
||
|
||
.user-info-card {
|
||
position: relative;
|
||
z-index: 2;
|
||
margin: 0 auto;
|
||
margin-top: 64rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
/* 上半部分:头像和用户信息左右布局 */
|
||
.user-main-info {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
/* gap: 30rpx; */
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.avatar-container {
|
||
position: absolute;
|
||
left: 30rpx;
|
||
top: 32rpx;
|
||
}
|
||
|
||
/* 右侧用户文本信息 */
|
||
.user-text-info {
|
||
/* flex: 1; */
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
gap: 8rpx;
|
||
margin-left: 8rpx;
|
||
padding: 24rpx 24rpx 24rpx 224rpx;
|
||
min-width: 356rpx;
|
||
}
|
||
|
||
.info-row-wrapper {
|
||
height: 140.8rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
opacity: 0.71;
|
||
border-top-left-radius: 15px;
|
||
border-bottom-right-radius: 15px;
|
||
|
||
background: linear-gradient(
|
||
133.78deg,
|
||
rgba(154, 146, 255, 0.46) 4.58%,
|
||
rgba(255, 202, 229, 0.46) 43.91%,
|
||
rgba(255, 250, 253, 0.4554) 76.13%,
|
||
rgba(211, 209, 255, 0.46) 91.61%
|
||
);
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 12rpx;
|
||
}
|
||
|
||
.edit-btn {
|
||
min-width: 144rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.edit-nickname-btn {
|
||
width: 46.4rpx;
|
||
height: 46.4rpx;
|
||
}
|
||
|
||
.edit-nickname-text {
|
||
display: flex;
|
||
border-radius: 18rpx;
|
||
background: linear-gradient(
|
||
100.7deg,
|
||
rgba(103, 226, 222, 0.68) -3.17%,
|
||
rgba(185, 78, 238, 0.68) 43.09%,
|
||
rgba(225, 137, 139, 0.68) 99.19%
|
||
);
|
||
}
|
||
|
||
.edit-nickname-placeholder {
|
||
font-size: 15rpx;
|
||
color: #fff9e7;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 16rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 22rpx;
|
||
color: #fff9e7;
|
||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.84);
|
||
min-width: 112rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.address-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 22rpx;
|
||
color: #fff9e7;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
min-width: 192rpx;
|
||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.84);
|
||
font-weight: 400;
|
||
}
|
||
|
||
.toggle-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
}
|
||
|
||
.info-btn {
|
||
font-size: 20rpx;
|
||
color: #ffd700;
|
||
padding: 4rpx 16rpx;
|
||
border: 2rpx solid rgba(255, 215, 0, 0.5);
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.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%;
|
||
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: 8rpx 30rpx 16rpx;
|
||
background: linear-gradient(
|
||
133.78deg,
|
||
rgba(154, 146, 255, 0.46) 4.58%,
|
||
rgba(255, 202, 229, 0.46) 43.91%,
|
||
rgba(255, 250, 253, 0.4554) 76.13%,
|
||
rgba(211, 209, 255, 0.46) 91.61%
|
||
);
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 32rpx;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.84);
|
||
font-weight: 500;
|
||
color: #fff9e7;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.assets-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20rpx;
|
||
margin-bottom: 0;
|
||
height: 128rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.asset-card {
|
||
background-image: url(/static/rank/activity-support-icon/beijingkuang.png);
|
||
background-size: 100% 125%;
|
||
background-position: center;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
height: 128rpx;
|
||
width: 224rpx;
|
||
box-shadow: 1px 3px 7px 0px #d833336e;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.asset-icon {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: absolute;
|
||
bottom: -20%;
|
||
left: 50%;
|
||
transform: translate(-50%, 0%);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.asset-text {
|
||
font-size: 32rpx;
|
||
color: #fff;
|
||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.arrow {
|
||
position: absolute;
|
||
right: 20rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 48rpx;
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 服务与工具按钮 */
|
||
.service-buttons-container {
|
||
display: flex;
|
||
flex-direction: row;
|
||
// justify-content: space-between;
|
||
margin-bottom: 0;
|
||
gap: 28rpx;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.service-button {
|
||
background-image: url(/static/rank/activity-support-icon/djbeij.jpg);
|
||
background-size: 100% 100%;
|
||
background-position: center;
|
||
border-radius: 20rpx;
|
||
width: 102rpx;
|
||
height: 102rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16rpx;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.service-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.service-icon-emoji {
|
||
font-size: 16rpx;
|
||
margin-bottom: 6rpx;
|
||
display: block;
|
||
}
|
||
|
||
.service-text {
|
||
font-size: 16rpx;
|
||
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 {
|
||
width: 6rem;
|
||
height: 2rem;
|
||
display: flex;
|
||
position: fixed;
|
||
bottom: 160rpx;
|
||
left: 50%;
|
||
transform: translate(-50%, 0);
|
||
border-radius: 40rpx;
|
||
background: linear-gradient(
|
||
115.98deg,
|
||
#f6e9b4 -19.66%,
|
||
#f08399 70.92%,
|
||
rgba(213, 107, 109, 0.83521) 138.79%,
|
||
rgba(105, 209, 230, 0.69) 198.61%
|
||
);
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
/* 使用 ::after 伪元素绘制完美的圆角横线 */
|
||
/* .logout-section::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: -6rpx;
|
||
left: 0;
|
||
right: 0;
|
||
height: 8rpx;
|
||
background: linear-gradient(to bottom right,
|
||
#F0E4B1 0%,
|
||
#F08399 50%,
|
||
#B94E73 100%);
|
||
border-radius: 8rpx;
|
||
width: calc(100% - 60rpx);
|
||
transform: translateX(30rpx);
|
||
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||
} */
|
||
|
||
.logout-button {
|
||
font-size: 30rpx;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.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; */
|
||
background-image: url("/static/starbookcontent/beijing.png");
|
||
background-size: cover;
|
||
background-position: center bottom;
|
||
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;
|
||
}
|
||
|
||
/* 修改密码弹窗 - 手机号 / 短信验证码 + 获取验证码 行(§5.4) */
|
||
.modal-row {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
/* margin-bottom: 40rpx; */
|
||
/* align-items: center; */
|
||
}
|
||
.modal-input.sms-mobile-input,
|
||
.modal-input.sms-code-input {
|
||
flex: 1;
|
||
height: 88rpx;
|
||
border-radius: 20rpx;
|
||
padding: 0 30rpx;
|
||
font-size: 32rpx;
|
||
background: #fff;
|
||
border: 1rpx solid #e5e5e5;
|
||
color: #333;
|
||
}
|
||
.modal-input.sms-mobile-input:focus,
|
||
.modal-input.sms-code-input:focus {
|
||
border-color: #f08399;
|
||
}
|
||
.btn-get-code {
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
padding: 0 24rpx;
|
||
border-radius: 20rpx;
|
||
background: linear-gradient(165deg, #f0e4b1 0%, #f08399 100%);
|
||
color: #fff;
|
||
font-size: 26rpx;
|
||
border: none;
|
||
white-space: nowrap;
|
||
min-width: 180rpx;
|
||
}
|
||
/* 提优先级,确保压过 uni-app 默认的 uni-button[disabled]:not([type]) { color: rgba(0,0,0,0.3); background-color: #f7f7f7; } */
|
||
uni-button.btn-get-code[disabled] {
|
||
color: #fff;
|
||
}
|
||
.btn-get-code::after {
|
||
border: none;
|
||
}
|
||
</style>
|