Files
webUI/src/pages/mine/mine.vue
2025-11-24 18:44:17 +08:00

891 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<!-- 夜空装饰背景 -->
<view class="night-sky-decoration">
<view class="star star-1"></view>
<view class="star star-2"></view>
<view class="star star-3"></view>
<view class="star star-4"></view>
<view class="star star-5"></view>
<view class="star star-6"></view>
<view class="star star-7"></view>
<view class="star star-8"></view>
</view>
<!-- 顶部自定义导航栏 -->
<view class="custom-navbar fixed-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-stars">
<view class="navbar-star star-left"></view>
<view class="navbar-star star-right"></view>
</view>
<text class="navbar-title">我的</text>
</view>
</view>
<!-- 可滚动内容区域 -->
<scroll-view
class="scroll-container"
:style="{ marginTop: navBarHeight + 'px' }"
scroll-y="true"
:show-scrollbar="false"
refresher-enabled
:refresher-triggered="refresherTriggered"
@refresherrefresh="onRefresh"
refresher-background="rgba(26, 11, 46, 0.5)"
>
<!-- 用户信息区域 -->
<view class="user-info">
<image class="avatar" :src="avatarUrl" mode="aspectFill"></image>
<view class="user-details" v-if="isLoggedIn">
<text class="login-text">{{ nickName }}</text>
<text class="user-id">ID: {{ openid }}</text>
</view>
<view class="user-details" v-else>
<text class="login-text">请登录</text>
<text class="user-id">ID:</text>
<button class="login-btn" @click="handleLogin">{{ loginButtonText }}</button>
</view>
</view>
<!-- 余额显示 -->
<view class="balance-section" v-if="isLoggedIn">
<view class="balance-card">
<view class="balance-info">
<view class="balance-label">账户余额</view>
<view class="balance-amount">¥{{ userBalance }}</view>
</view>
<view class="balance-actions">
<button class="recharge-btn" @click="goToRecharge">充值</button>
<button class="history-btn" @click="goToHistory">记录</button>
</view>
</view>
</view>
<!-- 菜单卡片 -->
<view class="menu-cards">
<!-- 用户须知 -->
<view class="menu-card" @click="showAgreement">
<view class="card-icon notice-icon">🔊</view>
<text class="card-title">用户须知</text>
</view>
<!-- 用户设备 -->
<view class="menu-card">
<view class="card-icon device-icon">📦</view>
<text class="card-title">用户设备</text>
</view>
</view>
<!-- 剧情角色区块 -->
<view class="section-block">
<view class="section-header">
<text class="section-title">剧情角色</text>
<text class="view-all">查看全部</text>
</view>
</view>
<!-- 克隆声音区块 -->
<view class="section-block">
<view class="section-header">
<text class="section-title">克隆声音</text>
<text class="view-all">查看全部</text>
</view>
</view>
<!-- 退出登录按钮仅登录后显示 -->
<view class="logout-section">
<button class="logout-btn" @click="handleLogout">退出登录</button>
<button class="logout-btn" style="margin-top: 20rpx; background-color: #ff9800; color: white;" @click="testDirectApiCall">直接测试API</button>
<text style="display: block; margin-top: 20rpx; text-align: center; color: #999;">
登录状态: {{ isLoggedIn ? '已登录' : '未登录' }}
</text>
</view>
<!-- 底部留白避免被tabbar遮挡 -->
<view class="bottom-spacing"></view>
</scroll-view>
<!-- 用户须知弹窗 -->
<user-agreement
:visible="showAgreementModal"
@agree="handleAgreeTerms"
@disagree="handleDisagreeTerms"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useUserStore } from '@/stores/user.js';
import { rechargeAPI } from '@/utils/api.js';
import UserAgreement from '@/components/UserAgreement.vue';
// 状态管理 - 避免直接依赖store的响应式
const userStore = useUserStore();
const showAgreementModal = ref(false);
// 本地状态避免直接引用store中的响应式状态
const isLoggedIn = ref(false);
const nickName = ref('');
const openid = ref('');
const avatarUrl = ref('/static/default-avatar.png');
const userBalance = ref(0.00);
// 状态栏高度适配
const statusBarHeight = ref(0);
const navBarHeight = ref(0);
// 下拉刷新相关
const refresherTriggered = ref(false);
// 登录按钮文本
const loginButtonText = computed(() => {
return '微信一键登录';
});
// 初始化函数
const initUserInfo = () => {
try {
// 先尝试加载本地存储的用户信息
const userInfo = uni.getStorageSync('userInfo');
if (userInfo) {
try {
const parsedInfo = JSON.parse(userInfo);
nickName.value = parsedInfo.nickName || '';
avatarUrl.value = parsedInfo.avatarUrl || '/static/default-avatar.png';
openid.value = parsedInfo.openid || '';
isLoggedIn.value = !!parsedInfo.token;
// 调试信息
uni.showToast({
title: `登录状态: ${isLoggedIn.value ? '已登录' : '未登录'}`,
icon: 'none',
duration: 2000
});
} catch (e) {
console.error('Parse user info error:', e);
}
} else {
uni.showToast({
title: '没有找到用户信息',
icon: 'none',
duration: 2000
});
}
} catch (e) {
console.error('Load user info error:', e);
}
};
// 获取系统状态栏高度
const getSystemInfo = () => {
const systemInfo = uni.getSystemInfoSync();
statusBarHeight.value = systemInfo.statusBarHeight || 0;
// 导航栏总高度 = 状态栏高度 + 导航栏内容高度44px
navBarHeight.value = statusBarHeight.value + 44;
console.log('状态栏高度:', statusBarHeight.value, '导航栏总高度:', navBarHeight.value);
};
// 页面加载时初始化
onMounted(() => {
// 获取系统信息
getSystemInfo();
// 初始化用户store
userStore.init();
// 初始化本地状态
initUserInfo();
// 加载用户余额
loadUserBalance();
});
// 登录处理
const handleLogin = async () => {
try {
uni.showLoading({
title: '登录中...'
});
// 使用store中的真实登录方法
await userStore.login();
// 登录成功后更新本地状态
initUserInfo();
uni.hideLoading();
uni.showToast({
title: '登录成功',
icon: 'success'
});
} catch (error) {
uni.hideLoading();
console.error('Login failed:', error);
// 登录失败
uni.showToast({
title: '登录失败',
icon: 'none',
duration: 2000
});
}
};
// 退出登录
const handleLogout = async () => {
// 用弹窗显示调试信息,确保用户能看到
uni.showToast({
title: '点击了退出登录',
icon: 'none',
duration: 2000
});
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: async (res) => {
if (res.confirm) {
uni.showToast({
title: '确认退出',
icon: 'none',
duration: 1000
});
try {
uni.showLoading({
title: '退出中...'
});
// 显示token信息
uni.showToast({
title: `Token: ${userStore.token ? '有' : '无'}`,
icon: 'none',
duration: 2000
});
console.log('mine.vue: 开始调用userStore.logout()');
console.log('mine.vue: 当前store中的token:', userStore.token);
// 使用store中的真实登出方法
await userStore.logout();
uni.showToast({
title: 'logout执行完成',
icon: 'none',
duration: 1000
});
// 清空本地状态
nickName.value = '';
avatarUrl.value = '/static/default-avatar.png';
openid.value = '';
isLoggedIn.value = false;
uni.hideLoading();
uni.showToast({
title: '已退出登录',
icon: 'success'
});
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '登出异常: ' + error.message,
icon: 'none',
duration: 3000
});
// 即使后台退出失败,也要清空本地状态
nickName.value = '';
avatarUrl.value = '/static/default-avatar.png';
openid.value = '';
isLoggedIn.value = false;
uni.showToast({
title: '已退出登录',
icon: 'success'
});
}
} else {
uni.showToast({
title: '取消退出',
icon: 'none',
duration: 1000
});
}
}
});
};
// 显示用户须知
const showAgreement = () => {
console.log('showAgreement clicked, current showAgreementModal:', showAgreementModal.value);
showAgreementModal.value = true;
console.log('showAgreementModal set to:', showAgreementModal.value);
};
// 处理同意用户须知
const handleAgreeTerms = async () => {
console.log('mine.vue: handleAgreeTerms called');
try {
// 先关闭弹窗
showAgreementModal.value = false;
// 等待DOM更新
await uni.$nextTick?.() || Promise.resolve();
// 设置本地存储
uni.setStorageSync('hasAgreedToTerms', 'true');
uni.setStorageSync('hasVisitedMinePage', 'true');
// 显示成功提示
uni.showToast({
title: '已确认用户须知',
icon: 'success',
duration: 2000
});
} catch (e) {
console.error('Save agreement status error:', e);
uni.showToast({
title: '操作失败,请重试',
icon: 'none',
duration: 2000
});
// 如果失败,重新显示弹窗
showAgreementModal.value = true;
}
};
// 处理不同意用户须知
const handleDisagreeTerms = async () => {
console.log('mine.vue: handleDisagreeTerms called');
try {
// 先关闭弹窗
showAgreementModal.value = false;
// 等待DOM更新
await uni.$nextTick?.() || Promise.resolve();
// 显示提示
uni.showToast({
title: '您可随时查看用户须知',
icon: 'none',
duration: 2000
});
} catch (e) {
console.error('Handle disagree error:', e);
showAgreementModal.value = false;
}
};
// 直接测试API
const testDirectApiCall = () => {
// 从store获取token
const currentToken = userStore.token;
uni.showModal({
title: '选择测试方法',
content: '请选择要测试的API调用方式',
cancelText: '普通方法',
confirmText: '请求体方法',
success: (res1) => {
if (res1.confirm) {
// 尝试在请求体中发送token
uni.showModal({
title: '请求体方法',
content: '在请求体中发送token',
showCancel: false,
success: () => {
uni.request({
url: 'https://www.aixsy.com.cn/app/logout',
method: 'POST',
header: {
'Content-Type': 'application/json'
},
data: {
token: currentToken || ''
},
complete: (res) => {
uni.showModal({
title: '调用结果(请求体)',
content: JSON.stringify(res),
showCancel: false
});
}
});
}
});
} else {
// 普通方法: 选择URL方式
uni.showModal({
title: '普通方法',
content: '选择URL方式',
cancelText: '带参数URL',
confirmText: 'Header方式',
success: (res2) => {
if (res2.confirm) {
// Header方式
uni.showModal({
title: '调用信息',
content: `
在Header中发送token
Token: ${currentToken || '无'}
`,
showCancel: false,
success: () => {
uni.request({
url: 'https://www.aixsy.com.cn/app/logout',
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': currentToken ? (currentToken.startsWith('Bearer ') ? currentToken : 'Bearer ' + currentToken) : ''
},
complete: (res) => {
uni.showModal({
title: '调用结果(Header)',
content: JSON.stringify(res),
showCancel: false
});
}
});
}
});
} else {
// 带参数URL方式
uni.showModal({
title: '调用信息',
content: `尝试带token参数的URL`,
showCancel: false,
success: () => {
const url = `https://www.aixsy.com.cn/app/logout?token=${currentToken || ''}`;
uni.request({
url: url,
method: 'POST',
header: {
'Content-Type': 'application/json'
},
complete: (res) => {
uni.showModal({
title: '调用结果(URL参数)',
content: JSON.stringify(res),
showCancel: false
});
}
});
}
});
}
}
});
}
}
});
};
// 加载用户余额
const loadUserBalance = async (isRefresh = false) => {
if (!isLoggedIn.value) return;
// 暂时注释掉API请求使用本地模拟数据
console.log('loadUserBalance called, 使用模拟数据');
userBalance.value = 0.00; // 默认余额
if (isRefresh) {
console.log('余额刷新成功(本地模拟)');
}
/* API请求已注释
try {
const result = await rechargeAPI.getUserBalance();
if (result.success) {
userBalance.value = result.data.balance || 0;
// 如果是下拉刷新,显示成功提示
if (isRefresh) {
console.log('余额刷新成功');
}
}
} catch (error) {
console.error('获取用户余额失败:', error);
if (isRefresh) {
uni.showToast({
title: '余额刷新失败',
icon: 'none',
duration: 1500
});
}
}
*/
};
// 跳转到充值页面
const goToRecharge = () => {
uni.navigateTo({
url: '/pages/recharge/recharge'
});
};
// 跳转到充值记录页面
const goToHistory = () => {
uni.navigateTo({
url: '/pages/recharge/history'
});
};
// 下拉刷新处理
const onRefresh = async () => {
console.log('触发下拉刷新');
refresherTriggered.value = true;
try {
// 刷新用户信息
initUserInfo();
// 刷新用户余额
await loadUserBalance(true);
// 显示刷新成功提示
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
} catch (error) {
console.error('刷新失败:', error);
uni.showToast({
title: '刷新失败',
icon: 'none',
duration: 1500
});
} finally {
// 刷新完成后,关闭刷新状态
setTimeout(() => {
refresherTriggered.value = false;
}, 500);
}
};
</script>
<style lang="scss" scoped>
.container {
min-height: 100vh;
background: linear-gradient(180deg, #1a0b2e 0%, #2d1b4e 50%, #1a0b2e 100%);
padding-bottom: 120rpx;
position: relative;
overflow: hidden;
}
/* 夜空装饰 */
.night-sky-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.star {
position: absolute;
width: 6rpx;
height: 6rpx;
background: #f9e076;
border-radius: 50%;
animation: twinkle 2s infinite;
box-shadow: 0 0 10rpx #f9e076;
}
@keyframes twinkle {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.star-1 { top: 10%; left: 15%; animation-delay: 0s; }
.star-2 { top: 20%; left: 80%; animation-delay: 0.3s; }
.star-3 { top: 30%; left: 45%; animation-delay: 0.6s; }
.star-4 { top: 50%; left: 25%; animation-delay: 0.9s; }
.star-5 { top: 60%; left: 70%; animation-delay: 1.2s; }
.star-6 { top: 70%; left: 40%; animation-delay: 1.5s; }
.star-7 { top: 15%; left: 60%; animation-delay: 0.4s; }
.star-8 { top: 85%; left: 55%; animation-delay: 1.8s; }
/* 自定义导航栏 */
.custom-navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: rgba(26, 11, 46, 0.95);
backdrop-filter: blur(20rpx);
border-bottom: 1rpx solid rgba(249, 224, 118, 0.1);
z-index: 1000;
}
.fixed-navbar {
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 1000;
}
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 30rpx;
position: relative;
}
.navbar-stars {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
}
.navbar-star {
position: absolute;
width: 8rpx;
height: 8rpx;
background: #f9e076;
border-radius: 50%;
box-shadow: 0 0 15rpx #f9e076;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
}
.star-left {
top: 50%;
left: 30rpx;
transform: translateY(-50%);
}
.star-right {
top: 50%;
right: 30rpx;
transform: translateY(-50%);
}
.navbar-title {
color: #f9e076;
font-weight: bold;
font-size: 36rpx;
text-shadow: 0 0 20rpx rgba(249, 224, 118, 0.5);
z-index: 1;
}
/* 滚动容器样式 */
.scroll-container {
flex: 1;
min-height: 0;
z-index: 1;
position: relative;
box-sizing: border-box;
}
/* 用户信息区域 */
.user-info {
display: flex;
align-items: center;
padding: 24rpx 28rpx;
margin: 20rpx 30rpx;
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
border: 2rpx solid rgba(249, 224, 118, 0.2);
border-radius: 25rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
/* 余额区域 */
.balance-section {
padding: 0 30rpx 20rpx;
}
.balance-card {
background: linear-gradient(135deg, rgba(249, 224, 118, 0.15) 0%, rgba(249, 224, 118, 0.08) 100%);
border: 2rpx solid rgba(249, 224, 118, 0.2);
border-radius: 25rpx;
padding: 24rpx 28rpx;
display: flex;
justify-content: space-between;
align-items: center;
color: #f9e076;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.balance-info .balance-label {
font-size: 24rpx;
color: rgba(249, 224, 118, 0.8);
margin-bottom: 6rpx;
}
.balance-info .balance-amount {
font-size: 40rpx;
font-weight: bold;
color: #f9e076;
}
.balance-actions {
display: flex;
gap: 20rpx;
}
.balance-actions .recharge-btn,
.balance-actions .history-btn {
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
color: #1a0b2e;
border: none;
border-radius: 30rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
font-weight: bold;
min-width: 90rpx;
box-shadow: 0 8rpx 20rpx rgba(249, 224, 118, 0.3);
transition: all 0.3s;
}
.balance-actions .recharge-btn:active,
.balance-actions .history-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 10rpx rgba(249, 224, 118, 0.3);
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background-color: rgba(249, 224, 118, 0.2);
border: 2rpx solid rgba(249, 224, 118, 0.3);
}
.user-details {
margin-left: 24rpx;
display: flex;
flex-direction: column;
}
.login-text {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 8rpx;
color: #f9e076;
}
.user-id {
font-size: 24rpx;
color: rgba(249, 224, 118, 0.6);
margin-bottom: 16rpx;
}
.login-btn {
margin-top: 12rpx;
background: linear-gradient(135deg, #f9e076 0%, #f5d042 100%);
color: #1a0b2e;
font-size: 24rpx;
font-weight: bold;
border-radius: 30rpx;
width: 200rpx;
height: 56rpx;
line-height: 56rpx;
padding: 0;
border: none;
box-shadow: 0 8rpx 20rpx rgba(249, 224, 118, 0.3);
transition: all 0.3s;
}
.login-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 10rpx rgba(249, 224, 118, 0.3);
}
/* 菜单卡片 */
.menu-cards {
padding: 0 30rpx;
}
.menu-card {
display: flex;
align-items: center;
padding: 28rpx 24rpx;
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
border: 2rpx solid rgba(249, 224, 118, 0.2);
border-radius: 25rpx;
margin-bottom: 20rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
transition: all 0.3s;
}
.menu-card:active {
transform: translateY(-4rpx);
box-shadow: 0 15rpx 50rpx rgba(249, 224, 118, 0.2);
border-color: rgba(249, 224, 118, 0.4);
}
.card-icon {
font-size: 40rpx;
margin-right: 20rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(249, 224, 118, 0.3));
}
.card-title {
font-size: 28rpx;
font-weight: 500;
color: #f9e076;
}
/* 区块样式 */
.section-block {
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
border: 2rpx solid rgba(249, 224, 118, 0.2);
border-radius: 25rpx;
margin: 0 30rpx 20rpx;
padding: 24rpx 28rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #f9e076;
}
.view-all {
font-size: 24rpx;
color: rgba(249, 224, 118, 0.6);
}
/* 退出登录按钮 */
.logout-section {
padding: 40rpx 30rpx;
}
.logout-btn {
width: 100%;
height: 80rpx;
line-height: 80rpx;
background: linear-gradient(135deg, rgba(249, 224, 118, 0.1) 0%, rgba(249, 224, 118, 0.05) 100%);
color: #f9e076;
font-size: 30rpx;
font-weight: bold;
border: 2rpx solid rgba(249, 224, 118, 0.2);
border-radius: 40rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
transition: all 0.3s;
}
.logout-btn:active {
transform: scale(0.95);
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.3);
}
/* 底部留白 */
.bottom-spacing {
height: 40rpx;
}
</style>