feat: 完善后端架构和service层实现

- 创建完整的entity实体类体系,包括所有业务实体
- 实现BaseEntity基类,统一管理公共字段
- 创建雪花算法ID生成器和自动填充处理器
- 简化所有mapper接口,只继承BaseMapper
- 重构service层,使用LambdaQueryWrapper进行数据库操作
- 创建BasePageRequest分页查询基类
- 完善用户上下文管理和JWT认证
- 新增WebSocket聊天功能和相关控制器
- 更新前端配置和组件,完善用户认证流程
- 同步数据库建表脚本
This commit is contained in:
2025-07-24 00:37:23 +08:00
parent 645036fcd2
commit 880e0e3c88
87 changed files with 8114 additions and 1106 deletions
+4 -4
View File
@@ -4,10 +4,10 @@
VITE_APP_TITLE=开心APP - 开发环境
VITE_APP_DESCRIPTION=你的情绪陪伴使者
# API配置 - 通过网关访问
VITE_API_BASE_URL=http://localhost:19000
VITE_UPLOAD_URL=http://localhost:19000/api/upload
VITE_WS_URL=http://localhost:19000/ws/chat
# API配置 - 直接访问backend-single
VITE_API_BASE_URL=http://localhost:8080/api
VITE_UPLOAD_URL=http://localhost:8080/api/upload
VITE_WS_URL=http://localhost:8080/ws/chat
# WebSocket配置
VITE_WS_RECONNECT_ATTEMPTS=5
+1 -1
View File
@@ -3,7 +3,7 @@ VITE_APP_TITLE=开心APP
VITE_APP_DESCRIPTION=你的情绪陪伴使者
# API配置 - 生产环境通过网关访问
VITE_API_BASE_URL=http://47.111.10.27:19000
VITE_API_BASE_URL=http://47.111.10.27:19000/api
VITE_UPLOAD_URL=http://47.111.10.27:19000/api/upload
VITE_WS_URL=http://47.111.10.27:19000/ws/chat
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Permissions-Policy" content="unload=()">
<title>开心APP - 你的情绪陪伴使者</title>
<meta name="description" content="开心APP是一款AI情绪陪伴应用,提供智能对话、情绪日记、个人展板等功能,陪伴你的每一个情绪时刻。" />
<meta name="keywords" content="AI助手,情绪陪伴,智能对话,情绪日记,心理健康" />
@@ -28,11 +28,21 @@
<!-- 已登录状态 -->
<template v-else>
<a-dropdown>
<a-button type="text" class="user-btn">
<UserOutlined />
{{ userStore.userInfo?.nickname || userStore.userInfo?.account || '用户' }}
<DownOutlined />
</a-button>
<div class="user-info-section">
<a-avatar
:size="32"
:src="userStore.userInfo?.avatar"
class="user-avatar"
>
<template #icon v-if="!userStore.userInfo?.avatar">
<UserOutlined />
</template>
</a-avatar>
<span class="user-nickname">
{{ userStore.userInfo?.nickname || userStore.userInfo?.username || '用户' }}
</span>
<DownOutlined class="dropdown-icon" />
</div>
<template #overlay>
<a-menu>
<a-menu-item key="profile" @click="$router.push('/dashboard')">
@@ -150,6 +160,39 @@
}
}
.user-info-section {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(74, 144, 226, 0.1);
}
.user-avatar {
border: 2px solid #f0f0f0;
}
.user-nickname {
font-weight: 500;
color: #4A90E2;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown-icon {
font-size: 12px;
color: #8c8c8c;
margin-left: 4px;
}
}
.user-btn {
color: #4A90E2;
font-weight: 500;
@@ -164,4 +207,25 @@
border-radius: 20px;
font-weight: 600;
}
// 响应式设计
@media (max-width: 768px) {
.header-content {
padding: 0 16px;
}
.nav-menu {
display: none;
}
.user-info-section {
.user-nickname {
display: none;
}
}
.header-actions {
gap: 8px;
}
}
</style>
+17
View File
@@ -38,6 +38,15 @@ const routes: RouteRecordRaw[] = [
requiresAuth: false
}
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/Profile/index.vue'),
meta: {
title: '个人中心',
requiresAuth: true
}
},
{
path: '/topic-tracker',
name: 'TopicTracker',
@@ -151,7 +160,15 @@ router.beforeEach(async (to, from, next) => {
const { useUserStore } = await import('@/stores/user')
const userStore = useUserStore()
console.log('路由守卫检查登录状态:', {
path: to.path,
isLoggedIn: userStore.isLoggedIn,
token: !!userStore.token,
userInfo: !!userStore.userInfo
})
if (userStore.isLoggedIn) {
console.log('用户已登录,重定向到首页')
next('/')
return
}
+16 -49
View File
@@ -1,4 +1,4 @@
import axios from 'axios'
import request from '@/utils/request'
import type {
LoginRequest,
LoginResponse,
@@ -12,80 +12,47 @@ import type {
UserInfo
} from '@/types/auth'
// 创建axios实例
const authApi = axios.create({
baseURL: '/api/auth',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
authApi.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
authApi.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
// token过期,清除本地存储并跳转到登录页
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
}
return Promise.reject(error.response?.data || error)
}
)
export const authService = {
// 获取验证码
async getCaptcha(): Promise<CaptchaResponse> {
const response: ApiResponse<CaptchaResponse> = await authApi.get('/captcha')
return response.data
const response = await request.get('/auth/captcha')
return response.data.data
},
// 用户登录
async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
return await authApi.post('/login', data)
const response = await request.post('/auth/login', data)
return response.data
},
// 用户注册
async register(data: RegisterRequest): Promise<ApiResponse<UserInfo>> {
return await authApi.post('/register', data)
async register(data: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
const response = await request.post('/auth/register', data)
return response.data
},
// 刷新token
async refreshToken(data: RefreshTokenRequest): Promise<ApiResponse<LoginResponse>> {
return await authApi.post('/refresh-token', data)
const response = await request.post('/auth/refresh-token', data)
return response.data
},
// 用户登出
async logout(): Promise<ApiResponse<void>> {
return await authApi.post('/logout')
const response = await request.post('/auth/logout')
return response.data
},
// 获取用户信息
async getUserInfo(): Promise<ApiResponse<UserInfo>> {
return await authApi.get('/user-info')
const response = await request.get('/auth/user-info')
return response.data
},
// 修改密码
async changePassword(data: ChangePasswordRequest): Promise<ApiResponse<void>> {
return await authApi.post('/change-password', data)
const response = await request.post('/auth/change-password', data)
return response.data
},
// 忘记密码
+41 -9
View File
@@ -1,5 +1,5 @@
import SockJS from 'sockjs-client'
import { Stomp, Client } from 'stompjs'
import * as Stomp from 'stompjs'
import type { ChatMessage } from '@/types'
// WebSocket消息类型
@@ -17,11 +17,21 @@ export interface WebSocketMessage {
// 聊天请求类型
export interface ChatRequest {
conversationId?: string
content: string
senderId: string
senderType: 'USER' | 'GUEST'
messageType: 'TEXT'
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
messageType: 'TEXT' | 'IMAGE' | 'FILE' | 'SYSTEM' | 'HEARTBEAT'
conversationId?: string
timestamp?: number
}
// 连接请求类型
export interface ConnectRequest {
userId?: string
username?: string
clientType?: string
clientVersion?: string
timestamp?: number
}
// WebSocket连接状态
@@ -37,7 +47,7 @@ export interface WebSocketCallbacks {
}
export class WebSocketService {
private client: Client | null = null
private client: Stomp.Client | null = null
private callbacks: WebSocketCallbacks = {}
private status: ConnectionStatus = 'DISCONNECTED'
private reconnectAttempts = 0
@@ -60,12 +70,18 @@ export class WebSocketService {
this.setStatus('CONNECTING')
// 创建SockJS连接
const socket = new SockJS(this.wsUrl)
const socket = new SockJS(this.wsUrl, null, {
transports: ['websocket', 'xhr-streaming', 'xhr-polling']
})
this.client = Stomp.over(socket)
// 禁用调试日志
this.client.debug = () => {}
// 设置心跳
this.client.heartbeat.outgoing = 20000
this.client.heartbeat.incoming = 20000
// 连接配置
const connectHeaders = {
'X-User-Id': this.userId
@@ -94,7 +110,12 @@ export class WebSocketService {
console.error('WebSocket连接失败:', error)
this.setStatus('ERROR')
this.callbacks.onError?.(error)
// 检查是否是网络错误
if (error && error.type === 'close' && error.code === 1006) {
console.log('WebSocket连接被异常关闭,尝试重连...')
}
// 尝试重连
this.scheduleReconnect()
reject(error)
@@ -133,12 +154,14 @@ export class WebSocketService {
return
}
// 使用新的后端接口格式
const chatRequest: ChatRequest = {
content,
senderId: this.userId!,
senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER',
messageType: 'TEXT',
conversationId: conversationId || this.conversationId || undefined
conversationId: conversationId || this.conversationId || undefined,
timestamp: Date.now()
}
try {
@@ -206,8 +229,17 @@ export class WebSocketService {
private sendConnectMessage(): void {
if (!this.client?.connected) return
const connectRequest: ConnectRequest = {
userId: this.userId!,
username: this.userId!,
clientType: 'web',
clientVersion: '1.0.0',
timestamp: Date.now()
}
try {
this.client.send('/app/chat.connect', {}, JSON.stringify({}))
this.client.send('/app/chat.connect', {}, JSON.stringify(connectRequest))
console.log('发送连接消息:', connectRequest)
} catch (error) {
console.error('发送连接消息失败:', error)
}
+32 -19
View File
@@ -27,26 +27,40 @@ export const useUserStore = defineStore('user', () => {
}
}
const setUserInfo = (userInfoData: UserInfo | null) => {
userInfo.value = userInfoData
// 存储到localStorage
if (userInfoData) {
localStorage.setItem('userInfo', JSON.stringify(userInfoData))
} else {
localStorage.removeItem('userInfo')
}
}
// 新的登录方法,支持认证服务
const loginWithAuth = async (loginData: LoginRequest) => {
isLoading.value = true
try {
const response = await authService.login(loginData)
if (response.success) {
token.value = response.data.token
userInfo.value = response.data.userInfo
console.log('登录API响应:', response)
// 保存到本地存储
authUtils.setToken(response.data.token)
authUtils.setUserInfo(response.data.userInfo)
// 修复:直接处理后端返回的数据格式 {code: 200, data: {...}}
if (response.code === 200 && response.data) {
// 使用store的方法来设置token和用户信息,确保响应式更新
setToken(response.data.accessToken)
setUserInfo(response.data.userInfo)
return { success: true, data: response.data }
console.log('登录成功,用户信息已保存:', response.data.userInfo)
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
return { code: 200, data: response.data, message: response.message }
} else {
return { success: false, message: response.message }
return { code: response.code || 500, message: response.message || '登录失败' }
}
} catch (error: any) {
return { success: false, message: error.message || '登录失败' }
console.error('登录请求失败:', error)
return { code: 500, message: error.message || '登录失败' }
} finally {
isLoading.value = false
}
@@ -102,24 +116,22 @@ export const useUserStore = defineStore('user', () => {
const initUser = () => {
const savedToken = authUtils.getToken()
const savedUserInfo = authUtils.getUserInfo()
const savedUser = localStorage.getItem('user')
console.log('初始化用户状态:', { savedToken: !!savedToken, savedUserInfo })
if (savedToken) {
setToken(savedToken)
}
if (savedUserInfo) {
userInfo.value = savedUserInfo
setUserInfo(savedUserInfo)
}
if (savedUser) {
try {
setUser(JSON.parse(savedUser))
} catch (error) {
console.error('Failed to parse saved user data:', error)
localStorage.removeItem('user')
}
}
console.log('用户状态初始化完成:', {
token: !!token.value,
userInfo: userInfo.value,
isLoggedIn: isLoggedIn.value
})
}
// 刷新用户信息
@@ -148,6 +160,7 @@ export const useUserStore = defineStore('user', () => {
// 方法
setUser,
setToken,
setUserInfo,
login,
loginWithAuth,
logout,
+2 -1
View File
@@ -32,10 +32,11 @@ export interface UserInfo {
// 登录响应
export interface LoginResponse {
token: string
accessToken: string
refreshToken: string
userInfo: UserInfo
expiresIn: number
loginTime: string
}
// 验证码响应
+113
View File
@@ -0,0 +1,113 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import router from '@/router'
// 获取API基础URL
const getApiBaseUrl = () => {
// 开发环境使用代理
if (import.meta.env.DEV) {
return '/api'
}
// 生产环境使用环境变量
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api'
}
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: getApiBaseUrl(),
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
console.log('API Base URL:', getApiBaseUrl())
// 请求拦截器
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 从localStorage获取token
const token = localStorage.getItem('token')
if (token && config.headers) {
// 在请求头中添加Authorization
config.headers.Authorization = `Bearer ${token}`
}
console.log('发送请求:', {
url: config.url,
method: config.method,
hasToken: !!token,
headers: config.headers
})
return config
},
(error) => {
console.error('请求拦截器错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
console.log('收到响应:', {
url: response.config.url,
status: response.status,
data: response.data
})
return response
},
(error) => {
console.error('响应拦截器错误:', error)
if (error.response) {
const { status, data } = error.response
switch (status) {
case 401:
// token过期或无效
message.error('登录已过期,请重新登录')
// 清除本地存储的用户信息
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
// 清除store中的用户信息
const userStore = useUserStore()
userStore.setToken('')
userStore.setUserInfo(null)
// 跳转到登录页
router.push('/login')
break
case 403:
message.error('没有权限访问该资源')
break
case 404:
message.error('请求的资源不存在')
break
case 500:
message.error('服务器内部错误')
break
default:
message.error(data?.message || '请求失败')
}
} else if (error.request) {
message.error('网络连接失败,请检查网络')
} else {
message.error('请求配置错误')
}
return Promise.reject(error)
}
)
export default request
+35 -3
View File
@@ -94,7 +94,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h } from 'vue'
import { ref, reactive, onMounted, nextTick, h } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
@@ -138,9 +138,12 @@
const getCaptcha = async () => {
try {
const response = await authService.getCaptcha()
console.log('验证码响应:', response)
captchaImage.value = `data:image/png;base64,${response.image}`
captchaKey.value = response.key
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
} catch (error) {
console.error('获取验证码失败:', error)
message.error('获取验证码失败')
}
}
@@ -161,12 +164,41 @@
const result = await userStore.loginWithAuth(loginData)
if (result.success) {
console.log('登录结果:', result)
console.log('用户store状态:', {
token: userStore.token,
userInfo: userStore.userInfo,
isLoggedIn: userStore.isLoggedIn
})
// 修复:检查 result.code === 200 而不是 result.success
if (result.code === 200) {
message.success('登录成功')
// 等待状态更新后再跳转
await nextTick()
// 跳转到首页或之前的页面
const redirect = router.currentRoute.value.query.redirect as string
router.push(redirect || '/')
const targetPath = redirect || '/'
console.log('准备跳转到:', targetPath)
// 延迟一下确保状态完全更新
setTimeout(() => {
try {
// 使用replace而不是push,避免路由守卫问题
router.replace(targetPath).then(() => {
console.log('路由跳转完成')
}).catch((error) => {
console.error('路由跳转失败,使用window.location:', error)
// 如果路由跳转失败,使用window.location作为备选
window.location.href = targetPath
})
} catch (error) {
console.error('路由跳转异常,使用window.location:', error)
window.location.href = targetPath
}
}, 100)
} else {
message.error(result.message || '登录失败')
refreshCaptcha() // 刷新验证码
+562
View File
@@ -0,0 +1,562 @@
<template>
<div class="profile-page">
<!-- 头部 -->
<header class="page-header">
<div class="header-content">
<div class="header-left">
<a-button type="text" @click="$router.back()" class="back-btn">
<ArrowLeftOutlined />
</a-button>
<h1 class="page-title">个人中心</h1>
</div>
<a-button type="text" @click="handleLogout" class="logout-btn">
<LogoutOutlined />
退出登录
</a-button>
</div>
</header>
<!-- 主要内容 -->
<main class="page-main">
<div class="container">
<!-- 用户信息卡片 -->
<a-card class="user-info-card" :loading="loading">
<div class="user-header">
<div class="avatar-section">
<a-avatar :size="80" :src="userInfo?.avatar" class="user-avatar">
<template #icon><UserOutlined /></template>
</a-avatar>
<a-button type="link" size="small" @click="showAvatarModal = true">
更换头像
</a-button>
</div>
<div class="user-details">
<h2 class="username">{{ userInfo?.nickname || userInfo?.username || '未设置昵称' }}</h2>
<p class="user-account">账号{{ userInfo?.account }}</p>
<p class="user-status">
<a-tag :color="userInfo?.status === 'ACTIVE' ? 'green' : 'red'">
{{ userInfo?.status === 'ACTIVE' ? '正常' : '禁用' }}
</a-tag>
</p>
</div>
</div>
</a-card>
<!-- 功能菜单 -->
<div class="menu-section">
<a-card title="账户管理" class="menu-card">
<div class="menu-list">
<div class="menu-item" @click="showEditProfileModal = true">
<EditOutlined class="menu-icon" />
<span class="menu-text">编辑个人信息</span>
<RightOutlined class="menu-arrow" />
</div>
<div class="menu-item" @click="showChangePasswordModal = true">
<LockOutlined class="menu-icon" />
<span class="menu-text">修改密码</span>
<RightOutlined class="menu-arrow" />
</div>
</div>
</a-card>
<a-card title="应用设置" class="menu-card">
<div class="menu-list">
<div class="menu-item" @click="$router.push('/settings')">
<SettingOutlined class="menu-icon" />
<span class="menu-text">系统设置</span>
<RightOutlined class="menu-arrow" />
</div>
<div class="menu-item" @click="showAboutModal = true">
<InfoCircleOutlined class="menu-icon" />
<span class="menu-text">关于应用</span>
<RightOutlined class="menu-arrow" />
</div>
</div>
</a-card>
</div>
<!-- 统计信息 -->
<a-card title="使用统计" class="stats-card">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">{{ stats.loginCount || 0 }}</div>
<div class="stat-label">登录次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.chatCount || 0 }}</div>
<div class="stat-label">聊天次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ stats.diaryCount || 0 }}</div>
<div class="stat-label">日记数量</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ formatDate(userInfo?.createTime) }}</div>
<div class="stat-label">注册时间</div>
</div>
</div>
</a-card>
</div>
</main>
<!-- 编辑个人信息模态框 -->
<a-modal
v-model:open="showEditProfileModal"
title="编辑个人信息"
@ok="handleUpdateProfile"
:confirm-loading="updateLoading"
>
<a-form :model="profileForm" layout="vertical">
<a-form-item label="昵称">
<a-input v-model:value="profileForm.nickname" placeholder="请输入昵称" />
</a-form-item>
<a-form-item label="邮箱">
<a-input v-model:value="profileForm.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="手机号">
<a-input v-model:value="profileForm.phone" placeholder="请输入手机号" />
</a-form-item>
</a-form>
</a-modal>
<!-- 修改密码模态框 -->
<a-modal
v-model:open="showChangePasswordModal"
title="修改密码"
@ok="handleChangePassword"
:confirm-loading="passwordLoading"
>
<a-form :model="passwordForm" layout="vertical">
<a-form-item label="当前密码">
<a-input-password v-model:value="passwordForm.oldPassword" placeholder="请输入当前密码" />
</a-form-item>
<a-form-item label="新密码">
<a-input-password v-model:value="passwordForm.newPassword" placeholder="请输入新密码" />
</a-form-item>
<a-form-item label="确认新密码">
<a-input-password v-model:value="passwordForm.confirmPassword" placeholder="请再次输入新密码" />
</a-form-item>
</a-form>
</a-modal>
<!-- 更换头像模态框 -->
<a-modal
v-model:open="showAvatarModal"
title="更换头像"
@ok="handleUpdateAvatar"
:confirm-loading="avatarLoading"
>
<div class="avatar-upload">
<a-upload
v-model:file-list="avatarFileList"
:before-upload="beforeAvatarUpload"
list-type="picture-card"
:show-upload-list="false"
>
<div v-if="avatarUrl">
<img :src="avatarUrl" alt="avatar" style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<div v-else>
<PlusOutlined />
<div style="margin-top: 8px">上传头像</div>
</div>
</a-upload>
</div>
</a-modal>
<!-- 关于应用模态框 -->
<a-modal
v-model:open="showAboutModal"
title="关于应用"
:footer="null"
>
<div class="about-content">
<div class="app-info">
<h3>情感博物馆</h3>
<p>版本v1.0.0</p>
<p>一个专注于情感记录与分析的智能应用</p>
</div>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import {
ArrowLeftOutlined,
UserOutlined,
EditOutlined,
LockOutlined,
SettingOutlined,
InfoCircleOutlined,
RightOutlined,
LogoutOutlined,
PlusOutlined
} from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
import { authService } from '@/services/auth'
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const updateLoading = ref(false)
const passwordLoading = ref(false)
const avatarLoading = ref(false)
// 模态框显示状态
const showEditProfileModal = ref(false)
const showChangePasswordModal = ref(false)
const showAvatarModal = ref(false)
const showAboutModal = ref(false)
// 表单数据
const profileForm = reactive({
nickname: '',
email: '',
phone: ''
})
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
// 头像相关
const avatarFileList = ref([])
const avatarUrl = ref('')
// 统计数据
const stats = reactive({
loginCount: 0,
chatCount: 0,
diaryCount: 0
})
// 计算属性
const userInfo = computed(() => userStore.userInfo)
// 方法
const formatDate = (dateString: string) => {
if (!dateString) return '未知'
return new Date(dateString).toLocaleDateString()
}
const handleLogout = async () => {
try {
await userStore.logout()
message.success('退出登录成功')
router.push('/login')
} catch (error) {
message.error('退出登录失败')
}
}
const handleUpdateProfile = async () => {
updateLoading.value = true
try {
// TODO: 调用更新个人信息API
message.success('个人信息更新成功')
showEditProfileModal.value = false
} catch (error) {
message.error('更新失败')
} finally {
updateLoading.value = false
}
}
const handleChangePassword = async () => {
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
message.error('两次输入的密码不一致')
return
}
passwordLoading.value = true
try {
// TODO: 调用修改密码API
message.success('密码修改成功')
showChangePasswordModal.value = false
// 清空表单
Object.assign(passwordForm, {
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
} catch (error) {
message.error('密码修改失败')
} finally {
passwordLoading.value = false
}
}
const beforeAvatarUpload = (file: File) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传 JPG/PNG 格式的图片!')
return false
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片大小不能超过 2MB!')
return false
}
// 预览图片
const reader = new FileReader()
reader.onload = (e) => {
avatarUrl.value = e.target?.result as string
}
reader.readAsDataURL(file)
return false // 阻止自动上传
}
const handleUpdateAvatar = async () => {
avatarLoading.value = true
try {
// TODO: 调用上传头像API
message.success('头像更新成功')
showAvatarModal.value = false
} catch (error) {
message.error('头像更新失败')
} finally {
avatarLoading.value = false
}
}
// 初始化数据
const initData = () => {
if (userInfo.value) {
profileForm.nickname = userInfo.value.nickname || ''
profileForm.email = userInfo.value.email || ''
profileForm.phone = userInfo.value.phone || ''
avatarUrl.value = userInfo.value.avatar || ''
}
}
onMounted(() => {
initData()
// TODO: 加载统计数据
})
</script>
<style lang="scss" scoped>
.profile-page {
min-height: 100vh;
background: #f5f5f5;
}
.page-header {
background: white;
border-bottom: 1px solid #e8e8e8;
padding: 0 16px;
position: sticky;
top: 0;
z-index: 100;
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 56px;
max-width: 1200px;
margin: 0 auto;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #262626;
}
.back-btn, .logout-btn {
display: flex;
align-items: center;
gap: 4px;
color: #666;
&:hover {
color: #1890ff;
}
}
}
.page-main {
padding: 24px 16px;
.container {
max-width: 800px;
margin: 0 auto;
}
}
.user-info-card {
margin-bottom: 24px;
.user-header {
display: flex;
gap: 20px;
align-items: flex-start;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
.user-avatar {
border: 2px solid #f0f0f0;
}
}
.user-details {
flex: 1;
.username {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #262626;
}
.user-account {
margin: 0 0 8px 0;
color: #666;
font-size: 14px;
}
.user-status {
margin: 0;
}
}
}
.menu-section {
margin-bottom: 24px;
.menu-card {
margin-bottom: 16px;
.menu-list {
.menu-item {
display: flex;
align-items: center;
padding: 12px 0;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: all 0.2s;
&:last-child {
border-bottom: none;
}
&:hover {
background: #fafafa;
margin: 0 -16px;
padding: 12px 16px;
}
.menu-icon {
width: 20px;
color: #666;
margin-right: 12px;
}
.menu-text {
flex: 1;
color: #262626;
}
.menu-arrow {
color: #bfbfbf;
font-size: 12px;
}
}
}
}
}
.stats-card {
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 16px;
.stat-item {
text-align: center;
padding: 16px;
background: #fafafa;
border-radius: 8px;
.stat-value {
font-size: 24px;
font-weight: 600;
color: #1890ff;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
}
}
}
.avatar-upload {
display: flex;
justify-content: center;
:deep(.ant-upload-select) {
width: 120px !important;
height: 120px !important;
}
}
.about-content {
text-align: center;
padding: 20px;
.app-info {
h3 {
color: #1890ff;
margin-bottom: 16px;
}
p {
margin-bottom: 8px;
color: #666;
}
}
}
// 响应式设计
@media (max-width: 768px) {
.page-main {
padding: 16px 12px;
}
.user-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
}
</style>
+19 -2
View File
@@ -26,6 +26,7 @@
placeholder="请输入手机号或邮箱"
size="large"
:prefix="h(UserOutlined)"
autocomplete="off"
/>
</a-form-item>
@@ -35,6 +36,7 @@
placeholder="请输入密码"
size="large"
:prefix="h(LockOutlined)"
autocomplete="new-password"
/>
</a-form-item>
@@ -44,6 +46,7 @@
placeholder="请再次输入密码"
size="large"
:prefix="h(LockOutlined)"
autocomplete="new-password"
/>
</a-form-item>
@@ -101,9 +104,11 @@
import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { authService } from '@/services/auth'
import { useUserStore } from '@/stores/user'
import type { RegisterRequest } from '@/types/auth'
const router = useRouter()
const userStore = useUserStore()
// 表单数据
const registerForm = reactive<RegisterRequest>({
@@ -150,9 +155,12 @@
const getCaptcha = async () => {
try {
const response = await authService.getCaptcha()
console.log('验证码响应:', response)
captchaImage.value = `data:image/png;base64,${response.image}`
captchaKey.value = response.key
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
} catch (error) {
console.error('获取验证码失败:', error)
message.error('获取验证码失败')
}
}
@@ -174,8 +182,17 @@
const response = await authService.register(registerData)
if (response.success) {
message.success('注册成功,登录')
router.push('/login')
message.success('注册成功,已自动登录')
// 使用userStore的方法保存用户信息和token
userStore.setToken(response.data.accessToken)
userStore.setUserInfo(response.data.userInfo)
console.log('注册成功,用户信息:', response.data.userInfo)
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
// 跳转到首页
router.push('/')
} else {
message.error(response.message || '注册失败')
refreshCaptcha() // 刷新验证码
+1 -1
View File
@@ -4,7 +4,7 @@ import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
base: '/emotion/happy/',
base: '/',
plugins: [vue()],
resolve: {
alias: {