feat(用户中心): 重构用户个人中心页面并添加HTTP常量

重构用户个人中心页面,新增HTTP请求方法常量模块
- 创建constants/http.ts定义HTTP方法常量
- 在API模块中使用HTTP常量替代字符串
- 重写用户中心页面,增加编辑资料和账号安全功能
- 添加头像上传、密码修改、邮箱绑定等功能
This commit is contained in:
2026-01-18 16:59:15 +08:00
parent 13b320ca93
commit 783ea21d55
6 changed files with 856 additions and 187 deletions

View File

@@ -1,5 +1,6 @@
import request from "@/plugins/axios";
import type { ApiResponse } from "../response";
import { HttpMethod } from "@/constants";
/**
* 认证服务 API 类型定义
@@ -21,6 +22,7 @@ export interface LoginResponse {
accessTokenExpireTime: number | null;
refreshTokenExpireTime: number | null;
}
/**
* 注册请求参数
*/
@@ -29,10 +31,12 @@ export interface RegisterRequest {
userPassword: string;
checkPassword: string;
}
/**
* 注册响应数据
*/
export type RegisterResponse = string;
// 刷新令牌响应数据
export interface RefreshTokenResponse {
id: number | null;
@@ -56,7 +60,11 @@ export interface RefreshTokenResponse {
export const login = (
data: LoginRequest
): Promise<ApiResponse<LoginResponse>> => {
return request.post("/v1/auth/login", data);
return request({
url: "/v1/auth/login",
method: HttpMethod.POST,
data,
});
};
/**
@@ -67,8 +75,13 @@ export const login = (
export const register = (
data: RegisterRequest
): Promise<ApiResponse<RegisterResponse>> => {
return request.post("/v1/user/register", data);
return request({
url: "/v1/user/register",
method: HttpMethod.POST,
data,
});
};
/**
* 令牌刷新
* @param refreshToken 刷新令牌
@@ -77,7 +90,9 @@ export const register = (
export const refreshToken = (
refreshToken: string
): Promise<ApiResponse<RefreshTokenResponse>> => {
return request.post("/v1/auth/refresh", null, {
return request({
url: "/v1/auth/refresh",
method: HttpMethod.POST,
params: {
refreshToken,
},
@@ -92,7 +107,11 @@ export const refreshToken = (
export const getAccessToken = (
data: LoginRequest
): Promise<ApiResponse<string>> => {
return request.post("/v1/auth/auth", data);
return request({
url: "/v1/auth/auth",
method: HttpMethod.POST,
data,
});
};
/**
@@ -100,5 +119,8 @@ export const getAccessToken = (
* @returns 验证结果
*/
export const validateToken = (): Promise<ApiResponse<boolean>> => {
return request.post("/v1/auth/validate");
return request({
url: "/v1/auth/validate",
method: HttpMethod.POST,
});
};

View File

@@ -1,96 +1,190 @@
import request from "@/plugins/axios";
import type { ApiResponse } from "../response";
import { HttpMethod } from "@/constants";
/**
*
*/
/**
* 用户信息
*/
* 用户信息
*/
export interface UserInfo {
/**
* id
*/
id: number;
/**
* id
*/
id: number;
/**
* 用户账号
*/
userAccount: string;
/**
* 用户账号
*/
userAccount: string;
/**
* 开放平台id
*/
unionId?: string;
/**
* 开放平台id
*/
unionId?: string;
/**
* 公众号openId
*/
mpOpenId?: string;
/**
* 公众号openId
*/
mpOpenId?: string;
/**
* 用户昵称
*/
userName: string;
/**
* 用户昵称
*/
userName: string;
/**
* 用户头像 是一个文件id需要去文件服务查询文件详情拿到文件url和当前前端url + “/file/” 拼接起来,才能访问到文件
*/
userAvatar?: string;
/**
* 用户头像 是一个文件id需要去文件服务查询文件详情拿到文件url和当前前端url + “/file/” 拼接起来,才能访问到文件
*/
userAvatar?: string;
/**
* 用户邮箱
*/
userEmail?: string;
/**
* 用户邮箱
*/
userEmail?: string;
/**
* 用户邮箱是否验证
*/
userEmailVerified?: boolean;
/**
* 用户邮箱是否验证
*/
userEmailVerified?: boolean;
/**
* 用户简介
*/
userProfile?: string;
/**
* 用户简介
*/
userProfile?: string;
/**
* 用户角色user/admin/ban
*/
userRole: "user" | "admin" | "ban";
/**
* 用户角色user/admin/ban
*/
userRole: 'user' | 'admin' | 'ban';
/**
* 创建时间
*/
createTime: string;
/**
* 创建时间
*/
createTime: string;
/**
* 更新时间
*/
updateTime: string;
/**
* 更新时间
*/
updateTime: string;
}
/**
* 获取当前登录用户信息
* @returns 当前登录用户信息
*/
export const getUserInfoByToken = (): Promise<ApiResponse<UserInfo>> => {
return request({
url: "/v1/auth/getUserInfo",
method: "GET",
})
}
return request({
url: "/v1/auth/getUserInfo",
method: HttpMethod.GET,
});
};
/**
* 更新用户头像
* @param fileId 文件id
* @returns
*/
export const updateUserAvatar = (fileId: string): Promise<ApiResponse<void>> => {
return request({
url: "/v1/user/avatar",
method: "PUT",
data: {
fileId: fileId,
}
})
export const updateUserAvatar = (
fileId: string,
): Promise<ApiResponse<void>> => {
return request({
url: "/v1/user/avatar",
method: HttpMethod.PUT,
data: {
fileId: fileId,
},
});
};
/**
* 发送邮箱验证码
* @param email 邮箱
* @returns
*/
export const sendEmailVerifyCode = (
email: string,
): Promise<ApiResponse<void>> => {
return request({
url: "/v1/user/email/send-code",
method: HttpMethod.GET,
params: {
email: email,
},
});
};
/**
* 绑定邮箱
* @param email 邮箱
* @param verifyCode 验证码
* @returns
*/
export const bindEmail = (
email: string,
verifyCode: string,
): Promise<ApiResponse<void>> => {
return request({
url: "/v1/user/email/bind",
method: HttpMethod.POST,
data: {
email: email,
verifyCode: verifyCode,
},
});
};
/**
* 解绑邮箱
* @param email 邮箱
* @returns
*/
export const unbindEmail = (email: string): Promise<ApiResponse<void>> => {
return request({
url: "/v1/user/email/unbind",
method: HttpMethod.POST,
data: {
email: email,
},
});
};
/**
* 修改用户密码
* @param oldPassword 旧密码
* @param newPassword 新密码
* @returns
*/
export const updateUserPassword = (
oldPassword: string,
newPassword: string,
): Promise<ApiResponse<void>> => {
return request({
url: "/v1/user/password",
method: HttpMethod.PUT,
data: {
oldPassword: oldPassword,
newPassword: newPassword,
},
});
};
// 更新请求体对象
export interface UpdateUserProfileRequest {
userName?: string;
userProfile?: string;
}
/**
* 更新用户个人信息
* @param userProfile 更新用户个人信息请求体
* @returns
*/
export const updateUserProfile = (
userProfile: UpdateUserProfileRequest,
): Promise<ApiResponse<void>> => {
return request({
url: "/v1/user/profile",
method: HttpMethod.PUT,
data: userProfile,
});
};

View File

@@ -1,12 +1,14 @@
import request from "@/plugins/axios";
import type { ApiResponse } from "../response";
import { HttpMethod } from "@/constants";
/**
* 分页查询文件列表
*/
export const getFileList = async(current: number,size: number) =>{
return request({
url: "/v1/file/page",
method: "GET",
method: HttpMethod.GET,
params: {
current,
size,
@@ -22,9 +24,10 @@ export const getFileList = async(current: number,size: number) =>{
export const getFileById = async (id: string): Promise<ApiResponse<FileListItem>> => {
return request({
url: "/v1/file/" + id,
method: "GET",
method: HttpMethod.GET,
});
}
/**
* 检查文件是否已经上传,本地计算文件哈希
* @param hash
@@ -33,7 +36,7 @@ export const getFileById = async (id: string): Promise<ApiResponse<FileListItem>
export const checkHash = async (hash: string) => {
return request({
url: "/v1/file/check",
method: "GET",
method: HttpMethod.GET,
params: {
hash,
},
@@ -51,7 +54,7 @@ export const uploadFile = async (file: File, hash: string) : Promise<ApiResponse
formData.append("hash", hash);
return request({
url: "/v1/file/upload",
method: "POST",
method: HttpMethod.POST,
data: formData,
});
}
@@ -64,7 +67,7 @@ export const uploadFile = async (file: File, hash: string) : Promise<ApiResponse
export const deleteFile = async (id: number) => {
return request({
url: "/v1/file/" + id,
method: "DELETE",
method: HttpMethod.DELETE,
});
}

15
src/constants/http.ts Normal file
View File

@@ -0,0 +1,15 @@
/**
* HTTP 请求方法常量
*/
export const HttpMethod = {
GET: 'GET',
POST: 'POST',
PUT: 'PUT',
DELETE: 'DELETE',
PATCH: 'PATCH',
HEAD: 'HEAD',
OPTIONS: 'OPTIONS',
} as const;
// 导出类型以供使用
export type HttpMethod = (typeof HttpMethod)[keyof typeof HttpMethod];

1
src/constants/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './http';

View File

@@ -1,129 +1,398 @@
<template>
<div id="userProfileView">
<a-card title="个人中心" :bordered="false" class="profile-card">
<a-space direction="vertical" size="large" fill>
<div class="avatar-section">
<a-upload
action="/"
:custom-request="customUpload"
:show-file-list="false"
:auto-upload="true"
accept="image/*"
>
<template #upload-button>
<div class="avatar-wrapper">
<a-avatar :size="100" trigger-type="mask">
<img
v-if="loginUser.userAvatar"
:src="avatarUrl"
alt="avatar"
/>
<span v-else>{{
loginUser.userName?.charAt(0)?.toUpperCase()
}}</span>
<template #trigger-icon>
<icon-camera />
</template>
</a-avatar>
</div>
</template>
</a-upload>
<div class="user-name">{{ loginUser.userName }}</div>
<div class="user-role">
<a-tag color="arcoblue">{{ loginUser.userRole }}</a-tag>
<a-row :gutter="24">
<!-- 左侧个人信息展示 -->
<a-col :span="8" :xs="24" :sm="24" :md="8">
<a-card title="个人名片" :bordered="false" class="profile-card">
<div class="profile-left">
<a-upload
action="/"
:custom-request="customUpload"
:show-file-list="false"
:auto-upload="true"
accept="image/*"
>
<template #upload-button>
<div class="avatar-wrapper">
<a-avatar :size="120" trigger-type="mask">
<img
v-if="avatarUrl"
:src="avatarUrl"
alt="avatar"
/>
<span v-else class="avatar-text">{{
loginUser.userName?.charAt(0)?.toUpperCase()
}}</span>
<template #trigger-icon>
<icon-camera />
</template>
</a-avatar>
<div class="avatar-hint">点击更换头像</div>
</div>
</template>
</a-upload>
<div class="user-name">{{ loginUser.userName }}</div>
<div class="user-role">
<a-tag :color="roleColor">{{ roleText }}</a-tag>
</div>
<a-divider />
<a-descriptions :data="staticData" :column="1" size="medium" />
</div>
</div>
</a-card>
</a-col>
<a-descriptions
:data="data"
size="large"
title="详细信息"
:column="1"
bordered
/>
</a-space>
</a-card>
<!-- 右侧编辑表单和账号安全 -->
<a-col :span="16" :xs="24" :sm="24" :md="16">
<a-space direction="vertical" size="large" fill>
<!-- 编辑资料卡片 -->
<a-card title="编辑资料" :bordered="false" class="edit-card">
<a-form
:model="formData"
:label-col-props="{ span: 4 }"
:wrapper-col-props="{ span: 20 }"
layout="horizontal"
@submit="handleSubmit"
>
<a-form-item label="昵称" field="userName">
<a-input
v-model="formData.userName"
placeholder="请输入昵称"
max-length="20"
show-word-limit
/>
</a-form-item>
<a-form-item label="个人简介" field="userProfile">
<a-textarea
v-model="formData.userProfile"
placeholder="介绍一下自己..."
:max-length="200"
show-word-limit
:auto-size="{ minRows: 4, maxRows: 8 }"
/>
</a-form-item>
<a-form-item :wrapper-col-props="{ offset: 4, span: 20 }">
<a-space>
<a-button type="primary" html-type="submit" :loading="loading">
保存修改
</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<!-- 账号安全卡片 -->
<a-card title="账号安全" :bordered="false" class="security-card">
<div class="security-list">
<!-- 修改密码 -->
<div class="security-item">
<div class="security-item-left">
<icon-lock class="security-icon" />
<span class="security-label">登录密码</span>
</div>
<span class="security-status">已设置</span>
<a class="security-action" @click="showPasswordModal = true">修改密码</a>
</div>
<!-- 绑定手机 -->
<div class="security-item">
<div class="security-item-left">
<icon-phone class="security-icon" />
<span class="security-label">手机号</span>
</div>
<span class="security-status">
{{ loginUser.userPhone ? maskPhone(loginUser.userPhone) : '未绑定' }}
</span>
<a class="security-action" @click="showPhoneModal = true">
{{ loginUser.userPhone ? '更换手机' : '绑定手机' }}
</a>
</div>
<!-- 绑定邮箱 -->
<div class="security-item">
<div class="security-item-left">
<icon-email class="security-icon" />
<span class="security-label">邮箱</span>
</div>
<span class="security-status">
{{ loginUser.userEmail ? maskEmail(loginUser.userEmail) : '未绑定' }}
</span>
<a class="security-action" @click="showEmailModal = true">
{{ loginUser.userEmail ? '更换邮箱' : '绑定邮箱' }}
</a>
</div>
<!-- 微信绑定 -->
<div class="security-item">
<div class="security-item-left">
<icon-wechat class="security-icon wechat-color" />
<span class="security-label">微信</span>
</div>
<span class="security-status">
{{ loginUser.mpOpenId ? '已授权绑定微信账号' : '未绑定' }}
</span>
<a
v-if="!loginUser.mpOpenId"
class="security-action"
@click="handleBindWechat"
>
绑定
</a>
<a v-else class="security-action" @click="handleUnbindWechat">解除绑定</a>
</div>
</div>
</a-card>
</a-space>
</a-col>
</a-row>
<!-- 修改密码弹窗 -->
<a-modal
v-model:visible="showPasswordModal"
title="修改密码"
@ok="handlePasswordSubmit"
@cancel="handlePasswordReset"
:confirm-loading="passwordLoading"
>
<a-form :model="passwordForm" layout="vertical">
<a-form-item label="当前密码" field="oldPassword">
<a-input-password
v-model="passwordForm.oldPassword"
placeholder="请输入当前密码"
max-length="20"
/>
</a-form-item>
<a-form-item label="新密码" field="newPassword">
<a-input-password
v-model="passwordForm.newPassword"
placeholder="请输入新密码6-20位"
max-length="20"
/>
</a-form-item>
<a-form-item label="确认密码" field="confirmPassword">
<a-input-password
v-model="passwordForm.confirmPassword"
placeholder="请再次输入新密码"
max-length="20"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 绑定手机弹窗 -->
<a-modal
v-model:visible="showPhoneModal"
:title="loginUser.userPhone ? '更换手机号' : '绑定手机号'"
@ok="handlePhoneSubmit"
@cancel="handlePhoneReset"
:confirm-loading="phoneLoading"
>
<a-form :model="phoneForm" layout="vertical">
<a-form-item label="手机号" field="phone">
<a-input
v-model="phoneForm.phone"
placeholder="请输入手机号"
max-length="11"
/>
</a-form-item>
<a-form-item label="验证码" field="code">
<a-input-search
v-model="phoneForm.code"
placeholder="请输入验证码"
button-text="发送验证码"
@search="handleSendPhoneCode"
:loading="codeSending"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 绑定邮箱弹窗 -->
<a-modal
v-model:visible="showEmailModal"
:title="loginUser.userEmail ? '更换邮箱' : '绑定邮箱'"
@ok="handleEmailSubmit"
@cancel="handleEmailReset"
:confirm-loading="emailLoading"
>
<a-form :model="emailForm" layout="vertical">
<a-form-item label="邮箱地址" field="email">
<a-input
v-model="emailForm.email"
placeholder="请输入邮箱地址"
type="email"
/>
</a-form-item>
<a-form-item label="验证码" field="code">
<a-input-search
v-model="emailForm.code"
placeholder="请输入验证码"
button-text="发送验证码"
@search="handleSendEmailCode"
:loading="codeSending"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { ref, computed, onMounted, watch, reactive } from 'vue';
import { useUserStore } from '@/store/user';
import { uploadFile, getFileById } from '@/api/file/file';
import { updateUserAvatar } from '@/api/auth/user';
import { isApiSuccess } from '@/api/response';
import { Message } from '@arco-design/web-vue';
import { IconCamera } from '@arco-design/web-vue/es/icon';
import {
IconCamera,
IconLock,
IconPhone,
IconEmail,
IconWechat
} from '@arco-design/web-vue/es/icon';
import * as SparkMD5 from 'spark-md5';
import type { RequestOption } from '@arco-design/web-vue/es/upload/interfaces';
const userStore = useUserStore();
const loginUser = computed(() => userStore.loginUser);
const avatarUrl = ref('');
const loading = ref(false);
const passwordLoading = ref(false);
const phoneLoading = ref(false);
const emailLoading = ref(false);
const codeSending = ref(false);
// 弹窗显示状态
const showPasswordModal = ref(false);
const showPhoneModal = ref(false);
const showEmailModal = ref(false);
// 表单数据
const formData = reactive({
userName: '',
userProfile: '',
});
// 修改密码表单
const passwordForm = reactive({
oldPassword: '',
newPassword: '',
confirmPassword: '',
});
// 手机号表单
const phoneForm = reactive({
phone: '',
code: '',
});
// 邮箱表单
const emailForm = reactive({
email: '',
code: '',
});
// 角色颜色
const roleColor = computed(() => {
const role = loginUser.value.userRole;
if (role === 'admin') return 'red';
if (role === 'user') return 'arcoblue';
return 'gray';
});
// 角色文字
const roleText = computed(() => {
const role = loginUser.value.userRole;
if (role === 'admin') return '管理员';
if (role === 'user') return '普通用户';
if (role === 'ban') return '被封禁';
return '未知';
});
// 静态数据(不可编辑)
const staticData = computed(() => [
{ label: '用户ID', value: loginUser.value.id?.toString() || '-' },
{ label: '账号', value: loginUser.value.userAccount || '-' },
{ label: '注册时间', value: loginUser.value.createTime || '-' },
]);
// 手机号脱敏
const maskPhone = (phone: string) => {
if (!phone) return '';
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
};
// 邮箱脱敏
const maskEmail = (email: string) => {
if (!email) return '';
const parts = email.split('@');
if (parts.length !== 2) return email;
const [name, domain] = parts;
if (name.length <= 2) return email;
return `${name.slice(0, 2)}***@${domain}`;
};
// 加载用户头像
const loadAvatarUrl = async () => {
if (!loginUser.value.userAvatar) {
avatarUrl.value = '';
return;
}
try {
const res = await getFileById(loginUser.value.userAvatar);
if (isApiSuccess(res) && res.data && res.data.storagePath) {
// 拼接 /file/ 前缀,通过 vite 代理访问后端静态资源
avatarUrl.value = `/api/file/${res.data.storagePath}`;
}
} catch (error) {
console.error("加载头像失败", error);
if (!loginUser.value.userAvatar) {
avatarUrl.value = '';
return;
}
try {
const res = await getFileById(loginUser.value.userAvatar);
if (isApiSuccess(res) && res.data && res.data.storagePath) {
avatarUrl.value = `/api/file/${res.data.storagePath}`;
}
} catch (error) {
console.error('加载头像失败', error);
}
};
// 初始化表单数据
const initFormData = () => {
formData.userName = loginUser.value.userName || '';
formData.userProfile = loginUser.value.userProfile || '';
};
// 监听用户头像变化
watch(() => loginUser.value.userAvatar, () => {
loadAvatarUrl();
loadAvatarUrl();
}, { immediate: true });
const data = computed(() => [
{ label: "用户ID", value: loginUser.value.id?.toString() || "-" },
{ label: "账号", value: loginUser.value.userAccount || "-" },
{ label: "昵称", value: loginUser.value.userName || "-" },
{ label: "邮箱", value: loginUser.value.userEmail || "-" },
{ label: "简介", value: loginUser.value.userProfile || "暂无简介" },
{ label: "注册时间", value: loginUser.value.createTime || "-" },
]);
// 监听用户信息变化,更新表单
watch(() => loginUser.value, () => {
initFormData();
}, { immediate: true, deep: true });
// 头像上传
const customUpload = async (option: RequestOption) => {
const { fileItem, onSuccess, onError } = option;
if (!fileItem.file) return;
try {
// Calculate hash
const hash = await computeFileHash(fileItem.file);
// Upload
const res = await uploadFile(fileItem.file, hash);
if (isApiSuccess(res) && res.data) {
// Update Avatar
const fileId: string = String(res.data.id);
console.log(fileId);
if (!fileId) {
throw new Error("上传返回数据缺少文件ID");
throw new Error('上传返回数据缺少文件ID');
}
// 确保是字符串
const updateRes = await updateUserAvatar(fileId);
if (isApiSuccess(updateRes)) {
Message.success("头像更新成功");
await userStore.getLoginUser(); // Refresh
Message.success('头像更新成功');
await userStore.getLoginUser();
onSuccess(res);
} else {
throw new Error(updateRes.message || "更新头像失败");
throw new Error(updateRes.message || '更新头像失败');
}
} else {
throw new Error(res.message || "上传失败");
throw new Error(res.message || '上传失败');
}
} catch (err: any) {
Message.error("上传出错: " + err.message);
Message.error('上传出错: ' + err.message);
onError(err);
}
};
@@ -138,46 +407,311 @@ const computeFileHash = (file: File): Promise<string> => {
spark.append(e.target.result as ArrayBuffer);
resolve(spark.end());
} else {
reject(new Error("File read failed"));
reject(new Error('File read failed'));
}
};
fileReader.onerror = () => reject(new Error("File read failed"));
fileReader.onerror = () => reject(new Error('File read failed'));
fileReader.readAsArrayBuffer(file);
});
};
// 提交资料表单
const handleSubmit = async () => {
// TODO: 等后端接口提供后对接
Message.info('更新用户信息接口开发中...');
console.log('提交数据:', formData);
};
// 重置资料表单
const handleReset = () => {
initFormData();
Message.info('已重置');
};
// 修改密码
const handlePasswordSubmit = async () => {
if (!passwordForm.oldPassword) {
Message.warning('请输入当前密码');
return;
}
if (!passwordForm.newPassword) {
Message.warning('请输入新密码');
return;
}
if (passwordForm.newPassword.length < 6 || passwordForm.newPassword.length > 20) {
Message.warning('新密码长度应为6-20位');
return;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
Message.warning('两次输入的密码不一致');
return;
}
if (passwordForm.oldPassword === passwordForm.newPassword) {
Message.warning('新密码不能与当前密码相同');
return;
}
// TODO: 等后端接口提供后对接
Message.info('修改密码接口开发中...');
console.log('修改密码:', {
oldPassword: passwordForm.oldPassword,
newPassword: passwordForm.newPassword,
});
};
// 重置密码表单
const handlePasswordReset = () => {
passwordForm.oldPassword = '';
passwordForm.newPassword = '';
passwordForm.confirmPassword = '';
};
// 绑定手机号
const handlePhoneSubmit = async () => {
if (!phoneForm.phone) {
Message.warning('请输入手机号');
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
Message.warning('请输入正确的手机号');
return;
}
if (!phoneForm.code) {
Message.warning('请输入验证码');
return;
}
// TODO: 等后端接口提供后对接
Message.info('绑定手机号接口开发中...');
console.log('绑定手机号:', phoneForm);
};
// 发送手机验证码
const handleSendPhoneCode = async () => {
if (!phoneForm.phone) {
Message.warning('请先输入手机号');
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
Message.warning('请输入正确的手机号');
return;
}
// TODO: 等后端接口提供后对接
Message.info('发送验证码接口开发中...');
};
// 重置手机表单
const handlePhoneReset = () => {
phoneForm.phone = '';
phoneForm.code = '';
};
// 绑定邮箱
const handleEmailSubmit = async () => {
if (!emailForm.email) {
Message.warning('请输入邮箱');
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailForm.email)) {
Message.warning('请输入正确的邮箱地址');
return;
}
if (!emailForm.code) {
Message.warning('请输入验证码');
return;
}
// TODO: 等后端接口提供后对接
Message.info('绑定邮箱接口开发中...');
console.log('绑定邮箱:', emailForm);
};
// 发送邮箱验证码
const handleSendEmailCode = async () => {
if (!emailForm.email) {
Message.warning('请先输入邮箱');
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailForm.email)) {
Message.warning('请输入正确的邮箱地址');
return;
}
// TODO: 等后端接口提供后对接
Message.info('发送验证码接口开发中...');
};
// 重置邮箱表单
const handleEmailReset = () => {
emailForm.email = '';
emailForm.code = '';
};
// 绑定微信
const handleBindWechat = () => {
// TODO: 等后端接口提供后对接
Message.info('微信绑定接口开发中...');
};
// 解绑微信
const handleUnbindWechat = () => {
// TODO: 等后端接口提供后对接
Message.info('微信解绑接口开发中...');
};
onMounted(() => {
userStore.getLoginUser();
});
</script>
<style scoped>
<style scoped lang="scss">
#userProfileView {
max-width: 800px;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
padding: 24px;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
.profile-card {
height: 100%;
.profile-left {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
opacity: 0.8;
.avatar-hint {
opacity: 1;
}
}
:deep(img) {
border-radius: 50%;
object-fit: cover;
}
.avatar-text {
font-size: 48px;
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.avatar-hint {
margin-top: 8px;
font-size: 12px;
color: #86909c;
opacity: 0;
transition: opacity 0.3s;
}
}
.user-name {
margin-top: 20px;
font-size: 22px;
font-weight: 600;
color: #1d2129;
}
.user-role {
margin-top: 8px;
}
}
.avatar-wrapper {
cursor: pointer;
position: relative;
transition: all 0.3s;
.edit-card,
.security-card {
:deep(.arco-form-item) {
margin-bottom: 24px;
}
:deep(.arco-input-wrapper),
:deep(.arco-textarea) {
border-radius: 8px;
}
}
.avatar-wrapper:hover {
opacity: 0.8;
.security-card {
.security-list {
padding: 0;
}
.security-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.security-item-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.security-icon {
font-size: 20px;
color: #4e5969;
&.wechat-color {
color: #07c160;
}
}
.security-label {
font-size: 15px;
color: #1d2129;
font-weight: 500;
}
.security-status {
font-size: 14px;
color: #86909c;
margin-right: 24px;
}
.security-action {
font-size: 14px;
color: #165dff;
cursor: pointer;
white-space: nowrap;
&:hover {
color: #4080ff;
text-decoration: underline;
}
}
}
}
.user-name {
font-size: 24px;
font-weight: bold;
margin-top: 10px;
}
.user-role {
margin-top: 5px;
// 响应式设计
@media (max-width: 768px) {
#userProfileView {
padding: 16px;
}
.profile-card,
.edit-card,
.security-card {
margin-bottom: 16px;
}
}
</style>