Files
geekai/web/src/views/mobile/Profile.vue
2025-08-09 23:25:47 +08:00

607 lines
15 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>
<div class="profile-page">
<div class="profile-header">
<div class="header-bg"></div>
<div class="header-content">
<div class="user-info" v-if="isLogin">
<div class="avatar-container">
<van-image :src="fileList[0].url" round width="80" height="80" />
</div>
<div class="user-details">
<h2 class="username">{{ form.nickname || form.username }}</h2>
<div class="user-meta">
<van-tag type="info">剩余算力{{ form.power || 0 }}</van-tag>
<span class="user-id">ID: {{ form.id }}</span>
</div>
</div>
</div>
<div class="login-prompt" v-else>
<button
class="py-3 px-5 bg-gradient-to-r from-green-400 to-blue-400 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-green-500 hover:to-blue-500 transition-all duration-200 flex items-center justify-center space-x-2"
@click="router.push('/login')"
>
立即登录
</button>
</div>
</div>
</div>
<div class="profile-content">
<!-- 快捷操作 -->
<div class="quick-actions" v-if="isLogin">
<h3 class="section-title">快捷操作</h3>
<van-row :gutter="12">
<van-col :span="8">
<div class="action-item" @click="router.push('/mobile/member')">
<div class="action-icon recharge">
<i class="iconfont icon-vip"></i>
</div>
<div class="action-label">会员中心</div>
</div>
</van-col>
<van-col :span="8">
<div class="action-item" @click="router.push('/mobile/invite')">
<div class="action-icon share">
<i class="iconfont icon-share"></i>
</div>
<div class="action-label">邀请</div>
</div>
</van-col>
<van-col :span="8">
<div class="action-item" @click="showSettings = true">
<div class="action-icon settings">
<i class="iconfont icon-config"></i>
</div>
<div class="action-label">设置</div>
</div>
</van-col>
</van-row>
</div>
<!-- 我的服务 -->
<div class="menu-section" v-if="isLogin">
<h3 class="section-title">我的服务</h3>
<van-cell-group>
<van-cell title="绑定邮箱" is-link @click="showBindEmailDialog = true">
<template #icon>
<i class="iconfont icon-email menu-icon"></i>
</template>
</van-cell>
<van-cell title="绑定手机" is-link @click="showBindMobileDialog = true">
<template #icon>
<i class="iconfont icon-mobile menu-icon"></i>
</template>
</van-cell>
<van-cell title="修改密码" is-link @click="showPasswordDialog = true">
<template #icon>
<i class="iconfont icon-password menu-icon"></i>
</template>
</van-cell>
<van-cell
title="消费记录"
icon="notes-o"
is-link
@click="router.push('/mobile/power-log')"
>
<template #icon>
<i class="iconfont icon-log menu-icon"></i>
</template>
</van-cell>
</van-cell-group>
</div>
<!-- 退出登录 -->
<div class="logout-section" v-if="isLogin">
<van-button size="large" block type="danger" plain @click="showLogoutConfirm = true">
退出登录
</van-button>
</div>
</div>
<!-- 修改密码弹窗 -->
<van-dialog
v-model:show="showPasswordDialog"
title="修改密码"
show-cancel-button
@confirm="updatePass"
@cancel="resetPasswordForm"
>
<van-form ref="passwordForm" @submit="updatePass">
<van-cell-group inset>
<van-field
v-model="pass.old"
type="password"
label="旧密码"
placeholder="请输入旧密码"
required
:rules="[{ required: true, message: '请输入旧密码' }]"
/>
<van-field
v-model="pass.new"
type="password"
label="新密码"
placeholder="请输入新密码"
required
:rules="passwordRules"
/>
<van-field
v-model="pass.renew"
type="password"
label="确认密码"
placeholder="请再次输入新密码"
required
:rules="[
{ required: true, message: '请再次输入新密码' },
{ validator: validateConfirmPassword },
]"
/>
</van-cell-group>
</van-form>
</van-dialog>
<!-- 设置弹窗 -->
<van-action-sheet v-model:show="showSettings" title="设置">
<div class="settings-content">
<van-cell-group>
<van-cell title="暗黑主题">
<template #right-icon>
<van-switch
v-model="dark"
@change="(val) => store.setTheme(val ? 'dark' : 'light')"
/>
</template>
</van-cell>
<van-cell title="流式输出">
<template #right-icon>
<van-switch v-model="stream" @change="(val) => store.setChatStream(val)" />
</template>
</van-cell>
</van-cell-group>
</div>
</van-action-sheet>
<!-- 绑定邮箱弹窗 -->
<van-dialog
v-model:show="showBindEmailDialog"
title="绑定邮箱"
:show-cancel-button="false"
:show-confirm-button="false"
width="90%"
:close-on-click-overlay="true"
>
<div class="p-4">
<bind-email @hide="showBindEmailDialog = false" />
</div>
</van-dialog>
<!-- 绑定手机弹窗 -->
<van-dialog
v-model:show="showBindMobileDialog"
title="绑定手机"
:show-cancel-button="false"
:show-confirm-button="false"
width="90%"
:close-on-click-overlay="true"
>
<div class="p-4">
<bind-mobile @hide="showBindMobileDialog = false" />
</div>
</van-dialog>
<!-- 退出登录确认 -->
<van-dialog
v-model:show="showLogoutConfirm"
title="退出登录"
message="确定要退出登录吗?"
show-cancel-button
@confirm="logout"
/>
</div>
</template>
<script setup>
import BindEmail from '@/components/BindEmail.vue'
import BindMobile from '@/components/BindMobile.vue'
import { checkSession, getSystemInfo } from '@/store/cache'
import { removeUserToken } from '@/store/session'
import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http'
import { showLoginDialog } from '@/utils/libs'
import { showFailToast, showLoadingToast, showSuccessToast } from 'vant'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const form = ref({
id: 0,
username: 'GeekMaster',
nickname: '极客学长@001',
mobile: '1300000000',
avatar: '',
power: 0,
expired_time: 0,
})
const fileList = ref([
{
url: '/images/avatar/default.jpg',
message: '上传中...',
},
])
const router = useRouter()
const isLogin = ref(false)
const showSettings = ref(false)
const showPasswordDialog = ref(false)
const showBindEmailDialog = ref(false)
const showBindMobileDialog = ref(false)
const showLogoutConfirm = ref(false)
const store = useSharedStore()
const stream = ref(store.chatStream)
const dark = ref(store.theme === 'dark')
const title = ref(import.meta.env.VITE_TITLE)
const appVersion = ref('2.1.0')
// 密码相关
const pass = ref({
old: '',
new: '',
renew: '',
})
// 密码验证规则
const passwordRules = [
{ required: true, message: '请输入新密码' },
{ min: 8, max: 16, message: '密码长度为8-16个字符' },
]
// 计算属性
const isVip = computed(() => {
const now = Date.now()
const expiredTime = form.value.expired_time ? form.value.expired_time * 1000 : 0
return expiredTime > now
})
onMounted(() => {
getSystemInfo()
.then((res) => {
title.value = res.data.title
})
.catch((e) => {
console.error('获取系统配置失败:', e.message)
})
checkSession()
.then((user) => {
isLogin.value = true
form.value = { ...form.value, ...user }
fileList.value[0].url = user.avatar || '/images/avatar/default.jpg'
// 获取用户详细信息
fetchUserProfile()
})
.catch(() => {
isLogin.value = false
})
})
// 获取用户详细信息
const fetchUserProfile = () => {
httpGet('/api/user/profile')
.then((res) => {
form.value = { ...form.value, ...res.data }
fileList.value[0].url = res.data.avatar || '/images/avatar/default.jpg'
})
.catch((e) => {
console.error('获取用户信息失败:', e.message)
})
}
// 确认密码验证
const validateConfirmPassword = (value) => {
if (value !== pass.value.new) {
return Promise.reject(new Error('两次输入的密码不一致'))
}
return Promise.resolve()
}
// 重置密码表单
const resetPasswordForm = () => {
pass.value = {
old: '',
new: '',
renew: '',
}
}
// 提交修改密码
const updatePass = () => {
if (!pass.value.old) {
return showFailToast('请输入旧密码')
}
if (!pass.value.new || pass.value.new.length < 8) {
return showFailToast('密码长度为8-16个字符')
}
if (pass.value.renew !== pass.value.new) {
return showFailToast('两次输入密码不一致')
}
showLoadingToast({
message: '正在修改密码...',
forbidClick: true,
})
httpPost('/api/user/password', {
old_pass: pass.value.old,
password: pass.value.new,
repass: pass.value.renew,
})
.then(() => {
showSuccessToast('密码修改成功!')
showPasswordDialog.value = false
resetPasswordForm()
})
.catch((e) => {
showFailToast('密码修改失败:' + e.message)
})
}
// 退出登录
const logout = function () {
showLoadingToast({
message: '正在退出...',
forbidClick: true,
})
httpGet('/api/user/logout')
.then(() => {
removeUserToken()
store.setIsLogin(false)
isLogin.value = false
showSuccessToast('退出登录成功')
showLogoutConfirm.value = false
// 清除用户数据
form.value = {
id: 0,
username: '',
nickname: '',
mobile: '',
avatar: '',
power: 0,
expired_time: 0,
}
fileList.value[0].url = '/images/avatar/default.jpg'
})
.catch((e) => {
showFailToast('退出登录失败:' + e.message)
})
}
</script>
<style lang="scss" scoped>
.profile-page {
min-height: calc(100vh - 60px);
background: var(--van-background);
.profile-header {
position: relative;
height: 240px;
overflow: hidden;
background: linear-gradient(135deg, var(--van-primary-color), #8b5cf6);
.header-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('/images/profile-bg.png') center/cover;
opacity: 0.3;
}
.header-content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
padding: 20px;
color: white;
.user-info {
text-align: center;
.avatar-container {
position: relative;
display: inline-block;
margin-bottom: 16px;
}
.user-details {
.username {
font-size: 22px;
font-weight: 700;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.user-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
.user-id {
font-size: 12px;
opacity: 0.8;
}
}
}
}
.login-prompt {
text-align: center;
}
}
}
.profile-content {
margin-top: 20px;
z-index: 3;
padding: 0 16px 20px;
.quick-actions,
.menu-section {
margin-bottom: 24px;
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
margin: 0 0 16px 4px;
}
}
.quick-actions {
.action-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
background: var(--van-cell-background);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
.action-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
&.recharge {
background: linear-gradient(135deg, #ffd700, #ffb300);
}
&.share {
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
}
&.settings {
background: linear-gradient(135deg, #6b7280, #4b5563);
}
.iconfont {
font-size: 18px;
color: white;
}
}
.action-label {
font-size: 12px;
color: var(--van-text-color);
text-align: center;
}
}
}
.menu-section {
:deep(.van-cell-group) {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
margin-bottom: 12px;
.van-cell {
padding: 16px;
.menu-icon {
font-size: 18px;
margin-right: 12px;
color: var(--van-primary-color);
}
.van-cell__title {
font-size: 15px;
font-weight: 500;
}
}
}
}
.logout-section {
margin-bottom: 24px;
}
}
// 弹窗样式
.settings-content {
padding: 16px;
:deep(.van-cell-group) {
.van-cell {
padding: 16px;
.van-cell__title {
font-size: 15px;
font-weight: 500;
}
}
}
}
}
// 深色主题优化
:deep(.van-theme-dark) {
.profile-page {
.action-item {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.van-cell-group {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
}
}
// 响应式优化
@media (max-width: 375px) {
.profile-page {
.profile-header {
height: 220px;
.header-content .user-info .username {
font-size: 20px;
}
}
.profile-content {
padding: 0 12px 20px;
.quick-actions .action-item {
padding: 12px 6px;
.action-icon {
width: 36px;
height: 36px;
.iconfont {
font-size: 16px;
}
}
}
}
}
}
</style>