Compare commits
3 Commits
1a82dfab35
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| edcc8611e5 | |||
| 783ea21d55 | |||
| 13b320ca93 |
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
* @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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -17,23 +19,24 @@ export const getFileList = async(current: number,size: number) =>{
|
||||
/**
|
||||
* 根据ID查询文件详情
|
||||
* @param id 文件id
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export const getFileById = async (id: string): Promise<ApiResponse<FileListItem>> => {
|
||||
return request({
|
||||
url: "/v1/file/" + id,
|
||||
method: "GET",
|
||||
method: HttpMethod.GET,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否已经上传,本地计算文件哈希
|
||||
* @param hash
|
||||
* @returns
|
||||
* @param hash
|
||||
* @returns
|
||||
*/
|
||||
export const checkHash = async (hash: string) => {
|
||||
return request({
|
||||
url: "/v1/file/check",
|
||||
method: "GET",
|
||||
method: HttpMethod.GET,
|
||||
params: {
|
||||
hash,
|
||||
},
|
||||
@@ -42,8 +45,8 @@ export const checkHash = async (hash: string) => {
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param file
|
||||
* @returns
|
||||
* @param file
|
||||
* @returns
|
||||
*/
|
||||
export const uploadFile = async (file: File, hash: string) : Promise<ApiResponse<FileListItem>> => {
|
||||
const formData = new FormData();
|
||||
@@ -51,20 +54,20 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param id
|
||||
* @returns
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteFile = async (id: number) => {
|
||||
return request({
|
||||
url: "/v1/file/" + id,
|
||||
method: "DELETE",
|
||||
method: HttpMethod.DELETE,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,12 @@
|
||||
<a-dropdown v-else trigger="hover" position="br">
|
||||
<div class="user-info">
|
||||
<a-avatar :size="36" class="user-avatar">
|
||||
<IconUser />
|
||||
<img
|
||||
v-if="avatarUrl"
|
||||
:src="avatarUrl"
|
||||
alt="avatar"
|
||||
/>
|
||||
<IconUser v-else />
|
||||
</a-avatar>
|
||||
<span class="username">{{ userStore.loginUser.userName }}</span>
|
||||
<icon-down class="dropdown-icon" />
|
||||
@@ -70,7 +75,7 @@
|
||||
<script setup lang="ts">
|
||||
import { routes } from "../router/index.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import {
|
||||
IconUser,
|
||||
@@ -82,13 +87,42 @@ import checkAccess from "../access/checkAccess.ts";
|
||||
import { useUserStore } from "../store/user.ts";
|
||||
import { clearTokens } from "@/utils/token";
|
||||
import ACCESS_ENUM from "@/access/accessEnum";
|
||||
import { getFileById } from "@/api/file/file";
|
||||
import { isApiSuccess } from "@/api/response";
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const avatarUrl = ref("");
|
||||
|
||||
// 默认主页
|
||||
const selectedKeys = ref(["/"]);
|
||||
|
||||
// 加载用户头像
|
||||
const loadAvatarUrl = async () => {
|
||||
if (!userStore.loginUser.userAvatar) {
|
||||
avatarUrl.value = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getFileById(userStore.loginUser.userAvatar);
|
||||
if (isApiSuccess(res) && res.data && res.data.storagePath) {
|
||||
// 拼接 /file/ 前缀,通过 vite 代理访问后端静态资源
|
||||
avatarUrl.value = `/api/file/${res.data.storagePath}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载头像失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听用户头像变化
|
||||
watch(
|
||||
() => userStore.loginUser.userAvatar,
|
||||
() => {
|
||||
loadAvatarUrl();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const doMenuItemClick = (path: string) => {
|
||||
router.push({ path });
|
||||
};
|
||||
@@ -305,6 +339,11 @@ router.afterEach((to) => {
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
||||
:deep(img) {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
|
||||
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';
|
||||
3
src/store/types.d.ts
vendored
3
src/store/types.d.ts
vendored
@@ -7,6 +7,9 @@ export interface LoginUesr {
|
||||
userRole?: string;
|
||||
userProfile?: string;
|
||||
userEmail?: string;
|
||||
userPhone?: string;
|
||||
unionId?: string;
|
||||
mpOpenId?: string;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
[key: string]: any;
|
||||
|
||||
@@ -1,128 +1,426 @@
|
||||
<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="修改密码"
|
||||
@before-ok="handlePasswordSubmit"
|
||||
@cancel="handlePasswordReset"
|
||||
:ok-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 ? '更换手机号' : '绑定手机号'"
|
||||
@before-ok="handlePhoneSubmit"
|
||||
@cancel="handlePhoneReset"
|
||||
:ok-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-group style="width: 100%">
|
||||
<a-input
|
||||
v-model="phoneForm.code"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="6"
|
||||
/>
|
||||
<a-button
|
||||
type="outline"
|
||||
:loading="codeSending"
|
||||
:disabled="phoneCountdown > 0 || !phoneForm.phone"
|
||||
@click="handleSendPhoneCode"
|
||||
>
|
||||
{{ phoneCountdown > 0 ? `${phoneCountdown}秒后重试` : '发送验证码' }}
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 绑定邮箱弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="showEmailModal"
|
||||
:title="loginUser.userEmail ? '更换邮箱' : '绑定邮箱'"
|
||||
@before-ok="handleEmailSubmit"
|
||||
@cancel="handleEmailReset"
|
||||
:ok-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-group style="width: 100%">
|
||||
<a-input
|
||||
v-model="emailForm.code"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="6"
|
||||
/>
|
||||
<a-button
|
||||
type="outline"
|
||||
:loading="codeSending"
|
||||
:disabled="emailCountdown > 0 || !emailForm.email"
|
||||
@click="handleSendEmailCode"
|
||||
>
|
||||
{{ emailCountdown > 0 ? `${emailCountdown}秒后重试` : '发送验证码' }}
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted, watch, reactive } from 'vue';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { uploadFile, getFileById } from '@/api/file/file';
|
||||
import { updateUserAvatar } from '@/api/auth/user';
|
||||
import {
|
||||
updateUserAvatar,
|
||||
updateUserPassword,
|
||||
updateUserProfile,
|
||||
sendEmailVerifyCode,
|
||||
bindEmail,
|
||||
} 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 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);
|
||||
}
|
||||
};
|
||||
// 验证码倒计时
|
||||
const emailCountdown = ref(0);
|
||||
const phoneCountdown = ref(0);
|
||||
let emailTimer: number | null = null;
|
||||
let phoneTimer: number | null = null;
|
||||
|
||||
watch(() => loginUser.value.userAvatar, () => {
|
||||
loadAvatarUrl();
|
||||
}, { immediate: true });
|
||||
// 弹窗显示状态
|
||||
const showPasswordModal = ref(false);
|
||||
const showPhoneModal = ref(false);
|
||||
const showEmailModal = ref(false);
|
||||
|
||||
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 || "-" },
|
||||
// 表单数据
|
||||
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 || !parts[0] || !parts[1]) 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) {
|
||||
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();
|
||||
}, { immediate: true });
|
||||
|
||||
// 监听用户信息变化,更新表单
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -137,46 +435,426 @@ 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 () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await updateUserProfile({
|
||||
userName: formData.userName,
|
||||
userProfile: formData.userProfile,
|
||||
});
|
||||
if (isApiSuccess(res)) {
|
||||
Message.success('资料更新成功');
|
||||
await userStore.getLoginUser();
|
||||
} else {
|
||||
Message.error(res.message || '更新失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error('更新出错: ' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置资料表单
|
||||
const handleReset = () => {
|
||||
initFormData();
|
||||
Message.info('已重置');
|
||||
};
|
||||
|
||||
// 修改密码
|
||||
const handlePasswordSubmit = async () => {
|
||||
if (!passwordForm.oldPassword) {
|
||||
Message.warning('请输入当前密码');
|
||||
return false;
|
||||
}
|
||||
if (!passwordForm.newPassword) {
|
||||
Message.warning('请输入新密码');
|
||||
return false;
|
||||
}
|
||||
if (passwordForm.newPassword.length < 6 || passwordForm.newPassword.length > 20) {
|
||||
Message.warning('新密码长度应为6-20位');
|
||||
return false;
|
||||
}
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
Message.warning('两次输入的密码不一致');
|
||||
return false;
|
||||
}
|
||||
if (passwordForm.oldPassword === passwordForm.newPassword) {
|
||||
Message.warning('新密码不能与当前密码相同');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
passwordLoading.value = true;
|
||||
const res = await updateUserPassword(
|
||||
passwordForm.oldPassword,
|
||||
passwordForm.newPassword
|
||||
);
|
||||
if (isApiSuccess(res)) {
|
||||
Message.success('密码修改成功,请重新登录');
|
||||
handlePasswordReset();
|
||||
// 修改密码后跳转到登录页
|
||||
setTimeout(() => {
|
||||
window.location.href = '/user/login';
|
||||
}, 1500);
|
||||
return true;
|
||||
} else {
|
||||
Message.error(res.message || '修改密码失败');
|
||||
return false;
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error('修改密码出错: ' + (err.message || '未知错误'));
|
||||
return false;
|
||||
} finally {
|
||||
passwordLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置密码表单
|
||||
const handlePasswordReset = () => {
|
||||
passwordForm.oldPassword = '';
|
||||
passwordForm.newPassword = '';
|
||||
passwordForm.confirmPassword = '';
|
||||
};
|
||||
|
||||
// 绑定手机号
|
||||
const handlePhoneSubmit = async () => {
|
||||
if (!phoneForm.phone) {
|
||||
Message.warning('请输入手机号');
|
||||
return false;
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
|
||||
Message.warning('请输入正确的手机号');
|
||||
return false;
|
||||
}
|
||||
if (!phoneForm.code) {
|
||||
Message.warning('请输入验证码');
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: 等后端接口提供后对接
|
||||
Message.info('绑定手机号接口开发中...');
|
||||
console.log('绑定手机号:', phoneForm);
|
||||
return false;
|
||||
};
|
||||
|
||||
// 发送手机验证码
|
||||
const handleSendPhoneCode = async () => {
|
||||
if (!phoneForm.phone) {
|
||||
Message.warning('请先输入手机号');
|
||||
return;
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
|
||||
Message.warning('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 等后端接口提供后对接
|
||||
Message.info('发送验证码接口开发中...');
|
||||
// 模拟倒计时(实际对接后端后移到成功回调中)
|
||||
phoneCountdown.value = 60;
|
||||
if (phoneTimer) clearInterval(phoneTimer);
|
||||
phoneTimer = window.setInterval(() => {
|
||||
phoneCountdown.value--;
|
||||
if (phoneCountdown.value <= 0) {
|
||||
if (phoneTimer) clearInterval(phoneTimer);
|
||||
phoneTimer = null;
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 重置手机表单
|
||||
const handlePhoneReset = () => {
|
||||
phoneForm.phone = '';
|
||||
phoneForm.code = '';
|
||||
// 清理倒计时
|
||||
if (phoneTimer) {
|
||||
clearInterval(phoneTimer);
|
||||
phoneTimer = null;
|
||||
}
|
||||
phoneCountdown.value = 0;
|
||||
};
|
||||
|
||||
// 绑定邮箱
|
||||
const handleEmailSubmit = async () => {
|
||||
if (!emailForm.email) {
|
||||
Message.warning('请输入邮箱');
|
||||
return false;
|
||||
}
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailForm.email)) {
|
||||
Message.warning('请输入正确的邮箱地址');
|
||||
return false;
|
||||
}
|
||||
if (!emailForm.code) {
|
||||
Message.warning('请输入验证码');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
emailLoading.value = true;
|
||||
const res = await bindEmail(emailForm.email, emailForm.code);
|
||||
if (isApiSuccess(res)) {
|
||||
Message.success('邮箱绑定成功');
|
||||
handleEmailReset();
|
||||
await userStore.getLoginUser();
|
||||
return true;
|
||||
} else {
|
||||
Message.error(res.message || '绑定失败');
|
||||
return false;
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error('绑定邮箱出错: ' + (err.message || '未知错误'));
|
||||
return false;
|
||||
} finally {
|
||||
emailLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 发送邮箱验证码
|
||||
const handleSendEmailCode = async () => {
|
||||
if (!emailForm.email) {
|
||||
Message.warning('请先输入邮箱');
|
||||
return;
|
||||
}
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailForm.email)) {
|
||||
Message.warning('请输入正确的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
codeSending.value = true;
|
||||
const res = await sendEmailVerifyCode(emailForm.email);
|
||||
if (isApiSuccess(res)) {
|
||||
Message.success('验证码已发送,请查收邮箱');
|
||||
// 开始倒计时
|
||||
emailCountdown.value = 60;
|
||||
if (emailTimer) clearInterval(emailTimer);
|
||||
emailTimer = window.setInterval(() => {
|
||||
emailCountdown.value--;
|
||||
if (emailCountdown.value <= 0) {
|
||||
if (emailTimer) clearInterval(emailTimer);
|
||||
emailTimer = null;
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
Message.error(res.message || '发送失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error('发送验证码出错: ' + (err.message || '未知错误'));
|
||||
} finally {
|
||||
codeSending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置邮箱表单
|
||||
const handleEmailReset = () => {
|
||||
emailForm.email = '';
|
||||
emailForm.code = '';
|
||||
// 清理倒计时
|
||||
if (emailTimer) {
|
||||
clearInterval(emailTimer);
|
||||
emailTimer = null;
|
||||
}
|
||||
emailCountdown.value = 0;
|
||||
};
|
||||
|
||||
// 绑定微信
|
||||
const handleBindWechat = () => {
|
||||
// TODO: 等后端接口提供后对接
|
||||
Message.info('微信绑定接口开发中...');
|
||||
};
|
||||
|
||||
// 解绑微信
|
||||
const handleUnbindWechat = () => {
|
||||
// TODO: 等后端接口提供后对接
|
||||
Message.info('微信解绑接口开发中...');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
userStore.getLoginUser();
|
||||
});
|
||||
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
if (emailTimer) {
|
||||
clearInterval(emailTimer);
|
||||
emailTimer = null;
|
||||
}
|
||||
if (phoneTimer) {
|
||||
clearInterval(phoneTimer);
|
||||
phoneTimer = null;
|
||||
}
|
||||
});
|
||||
</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;
|
||||
}
|
||||
|
||||
// 验证码输入组样式
|
||||
:deep(.arco-input-group) {
|
||||
.arco-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.arco-btn {
|
||||
flex-shrink: 0;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user