feat(profile): 重构个人中心页面并优化日志模块
- 重构个人中心页面布局和样式 - 优化日志模块类型定义和页面展示 - 添加用户统计接口 - 更新环境配置切换到线上API
This commit is contained in:
@@ -6,13 +6,9 @@ VITE_APP_TITLE=vue3-element-admin
|
|||||||
VITE_APP_BASE_API=/dev-api
|
VITE_APP_BASE_API=/dev-api
|
||||||
|
|
||||||
# 接口地址
|
# 接口地址
|
||||||
# VITE_APP_API_URL=https://api.youlai.tech # 线上
|
VITE_APP_API_URL=https://api.youlai.tech # 线上
|
||||||
# VITE_APP_API_URL=https://api.youlai.tech/v2 # 线上(多租户)
|
# VITE_APP_API_URL=https://api.youlai.tech/v2 # 线上(多租户)
|
||||||
VITE_APP_API_URL=http://localhost:8000 # 本地
|
# VITE_APP_API_URL=http://localhost:8000 # 本地
|
||||||
|
|
||||||
# SSE 端点(默认 /api/v1/sse/connect)
|
|
||||||
# VITE_APP_SSE_ENDPOINT=/api/v1/sse/connect
|
|
||||||
|
|
||||||
|
|
||||||
# 启用 Mock 服务(true:开启 false:关闭)
|
# 启用 Mock 服务(true:开启 false:关闭)
|
||||||
VITE_MOCK_DEV_SERVER=false
|
VITE_MOCK_DEV_SERVER=false
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
import type { VisitTrendQueryParams, VisitTrendDetail, VisitStatsDetail } from "@/types/api";
|
import type { VisitTrendQueryParams, VisitTrendDetail, VisitStatsDetail } from "@/types/api";
|
||||||
|
|
||||||
const STATISTICS_BASE_URL = "/api/v1/statistics";
|
const STATISTICS_BASE_URL = "/api/v1/logs";
|
||||||
|
|
||||||
const StatisticsAPI = {
|
const StatisticsAPI = {
|
||||||
/** 获取访问趋势统计 */
|
/** 获取访问趋势统计 */
|
||||||
getVisitTrend(queryParams: VisitTrendQueryParams) {
|
getVisitTrend(queryParams: VisitTrendQueryParams) {
|
||||||
return request<any, VisitTrendDetail>({
|
return request<any, VisitTrendDetail>({
|
||||||
url: `${STATISTICS_BASE_URL}/visits/trend`,
|
url: `${STATISTICS_BASE_URL}/views/trend`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params: queryParams,
|
params: queryParams,
|
||||||
});
|
});
|
||||||
@@ -15,7 +15,7 @@ const StatisticsAPI = {
|
|||||||
/** 获取访问概览统计 */
|
/** 获取访问概览统计 */
|
||||||
getVisitOverview() {
|
getVisitOverview() {
|
||||||
return request<any, VisitStatsDetail>({
|
return request<any, VisitStatsDetail>({
|
||||||
url: `${STATISTICS_BASE_URL}/visits/overview`,
|
url: `${STATISTICS_BASE_URL}/views`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import type {
|
|||||||
MobileUpdateForm,
|
MobileUpdateForm,
|
||||||
EmailUpdateForm,
|
EmailUpdateForm,
|
||||||
OptionItem,
|
OptionItem,
|
||||||
|
UserEventQueryParams,
|
||||||
|
UserEventItem,
|
||||||
|
LoginDeviceItem,
|
||||||
} from "@/types/api";
|
} from "@/types/api";
|
||||||
|
|
||||||
const USER_BASE_URL = "/api/v1/users";
|
const USER_BASE_URL = "/api/v1/users";
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function useNotice() {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
function setupSubscription() {
|
function setupSubscription() {
|
||||||
if (unsubscribe || !isConnected.value) return;
|
if (unsubscribe) return;
|
||||||
|
|
||||||
// 订阅新通知事件
|
// 订阅新通知事件
|
||||||
unsubscribe = on(NOTICE_EVENT, (data: any) => {
|
unsubscribe = on(NOTICE_EVENT, (data: any) => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { BaseQueryParams } from "./common";
|
|||||||
|
|
||||||
/** 日志分页查询参数 */
|
/** 日志分页查询参数 */
|
||||||
export interface LogQueryParams extends BaseQueryParams {
|
export interface LogQueryParams extends BaseQueryParams {
|
||||||
/** 搜索关键字 */
|
/** 搜索关键字(IP/操作人) */
|
||||||
keywords?: string;
|
keywords?: string;
|
||||||
/** 操作时间 */
|
/** 操作时间 */
|
||||||
createTime?: [string, string];
|
createTime?: [string, string];
|
||||||
@@ -15,29 +15,39 @@ export interface LogQueryParams extends BaseQueryParams {
|
|||||||
/** 日志分页对象 */
|
/** 日志分页对象 */
|
||||||
export interface LogItem {
|
export interface LogItem {
|
||||||
/** 日志ID */
|
/** 日志ID */
|
||||||
id: string;
|
id: number;
|
||||||
/** 日志模块 */
|
/** 模块 */
|
||||||
module: string;
|
module?: string;
|
||||||
/** 日志内容 */
|
/** 操作类型 */
|
||||||
content: string;
|
actionType?: string;
|
||||||
|
/** 操作标题 */
|
||||||
|
title?: string;
|
||||||
|
/** 自定义日志内容 */
|
||||||
|
content?: string;
|
||||||
|
/** 操作人ID */
|
||||||
|
operatorId?: number;
|
||||||
|
/** 操作人名称 */
|
||||||
|
operatorName?: string;
|
||||||
/** 请求路径 */
|
/** 请求路径 */
|
||||||
requestUri?: string;
|
requestUri?: string;
|
||||||
/** 请求方法 */
|
/** 请求方法 */
|
||||||
method?: string;
|
requestMethod?: string;
|
||||||
/** IP地址 */
|
/** IP地址 */
|
||||||
ip: string;
|
ip?: string;
|
||||||
/** 地区 */
|
/** 地区 */
|
||||||
region: string;
|
region?: string;
|
||||||
|
/** 设备 */
|
||||||
|
device?: string;
|
||||||
/** 浏览器 */
|
/** 浏览器 */
|
||||||
browser: string;
|
browser?: string;
|
||||||
/** 终端系统 */
|
/** 操作系统 */
|
||||||
os: string;
|
os?: string;
|
||||||
|
/** 状态:0失败 1成功 */
|
||||||
|
status?: number;
|
||||||
/** 执行时间(毫秒) */
|
/** 执行时间(毫秒) */
|
||||||
executionTime: number;
|
executionTime?: number;
|
||||||
/** 创建人ID */
|
/** 错误信息 */
|
||||||
createBy?: string;
|
errorMsg?: string;
|
||||||
/** 操作时间 */
|
/** 操作时间 */
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
/** 操作人 */
|
|
||||||
operator: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,10 +106,10 @@ const formComponents = {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: clamp(1rem, 3vw, 2rem);
|
padding: clamp(1rem, 3vw, 2rem);
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
background-color: #f5f7ff;
|
background-color: #f5f7ff;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="profile-container">
|
<div class="p-4">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<!-- 左侧个人信息卡片 -->
|
<!-- 左侧个人信息卡片 -->
|
||||||
<el-col :span="8">
|
<el-col :xs="24" :sm="24" :md="8" :lg="6">
|
||||||
|
<div class="left-column">
|
||||||
|
<!-- 用户信息卡片 -->
|
||||||
<el-card class="user-card">
|
<el-card class="user-card">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="avatar-wrapper">
|
<div class="avatar-wrapper">
|
||||||
@@ -30,98 +32,128 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-role">{{ userProfile.roleNames }}</div>
|
<div class="user-role">{{ userProfile.roleNames }}</div>
|
||||||
</div>
|
|
||||||
<el-divider />
|
|
||||||
<div class="user-stats">
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-value">0</div>
|
|
||||||
<div class="stat-label">待办</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-value">0</div>
|
|
||||||
<div class="stat-label">消息</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-value">0</div>
|
|
||||||
<div class="stat-label">通知</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<!-- 右侧信息卡片 -->
|
<el-descriptions :column="1" size="small" border class="profile-desc">
|
||||||
<el-col :span="16">
|
<el-descriptions-item>
|
||||||
<el-card class="info-card">
|
<template #label>
|
||||||
<template #header>
|
<div class="cell-item">
|
||||||
<div class="card-header">
|
<el-icon><User /></el-icon>
|
||||||
<span>账号信息</span>
|
用户名
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-descriptions :column="1" border>
|
<span>{{ userProfile.username }}</span>
|
||||||
<el-descriptions-item label="用户名">
|
|
||||||
{{ userProfile.username }}
|
|
||||||
<el-icon v-if="userProfile.gender === 1" class="gender-icon male">
|
<el-icon v-if="userProfile.gender === 1" class="gender-icon male">
|
||||||
<Male />
|
<Male />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<el-icon v-else class="gender-icon female">
|
<el-icon v-else class="gender-icon female"><Female /></el-icon>
|
||||||
<Female />
|
|
||||||
</el-icon>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="手机号码">
|
<el-descriptions-item>
|
||||||
|
<template #label>
|
||||||
|
<div class="cell-item">
|
||||||
|
<el-icon><Iphone /></el-icon>
|
||||||
|
手机号码
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span :class="{ 'text-muted': !userProfile.mobile }">
|
||||||
{{ userProfile.mobile || "未绑定" }}
|
{{ userProfile.mobile || "未绑定" }}
|
||||||
|
</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱">
|
<el-descriptions-item>
|
||||||
|
<template #label>
|
||||||
|
<div class="cell-item">
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
邮箱
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span :class="{ 'text-muted': !userProfile.email }">
|
||||||
{{ userProfile.email || "未绑定" }}
|
{{ userProfile.email || "未绑定" }}
|
||||||
|
</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="部门">
|
<el-descriptions-item>
|
||||||
{{ userProfile.deptName }}
|
<template #label>
|
||||||
|
<div class="cell-item">
|
||||||
|
<el-icon><OfficeBuilding /></el-icon>
|
||||||
|
部门
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ userProfile.deptName || "-" }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">
|
<el-descriptions-item>
|
||||||
|
<template #label>
|
||||||
|
<div class="cell-item">
|
||||||
|
<el-icon><Timer /></el-icon>
|
||||||
|
创建时间
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
{{ userProfile.createTime }}
|
{{ userProfile.createTime }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧信息卡片 -->
|
||||||
|
<el-col :xs="24" :sm="24" :md="16" :lg="18">
|
||||||
|
<div class="right-column">
|
||||||
|
<!-- 安全设置卡片 -->
|
||||||
<el-card class="security-card">
|
<el-card class="security-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>安全设置</span>
|
<span class="card-header-title">
|
||||||
|
<el-icon><Key /></el-icon>
|
||||||
|
安全设置
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="security-list">
|
||||||
<div class="security-item">
|
<div class="security-item">
|
||||||
<div class="security-info">
|
<div class="security-left">
|
||||||
|
<div class="security-icon password">
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="security-content">
|
||||||
<div class="security-title">账户密码</div>
|
<div class="security-title">账户密码</div>
|
||||||
<div class="security-desc">定期修改密码有助于保护账户安全</div>
|
<div class="security-desc">定期修改密码有助于保护账户安全</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" link @click="() => handleOpenDialog(DialogType.PASSWORD)">
|
</div>
|
||||||
|
<el-button type="primary" link @click="handleOpenDialog(DialogType.PASSWORD)">
|
||||||
修改
|
修改
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="security-item">
|
<div class="security-item">
|
||||||
<div class="security-info">
|
<div class="security-left">
|
||||||
|
<div class="security-icon mobile">
|
||||||
|
<el-icon><Iphone /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="security-content">
|
||||||
<div class="security-title">手机号</div>
|
<div class="security-title">手机号</div>
|
||||||
<div class="security-desc">
|
<div class="security-desc">{{ mobileSecurityDesc }}</div>
|
||||||
{{ mobileSecurityDesc }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="security-actions">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="userProfile.mobile"
|
v-if="userProfile.mobile"
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@click="() => handleOpenDialog(DialogType.MOBILE)"
|
@click="handleOpenDialog(DialogType.MOBILE)"
|
||||||
>
|
>
|
||||||
更换
|
更换
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="userProfile.mobile" type="danger" link @click="handleUnbindMobile">
|
<el-button
|
||||||
|
v-if="userProfile.mobile"
|
||||||
|
type="danger"
|
||||||
|
link
|
||||||
|
@click="handleUnbindMobile"
|
||||||
|
>
|
||||||
解绑
|
解绑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-else
|
v-else
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@click="() => handleOpenDialog(DialogType.MOBILE)"
|
@click="handleOpenDialog(DialogType.MOBILE)"
|
||||||
>
|
>
|
||||||
绑定
|
绑定
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -129,35 +161,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="security-item">
|
<div class="security-item">
|
||||||
<div class="security-info">
|
<div class="security-left">
|
||||||
|
<div class="security-icon email">
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="security-content">
|
||||||
<div class="security-title">邮箱</div>
|
<div class="security-title">邮箱</div>
|
||||||
<div class="security-desc">
|
<div class="security-desc">{{ emailSecurityDesc }}</div>
|
||||||
{{ emailSecurityDesc }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="security-actions">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="userProfile.email"
|
v-if="userProfile.email"
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@click="() => handleOpenDialog(DialogType.EMAIL)"
|
@click="handleOpenDialog(DialogType.EMAIL)"
|
||||||
>
|
>
|
||||||
更换
|
更换
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button v-if="userProfile.email" type="danger" link @click="handleUnbindEmail">
|
<el-button v-if="userProfile.email" type="danger" link @click="handleUnbindEmail">
|
||||||
解绑
|
解绑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button v-else type="primary" link @click="handleOpenDialog(DialogType.EMAIL)">
|
||||||
v-else
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
@click="() => handleOpenDialog(DialogType.EMAIL)"
|
|
||||||
>
|
|
||||||
绑定
|
绑定
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
@@ -282,7 +314,19 @@ import FileAPI from "@/api/file";
|
|||||||
import { useUserStoreHook } from "@/store";
|
import { useUserStoreHook } from "@/store";
|
||||||
import { redirectToLogin } from "@/utils/auth";
|
import { redirectToLogin } from "@/utils/auth";
|
||||||
|
|
||||||
import { Camera } from "@element-plus/icons-vue";
|
import {
|
||||||
|
Camera,
|
||||||
|
Edit,
|
||||||
|
Lock,
|
||||||
|
Iphone,
|
||||||
|
Message,
|
||||||
|
User,
|
||||||
|
Male,
|
||||||
|
Female,
|
||||||
|
OfficeBuilding,
|
||||||
|
Timer,
|
||||||
|
Key,
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const userStore = useUserStoreHook();
|
const userStore = useUserStoreHook();
|
||||||
|
|
||||||
@@ -298,8 +342,9 @@ const enum DialogType {
|
|||||||
const dialogState = reactive({
|
const dialogState = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
title: "",
|
title: "",
|
||||||
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱"
|
type: "" as DialogType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userProfileFormRef = ref();
|
const userProfileFormRef = ref();
|
||||||
const passwordChangeFormRef = ref();
|
const passwordChangeFormRef = ref();
|
||||||
const mobileBindingFormRef = ref();
|
const mobileBindingFormRef = ref();
|
||||||
@@ -316,7 +361,6 @@ const mobileTimer = ref();
|
|||||||
const emailCountdown = ref(0);
|
const emailCountdown = ref(0);
|
||||||
const emailTimer = ref();
|
const emailTimer = ref();
|
||||||
|
|
||||||
// 修改密码校验规则
|
|
||||||
const passwordChangeRules = {
|
const passwordChangeRules = {
|
||||||
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
|
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
|
||||||
newPassword: [{ required: true, message: "请输入新密码", trigger: "blur" }],
|
newPassword: [{ required: true, message: "请输入新密码", trigger: "blur" }],
|
||||||
@@ -379,24 +423,21 @@ function maskEmail(email?: string) {
|
|||||||
const mobileSecurityDesc = computed(() => {
|
const mobileSecurityDesc = computed(() => {
|
||||||
return userProfile.value.mobile
|
return userProfile.value.mobile
|
||||||
? `已绑定:${maskMobile(userProfile.value.mobile)}`
|
? `已绑定:${maskMobile(userProfile.value.mobile)}`
|
||||||
: "未绑定手机号";
|
: "未绑定手机号,建议立即绑定";
|
||||||
});
|
});
|
||||||
|
|
||||||
const emailSecurityDesc = computed(() => {
|
const emailSecurityDesc = computed(() => {
|
||||||
return userProfile.value.email ? `已绑定:${maskEmail(userProfile.value.email)}` : "未绑定邮箱";
|
return userProfile.value.email
|
||||||
|
? `已绑定:${maskEmail(userProfile.value.email)}`
|
||||||
|
: "未绑定邮箱,建议立即绑定";
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开弹窗
|
|
||||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
|
||||||
*/
|
|
||||||
const handleOpenDialog = (type: DialogType) => {
|
const handleOpenDialog = (type: DialogType) => {
|
||||||
dialogState.type = type;
|
dialogState.type = type;
|
||||||
dialogState.visible = true;
|
dialogState.visible = true;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case DialogType.ACCOUNT:
|
case DialogType.ACCOUNT:
|
||||||
dialogState.title = "账号资料";
|
dialogState.title = "编辑资料";
|
||||||
// 初始化表单数据
|
|
||||||
userProfileForm.nickname = userProfile.value.nickname;
|
userProfileForm.nickname = userProfile.value.nickname;
|
||||||
userProfileForm.avatar = userProfile.value.avatar;
|
userProfileForm.avatar = userProfile.value.avatar;
|
||||||
userProfileForm.gender = userProfile.value.gender;
|
userProfileForm.gender = userProfile.value.gender;
|
||||||
@@ -459,25 +500,18 @@ async function handleUnbindEmail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送手机验证码
|
|
||||||
*/
|
|
||||||
function handleSendMobileCode() {
|
function handleSendMobileCode() {
|
||||||
if (!mobileUpdateForm.mobile) {
|
if (!mobileUpdateForm.mobile) {
|
||||||
ElMessage.error("请输入手机号");
|
ElMessage.error("请输入手机号");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 验证手机号格式
|
|
||||||
const reg = /^1[3-9]\d{9}$/;
|
const reg = /^1[3-9]\d{9}$/;
|
||||||
if (!reg.test(mobileUpdateForm.mobile)) {
|
if (!reg.test(mobileUpdateForm.mobile)) {
|
||||||
ElMessage.error("手机号格式不正确");
|
ElMessage.error("手机号格式不正确");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 发送短信验证码
|
|
||||||
UserAPI.sendMobileCode(mobileUpdateForm.mobile).then(() => {
|
UserAPI.sendMobileCode(mobileUpdateForm.mobile).then(() => {
|
||||||
ElMessage.success("验证码发送成功");
|
ElMessage.success("验证码发送成功");
|
||||||
|
|
||||||
// 倒计时 60s 重新发送
|
|
||||||
mobileCountdown.value = 60;
|
mobileCountdown.value = 60;
|
||||||
mobileTimer.value = setInterval(() => {
|
mobileTimer.value = setInterval(() => {
|
||||||
if (mobileCountdown.value > 0) {
|
if (mobileCountdown.value > 0) {
|
||||||
@@ -489,25 +523,19 @@ function handleSendMobileCode() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送邮箱验证码
|
|
||||||
*/
|
|
||||||
function handleSendEmailCode() {
|
function handleSendEmailCode() {
|
||||||
if (!emailUpdateForm.email) {
|
if (!emailUpdateForm.email) {
|
||||||
ElMessage.error("请输入邮箱");
|
ElMessage.error("请输入邮箱");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 验证邮箱格式
|
|
||||||
const reg = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/;
|
const reg = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/;
|
||||||
if (!reg.test(emailUpdateForm.email)) {
|
if (!reg.test(emailUpdateForm.email)) {
|
||||||
ElMessage.error("邮箱格式不正确");
|
ElMessage.error("邮箱格式不正确");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送邮箱验证码
|
|
||||||
UserAPI.sendEmailCode(emailUpdateForm.email).then(() => {
|
UserAPI.sendEmailCode(emailUpdateForm.email).then(() => {
|
||||||
ElMessage.success("验证码发送成功");
|
ElMessage.success("验证码发送成功");
|
||||||
// 倒计时 60s 重新发送
|
|
||||||
emailCountdown.value = 60;
|
emailCountdown.value = 60;
|
||||||
emailTimer.value = setInterval(() => {
|
emailTimer.value = setInterval(() => {
|
||||||
if (emailCountdown.value > 0) {
|
if (emailCountdown.value > 0) {
|
||||||
@@ -519,9 +547,6 @@ function handleSendEmailCode() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 提交表单
|
|
||||||
*/
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
if (dialogState.type === DialogType.ACCOUNT) {
|
if (dialogState.type === DialogType.ACCOUNT) {
|
||||||
@@ -561,9 +586,6 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消
|
|
||||||
*/
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
dialogState.visible = false;
|
dialogState.visible = false;
|
||||||
if (dialogState.type === DialogType.ACCOUNT) {
|
if (dialogState.type === DialogType.ACCOUNT) {
|
||||||
@@ -587,18 +609,14 @@ const handleFileChange = async (event: Event) => {
|
|||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const file = target.files ? target.files[0] : null;
|
const file = target.files ? target.files[0] : null;
|
||||||
if (file) {
|
if (file) {
|
||||||
// 调用文件上传API
|
|
||||||
const data = await FileAPI.uploadFile(file);
|
const data = await FileAPI.uploadFile(file);
|
||||||
// 更新用户信息
|
|
||||||
await UserAPI.updateProfile({
|
await UserAPI.updateProfile({
|
||||||
avatar: data.url,
|
avatar: data.url,
|
||||||
});
|
});
|
||||||
// 更新用户头像
|
|
||||||
userStore.userInfo.avatar = data.url;
|
userStore.userInfo.avatar = data.url;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 加载用户信息 */
|
|
||||||
const loadUserProfile = async () => {
|
const loadUserProfile = async () => {
|
||||||
const data = await UserAPI.getProfile();
|
const data = await UserAPI.getProfile();
|
||||||
userProfile.value = data;
|
userProfile.value = data;
|
||||||
@@ -625,21 +643,27 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.profile-container {
|
.left-column {
|
||||||
min-height: calc(100vh - 84px);
|
display: flex;
|
||||||
padding: 20px;
|
flex-direction: column;
|
||||||
background: var(--el-fill-color-blank);
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户信息卡片
|
||||||
.user-card {
|
.user-card {
|
||||||
.user-info {
|
.user-info {
|
||||||
padding: 20px 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
.avatar-edit-btn {
|
.avatar-edit-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -656,7 +680,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 4px;
|
||||||
|
|
||||||
.nickname {
|
.nickname {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -665,10 +689,10 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.edit-icon {
|
.edit-icon {
|
||||||
margin-left: 8px;
|
margin-left: 6px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: color 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
@@ -677,115 +701,128 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-role {
|
.user-role {
|
||||||
font-size: 14px;
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.user-stats {
|
.profile-desc {
|
||||||
display: flex;
|
text-align: left;
|
||||||
justify-content: space-around;
|
|
||||||
padding: 16px 0;
|
.cell-item {
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card,
|
|
||||||
.security-card {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.security-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 0;
|
|
||||||
|
|
||||||
.security-info {
|
|
||||||
.security-title {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.security-desc {
|
.text-muted {
|
||||||
font-size: 14px;
|
color: var(--el-text-color-placeholder);
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-descriptions {
|
|
||||||
.el-descriptions__label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-descriptions__content {
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gender-icon {
|
.gender-icon {
|
||||||
margin-left: 8px;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
&.male {
|
&.male {
|
||||||
color: #409eff;
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.female {
|
&.female {
|
||||||
color: #f56c6c;
|
color: #f56c6c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog {
|
|
||||||
.el-dialog__header {
|
|
||||||
padding: 20px;
|
|
||||||
margin: 0;
|
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-dialog__body {
|
|
||||||
padding: 30px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__footer {
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid var(--el-border-color-light);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式适配
|
// 右侧卡片通用样式
|
||||||
@media (max-width: 768px) {
|
.security-card {
|
||||||
.profile-container {
|
.card-header {
|
||||||
padding: 10px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.card-header-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全设置
|
||||||
|
.security-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-col {
|
&:hover {
|
||||||
width: 100%;
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&.password {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mobile {
|
||||||
|
color: var(--el-color-success);
|
||||||
|
background: var(--el-color-success-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.email {
|
||||||
|
color: var(--el-color-warning);
|
||||||
|
background: var(--el-color-warning-light-9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-content {
|
||||||
|
.security-title {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<el-form-item prop="keywords" label="关键字">
|
<el-form-item prop="keywords" label="关键字">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.keywords"
|
v-model="queryParams.keywords"
|
||||||
placeholder="日志内容"
|
placeholder="IP/操作人"
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleQuery"
|
@keyup.enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
@@ -39,15 +39,31 @@
|
|||||||
border
|
border
|
||||||
class="data-table__content"
|
class="data-table__content"
|
||||||
>
|
>
|
||||||
<el-table-column label="操作时间" prop="createTime" width="220" />
|
<el-table-column label="操作标题" prop="title" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column label="操作人" prop="operator" width="120" />
|
<el-table-column label="状态" prop="status" width="80" align="center">
|
||||||
<el-table-column label="日志模块" prop="module" width="100" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="日志内容" prop="content" min-width="200" />
|
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
<el-table-column label="IP 地址" prop="ip" width="150" />
|
{{ row.status === 1 ? "成功" : "失败" }}
|
||||||
<el-table-column label="地区" prop="region" width="150" />
|
</el-tag>
|
||||||
<el-table-column label="浏览器" prop="browser" width="150" />
|
</template>
|
||||||
<el-table-column label="终端系统" prop="os" width="200" show-overflow-tooltip />
|
</el-table-column>
|
||||||
<el-table-column label="执行时间(ms)" prop="executionTime" width="150" />
|
<el-table-column label="IP地址" prop="ip" width="140" />
|
||||||
|
<el-table-column label="请求路径" prop="requestUri" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="请求方法" prop="requestMethod" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getMethodTagType(row.requestMethod)" size="small" effect="plain">
|
||||||
|
{{ row.requestMethod }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="执行时间(ms)" prop="executionTime" width="120" align="center" />
|
||||||
|
<el-table-column label="操作人" prop="operatorName" width="120" />
|
||||||
|
<el-table-column label="操作时间" prop="createTime" width="180" />
|
||||||
|
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<pagination
|
<pagination
|
||||||
@@ -58,6 +74,39 @@
|
|||||||
@pagination="fetchData"
|
@pagination="fetchData"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 详情弹窗 -->
|
||||||
|
<el-dialog v-model="detailVisible" title="日志详情" width="720px">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="操作标题" :span="2">
|
||||||
|
{{ detailData.title }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="detailData.status === 1 ? 'success' : 'danger'" size="small">
|
||||||
|
{{ detailData.status === 1 ? "成功" : "失败" }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="执行时间">
|
||||||
|
{{ detailData.executionTime }}ms
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作人">{{ detailData.operatorName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作时间">{{ detailData.createTime }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="IP地址">{{ detailData.ip }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="请求方法">{{ detailData.requestMethod }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="请求路径" :span="2">
|
||||||
|
{{ detailData.requestUri }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="浏览器">{{ detailData.browser }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="操作系统">{{ detailData.os }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="自定义内容" :span="2">
|
||||||
|
<div v-if="detailData.content" class="whitespace-pre-wrap">{{ detailData.content }}</div>
|
||||||
|
<span v-else class="color-text-placeholder">无</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item v-if="detailData.errorMsg" label="错误信息" :span="2">
|
||||||
|
<span class="color-danger">{{ detailData.errorMsg }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -69,7 +118,18 @@ defineOptions({
|
|||||||
|
|
||||||
import LogAPI from "@/api/system/log";
|
import LogAPI from "@/api/system/log";
|
||||||
import type { LogItem, LogQueryParams } from "@/types/api";
|
import type { LogItem, LogQueryParams } from "@/types/api";
|
||||||
import type { FormInstance } from "element-plus";
|
import type { FormInstance, TagProps } from "element-plus";
|
||||||
|
|
||||||
|
function getMethodTagType(method: string): TagProps["type"] {
|
||||||
|
const map: Record<string, TagProps["type"]> = {
|
||||||
|
GET: undefined,
|
||||||
|
POST: "success",
|
||||||
|
PUT: "warning",
|
||||||
|
DELETE: "danger",
|
||||||
|
PATCH: "info",
|
||||||
|
};
|
||||||
|
return map[method?.toUpperCase()] ?? "info";
|
||||||
|
}
|
||||||
|
|
||||||
// 表单引用
|
// 表单引用
|
||||||
const queryFormRef = ref<FormInstance>();
|
const queryFormRef = ref<FormInstance>();
|
||||||
@@ -87,9 +147,10 @@ const pageData = ref<LogItem[]>();
|
|||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
/**
|
// 详情弹窗
|
||||||
* 加载日志列表数据
|
const detailVisible = ref(false);
|
||||||
*/
|
const detailData = ref<Partial<LogItem>>({});
|
||||||
|
|
||||||
function fetchData(): void {
|
function fetchData(): void {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
LogAPI.getPage(queryParams)
|
LogAPI.getPage(queryParams)
|
||||||
@@ -102,17 +163,11 @@ function fetchData(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询按钮点击事件
|
|
||||||
*/
|
|
||||||
function handleQuery(): void {
|
function handleQuery(): void {
|
||||||
queryParams.pageNum = 1;
|
queryParams.pageNum = 1;
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置查询
|
|
||||||
*/
|
|
||||||
function handleResetQuery(): void {
|
function handleResetQuery(): void {
|
||||||
queryFormRef.value?.resetFields();
|
queryFormRef.value?.resetFields();
|
||||||
queryParams.pageNum = 1;
|
queryParams.pageNum = 1;
|
||||||
@@ -120,6 +175,11 @@ function handleResetQuery(): void {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDetail(row: LogItem): void {
|
||||||
|
detailData.value = row;
|
||||||
|
detailVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handleQuery();
|
handleQuery();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user