1364 lines
45 KiB
Vue
1364 lines
45 KiB
Vue
<template>
|
||
<view class="mine-page" :style="{ background: themeConfig.bgColor }">
|
||
<view class="page-content">
|
||
<view class="section-header">
|
||
<view class="section-title-row">
|
||
<view class="section-accent" :style="{ background: themeConfig.gradientSoft }"></view>
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">个人资料</text>
|
||
<text class="section-subtitle" :style="{ color: themeConfig.textSecondary }">点击头像可编辑</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="profile-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 8rpx 24rpx ${themeConfig.shadowLight}`, borderColor: themeConfig.borderLight }">
|
||
<view class="profile-row">
|
||
<view class="avatar-wrapper" @click="openProfileEditor">
|
||
<image v-if="userAvatar" class="avatar-img" :src="userAvatar" mode="aspectFill"></image>
|
||
<view v-else class="avatar-placeholder" :style="{ background: themeConfig.gradientSoft }">
|
||
<text class="avatar-init">{{ (userName || '我')[0] }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="profile-info">
|
||
<view class="profile-info-top">
|
||
<text class="user-name" :style="{ color: themeConfig.textPrimary }">{{ userName || '点击设置昵称' }}</text>
|
||
<view class="level-badge" :style="{ background: themeConfig.primaryColor + '15' }">
|
||
<text class="level-text" :style="{ color: themeConfig.primaryColor }" @click="goToGrowth">Lv.{{ growthInfo.level }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="user-stats-row">
|
||
<view class="user-stat">
|
||
<text class="stat-num" :style="{ color: themeConfig.textPrimary }">{{ dayCount }}</text>
|
||
<text class="stat-label" :style="{ color: themeConfig.textSecondary }">天数</text>
|
||
</view>
|
||
<view class="user-stat-divider" :style="{ background: themeConfig.borderColor }"></view>
|
||
<view class="user-stat">
|
||
<text class="stat-num" :style="{ color: themeConfig.textPrimary }">{{ growthInfo.streakDays || 0 }}</text>
|
||
<text class="stat-label" :style="{ color: themeConfig.textSecondary }">连续</text>
|
||
</view>
|
||
<view class="user-stat-divider" :style="{ background: themeConfig.borderColor }"></view>
|
||
<view class="user-stat">
|
||
<text class="stat-num" :style="{ color: themeConfig.textPrimary }">{{ growthInfo.achievements.unlocked || 0 }}</text>
|
||
<text class="stat-label" :style="{ color: themeConfig.textSecondary }">成就</text>
|
||
</view>
|
||
</view>
|
||
<text class="user-signature" v-if="userSignature" :style="{ color: themeConfig.textSecondary }">{{ userSignature }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-header">
|
||
<view class="section-title-row">
|
||
<view class="section-accent" :style="{ background: themeConfig.gradientSoft }"></view>
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">本月数据</text>
|
||
<text class="section-subtitle" :style="{ color: themeConfig.textSecondary }">收支与完成度</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="exp-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 8rpx 24rpx ${themeConfig.shadowLight}`, borderColor: themeConfig.borderLight }">
|
||
<view class="exp-header">
|
||
<text class="exp-label" :style="{ color: themeConfig.textPrimary }">升级进度</text>
|
||
<text class="exp-text" :style="{ color: themeConfig.textSecondary }">{{ growthInfo.experience }}/{{ growthInfo.nextLevelExp }} EXP</text>
|
||
</view>
|
||
<view class="exp-progress-bar">
|
||
<view class="exp-progress-fill" :style="{ width: expPercent + '%', background: themeConfig.gradientSoft }"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="stats-row">
|
||
<view class="stat-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 6rpx 20rpx ${themeConfig.shadowLight}`, borderColor: themeConfig.borderLight }">
|
||
<view class="stat-icon-wrap" :style="{ background: themeConfig.dangerColor + '15' }">
|
||
<FaIcon icon="wallet" :size="22" :color="themeConfig.dangerColor" />
|
||
</view>
|
||
<view class="stat-info">
|
||
<text class="stat-val" :style="{ color: themeConfig.textPrimary }">¥{{ formatNumber(totalAmount) }}</text>
|
||
<text class="stat-lbl" :style="{ color: themeConfig.textSecondary }">净收支</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 6rpx 20rpx ${themeConfig.shadowLight}`, borderColor: themeConfig.borderLight }">
|
||
<view class="stat-icon-wrap" :style="{ background: themeConfig.primaryColor + '15' }">
|
||
<FaIcon icon="check-circle" :size="22" :color="themeConfig.primaryColor" />
|
||
</view>
|
||
<view class="stat-info">
|
||
<text class="stat-val" :style="{ color: themeConfig.textPrimary }">{{ finishRate }}%</text>
|
||
<text class="stat-lbl" :style="{ color: themeConfig.textSecondary }">完成率</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 6rpx 20rpx ${themeConfig.shadowLight}`, borderColor: themeConfig.borderLight }">
|
||
<view class="stat-icon-wrap" :style="{ background: themeConfig.secondaryColor + '15' }">
|
||
<FaIcon icon="calendar" :size="22" :color="themeConfig.secondaryColor" />
|
||
</view>
|
||
<view class="stat-info">
|
||
<text class="stat-val" :style="{ color: themeConfig.textPrimary }">{{ monthBudget > 0 ? '¥' + formatNumber(monthBudget) : '未设' }}</text>
|
||
<text class="stat-lbl" :style="{ color: themeConfig.textSecondary }">月预算</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-header">
|
||
<view class="section-title-row">
|
||
<view class="section-accent" :style="{ background: themeConfig.gradientSoft }"></view>
|
||
<text class="section-title" :style="{ color: themeConfig.textPrimary }">更多功能</text>
|
||
<text class="section-subtitle" :style="{ color: themeConfig.textSecondary }">设置与账户</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="menu-card" :style="{ background: themeConfig.cardBgColor, boxShadow: `0 8rpx 24rpx ${themeConfig.shadowLight}`, borderColor: themeConfig.borderLight }">
|
||
<view class="menu-item" @click="goToGrowth">
|
||
<view class="menu-icon" :style="{ background: themeConfig.primaryColor + '15' }">
|
||
<FaIcon icon="trophy" :size="22" :color="themeConfig.primaryColor" />
|
||
</view>
|
||
<text class="menu-title" :style="{ color: themeConfig.textPrimary }">成长中心</text>
|
||
<text class="menu-arrow" :style="{ color: themeConfig.textDisabled }">
|
||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||
</text>
|
||
</view>
|
||
<view class="menu-divider" :style="{ background: themeConfig.borderColor }"></view>
|
||
<view class="menu-item" @click="handleBudgetSetting">
|
||
<view class="menu-icon" :style="{ background: themeConfig.secondaryColor + '15' }">
|
||
<FaIcon icon="wallet" :size="22" :color="themeConfig.secondaryColor" />
|
||
</view>
|
||
<text class="menu-title" :style="{ color: themeConfig.textPrimary }">月度预算</text>
|
||
<text class="menu-arrow" :style="{ color: themeConfig.textDisabled }">
|
||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||
</text>
|
||
</view>
|
||
<view class="menu-divider" :style="{ background: themeConfig.borderColor }"></view>
|
||
<view class="menu-item" @click="handleThemeSetting">
|
||
<view class="menu-icon" :style="{ background: themeConfig.primaryColor + '15' }">
|
||
<FaIcon icon="palette" :size="22" :color="themeConfig.primaryColor" />
|
||
</view>
|
||
<text class="menu-title" :style="{ color: themeConfig.textPrimary }">主题皮肤</text>
|
||
<text class="menu-desc" :style="{ color: themeConfig.textSecondary }">{{ themeConfig.name || '默认' }}</text>
|
||
<text class="menu-arrow" :style="{ color: themeConfig.textDisabled }">
|
||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||
</text>
|
||
</view>
|
||
<view class="menu-divider" :style="{ background: themeConfig.borderColor }"></view>
|
||
<view class="menu-item" @click="handleEmailSetting">
|
||
<view class="menu-icon" :style="{ background: (themeConfig.warningColor || '#ffaa33') + '20' }">
|
||
<FaIcon icon="envelope" :size="22" :color="themeConfig.warningColor || '#ffaa33'" />
|
||
</view>
|
||
<text class="menu-title" :style="{ color: themeConfig.textPrimary }">绑定邮箱</text>
|
||
<text class="menu-desc" :style="{ color: userEmail ? themeConfig.textSecondary : themeConfig.textDisabled }">{{ userEmail || '未绑定' }}</text>
|
||
<text class="menu-arrow" :style="{ color: themeConfig.textDisabled }">
|
||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||
</text>
|
||
</view>
|
||
<view class="menu-divider" :style="{ background: themeConfig.borderColor }"></view>
|
||
<view class="menu-item" :class="{ 'menu-item--disabled': isBindWechat }" @click="handleWechatBind">
|
||
<view class="menu-icon" :style="{ background: themeConfig.secondaryColor + '15' }">
|
||
<FaIcon icon="comment" :size="22" :color="themeConfig.secondaryColor" />
|
||
</view>
|
||
<text class="menu-title" :style="{ color: themeConfig.textPrimary }">绑定微信</text>
|
||
<text class="menu-desc" :style="{ color: isBindWechat ? themeConfig.primaryColor : themeConfig.textDisabled }">{{ isBindWechat ? '已绑定' : '未绑定' }}</text>
|
||
<text v-if="!isBindWechat" class="menu-arrow" :style="{ color: themeConfig.textDisabled }">
|
||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||
</text>
|
||
</view>
|
||
<view class="menu-divider" :style="{ background: themeConfig.borderColor }"></view>
|
||
<view class="menu-item" @click="handleAbout">
|
||
<view class="menu-icon" :style="{ background: themeConfig.textSecondary + '15' }">
|
||
<FaIcon icon="circle-info" :size="22" :color="themeConfig.textSecondary" />
|
||
</view>
|
||
<text class="menu-title" :style="{ color: themeConfig.textPrimary }">关于我们</text>
|
||
<text class="menu-desc" :style="{ color: themeConfig.textSecondary }">v{{ version }}</text>
|
||
<text class="menu-arrow" :style="{ color: themeConfig.textDisabled }">
|
||
<FaIcon icon="chevron-right" :size="18" :color="themeConfig.textDisabled" />
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="safe-area-bottom"></view>
|
||
|
||
<view class="modal" v-if="showBudgetModal" @click="showBudgetModal = false">
|
||
<view class="modal-box" :style="{ background: themeConfig.cardBgColor }" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title" :style="{ color: themeConfig.textPrimary }">设置月度预算</text>
|
||
<view class="modal-close" @click="showBudgetModal = false">
|
||
<FaIcon icon="xmark" :size="20" :color="themeConfig.textSecondary" />
|
||
</view>
|
||
</view>
|
||
<view class="modal-form">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">预算金额(元)</text>
|
||
<input v-model="tempBudget" type="digit" placeholder="请输入月度预算"
|
||
class="form-input"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }" />
|
||
</view>
|
||
<view class="modal-actions">
|
||
<view class="modal-btn cancel-btn btn btn--secondary" @click="showBudgetModal = false">取消</view>
|
||
<view class="modal-btn confirm-btn btn btn--primary" @click="saveBudget">确定</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal" v-if="showProfileModal" @click="showProfileModal = false">
|
||
<view class="modal-box" :style="{ background: themeConfig.cardBgColor }" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title" :style="{ color: themeConfig.textPrimary }">编辑资料</text>
|
||
<view class="modal-close" @click="showProfileModal = false">
|
||
<FaIcon icon="xmark" :size="20" :color="themeConfig.textSecondary" />
|
||
</view>
|
||
</view>
|
||
<view class="profile-modal-content">
|
||
<view class="profile-section">
|
||
<view class="avatar-preview-wrapper" @click="showAvatarPicker = !showAvatarPicker">
|
||
<view class="profile-avatar-frame" :style="{ borderColor: themeConfig.primaryColor }">
|
||
<image class="profile-avatar" :src="profileForm.avatar || userAvatar" mode="aspectFill" />
|
||
</view>
|
||
<view class="avatar-edit-badge" :style="{ background: themeConfig.primaryColor }">
|
||
<FaIcon icon="pen" :size="14" color="#fff" />
|
||
</view>
|
||
</view>
|
||
<view class="avatar-picker" v-if="showAvatarPicker">
|
||
<view class="avatar-grid">
|
||
<view class="avatar-option" :class="{ 'is-selected': profileForm.avatar === avatar }"
|
||
v-for="(avatar, index) in validAvatars" :key="index"
|
||
@click="selectAvatarForProfile(avatar)">
|
||
<view class="avatar-frame"
|
||
:style="{ borderColor: profileForm.avatar === avatar ? themeConfig.primaryColor : themeConfig.borderColor }">
|
||
<image class="option-avatar" :src="avatar" mode="aspectFill"></image>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="profile-section">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">昵称</text>
|
||
<input v-model="profileForm.nickname" placeholder="请输入昵称"
|
||
class="form-input" :maxlength="10"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }" />
|
||
</view>
|
||
<view class="profile-section">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">个性签名</text>
|
||
<input v-model="profileForm.signature" placeholder="请输入个性签名"
|
||
class="form-input" :maxlength="20"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }" />
|
||
</view>
|
||
<view class="modal-actions">
|
||
<view class="modal-btn cancel-btn btn btn--secondary" @click="showProfileModal = false">取消</view>
|
||
<view class="modal-btn confirm-btn btn btn--primary" @click="saveProfile">保存</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="modal" v-if="showEmailModal" @click="showEmailModal = false">
|
||
<view class="modal-box" :style="{ background: themeConfig.cardBgColor }" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title" :style="{ color: themeConfig.textPrimary }">{{ userEmail ? '邮箱设置' : '绑定邮箱' }}</text>
|
||
<view class="modal-close" @click="showEmailModal = false">
|
||
<FaIcon icon="xmark" :size="20" :color="themeConfig.textSecondary" />
|
||
</view>
|
||
</view>
|
||
<view class="email-modal-content">
|
||
<view class="email-form">
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">邮箱地址</text>
|
||
<input v-model="emailForm.email" type="email" placeholder="请输入邮箱地址"
|
||
class="form-input"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
:disabled="userEmail && !isEditingEmail" />
|
||
</view>
|
||
|
||
<view v-if="userEmail && !isEditingEmail && !showChangePassword" class="form-item">
|
||
<view class="change-password-btn btn btn--primary" @click="showChangePassword = true">
|
||
<text class="btn-text" style="color:#fff;">修改密码</text>
|
||
</view>
|
||
<view class="change-password-btn secondary btn btn--secondary" @click="handleEditEmail">
|
||
<text class="btn-text" :style="{ color: themeConfig.textPrimary }">修改邮箱</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="!userEmail || showChangePassword || isEditingEmail" class="form-item">
|
||
<view v-if="userEmail && showChangePassword && !isEditingEmail" class="form-item">
|
||
<view class="change-password-btn cancel-change btn btn--secondary" @click="cancelChangePassword">
|
||
<text class="btn-text" :style="{ color: themeConfig.textPrimary }">收起密码修改</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">密码</text>
|
||
<view class="password-input-wrapper">
|
||
<input v-model="emailForm.password" type="password" :password="!showEmailPassword1" placeholder="请输入密码"
|
||
class="form-input password-input"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }" />
|
||
<text class="password-toggle-btn" @tap="showEmailPassword1 = !showEmailPassword1">
|
||
<FaIcon :icon="showEmailPassword1 ? 'eye-slash' : 'eye'" :size="20" :color="themeConfig.textSecondary" />
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">确认密码</text>
|
||
<view class="password-input-wrapper">
|
||
<input v-model="emailForm.confirmPassword" type="password" :password="!showEmailPassword2" placeholder="请再次输入密码"
|
||
class="form-input password-input"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }" />
|
||
<text class="password-toggle-btn" @tap="showEmailPassword2 = !showEmailPassword2">
|
||
<FaIcon :icon="showEmailPassword2 ? 'eye-slash' : 'eye'" :size="20" :color="themeConfig.textSecondary" />
|
||
</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">图形验证码</text>
|
||
<view class="captcha-row">
|
||
<input v-model="emailForm.captcha" type="text" placeholder="请输入验证码" maxlength="4"
|
||
class="form-input captcha-input"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }" />
|
||
<view class="captcha-img" @click="refreshCaptcha">
|
||
<image :src="captchaImage" mode="aspectFill" class="captcha-image" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="emailForm.email !== userEmail" class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textPrimary }">邮箱验证码</text>
|
||
<view class="code-row">
|
||
<input v-model="emailForm.emailCode" type="digit" placeholder="请输入邮箱验证码"
|
||
class="form-input code-input"
|
||
:style="{ backgroundColor: themeConfig.bgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }" />
|
||
<view class="send-code-btn btn btn--primary btn--sm" :class="{ disabled: emailCodeCountdown > 0 }" @click="sendEmailCode">
|
||
{{ emailCodeCountdown > 0 ? emailCodeCountdown + 's' : '发送验证码' }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="emailForm.password && emailForm.confirmPassword && emailForm.password !== emailForm.confirmPassword" class="form-error">两次密码不一致</view>
|
||
</view>
|
||
</view>
|
||
<view class="modal-actions" v-if="!userEmail || showChangePassword || isEditingEmail">
|
||
<view class="modal-btn cancel-btn btn btn--secondary" @click="showEmailModal = false">取消</view>
|
||
<view class="modal-btn confirm-btn btn btn--primary" @click="submitEmail">确定</view>
|
||
</view>
|
||
<view class="modal-actions" v-else>
|
||
<view class="modal-btn confirm-btn full-width btn btn--primary" @click="showEmailModal = false">关闭</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {
|
||
getThemeConfigFromStorage,
|
||
THEME_PRESETS
|
||
} from '@/utils/theme'
|
||
import {
|
||
refreshNavBarTheme,
|
||
forceApplyTabBarTheme
|
||
} from '@/utils/themeGlobal'
|
||
import {
|
||
DEFAULT_AVATARS,
|
||
MINE_MENU_ICONS
|
||
} from '@/utils/constants.js'
|
||
import {
|
||
getUserInfo,
|
||
editUserInfo,
|
||
getMonthStat,
|
||
getMonthBudget,
|
||
setMonthBudget,
|
||
getGrowthInfo,
|
||
sendEmailCode,
|
||
setUserEmail,
|
||
bindWechat,
|
||
getCaptcha,
|
||
changePassword
|
||
} from '@/api/index'
|
||
import FaIcon from '@/components/FaIcon.vue'
|
||
|
||
export default {
|
||
components: {
|
||
FaIcon
|
||
},
|
||
data() {
|
||
return {
|
||
version: '1.0.0',
|
||
monthIncome: 0,
|
||
monthExpend: 0,
|
||
totalAmount: 0,
|
||
finishTaskCount: 0,
|
||
unfinishTaskCount: 0,
|
||
finishRate: 0,
|
||
monthBudget: 0,
|
||
tempBudget: '',
|
||
showBudgetModal: false,
|
||
DEFAULT_AVATARS: DEFAULT_AVATARS,
|
||
MINE_MENU_ICONS: MINE_MENU_ICONS,
|
||
userName: '',
|
||
userAvatar: '',
|
||
userEmail: '',
|
||
userCode: '',
|
||
isBindWechat: false,
|
||
isBindEmail: false,
|
||
currentMonth: new Date().getMonth() + 1,
|
||
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS['default'],
|
||
dayCount: 0,
|
||
joinDate: '2026-01-01',
|
||
userSignature: '',
|
||
showProfileModal: false,
|
||
showAvatarPicker: false,
|
||
showEmailModal: false,
|
||
showChangePassword: false,
|
||
isEditingEmail: false,
|
||
emailForm: {
|
||
email: '',
|
||
password: '',
|
||
confirmPassword: '',
|
||
captcha: '',
|
||
captchaId: '',
|
||
emailCode: ''
|
||
},
|
||
showPassword: false,
|
||
showConfirmPassword: false,
|
||
showEmailPassword: false,
|
||
showEmailPassword1: false,
|
||
showEmailPassword2: false,
|
||
codeCountdown: 0,
|
||
emailCodeCountdown: 0,
|
||
captchaImage: '',
|
||
profileForm: {
|
||
nickname: '',
|
||
signature: '',
|
||
avatar: ''
|
||
},
|
||
growthInfo: {
|
||
level: 1,
|
||
title: '初出茅庐',
|
||
titleIcon: '',
|
||
experience: 0,
|
||
nextLevelExp: 100,
|
||
totalDays: 0,
|
||
streakDays: 0,
|
||
maxStreakDays: 0,
|
||
avgCompletionRate: 0,
|
||
todayProgress: {
|
||
tasksCompleted: 0,
|
||
taskExp: 0,
|
||
billsRecorded: 0,
|
||
billExp: 0,
|
||
moodRecorded: false,
|
||
moodExp: 0,
|
||
streakBonus: 0,
|
||
protectBonus: 0,
|
||
totalExp: 0,
|
||
maxDailyExp: 120
|
||
},
|
||
achievements: [],
|
||
privileges: []
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
validAvatars() {
|
||
return this.DEFAULT_AVATARS.filter((avatar) => !!avatar && avatar.trim() !== '')
|
||
},
|
||
expPercent() {
|
||
if (!this.growthInfo.nextLevelExp || this.growthInfo.nextLevelExp <= 0) return 0
|
||
return Math.min(100, Math.round((this.growthInfo.experience / this.growthInfo.nextLevelExp) * 100))
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.themeChangeHandler = (themeData) => {
|
||
try {
|
||
let newTheme = null
|
||
if (themeData?.key && THEME_PRESETS[themeData.key]) {
|
||
newTheme = THEME_PRESETS[themeData.key]
|
||
} else if (themeData?.config) {
|
||
newTheme = themeData.config
|
||
}
|
||
if (newTheme) {
|
||
this.themeConfig = newTheme
|
||
}
|
||
} catch (e) {
|
||
console.error('主题更新失败:', e)
|
||
}
|
||
}
|
||
uni.$on('themeChanged', this.themeChangeHandler)
|
||
this.loadUserInfo()
|
||
this.calculateDayCount()
|
||
this.initData()
|
||
this.loadGrowthInfo()
|
||
},
|
||
onUnload() {
|
||
uni.$off('themeChanged', this.themeChangeHandler)
|
||
},
|
||
onShow() {
|
||
const latestTheme = getThemeConfigFromStorage() || THEME_PRESETS['default']
|
||
if (JSON.stringify(latestTheme) !== JSON.stringify(this.themeConfig)) {
|
||
this.themeConfig = latestTheme
|
||
}
|
||
refreshNavBarTheme()
|
||
setTimeout(() => {
|
||
forceApplyTabBarTheme(latestTheme)
|
||
}, 100)
|
||
this.loadGrowthInfo()
|
||
this.loadUserInfo()
|
||
this.initData()
|
||
},
|
||
methods: {
|
||
async loadGrowthInfo() {
|
||
try {
|
||
const res = await getGrowthInfo()
|
||
if (res) {
|
||
this.growthInfo = {
|
||
level: res.level || 1,
|
||
title: res.title || '',
|
||
titleIcon: res.titleIcon || '',
|
||
experience: res.experience || 0,
|
||
nextLevelExp: res.nextLevelExp || 100,
|
||
totalDays: res.totalDays || 0,
|
||
streakDays: res.streakDays || 0,
|
||
maxStreakDays: res.maxStreakDays || 0,
|
||
avgCompletionRate: res.avgCompletionRate || 0,
|
||
todayProgress: {
|
||
tasksCompleted: res.todayProgress?.tasksCompleted || 0,
|
||
taskExp: res.todayProgress?.taskExp || 0,
|
||
billsRecorded: res.todayProgress?.billsRecorded || 0,
|
||
billExp: res.todayProgress?.billExp || 0,
|
||
moodRecorded: res.todayProgress?.moodRecorded || false,
|
||
moodExp: res.todayProgress?.moodExp || 0,
|
||
streakBonus: res.todayProgress?.streakBonus || 0,
|
||
protectBonus: res.todayProgress?.protectBonus || 0,
|
||
totalExp: res.todayProgress?.totalExp || 0,
|
||
maxDailyExp: res.todayProgress?.maxDailyExp || 120
|
||
},
|
||
achievements: res.achievements || [],
|
||
privileges: res.privileges || []
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('加载成长信息失败:', e)
|
||
}
|
||
},
|
||
goToGrowth() {
|
||
uni.navigateTo({ url: '/pages/growth/growth' })
|
||
},
|
||
formatNumber(num) {
|
||
if (num === null || num === undefined || isNaN(num) || num === '') return '0'
|
||
const number = Number(num)
|
||
const formatted = number.toFixed(2)
|
||
return formatted.replace(/\.00$/, '')
|
||
},
|
||
async loadUserInfo() {
|
||
try {
|
||
const token = uni.getStorageSync('user_token')
|
||
if (!token) return
|
||
const res = await getUserInfo()
|
||
this.userName = res.nickname || ''
|
||
this.userAvatar = res.avatar || this.validAvatars[0] || ''
|
||
this.userSignature = res.signature || ''
|
||
this.userEmail = res.email || ''
|
||
this.userCode = res.user_code || ''
|
||
this.isBindWechat = res.is_bind_wechat || false
|
||
this.isBindEmail = res.is_bind_email || false
|
||
uni.setStorageSync('userName', this.userName)
|
||
uni.setStorageSync('userAvatar', this.userAvatar)
|
||
uni.setStorageSync('userSignature', this.userSignature)
|
||
uni.setStorageSync('userEmail', this.userEmail)
|
||
uni.setStorageSync('userCode', this.userCode)
|
||
} catch (e) {
|
||
console.error('加载用户信息失败:', e)
|
||
}
|
||
},
|
||
calculateDayCount() {
|
||
const joinDate = new Date(this.joinDate)
|
||
const today = new Date()
|
||
const diffTime = today.getTime() - joinDate.getTime()
|
||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
|
||
this.dayCount = Math.max(0, diffDays)
|
||
},
|
||
async copyUserCode() {
|
||
if (!this.userCode) return
|
||
try {
|
||
await uni.setClipboardData({ data: this.userCode })
|
||
uni.showToast({ title: '复制成功', icon: 'success' })
|
||
} catch (e) {
|
||
uni.showToast({ title: '复制失败', icon: 'none' })
|
||
}
|
||
},
|
||
openProfileEditor() {
|
||
this.profileForm = {
|
||
nickname: this.userName || '',
|
||
signature: this.userSignature || '',
|
||
avatar: this.userAvatar || ''
|
||
}
|
||
this.showAvatarPicker = false
|
||
this.showProfileModal = true
|
||
},
|
||
selectAvatarForProfile(avatar) {
|
||
this.profileForm.avatar = avatar
|
||
},
|
||
async saveProfile() {
|
||
try {
|
||
const avatar = this.profileForm.avatar?.trim()
|
||
if (!avatar) {
|
||
uni.showToast({ title: '请选择头像', icon: 'none' })
|
||
return
|
||
}
|
||
const nickname = this.profileForm.nickname?.trim()
|
||
if (!nickname) {
|
||
uni.showToast({ title: '昵称不能为空', icon: 'none' })
|
||
return
|
||
}
|
||
const signature = this.profileForm.signature?.trim()
|
||
if (!signature) {
|
||
uni.showToast({ title: '个性签名不能为空', icon: 'none' })
|
||
return
|
||
}
|
||
const params = {}
|
||
if (nickname !== this.userName) params.nickname = nickname
|
||
if (signature !== this.userSignature) params.signature = signature
|
||
if (avatar !== this.userAvatar) params.avatar = avatar
|
||
if (Object.keys(params).length === 0) {
|
||
uni.showToast({ title: '未修改任何内容', icon: 'none' })
|
||
return
|
||
}
|
||
const res = await editUserInfo(params)
|
||
if (res) {
|
||
if (params.nickname) {
|
||
this.userName = params.nickname
|
||
uni.setStorageSync('userName', this.userName)
|
||
}
|
||
if (params.signature) this.userSignature = params.signature
|
||
if (params.avatar) {
|
||
this.userAvatar = params.avatar
|
||
uni.setStorageSync('userAvatar', this.userAvatar)
|
||
}
|
||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||
this.showProfileModal = false
|
||
}
|
||
} catch (e) {
|
||
console.error('保存资料失败:', e)
|
||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||
}
|
||
},
|
||
async initData() {
|
||
try {
|
||
const now = new Date()
|
||
const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||
const res = await getMonthStat({ month })
|
||
this.monthIncome = res.income || 0
|
||
this.monthExpend = res.expend || 0
|
||
this.finishTaskCount = res.finishTask || 0
|
||
const totalTask = res.totalTask || 0
|
||
this.unfinishTaskCount = totalTask - this.finishTaskCount
|
||
this.totalAmount = this.monthIncome - this.monthExpend
|
||
this.finishRate = totalTask > 0 ? Math.round(this.finishTaskCount / totalTask * 100) : 0
|
||
} catch (e) {
|
||
this.totalAmount = 0
|
||
this.finishRate = 0
|
||
}
|
||
try {
|
||
const res = await getMonthBudget()
|
||
this.monthBudget = res.amount || 0
|
||
} catch (e) {}
|
||
},
|
||
handleThemeSetting() {
|
||
uni.navigateTo({ url: '/pages/themeSetting/themeSetting' })
|
||
},
|
||
handleBudgetSetting() {
|
||
this.tempBudget = this.formatNumber(this.monthBudget)
|
||
this.showBudgetModal = true
|
||
},
|
||
async saveBudget() {
|
||
const num = Number(this.tempBudget)
|
||
if (!this.tempBudget || isNaN(num) || num < 0.01) {
|
||
uni.showToast({ title: '请输入大于0的有效预算', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
await setMonthBudget({ amount: num })
|
||
this.monthBudget = num
|
||
this.showBudgetModal = false
|
||
uni.showToast({ title: '设置成功', icon: 'success' })
|
||
} catch (err) {
|
||
uni.showToast({ title: '设置预算失败', icon: 'none' })
|
||
}
|
||
},
|
||
handleAbout() {
|
||
uni.navigateTo({ url: '/pages/about/about' })
|
||
},
|
||
async handleWechatBind() {
|
||
if (this.isBindWechat) {
|
||
return
|
||
}
|
||
try {
|
||
const loginRes = await uni.login().catch(() => { throw new Error('获取微信登录凭证失败') })
|
||
const code = loginRes.code
|
||
if (!code) throw new Error('微信登录凭证为空')
|
||
const result = await bindWechat({ code })
|
||
if (result) {
|
||
this.isBindWechat = true
|
||
uni.showToast({ title: '绑定成功', icon: 'success' })
|
||
} else {
|
||
uni.showToast({ title: '绑定失败', icon: 'none' })
|
||
}
|
||
} catch (err) {
|
||
uni.showToast({ title: err.message || '绑定失败', icon: 'none' })
|
||
}
|
||
},
|
||
handleEmailSetting() {
|
||
this.emailForm = {
|
||
email: this.userEmail || '',
|
||
password: '',
|
||
confirmPassword: '',
|
||
captcha: '',
|
||
captchaId: '',
|
||
emailCode: ''
|
||
}
|
||
this.showPassword = false
|
||
this.showConfirmPassword = false
|
||
this.showEmailPassword1 = false
|
||
this.showEmailPassword2 = false
|
||
this.showChangePassword = false
|
||
this.isEditingEmail = false
|
||
this.codeCountdown = 0
|
||
this.showEmailModal = true
|
||
this.refreshCaptcha()
|
||
},
|
||
handleEditEmail() {
|
||
this.isEditingEmail = true
|
||
this.showChangePassword = false
|
||
this.emailForm.email = ''
|
||
this.emailForm.password = ''
|
||
this.emailForm.confirmPassword = ''
|
||
this.emailForm.captcha = ''
|
||
this.emailForm.emailCode = ''
|
||
this.refreshCaptcha()
|
||
},
|
||
cancelChangePassword() {
|
||
this.showChangePassword = false
|
||
this.emailForm.password = ''
|
||
this.emailForm.confirmPassword = ''
|
||
this.emailForm.captcha = ''
|
||
this.emailForm.emailCode = ''
|
||
this.refreshCaptcha()
|
||
},
|
||
async refreshCaptcha() {
|
||
try {
|
||
const res = await getCaptcha()
|
||
if (res?.image) {
|
||
this.captchaImage = res.image
|
||
this.emailForm.captchaId = res.captchaId
|
||
}
|
||
} catch (e) {
|
||
console.error('获取验证码失败:', e)
|
||
}
|
||
},
|
||
async sendEmailCode() {
|
||
if (this.emailCodeCountdown > 0) return
|
||
const captcha = this.emailForm.captcha?.trim()
|
||
if (!captcha) {
|
||
uni.showToast({ title: '请输入图形验证码', icon: 'none' })
|
||
return
|
||
}
|
||
const email = this.emailForm.email?.trim()
|
||
if (!email) {
|
||
uni.showToast({ title: '请输入邮箱地址', icon: 'none' })
|
||
return
|
||
}
|
||
const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||
if (!emailReg.test(email)) {
|
||
uni.showToast({ title: '请输入正确的邮箱格式', icon: 'none' })
|
||
return
|
||
}
|
||
if (email === this.userEmail) {
|
||
uni.showToast({ title: '邮箱未更改,无需验证', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
await sendEmailCode({ email, captcha, captchaId: this.emailForm.captchaId })
|
||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||
this.emailCodeCountdown = 60
|
||
const timer = setInterval(() => {
|
||
this.emailCodeCountdown--
|
||
if (this.emailCodeCountdown <= 0) clearInterval(timer)
|
||
}, 1000)
|
||
this.emailForm.captcha = ''
|
||
this.refreshCaptcha()
|
||
} catch (e) {
|
||
uni.showToast({ title: '发送失败,请重试', icon: 'none' })
|
||
this.refreshCaptcha()
|
||
}
|
||
},
|
||
async submitEmail() {
|
||
const email = this.emailForm.email?.trim()
|
||
const emailCode = this.emailForm.emailCode?.trim()
|
||
const password = this.emailForm.password?.trim()
|
||
const confirmPassword = this.emailForm.confirmPassword?.trim()
|
||
const captcha = this.emailForm.captcha?.trim()
|
||
const isEmailChanged = email !== this.userEmail
|
||
const isChangePasswordOnly = this.userEmail && !isEmailChanged && this.showChangePassword
|
||
|
||
if (!email) {
|
||
uni.showToast({ title: '请输入邮箱地址', icon: 'none' })
|
||
return
|
||
}
|
||
const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||
if (!emailReg.test(email)) {
|
||
uni.showToast({ title: '请输入正确的邮箱格式', icon: 'none' })
|
||
return
|
||
}
|
||
if (!password) {
|
||
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||
return
|
||
}
|
||
if (password.length < 6) {
|
||
uni.showToast({ title: '密码长度不能少于6位', icon: 'none' })
|
||
return
|
||
}
|
||
if (!confirmPassword) {
|
||
uni.showToast({ title: '请确认密码', icon: 'none' })
|
||
return
|
||
}
|
||
if (password !== confirmPassword) {
|
||
uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||
return
|
||
}
|
||
if (!captcha) {
|
||
uni.showToast({ title: '请输入图形验证码', icon: 'none' })
|
||
return
|
||
}
|
||
if (isEmailChanged && !emailCode) {
|
||
uni.showToast({ title: '请输入邮箱验证码', icon: 'none' })
|
||
return
|
||
}
|
||
if (isEmailChanged && emailCode.length < 4) {
|
||
uni.showToast({ title: '验证码格式不正确', icon: 'none' })
|
||
return
|
||
}
|
||
try {
|
||
if (isChangePasswordOnly) {
|
||
await changePassword({ email, password, confirmPassword, captcha, captchaId: this.emailForm.captchaId })
|
||
uni.showToast({ title: '密码修改成功', icon: 'success' })
|
||
this.showEmailModal = false
|
||
} else {
|
||
const submitData = { email, password, captcha, captchaId: this.emailForm.captchaId }
|
||
if (isEmailChanged) submitData.code = emailCode
|
||
const res = await setUserEmail(submitData)
|
||
if (res) {
|
||
this.userEmail = email
|
||
uni.setStorageSync('userEmail', email)
|
||
uni.showToast({ title: this.userEmail ? '修改成功' : '绑定成功', icon: 'success' })
|
||
}
|
||
this.showEmailModal = false
|
||
}
|
||
} catch (e) {
|
||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||
this.refreshCaptcha()
|
||
this.emailForm.captcha = ''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.mine-page {
|
||
min-height: 100vh;
|
||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.page-content {
|
||
padding: calc(20rpx + env(safe-area-inset-top)) 32rpx 0;
|
||
}
|
||
|
||
/* ========== 区域标题 ========== */
|
||
.section-header {
|
||
padding: 24rpx 4rpx 16rpx;
|
||
}
|
||
|
||
.section-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.section-accent {
|
||
width: 8rpx;
|
||
height: 28rpx;
|
||
border-radius: 4rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: 800;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.section-subtitle {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* ========== 个人资料卡片 ========== */
|
||
.profile-card {
|
||
border-radius: 20rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 8rpx;
|
||
border: 2rpx solid transparent;
|
||
}
|
||
|
||
.profile-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.avatar-wrapper {
|
||
position: relative;
|
||
width: 108rpx;
|
||
height: 108rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.avatar-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-init {
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
|
||
.profile-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
min-width: 0;
|
||
}
|
||
|
||
.profile-info-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.level-badge {
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.level-text {
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.user-stats-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.user-stat {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
.stat-num {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.user-stat-divider {
|
||
width: 2rpx;
|
||
height: 28rpx;
|
||
}
|
||
|
||
.user-signature {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
line-height: 1.3;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ========== 升级进度卡片 ========== */
|
||
.exp-card {
|
||
border-radius: 20rpx;
|
||
padding: 20rpx 24rpx;
|
||
margin-bottom: 12rpx;
|
||
border: 2rpx solid transparent;
|
||
}
|
||
|
||
.exp-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.exp-label {
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.exp-text {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.exp-progress-bar {
|
||
height: 12rpx;
|
||
background: rgba(0, 0, 0, 0.06);
|
||
border-radius: 6rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.exp-progress-fill {
|
||
height: 100%;
|
||
border-radius: 6rpx;
|
||
}
|
||
|
||
/* ========== 数据统计卡片 ========== */
|
||
.stats-row {
|
||
display: flex;
|
||
gap: 14rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
border-radius: 16rpx;
|
||
padding: 18rpx 12rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
border: 2rpx solid transparent;
|
||
}
|
||
|
||
.stat-icon-wrap {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.stat-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.stat-val {
|
||
font-size: 26rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.stat-lbl {
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* ========== 菜单卡片 ========== */
|
||
.menu-card {
|
||
border-radius: 20rpx;
|
||
padding: 0 20rpx;
|
||
border: 2rpx solid transparent;
|
||
}
|
||
|
||
.menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.menu-icon {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.menu-title {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.menu-desc {
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.menu-item--disabled {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.menu-arrow {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.menu-divider {
|
||
height: 2rpx;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.safe-area-bottom {
|
||
height: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.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-box {
|
||
width: 600rpx;
|
||
border-radius: 20rpx;
|
||
padding: 28rpx;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-form {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
margin-bottom: 10rpx;
|
||
display: block;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
height: 80rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 28rpx;
|
||
border: 2rpx solid;
|
||
border-radius: 12rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.modal-btn {
|
||
flex: 1;
|
||
}
|
||
|
||
.modal-btn.full-width {
|
||
flex: 1;
|
||
}
|
||
|
||
.profile-modal-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.profile-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.avatar-preview-wrapper {
|
||
position: relative;
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
align-self: center;
|
||
}
|
||
|
||
.profile-avatar-frame {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 4rpx solid;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.profile-avatar {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.avatar-edit-badge {
|
||
position: absolute;
|
||
bottom: -4rpx;
|
||
right: -4rpx;
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 10rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-picker {
|
||
margin-top: 16rpx;
|
||
}
|
||
|
||
.avatar-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.avatar-option {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.avatar-frame {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border: 3rpx solid;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.avatar-option.is-selected .avatar-frame {
|
||
border-width: 4rpx;
|
||
}
|
||
|
||
.option-avatar {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.email-modal-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.email-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.password-input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.password-toggle-btn {
|
||
position: absolute;
|
||
right: 16rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
padding: 8rpx;
|
||
}
|
||
|
||
.captcha-row {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.captcha-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.captcha-img {
|
||
width: 200rpx;
|
||
height: 80rpx;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.captcha-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.code-row {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.code-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.send-code-btn {
|
||
padding: 0 20rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.change-password-btn {
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.btn-text {
|
||
color: #fff;
|
||
}
|
||
|
||
.form-error {
|
||
font-size: 22rpx;
|
||
color: #e74c3c;
|
||
}
|
||
</style>
|