feat(用户中心): 重构用户个人中心页面并添加HTTP常量
重构用户个人中心页面,新增HTTP请求方法常量模块 - 创建constants/http.ts定义HTTP方法常量 - 在API模块中使用HTTP常量替代字符串 - 重写用户中心页面,增加编辑资料和账号安全功能 - 添加头像上传、密码修改、邮箱绑定等功能
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import request from "@/plugins/axios";
|
import request from "@/plugins/axios";
|
||||||
import type { ApiResponse } from "../response";
|
import type { ApiResponse } from "../response";
|
||||||
|
import { HttpMethod } from "@/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证服务 API 类型定义
|
* 认证服务 API 类型定义
|
||||||
@@ -21,6 +22,7 @@ export interface LoginResponse {
|
|||||||
accessTokenExpireTime: number | null;
|
accessTokenExpireTime: number | null;
|
||||||
refreshTokenExpireTime: number | null;
|
refreshTokenExpireTime: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册请求参数
|
* 注册请求参数
|
||||||
*/
|
*/
|
||||||
@@ -29,10 +31,12 @@ export interface RegisterRequest {
|
|||||||
userPassword: string;
|
userPassword: string;
|
||||||
checkPassword: string;
|
checkPassword: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册响应数据
|
* 注册响应数据
|
||||||
*/
|
*/
|
||||||
export type RegisterResponse = string;
|
export type RegisterResponse = string;
|
||||||
|
|
||||||
// 刷新令牌响应数据
|
// 刷新令牌响应数据
|
||||||
export interface RefreshTokenResponse {
|
export interface RefreshTokenResponse {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
@@ -56,7 +60,11 @@ export interface RefreshTokenResponse {
|
|||||||
export const login = (
|
export const login = (
|
||||||
data: LoginRequest
|
data: LoginRequest
|
||||||
): Promise<ApiResponse<LoginResponse>> => {
|
): 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 = (
|
export const register = (
|
||||||
data: RegisterRequest
|
data: RegisterRequest
|
||||||
): Promise<ApiResponse<RegisterResponse>> => {
|
): Promise<ApiResponse<RegisterResponse>> => {
|
||||||
return request.post("/v1/user/register", data);
|
return request({
|
||||||
|
url: "/v1/user/register",
|
||||||
|
method: HttpMethod.POST,
|
||||||
|
data,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 令牌刷新
|
* 令牌刷新
|
||||||
* @param refreshToken 刷新令牌
|
* @param refreshToken 刷新令牌
|
||||||
@@ -77,7 +90,9 @@ export const register = (
|
|||||||
export const refreshToken = (
|
export const refreshToken = (
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
): Promise<ApiResponse<RefreshTokenResponse>> => {
|
): Promise<ApiResponse<RefreshTokenResponse>> => {
|
||||||
return request.post("/v1/auth/refresh", null, {
|
return request({
|
||||||
|
url: "/v1/auth/refresh",
|
||||||
|
method: HttpMethod.POST,
|
||||||
params: {
|
params: {
|
||||||
refreshToken,
|
refreshToken,
|
||||||
},
|
},
|
||||||
@@ -92,7 +107,11 @@ export const refreshToken = (
|
|||||||
export const getAccessToken = (
|
export const getAccessToken = (
|
||||||
data: LoginRequest
|
data: LoginRequest
|
||||||
): Promise<ApiResponse<string>> => {
|
): 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 验证结果
|
* @returns 验证结果
|
||||||
*/
|
*/
|
||||||
export const validateToken = (): Promise<ApiResponse<boolean>> => {
|
export const validateToken = (): Promise<ApiResponse<boolean>> => {
|
||||||
return request.post("/v1/auth/validate");
|
return request({
|
||||||
|
url: "/v1/auth/validate",
|
||||||
|
method: HttpMethod.POST,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,96 +1,190 @@
|
|||||||
import request from "@/plugins/axios";
|
import request from "@/plugins/axios";
|
||||||
import type { ApiResponse } from "../response";
|
import type { ApiResponse } from "../response";
|
||||||
|
import { HttpMethod } from "@/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* 用户信息
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* 用户信息
|
|
||||||
*/
|
|
||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
/**
|
/**
|
||||||
* id
|
* id
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户账号
|
* 用户账号
|
||||||
*/
|
*/
|
||||||
userAccount: string;
|
userAccount: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开放平台id
|
||||||
|
*/
|
||||||
|
unionId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开放平台id
|
* 公众号openId
|
||||||
*/
|
*/
|
||||||
unionId?: string;
|
mpOpenId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公众号openId
|
* 用户昵称
|
||||||
*/
|
*/
|
||||||
mpOpenId?: string;
|
userName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户昵称
|
* 用户头像 是一个文件id,需要去文件服务查询文件详情,拿到文件url,和当前前端url + “/file/” 拼接起来,才能访问到文件
|
||||||
*/
|
*/
|
||||||
userName: string;
|
userAvatar?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户头像 是一个文件id,需要去文件服务查询文件详情,拿到文件url,和当前前端url + “/file/” 拼接起来,才能访问到文件
|
* 用户邮箱
|
||||||
*/
|
*/
|
||||||
userAvatar?: string;
|
userEmail?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户邮箱
|
* 用户邮箱是否验证
|
||||||
*/
|
*/
|
||||||
userEmail?: string;
|
userEmailVerified?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户邮箱是否验证
|
* 用户简介
|
||||||
*/
|
*/
|
||||||
userEmailVerified?: boolean;
|
userProfile?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户简介
|
* 用户角色:user/admin/ban
|
||||||
*/
|
*/
|
||||||
userProfile?: string;
|
userRole: "user" | "admin" | "ban";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户角色:user/admin/ban
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
userRole: 'user' | 'admin' | 'ban';
|
createTime: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 更新时间
|
||||||
*/
|
*/
|
||||||
createTime: string;
|
updateTime: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新时间
|
|
||||||
*/
|
|
||||||
updateTime: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前登录用户信息
|
* 获取当前登录用户信息
|
||||||
* @returns 当前登录用户信息
|
* @returns 当前登录用户信息
|
||||||
*/
|
*/
|
||||||
export const getUserInfoByToken = (): Promise<ApiResponse<UserInfo>> => {
|
export const getUserInfoByToken = (): Promise<ApiResponse<UserInfo>> => {
|
||||||
return request({
|
return request({
|
||||||
url: "/v1/auth/getUserInfo",
|
url: "/v1/auth/getUserInfo",
|
||||||
method: "GET",
|
method: HttpMethod.GET,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新用户头像
|
* 更新用户头像
|
||||||
* @param fileId 文件id
|
* @param fileId 文件id
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateUserAvatar = (fileId: string): Promise<ApiResponse<void>> => {
|
export const updateUserAvatar = (
|
||||||
return request({
|
fileId: string,
|
||||||
url: "/v1/user/avatar",
|
): Promise<ApiResponse<void>> => {
|
||||||
method: "PUT",
|
return request({
|
||||||
data: {
|
url: "/v1/user/avatar",
|
||||||
fileId: fileId,
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import request from "@/plugins/axios";
|
import request from "@/plugins/axios";
|
||||||
import type { ApiResponse } from "../response";
|
import type { ApiResponse } from "../response";
|
||||||
|
import { HttpMethod } from "@/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询文件列表
|
* 分页查询文件列表
|
||||||
*/
|
*/
|
||||||
export const getFileList = async(current: number,size: number) =>{
|
export const getFileList = async(current: number,size: number) =>{
|
||||||
return request({
|
return request({
|
||||||
url: "/v1/file/page",
|
url: "/v1/file/page",
|
||||||
method: "GET",
|
method: HttpMethod.GET,
|
||||||
params: {
|
params: {
|
||||||
current,
|
current,
|
||||||
size,
|
size,
|
||||||
@@ -22,9 +24,10 @@ export const getFileList = async(current: number,size: number) =>{
|
|||||||
export const getFileById = async (id: string): Promise<ApiResponse<FileListItem>> => {
|
export const getFileById = async (id: string): Promise<ApiResponse<FileListItem>> => {
|
||||||
return request({
|
return request({
|
||||||
url: "/v1/file/" + id,
|
url: "/v1/file/" + id,
|
||||||
method: "GET",
|
method: HttpMethod.GET,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查文件是否已经上传,本地计算文件哈希
|
* 检查文件是否已经上传,本地计算文件哈希
|
||||||
* @param hash
|
* @param hash
|
||||||
@@ -33,7 +36,7 @@ export const getFileById = async (id: string): Promise<ApiResponse<FileListItem>
|
|||||||
export const checkHash = async (hash: string) => {
|
export const checkHash = async (hash: string) => {
|
||||||
return request({
|
return request({
|
||||||
url: "/v1/file/check",
|
url: "/v1/file/check",
|
||||||
method: "GET",
|
method: HttpMethod.GET,
|
||||||
params: {
|
params: {
|
||||||
hash,
|
hash,
|
||||||
},
|
},
|
||||||
@@ -51,7 +54,7 @@ export const uploadFile = async (file: File, hash: string) : Promise<ApiResponse
|
|||||||
formData.append("hash", hash);
|
formData.append("hash", hash);
|
||||||
return request({
|
return request({
|
||||||
url: "/v1/file/upload",
|
url: "/v1/file/upload",
|
||||||
method: "POST",
|
method: HttpMethod.POST,
|
||||||
data: formData,
|
data: formData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -64,7 +67,7 @@ export const uploadFile = async (file: File, hash: string) : Promise<ApiResponse
|
|||||||
export const deleteFile = async (id: number) => {
|
export const deleteFile = async (id: number) => {
|
||||||
return request({
|
return request({
|
||||||
url: "/v1/file/" + id,
|
url: "/v1/file/" + id,
|
||||||
method: "DELETE",
|
method: HttpMethod.DELETE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/constants/http.ts
Normal file
15
src/constants/http.ts
Normal 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
1
src/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './http';
|
||||||
@@ -1,129 +1,398 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="userProfileView">
|
<div id="userProfileView">
|
||||||
<a-card title="个人中心" :bordered="false" class="profile-card">
|
<a-row :gutter="24">
|
||||||
<a-space direction="vertical" size="large" fill>
|
<!-- 左侧:个人信息展示 -->
|
||||||
<div class="avatar-section">
|
<a-col :span="8" :xs="24" :sm="24" :md="8">
|
||||||
<a-upload
|
<a-card title="个人名片" :bordered="false" class="profile-card">
|
||||||
action="/"
|
<div class="profile-left">
|
||||||
:custom-request="customUpload"
|
<a-upload
|
||||||
:show-file-list="false"
|
action="/"
|
||||||
:auto-upload="true"
|
:custom-request="customUpload"
|
||||||
accept="image/*"
|
:show-file-list="false"
|
||||||
>
|
:auto-upload="true"
|
||||||
<template #upload-button>
|
accept="image/*"
|
||||||
<div class="avatar-wrapper">
|
>
|
||||||
<a-avatar :size="100" trigger-type="mask">
|
<template #upload-button>
|
||||||
<img
|
<div class="avatar-wrapper">
|
||||||
v-if="loginUser.userAvatar"
|
<a-avatar :size="120" trigger-type="mask">
|
||||||
:src="avatarUrl"
|
<img
|
||||||
alt="avatar"
|
v-if="avatarUrl"
|
||||||
/>
|
:src="avatarUrl"
|
||||||
<span v-else>{{
|
alt="avatar"
|
||||||
loginUser.userName?.charAt(0)?.toUpperCase()
|
/>
|
||||||
}}</span>
|
<span v-else class="avatar-text">{{
|
||||||
<template #trigger-icon>
|
loginUser.userName?.charAt(0)?.toUpperCase()
|
||||||
<icon-camera />
|
}}</span>
|
||||||
</template>
|
<template #trigger-icon>
|
||||||
</a-avatar>
|
<icon-camera />
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</a-avatar>
|
||||||
</a-upload>
|
<div class="avatar-hint">点击更换头像</div>
|
||||||
<div class="user-name">{{ loginUser.userName }}</div>
|
</div>
|
||||||
<div class="user-role">
|
</template>
|
||||||
<a-tag color="arcoblue">{{ loginUser.userRole }}</a-tag>
|
</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>
|
||||||
</div>
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
<a-descriptions
|
<!-- 右侧:编辑表单和账号安全 -->
|
||||||
:data="data"
|
<a-col :span="16" :xs="24" :sm="24" :md="16">
|
||||||
size="large"
|
<a-space direction="vertical" size="large" fill>
|
||||||
title="详细信息"
|
<!-- 编辑资料卡片 -->
|
||||||
:column="1"
|
<a-card title="编辑资料" :bordered="false" class="edit-card">
|
||||||
bordered
|
<a-form
|
||||||
/>
|
:model="formData"
|
||||||
</a-space>
|
:label-col-props="{ span: 4 }"
|
||||||
</a-card>
|
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useUserStore } from '@/store/user';
|
||||||
import { uploadFile, getFileById } from '@/api/file/file';
|
import { uploadFile, getFileById } from '@/api/file/file';
|
||||||
import { updateUserAvatar } from '@/api/auth/user';
|
import { updateUserAvatar } from '@/api/auth/user';
|
||||||
import { isApiSuccess } from '@/api/response';
|
import { isApiSuccess } from '@/api/response';
|
||||||
import { Message } from '@arco-design/web-vue';
|
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 * as SparkMD5 from 'spark-md5';
|
||||||
import type { RequestOption } from '@arco-design/web-vue/es/upload/interfaces';
|
import type { RequestOption } from '@arco-design/web-vue/es/upload/interfaces';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const loginUser = computed(() => userStore.loginUser);
|
const loginUser = computed(() => userStore.loginUser);
|
||||||
const avatarUrl = ref('');
|
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 () => {
|
const loadAvatarUrl = async () => {
|
||||||
if (!loginUser.value.userAvatar) {
|
if (!loginUser.value.userAvatar) {
|
||||||
avatarUrl.value = '';
|
avatarUrl.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await getFileById(loginUser.value.userAvatar);
|
const res = await getFileById(loginUser.value.userAvatar);
|
||||||
if (isApiSuccess(res) && res.data && res.data.storagePath) {
|
if (isApiSuccess(res) && res.data && res.data.storagePath) {
|
||||||
// 拼接 /file/ 前缀,通过 vite 代理访问后端静态资源
|
avatarUrl.value = `/api/file/${res.data.storagePath}`;
|
||||||
avatarUrl.value = `/api/file/${res.data.storagePath}`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("加载头像失败", error);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载头像失败', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 初始化表单数据
|
||||||
|
const initFormData = () => {
|
||||||
|
formData.userName = loginUser.value.userName || '';
|
||||||
|
formData.userProfile = loginUser.value.userProfile || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听用户头像变化
|
||||||
watch(() => loginUser.value.userAvatar, () => {
|
watch(() => loginUser.value.userAvatar, () => {
|
||||||
loadAvatarUrl();
|
loadAvatarUrl();
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
const data = computed(() => [
|
// 监听用户信息变化,更新表单
|
||||||
{ label: "用户ID", value: loginUser.value.id?.toString() || "-" },
|
watch(() => loginUser.value, () => {
|
||||||
{ label: "账号", value: loginUser.value.userAccount || "-" },
|
initFormData();
|
||||||
{ label: "昵称", value: loginUser.value.userName || "-" },
|
}, { immediate: true, deep: true });
|
||||||
{ label: "邮箱", value: loginUser.value.userEmail || "-" },
|
|
||||||
{ label: "简介", value: loginUser.value.userProfile || "暂无简介" },
|
|
||||||
{ label: "注册时间", value: loginUser.value.createTime || "-" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
// 头像上传
|
||||||
const customUpload = async (option: RequestOption) => {
|
const customUpload = async (option: RequestOption) => {
|
||||||
const { fileItem, onSuccess, onError } = option;
|
const { fileItem, onSuccess, onError } = option;
|
||||||
if (!fileItem.file) return;
|
if (!fileItem.file) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Calculate hash
|
|
||||||
const hash = await computeFileHash(fileItem.file);
|
const hash = await computeFileHash(fileItem.file);
|
||||||
|
|
||||||
// Upload
|
|
||||||
const res = await uploadFile(fileItem.file, hash);
|
const res = await uploadFile(fileItem.file, hash);
|
||||||
if (isApiSuccess(res) && res.data) {
|
if (isApiSuccess(res) && res.data) {
|
||||||
// Update Avatar
|
|
||||||
const fileId: string = String(res.data.id);
|
const fileId: string = String(res.data.id);
|
||||||
console.log(fileId);
|
|
||||||
|
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
throw new Error("上传返回数据缺少文件ID");
|
throw new Error('上传返回数据缺少文件ID');
|
||||||
}
|
}
|
||||||
// 确保是字符串
|
|
||||||
|
|
||||||
const updateRes = await updateUserAvatar(fileId);
|
const updateRes = await updateUserAvatar(fileId);
|
||||||
if (isApiSuccess(updateRes)) {
|
if (isApiSuccess(updateRes)) {
|
||||||
Message.success("头像更新成功");
|
Message.success('头像更新成功');
|
||||||
await userStore.getLoginUser(); // Refresh
|
await userStore.getLoginUser();
|
||||||
onSuccess(res);
|
onSuccess(res);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(updateRes.message || "更新头像失败");
|
throw new Error(updateRes.message || '更新头像失败');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.message || "上传失败");
|
throw new Error(res.message || '上传失败');
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Message.error("上传出错: " + err.message);
|
Message.error('上传出错: ' + err.message);
|
||||||
onError(err);
|
onError(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -138,46 +407,311 @@ const computeFileHash = (file: File): Promise<string> => {
|
|||||||
spark.append(e.target.result as ArrayBuffer);
|
spark.append(e.target.result as ArrayBuffer);
|
||||||
resolve(spark.end());
|
resolve(spark.end());
|
||||||
} else {
|
} 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);
|
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(() => {
|
onMounted(() => {
|
||||||
userStore.getLoginUser();
|
userStore.getLoginUser();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
#userProfileView {
|
#userProfileView {
|
||||||
max-width: 800px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
.avatar-section {
|
|
||||||
display: flex;
|
.profile-card {
|
||||||
flex-direction: column;
|
height: 100%;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
.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;
|
.edit-card,
|
||||||
position: relative;
|
.security-card {
|
||||||
transition: all 0.3s;
|
: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;
|
@media (max-width: 768px) {
|
||||||
margin-top: 10px;
|
#userProfileView {
|
||||||
}
|
padding: 16px;
|
||||||
.user-role {
|
}
|
||||||
margin-top: 5px;
|
|
||||||
|
.profile-card,
|
||||||
|
.edit-card,
|
||||||
|
.security-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user