serv\web- 日志
This commit is contained in:
@@ -42,6 +42,11 @@ export const learningTaskApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getTaskUsers(taskID: string): Promise<ResultDomain<TaskItemVO>> {
|
||||
const response = await api.get<TaskItemVO>(`${this.learningTaskPrefix}/${taskID}/users`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取任务分页列表
|
||||
* @param pageParam 分页参数
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { api } from '@/apis/index';
|
||||
import type { SysDept, SysRole, DeptRoleVO, SysDeptRole, ResultDomain } from '@/types';
|
||||
import type { SysDept, SysRole, SysDeptRole, ResultDomain, UserDeptRoleVO } from '@/types';
|
||||
|
||||
/**
|
||||
* 部门API服务
|
||||
@@ -89,8 +89,8 @@ export const deptApi = {
|
||||
* @author yslg
|
||||
* @ since 2025-10-06
|
||||
*/
|
||||
async getDeptByRole(dept: SysDept): Promise<ResultDomain<SysRole>> {
|
||||
const response = await api.post<SysRole>('/depts/role', dept);
|
||||
async getDeptByRole(dept: SysDept): Promise<ResultDomain<UserDeptRoleVO>> {
|
||||
const response = await api.post<UserDeptRoleVO>('/depts/role', dept);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -113,7 +113,7 @@ export const deptApi = {
|
||||
* @author yslg
|
||||
* @ since 2025-10-06
|
||||
*/
|
||||
async bindDeptRole(deptRole: DeptRoleVO): Promise<ResultDomain<SysDeptRole>> {
|
||||
async bindDeptRole(deptRole: UserDeptRoleVO): Promise<ResultDomain<SysDeptRole>> {
|
||||
const response = await api.post<SysDeptRole>('/depts/bind/role', deptRole);
|
||||
return response.data;
|
||||
},
|
||||
@@ -125,7 +125,7 @@ export const deptApi = {
|
||||
* @author yslg
|
||||
* @ since 2025-10-06
|
||||
*/
|
||||
async unbindDeptRole(deptRole: DeptRoleVO): Promise<ResultDomain<SysDeptRole>> {
|
||||
async unbindDeptRole(deptRole: UserDeptRoleVO): Promise<ResultDomain<SysDeptRole>> {
|
||||
const response = await api.post<SysDeptRole>('/depts/unbind/role', deptRole);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ export { permissionApi } from './permission';
|
||||
export { authApi } from './auth';
|
||||
export { fileApi } from './file';
|
||||
export { moduleApi } from './module';
|
||||
export { logApi } from './log';
|
||||
|
||||
|
||||
33
schoolNewsWeb/src/apis/system/log.ts
Normal file
33
schoolNewsWeb/src/apis/system/log.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @description 系统日志API
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
|
||||
import { api } from '../index';
|
||||
import type { LoginLog, OperationLog } from '@/types/log';
|
||||
import type { PageParam, ResultDomain } from '@/types';
|
||||
|
||||
/**
|
||||
* 日志API接口
|
||||
*/
|
||||
export const logApi = {
|
||||
baseUrl: '/sys/log',
|
||||
|
||||
async getLoginLogPage(pageParam: PageParam, filter: LoginLog): Promise<ResultDomain<LoginLog>> {
|
||||
const response = await api.post<LoginLog>(`${this.baseUrl}/login/page`, {
|
||||
pageParam,
|
||||
filter,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getOperationLogPage(pageParam: PageParam, filter: OperationLog): Promise<ResultDomain<OperationLog>> {
|
||||
const response = await api.post<OperationLog>(`${this.baseUrl}/operation/page`, {
|
||||
pageParam,
|
||||
filter,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -43,8 +43,8 @@ export const userApi = {
|
||||
* @param filter 过滤条件
|
||||
* @returns Promise<ResultDomain<SysUser>>
|
||||
*/
|
||||
async getUserList(filter: SysUser): Promise<ResultDomain<SysUser>> {
|
||||
const response = await api.post<SysUser>('/users/list', filter);
|
||||
async getUserList(filter: SysUser): Promise<ResultDomain<UserVO>> {
|
||||
const response = await api.post<UserVO>('/users/list', filter);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -625,12 +625,6 @@ function toggleAvailable(itemId: string) {
|
||||
// 树形模式下的级联选择
|
||||
const node = findNodeInTree(itemId, treeData.value);
|
||||
if (node) {
|
||||
// 如果只允许选择叶子节点,检查是否为叶子节点
|
||||
if (props.onlyLeafSelectable && !isLeafNode(node)) {
|
||||
// 非叶子节点,不允许选择
|
||||
return;
|
||||
}
|
||||
|
||||
const childrenIds = getAllChildrenIds(node);
|
||||
|
||||
if (index > -1) {
|
||||
@@ -701,6 +695,11 @@ function moveSelectedToTarget() {
|
||||
itemsToMove = flatItems.filter(item =>
|
||||
selectedAvailable.value.includes(getNodeId(item))
|
||||
);
|
||||
|
||||
// 如果只允许选择叶子节点,过滤掉非叶子节点
|
||||
if (props.onlyLeafSelectable) {
|
||||
itemsToMove = itemsToMove.filter(item => isLeafNode(item));
|
||||
}
|
||||
} else {
|
||||
// 列表模式:从可选列表中查找
|
||||
itemsToMove = availableList.value.filter(item =>
|
||||
@@ -712,8 +711,10 @@ function moveSelectedToTarget() {
|
||||
|
||||
if (props.useTree && treeData.value.length > 0) {
|
||||
// 树形模式:需要重新构建树(移除已选项)
|
||||
// 只移除叶子节点
|
||||
const idsToRemove = new Set(itemsToMove.map(item => getNodeId(item)));
|
||||
availableList.value = availableList.value.filter(item =>
|
||||
!selectedAvailable.value.includes(getItemId(item))
|
||||
!idsToRemove.has(getItemId(item))
|
||||
);
|
||||
// 重新转换为树形结构
|
||||
if (props.treeTransform) {
|
||||
@@ -782,10 +783,27 @@ function moveToTarget(itemId: string) {
|
||||
function moveAllToTarget() {
|
||||
if (props.useTree && treeData.value.length > 0) {
|
||||
// 树形模式:扁平化所有节点
|
||||
const flatItems = flattenTree(treeData.value);
|
||||
let flatItems = flattenTree(treeData.value);
|
||||
|
||||
// 如果只允许选择叶子节点,过滤掉非叶子节点
|
||||
if (props.onlyLeafSelectable) {
|
||||
flatItems = flatItems.filter(item => isLeafNode(item));
|
||||
}
|
||||
|
||||
targetList.value.push(...flatItems);
|
||||
availableList.value = [];
|
||||
treeData.value = [];
|
||||
|
||||
// 从availableList中移除已移动的项
|
||||
const idsToRemove = new Set(flatItems.map(item => getNodeId(item)));
|
||||
availableList.value = availableList.value.filter(item =>
|
||||
!idsToRemove.has(getItemId(item))
|
||||
);
|
||||
|
||||
// 重新构建树(如果还有非叶子节点)
|
||||
if (props.onlyLeafSelectable && props.treeTransform) {
|
||||
treeData.value = props.treeTransform(availableList.value);
|
||||
} else {
|
||||
treeData.value = [];
|
||||
}
|
||||
} else {
|
||||
// 列表模式
|
||||
targetList.value.push(...availableList.value);
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
<div
|
||||
class="tree-node-content"
|
||||
:style="{ paddingLeft: `${level * 20}px` }"
|
||||
:class="{
|
||||
selected: selectedIds.includes(nodeId),
|
||||
disabled: isCheckboxDisabled
|
||||
}"
|
||||
:class="{ selected: selectedIds.includes(nodeId) }"
|
||||
@click.stop="handleClick"
|
||||
@dblclick.stop="handleDblClick"
|
||||
>
|
||||
@@ -25,7 +22,6 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedIds.includes(nodeId)"
|
||||
:disabled="isCheckboxDisabled"
|
||||
@click.stop="handleCheckboxClick"
|
||||
/>
|
||||
|
||||
@@ -107,36 +103,28 @@ const hasChildren = computed(() => children.value && children.value.length > 0);
|
||||
|
||||
const expanded = computed(() => props.expandedKeys.has(nodeId.value));
|
||||
|
||||
// 判断复选框是否应该被禁用
|
||||
const isCheckboxDisabled = computed(() => {
|
||||
// 如果只允许选择叶子节点,且当前节点有子节点,则禁用复选框
|
||||
return props.onlyLeafSelectable && hasChildren.value;
|
||||
});
|
||||
|
||||
function getChildId(child: any): string {
|
||||
const idProp = props.treeProps?.id || 'id';
|
||||
return String(child[idProp] || '');
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
// 点击节点主体时切换选中状态(如果复选框未禁用)
|
||||
if (!isCheckboxDisabled.value) {
|
||||
emit('toggle-select', nodeId.value);
|
||||
}
|
||||
// 点击节点主体时切换选中状态
|
||||
emit('toggle-select', nodeId.value);
|
||||
}
|
||||
|
||||
function handleCheckboxClick() {
|
||||
// 复选框点击事件(如果未禁用)
|
||||
if (!isCheckboxDisabled.value) {
|
||||
emit('toggle-select', nodeId.value);
|
||||
}
|
||||
// 复选框点击事件
|
||||
emit('toggle-select', nodeId.value);
|
||||
}
|
||||
|
||||
function handleDblClick() {
|
||||
// 双击时触发(如果复选框未禁用)
|
||||
if (!isCheckboxDisabled.value) {
|
||||
emit('dblclick', nodeId.value);
|
||||
// 双击时触发
|
||||
// 如果只允许选择叶子节点,且当前是非叶子节点,则不触发双击
|
||||
if (props.onlyLeafSelectable && hasChildren.value) {
|
||||
return;
|
||||
}
|
||||
emit('dblclick', nodeId.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -191,11 +179,6 @@ function handleDblClick() {
|
||||
input[type="checkbox"] {
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.node-label {
|
||||
@@ -203,14 +186,6 @@ function handleDblClick() {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
.node-label {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-children {
|
||||
|
||||
@@ -48,6 +48,9 @@ export * from './usercenter';
|
||||
// 定时任务相关
|
||||
export * from './crontab';
|
||||
|
||||
// 日志相关
|
||||
export * from './log';
|
||||
|
||||
// 枚举类型
|
||||
export * from './enums';
|
||||
export * from './enums/achievement-enums';
|
||||
|
||||
84
schoolNewsWeb/src/types/log/index.ts
Normal file
84
schoolNewsWeb/src/types/log/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @description 系统日志类型定义
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
|
||||
import { BaseDTO } from '../base';
|
||||
|
||||
/**
|
||||
* 登录日志
|
||||
*/
|
||||
export interface LoginLog extends BaseDTO {
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
/** 用户ID */
|
||||
userId?: string;
|
||||
/** 登录IP */
|
||||
ipAddress?: string;
|
||||
/** 登录地点 */
|
||||
location?: string;
|
||||
/** 浏览器 */
|
||||
browser?: string;
|
||||
/** 操作系统 */
|
||||
os?: string;
|
||||
/** 登录状态: success-成功, failed-失败 */
|
||||
status?: number;
|
||||
/** 登录信息 */
|
||||
message?: string;
|
||||
/** 登录时间 */
|
||||
loginTime?: string;
|
||||
/** 部门ID */
|
||||
deptId?: string;
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
/** 开始时间 (查询条件) */
|
||||
startTime?: string;
|
||||
/** 结束时间 (查询条件) */
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作日志
|
||||
*/
|
||||
export interface OperationLog extends BaseDTO {
|
||||
/** 操作人用户名 */
|
||||
username?: string;
|
||||
/** 操作人ID */
|
||||
userId?: string;
|
||||
/** 操作模块 */
|
||||
module?: string;
|
||||
/** 操作类型: create-新增, update-修改, delete-删除, read-查询 */
|
||||
operation?: 'create' | 'update' | 'delete' | 'read' | string;
|
||||
/** 操作描述 */
|
||||
description?: string;
|
||||
/** 请求方法 */
|
||||
method?: string;
|
||||
/** 请求路径 */
|
||||
requestUrl?: string;
|
||||
/** 请求参数 */
|
||||
requestParams?: string;
|
||||
/** 响应结果 */
|
||||
responseData?: string;
|
||||
/** IP地址 */
|
||||
ipAddress?: string;
|
||||
/** 操作地点 */
|
||||
location?: string;
|
||||
/** 操作耗时(毫秒) */
|
||||
duration?: number;
|
||||
/** 操作状态: success-成功, failed-失败 */
|
||||
status?: 'success' | 'failed' | string;
|
||||
/** 错误信息 */
|
||||
errorMessage?: string;
|
||||
/** 操作时间 */
|
||||
operationTime?: string;
|
||||
/** 部门ID */
|
||||
deptId?: string;
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
/** 开始时间 (查询条件) */
|
||||
startTime?: string;
|
||||
/** 结束时间 (查询条件) */
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ export interface SysPermission extends BaseDTO {
|
||||
/** 权限ID */
|
||||
permissionID?: string;
|
||||
moduleID?: string;
|
||||
moduleName?: string;
|
||||
moduleCode?: string;
|
||||
moduleDescription?: string;
|
||||
menuID?: string;
|
||||
menuName?: string;
|
||||
menuUrl?: string;
|
||||
roleID?: string;
|
||||
roleName?: string;
|
||||
roleDescription?: string;
|
||||
/** 权限名称 */
|
||||
name?: string;
|
||||
/** 权限描述 */
|
||||
|
||||
@@ -317,6 +317,9 @@ export interface TaskItemVO extends LearningTask {
|
||||
userID?: string;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
deptID?: string;
|
||||
deptName?: string;
|
||||
parentDeptID?: string;
|
||||
/** 是否必修 */
|
||||
required?: boolean;
|
||||
/** 排序号 */
|
||||
|
||||
@@ -61,16 +61,19 @@ export interface UserVO extends BaseDTO {
|
||||
wechatID?: string;
|
||||
/** 用户状态 0-正常 1-禁用 */
|
||||
status?: number;
|
||||
/** 真实姓名 */
|
||||
realName?: string;
|
||||
familyName?: string;
|
||||
/** 名 */
|
||||
givenName?: string;
|
||||
/** 昵称 */
|
||||
nickname?: string;
|
||||
fullName?: string;
|
||||
/** 头像URL */
|
||||
avatar?: string;
|
||||
/** 性别 0-未知 1-男 2-女 */
|
||||
gender?: number;
|
||||
/** 学习等级 */
|
||||
level?: number;
|
||||
deptID?: string;
|
||||
parentDeptID?: string;
|
||||
/** 部门名称 */
|
||||
deptName?: string;
|
||||
/** 角色名称 */
|
||||
|
||||
@@ -1,110 +1,312 @@
|
||||
<template>
|
||||
<AdminLayout title="登录日志" subtitle="登录日志管理">
|
||||
<div class="login-logs">
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户名..."
|
||||
style="width: 200px"
|
||||
clearable
|
||||
/>
|
||||
<el-select v-model="loginStatus" placeholder="登录状态" style="width: 150px" clearable>
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleExport">导出</el-button>
|
||||
<el-button type="danger" @click="handleClear">清空日志</el-button>
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-group">
|
||||
<label class="search-label">用户名</label>
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="输入用户名搜索"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-group">
|
||||
<label class="search-label">登录状态</label>
|
||||
<select v-model="loginStatus" class="search-select">
|
||||
<option value="">全部</option>
|
||||
<option value="1">成功</option>
|
||||
<option value="0">失败</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="search-group">
|
||||
<label class="search-label">开始日期</label>
|
||||
<input
|
||||
v-model="startDate"
|
||||
type="date"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-group">
|
||||
<label class="search-label">结束日期</label>
|
||||
<input
|
||||
v-model="endDate"
|
||||
type="date"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-actions">
|
||||
<button class="btn-search" @click="handleSearch">搜索</button>
|
||||
<button class="btn-reset" @click="handleReset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="logs" style="width: 100%">
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="ipAddress" label="IP地址" width="140" />
|
||||
<el-table-column prop="location" label="登录地点" width="150" />
|
||||
<el-table-column prop="browser" label="浏览器" width="120" />
|
||||
<el-table-column prop="os" label="操作系统" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'success' ? 'success' : 'danger'">
|
||||
{{ row.status === 'success' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="message" label="信息" min-width="150" />
|
||||
<el-table-column prop="loginTime" label="登录时间" width="180" />
|
||||
</el-table>
|
||||
<!-- 操作按钮 -->
|
||||
<!-- <div class="toolbar">
|
||||
<button class="btn-danger" @click="handleClear">清空日志</button>
|
||||
</div> -->
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
<!-- 日志表格 -->
|
||||
<div class="log-table-wrapper">
|
||||
<table class="log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="120">用户名</th>
|
||||
<th width="140">IP地址</th>
|
||||
<th width="150">登录地点</th>
|
||||
<th width="120">浏览器</th>
|
||||
<th width="120">操作系统</th>
|
||||
<th width="100">状态</th>
|
||||
<th>信息</th>
|
||||
<th width="180">登录时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="loading">
|
||||
<tr>
|
||||
<td colspan="8" class="loading-cell">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>加载中...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else-if="logs.length === 0">
|
||||
<tr>
|
||||
<td colspan="8" class="empty-cell">
|
||||
<div class="empty-icon">📋</div>
|
||||
<p>暂无登录日志</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr v-for="log in logs" :key="log.id" class="table-row">
|
||||
<td>{{ log.username || '-' }}</td>
|
||||
<td>{{ log.ipAddress || '-' }}</td>
|
||||
<td>{{ log.location || '-' }}</td>
|
||||
<td>{{ log.browser || '-' }}</td>
|
||||
<td>{{ log.os || '-' }}</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="log.status === 1 ? 'status-success' : 'status-failed'">
|
||||
{{ log.status === 1 ? '成功' : '失败' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="log-message">
|
||||
<div class="message-text">{{ log.message || '-' }}</div>
|
||||
</td>
|
||||
<td>{{ log.loginTime || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="!loading && logs.length > 0" class="pagination">
|
||||
<div class="pagination-info">
|
||||
共 {{ total }} 条数据,每页 {{ pageSize }} 条
|
||||
</div>
|
||||
|
||||
<div class="pagination-controls">
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="handlePageChange(1)"
|
||||
>
|
||||
首页
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="handlePageChange(currentPage - 1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<div class="page-numbers">
|
||||
<button
|
||||
v-for="page in displayPages"
|
||||
:key="page"
|
||||
class="page-number"
|
||||
:class="{ active: page === currentPage }"
|
||||
@click="handlePageChange(page)"
|
||||
:disabled="page === -1"
|
||||
>
|
||||
{{ page === -1 ? '...' : page }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="handlePageChange(currentPage + 1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="handlePageChange(totalPages)"
|
||||
>
|
||||
末页
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pagination-jump">
|
||||
<span>跳转到</span>
|
||||
<input
|
||||
v-model.number="jumpPage"
|
||||
type="number"
|
||||
class="jump-input"
|
||||
@keyup.enter="handleJumpPage"
|
||||
/>
|
||||
<span>页</span>
|
||||
<button class="jump-btn" @click="handleJumpPage">跳转</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
import { logApi } from '@/apis/system';
|
||||
import type { LoginLog } from '@/types/log';
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginLogsView'
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const loginStatus = ref('');
|
||||
const dateRange = ref<[Date, Date] | null>(null);
|
||||
const startDate = ref('');
|
||||
const endDate = ref('');
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
const logs = ref<any[]>([]);
|
||||
const logs = ref<LoginLog[]>([]);
|
||||
const jumpPage = ref<number>();
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1);
|
||||
|
||||
// 计算显示的页码
|
||||
const displayPages = computed(() => {
|
||||
const pages: number[] = [];
|
||||
const maxDisplay = 7;
|
||||
|
||||
if (totalPages.value <= maxDisplay) {
|
||||
for (let i = 1; i <= totalPages.value; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
if (currentPage.value <= 4) {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push(-1);
|
||||
pages.push(totalPages.value);
|
||||
} else if (currentPage.value >= totalPages.value - 3) {
|
||||
pages.push(1);
|
||||
pages.push(-1);
|
||||
for (let i = totalPages.value - 4; i <= totalPages.value; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
pages.push(-1);
|
||||
for (let i = currentPage.value - 1; i <= currentPage.value + 1; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push(-1);
|
||||
pages.push(totalPages.value);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs();
|
||||
});
|
||||
|
||||
function loadLogs() {
|
||||
// TODO: 加载登录日志
|
||||
/**
|
||||
* 加载登录日志
|
||||
*/
|
||||
async function loadLogs() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 构建查询条件
|
||||
const query: LoginLog = {
|
||||
username: searchKeyword.value || undefined,
|
||||
status: loginStatus.value ? Number(loginStatus.value) : undefined,
|
||||
startTime: startDate.value || undefined,
|
||||
endTime: endDate.value || undefined,
|
||||
};
|
||||
|
||||
const result = await logApi.getLoginLogPage(
|
||||
{pageNumber: currentPage.value, pageSize: pageSize.value},
|
||||
query
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
logs.value = result.pageDomain?.dataList || [];
|
||||
total.value = result.pageDomain?.pageParam.totalElements || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载登录日志失败:', error);
|
||||
alert('加载登录日志失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function handleSearch() {
|
||||
currentPage.value = 1;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
// TODO: 导出日志
|
||||
ElMessage.info('导出功能开发中');
|
||||
}
|
||||
|
||||
async function handleClear() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要清空所有登录日志吗?此操作不可恢复!', '警告', {
|
||||
type: 'warning'
|
||||
});
|
||||
// TODO: 清空日志
|
||||
ElMessage.success('日志已清空');
|
||||
} catch {
|
||||
// 取消操作
|
||||
}
|
||||
}
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
pageSize.value = val;
|
||||
/**
|
||||
* 重置查询条件
|
||||
*/
|
||||
function handleReset() {
|
||||
searchKeyword.value = '';
|
||||
loginStatus.value = '';
|
||||
startDate.value = '';
|
||||
endDate.value = '';
|
||||
currentPage.value = 1;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
currentPage.value = val;
|
||||
/**
|
||||
* 页码改变
|
||||
*/
|
||||
function handlePageChange(page: number) {
|
||||
if (page < 1 || page > totalPages.value || page === -1) return;
|
||||
currentPage.value = page;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转页面
|
||||
*/
|
||||
function handleJumpPage() {
|
||||
if (!jumpPage.value || jumpPage.value < 1 || jumpPage.value > totalPages.value) {
|
||||
alert('请输入有效的页码');
|
||||
return;
|
||||
}
|
||||
currentPage.value = jumpPage.value;
|
||||
loadLogs();
|
||||
}
|
||||
</script>
|
||||
@@ -114,15 +316,310 @@ function handleCurrentChange(val: number) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.search-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-input,
|
||||
.search-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.search-select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-search,
|
||||
.btn-reset {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border: 1px solid #dcdfe6;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #f78989;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.log-table-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.log-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th {
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-message {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-success {
|
||||
background: #e1f3d8;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.status-failed {
|
||||
background: #fde2e2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-cell,
|
||||
.empty-cell {
|
||||
text-align: center;
|
||||
padding: 60px 20px !important;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
margin-bottom: 20px;
|
||||
.page-btn,
|
||||
.page-number {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pagination-jump {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
|
||||
.jump-input {
|
||||
width: 60px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.jump-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,968 @@
|
||||
<template>
|
||||
<AdminLayout title="系统日志" subtitle="系统日志管理">
|
||||
<div class="system-logs">
|
||||
<h1 class="page-title">系统日志</h1>
|
||||
<AdminLayout title="操作日志" subtitle="操作日志管理">
|
||||
<div class="operation-logs">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-group">
|
||||
<label class="search-label">用户名</label>
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="输入用户名搜索"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="登录日志" name="login">
|
||||
<LoginLogs />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="操作日志" name="operation">
|
||||
<OperationLogs />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="系统配置" name="config">
|
||||
<SystemConfig />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="search-group">
|
||||
<label class="search-label">操作模块</label>
|
||||
<input
|
||||
v-model="searchModule"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="输入模块名搜索"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- <div class="search-group">
|
||||
<label class="search-label">操作类型</label>
|
||||
<select v-model="operationType" class="search-select">
|
||||
<option value="">全部</option>
|
||||
<option value="create">新增</option>
|
||||
<option value="update">修改</option>
|
||||
<option value="delete">删除</option>
|
||||
<option value="read">查询</option>
|
||||
</select>
|
||||
</div> -->
|
||||
<!--
|
||||
<div class="search-group">
|
||||
<label class="search-label">操作状态</label>
|
||||
<select v-model="operationStatus" class="search-select">
|
||||
<option value="">全部</option>
|
||||
<option value="success">成功</option>
|
||||
<option value="failed">失败</option>
|
||||
</select>
|
||||
</div> -->
|
||||
|
||||
<div class="search-group">
|
||||
<label class="search-label">开始日期</label>
|
||||
<input
|
||||
v-model="startDate"
|
||||
type="date"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-group">
|
||||
<label class="search-label">结束日期</label>
|
||||
<input
|
||||
v-model="endDate"
|
||||
type="date"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-actions">
|
||||
<button class="btn-search" @click="handleSearch">搜索</button>
|
||||
<button class="btn-reset" @click="handleReset">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志表格 -->
|
||||
<div class="log-table-wrapper">
|
||||
<table class="log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="10%">操作人</th>
|
||||
<th width="10%">操作模块</th>
|
||||
<th width="10%">操作类型</th>
|
||||
<th width="10%">请求链接</th>
|
||||
<th width="10%">IP地址</th>
|
||||
<th width="10%">操作描述</th>
|
||||
<!-- <th width="100">耗时(ms)</th> -->
|
||||
<!-- <th width="100">状态</th> -->
|
||||
<th width="10%">操作时间</th>
|
||||
<th width="10%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="loading">
|
||||
<tr>
|
||||
<td colspan="9" class="loading-cell">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>加载中...</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else-if="logs.length === 0">
|
||||
<tr>
|
||||
<td colspan="9" class="empty-cell">
|
||||
<div class="empty-icon">📋</div>
|
||||
<p>暂无操作日志</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr v-for="log in logs" :key="log.id" class="table-row">
|
||||
<td>{{ log.username || '-' }}</td>
|
||||
<td>{{ log.module || '-' }}</td>
|
||||
<td>
|
||||
<span class="status-tag" :class="getOperationClass(log.operation)">
|
||||
{{ getOperationText(log.operation) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="log-desc">
|
||||
<div class="desc-text">{{ log.requestUrl || '-' }}</div>
|
||||
</td>
|
||||
<td>{{ log.ipAddress || '-' }}</td>
|
||||
<td class="log-desc">
|
||||
<div class="desc-text">{{ truncateText(log.responseData, 50) }}</div>
|
||||
</td>
|
||||
<!-- <td>
|
||||
<span class="status-tag" :class="log.status === 'success' ? 'status-success' : 'status-failed'">
|
||||
{{ log.status === 'success' ? '成功' : '失败' }}
|
||||
</span>
|
||||
</td> -->
|
||||
<td>{{ formatTime(log?.createTime || '') || '-' }}</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<button class="btn-link btn-primary" @click="viewDetail(log)">详情</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="!loading && logs.length > 0" class="pagination">
|
||||
<div class="pagination-info">
|
||||
共 {{ total }} 条数据,每页 {{ pageSize }} 条
|
||||
</div>
|
||||
|
||||
<div class="pagination-controls">
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="handlePageChange(1)"
|
||||
>
|
||||
首页
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="handlePageChange(currentPage - 1)"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<div class="page-numbers">
|
||||
<button
|
||||
v-for="page in displayPages"
|
||||
:key="page"
|
||||
class="page-number"
|
||||
:class="{ active: page === currentPage }"
|
||||
@click="handlePageChange(page)"
|
||||
:disabled="page === -1"
|
||||
>
|
||||
{{ page === -1 ? '...' : page }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="handlePageChange(currentPage + 1)"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="handlePageChange(totalPages)"
|
||||
>
|
||||
末页
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pagination-jump">
|
||||
<span>跳转到</span>
|
||||
<input
|
||||
v-model.number="jumpPage"
|
||||
type="number"
|
||||
class="jump-input"
|
||||
@keyup.enter="handleJumpPage"
|
||||
/>
|
||||
<span>页</span>
|
||||
<button class="jump-btn" @click="handleJumpPage">跳转</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<div v-if="detailVisible" class="modal-overlay" @click.self="detailVisible = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">操作日志详情</h3>
|
||||
<button class="modal-close" @click="detailVisible = false">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div v-if="currentLog" class="log-detail">
|
||||
<div class="detail-section">
|
||||
<h4 class="detail-title">基本信息</h4>
|
||||
<div class="detail-content">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">操作人:</span>
|
||||
<span class="detail-value">{{ currentLog.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">操作模块:</span>
|
||||
<span class="detail-value">{{ currentLog.module || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">操作类型:</span>
|
||||
<span class="detail-value">
|
||||
<span class="status-tag" :class="getOperationClass(currentLog.operation)">
|
||||
{{ getOperationText(currentLog.operation) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div class="detail-row">
|
||||
<span class="detail-label">操作状态:</span>
|
||||
<span class="detail-value">
|
||||
<span class="status-tag" :class="currentLog.status === 'success' ? 'status-success' : 'status-failed'">
|
||||
{{ currentLog.status === 'success' ? '成功' : '失败' }}
|
||||
</span>
|
||||
</span>
|
||||
</div> -->
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">请求链接:</span>
|
||||
<span class="detail-value">{{ currentLog.requestUrl || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">IP地址:</span>
|
||||
<span class="detail-value">{{ currentLog.ipAddress || '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">日志内容:</span>
|
||||
<span class="detail-value">{{ currentLog.responseData || '-' }}</span>
|
||||
</div>
|
||||
<!-- <div class="detail-row">
|
||||
<span class="detail-label">耗时:</span>
|
||||
<span class="detail-value">{{ currentLog.duration }}ms</span>
|
||||
</div> -->
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">操作时间:</span>
|
||||
<span class="detail-value">{{ formatTime(currentLog?.createTime || '') || '-' }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentLog.requestParams">
|
||||
<h4 class="detail-title">请求参数</h4>
|
||||
<div class="detail-content">
|
||||
<pre class="code-block">{{ currentLog.requestParams }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentLog.responseData">
|
||||
<h4 class="detail-title">响应结果</h4>
|
||||
<div class="detail-content">
|
||||
<pre class="code-block">{{ currentLog.responseData }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section" v-if="currentLog.errorMessage">
|
||||
<h4 class="detail-title">错误信息</h4>
|
||||
<div class="detail-content">
|
||||
<div class="error-message">{{ currentLog.errorMessage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-primary" @click="detailVisible = false">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ElTabs, ElTabPane } from 'element-plus';
|
||||
import LoginLogs from './components/LoginLogs.vue';
|
||||
import OperationLogs from './components/OperationLogs.vue';
|
||||
import SystemConfig from './components/SystemConfig.vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
import { logApi } from '@/apis/system';
|
||||
import type { OperationLog } from '@/types/log';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
defineOptions({
|
||||
name: 'SystemLogsView'
|
||||
});
|
||||
const activeTab = ref('login');
|
||||
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
const searchModule = ref('');
|
||||
const operationType = ref('');
|
||||
const operationStatus = ref('');
|
||||
const startDate = ref('');
|
||||
const endDate = ref('');
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
const logs = ref<OperationLog[]>([]);
|
||||
const jumpPage = ref<number>();
|
||||
const detailVisible = ref(false);
|
||||
const currentLog = ref<OperationLog | null>(null);
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value) || 1);
|
||||
|
||||
// 计算显示的页码
|
||||
const displayPages = computed(() => {
|
||||
const pages: number[] = [];
|
||||
const maxDisplay = 7;
|
||||
|
||||
if (totalPages.value <= maxDisplay) {
|
||||
for (let i = 1; i <= totalPages.value; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
if (currentPage.value <= 4) {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push(-1);
|
||||
pages.push(totalPages.value);
|
||||
} else if (currentPage.value >= totalPages.value - 3) {
|
||||
pages.push(1);
|
||||
pages.push(-1);
|
||||
for (let i = totalPages.value - 4; i <= totalPages.value; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
pages.push(-1);
|
||||
for (let i = currentPage.value - 1; i <= currentPage.value + 1; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push(-1);
|
||||
pages.push(totalPages.value);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadLogs();
|
||||
});
|
||||
|
||||
function formatTime(time: string) {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取文本并添加省略号
|
||||
*/
|
||||
function truncateText(text?: string, maxLength = 20): string {
|
||||
if (!text) return '-';
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载操作日志
|
||||
*/
|
||||
async function loadLogs() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// 构建查询条件
|
||||
const query: OperationLog = {
|
||||
username: searchKeyword.value || undefined,
|
||||
module: searchModule.value || undefined,
|
||||
operation: operationType.value || undefined,
|
||||
status: operationStatus.value || undefined,
|
||||
startTime: startDate.value || undefined,
|
||||
endTime: endDate.value || undefined,
|
||||
};
|
||||
|
||||
const result = await logApi.getOperationLogPage(
|
||||
{pageNumber: currentPage.value, pageSize: pageSize.value},
|
||||
query
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
logs.value = result.pageDomain?.dataList || [];
|
||||
total.value = result.pageDomain?.pageParam.totalElements || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载操作日志失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function handleSearch() {
|
||||
currentPage.value = 1;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置查询条件
|
||||
*/
|
||||
function handleReset() {
|
||||
searchKeyword.value = '';
|
||||
searchModule.value = '';
|
||||
operationType.value = '';
|
||||
operationStatus.value = '';
|
||||
startDate.value = '';
|
||||
endDate.value = '';
|
||||
currentPage.value = 1;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作类型样式类
|
||||
*/
|
||||
function getOperationClass(type?: string) {
|
||||
const classMap: Record<string, string> = {
|
||||
'create': 'operation-create',
|
||||
'update': 'operation-update',
|
||||
'delete': 'operation-delete',
|
||||
'read': 'operation-read'
|
||||
};
|
||||
return classMap[type || ''] || 'operation-read';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作类型文本
|
||||
*/
|
||||
function getOperationText(type?: string) {
|
||||
const textMap: Record<string, string> = {
|
||||
'create': '新增',
|
||||
'update': '修改',
|
||||
'delete': '删除',
|
||||
'read': '查询'
|
||||
};
|
||||
return textMap[type || ''] || (type || '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看详情
|
||||
*/
|
||||
function viewDetail(log: OperationLog) {
|
||||
currentLog.value = { ...log };
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页码改变
|
||||
*/
|
||||
function handlePageChange(page: number) {
|
||||
if (page < 1 || page > totalPages.value || page === -1) return;
|
||||
currentPage.value = page;
|
||||
loadLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转页面
|
||||
*/
|
||||
function handleJumpPage() {
|
||||
if (!jumpPage.value || jumpPage.value < 1 || jumpPage.value > totalPages.value) {
|
||||
alert('请输入有效的页码');
|
||||
return;
|
||||
}
|
||||
currentPage.value = jumpPage.value;
|
||||
loadLogs();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.system-logs {
|
||||
.operation-logs {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.search-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-input,
|
||||
.search-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.search-select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-search,
|
||||
.btn-reset {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border: 1px solid #dcdfe6;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.log-table-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.log-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th {
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-desc {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.desc-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-success {
|
||||
background: #e1f3d8;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.status-failed {
|
||||
background: #fde2e2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.operation-create {
|
||||
background: #e1f3d8;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.operation-update {
|
||||
background: #fdf6ec;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.operation-delete {
|
||||
background: #fde2e2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.operation-read {
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-radius: 4px;
|
||||
min-width: 64px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
background: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-cell,
|
||||
.empty-cell {
|
||||
text-align: center;
|
||||
padding: 60px 20px !important;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-btn,
|
||||
.page-number {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pagination-jump {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
|
||||
.jump-input {
|
||||
width: 60px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.jump-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框样式
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #141F38;
|
||||
margin-bottom: 24px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 24px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
background: #fafafa;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #909399;
|
||||
min-width: 100px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #606266;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #303133;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef0f0;
|
||||
border: 1px solid #fde2e2;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -424,7 +424,19 @@ async function fetchDeptRoles() {
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
return await deptApi.getDeptByRole(currentDept.value);
|
||||
|
||||
const result = await deptApi.getDeptByRole(currentDept.value);
|
||||
|
||||
// 转换数据格式,将 UserDeptRoleVO 转换为与 SysRole 一致的格式
|
||||
if (result.success && result.dataList) {
|
||||
result.dataList = result.dataList.map(item => ({
|
||||
roleID: item.roleID,
|
||||
name: item.roleName, // roleName -> name
|
||||
description: item.roleDescription // roleDescription -> description
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 查看绑定角色
|
||||
|
||||
@@ -166,7 +166,12 @@
|
||||
right-title="已选权限"
|
||||
:fetch-available-api="fetchAllPermissions"
|
||||
:fetch-selected-api="fetchMenuPermissions"
|
||||
:filter-selected="filterPermissions"
|
||||
:item-config="{ id: 'permissionID', label: 'name', sublabel: 'code' }"
|
||||
:use-tree="true"
|
||||
:tree-transform="transformPermissionsToTree"
|
||||
:tree-props="{ children: 'children', label: 'displayName', id: 'permissionID' }"
|
||||
:only-leaf-selectable="true"
|
||||
unit-name="个"
|
||||
search-placeholder="搜索权限名称或编码..."
|
||||
@confirm="handlePermissionConfirm"
|
||||
@@ -505,11 +510,80 @@ async function fetchAllPermissions() {
|
||||
// 获取菜单已绑定权限的接口
|
||||
async function fetchMenuPermissions() {
|
||||
if (!currentMenu.value?.menuID) {
|
||||
return { success: true, dataList: [] };
|
||||
return {
|
||||
success: true,
|
||||
dataList: [],
|
||||
code: 200,
|
||||
message: '',
|
||||
login: true,
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
return await menuApi.getMenuPermission(currentMenu.value.menuID);
|
||||
}
|
||||
|
||||
// 过滤已选项
|
||||
function filterPermissions(available: any[], selected: any[]) {
|
||||
const selectedIds = new Set(selected.map(item => item.permissionID));
|
||||
return available.filter(item => !selectedIds.has(item.permissionID));
|
||||
}
|
||||
|
||||
// 将权限按模块ID构建树形结构
|
||||
function transformPermissionsToTree(flatData: any[]) {
|
||||
if (!flatData || flatData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 按模块分组
|
||||
const moduleMap = new Map<string, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
flatData.forEach(item => {
|
||||
const moduleID = item.moduleID;
|
||||
|
||||
if (!moduleID) {
|
||||
// 没有模块ID的权限作为根节点
|
||||
tree.push({
|
||||
...item,
|
||||
displayName: item.name,
|
||||
isModule: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!moduleMap.has(moduleID)) {
|
||||
// 创建模块节点
|
||||
moduleMap.set(moduleID, {
|
||||
permissionID: moduleID, // 使用moduleID作为ID,确保唯一性
|
||||
displayName: item.moduleName || moduleID,
|
||||
moduleID: moduleID,
|
||||
moduleName: item.moduleName,
|
||||
moduleCode: item.moduleCode,
|
||||
moduleDescription: item.moduleDescription,
|
||||
children: [],
|
||||
isModule: true // 标记这是模块节点
|
||||
});
|
||||
}
|
||||
|
||||
// 添加权限到模块的children中
|
||||
const moduleNode = moduleMap.get(moduleID);
|
||||
if (moduleNode) {
|
||||
moduleNode.children.push({
|
||||
...item,
|
||||
displayName: item.name,
|
||||
isModule: false // 标记这是权限节点
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 将所有模块添加到树中
|
||||
moduleMap.forEach(moduleNode => {
|
||||
tree.push(moduleNode);
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
// 查看绑定权限
|
||||
async function handleBindPermission(row: SysMenu) {
|
||||
currentMenu.value = row;
|
||||
@@ -535,8 +609,11 @@ async function handlePermissionConfirm(items: SysPermission[]) {
|
||||
const currentBoundResult = await menuApi.getMenuPermission(currentMenu.value.menuID);
|
||||
const currentBoundIds = (currentBoundResult.dataList || []).map(p => p.permissionID).filter((id): id is string => !!id);
|
||||
|
||||
// 新选择的权限ID
|
||||
const newSelectedIds = items.map(p => p.permissionID).filter((id): id is string => !!id);
|
||||
// 新选择的权限ID(过滤掉模块节点,只保留权限节点)
|
||||
const newSelectedIds = items
|
||||
.filter((item: any) => !item.isModule) // 只保留权限节点,过滤模块节点
|
||||
.map(p => p.permissionID)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
// 找出需要绑定的权限(新增的)
|
||||
const permissionsToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id));
|
||||
@@ -546,7 +623,8 @@ async function handlePermissionConfirm(items: SysPermission[]) {
|
||||
|
||||
// 构建需要绑定的权限对象数组
|
||||
if (permissionsToBind.length > 0) {
|
||||
const permissionsToBindObjects = items.filter(p => p.permissionID && permissionsToBind.includes(p.permissionID));
|
||||
const permissionsToBindObjects = items
|
||||
.filter((item: any) => !item.isModule && item.permissionID && permissionsToBind.includes(item.permissionID));
|
||||
|
||||
const bindMenu = {
|
||||
...currentMenu.value,
|
||||
@@ -569,6 +647,8 @@ async function handlePermissionConfirm(items: SysPermission[]) {
|
||||
}
|
||||
|
||||
ElMessage.success('权限绑定保存成功');
|
||||
bindPermissionDialogVisible.value = false;
|
||||
resetBindList();
|
||||
|
||||
// 刷新菜单列表
|
||||
await loadMenuList();
|
||||
|
||||
@@ -286,7 +286,12 @@
|
||||
right-title="已选菜单"
|
||||
:fetch-available-api="fetchAllMenus"
|
||||
:fetch-selected-api="fetchPermissionMenus"
|
||||
:filter-selected="filterMenus"
|
||||
:item-config="{ id: 'menuID', label: 'name', sublabel: 'url' }"
|
||||
:use-tree="true"
|
||||
:tree-transform="transformMenusToTree"
|
||||
:tree-props="{ children: 'children', label: 'name', id: 'menuID' }"
|
||||
:only-leaf-selectable="true"
|
||||
unit-name="个"
|
||||
search-placeholder="搜索菜单名称..."
|
||||
@confirm="handleMenuConfirm"
|
||||
@@ -301,6 +306,7 @@
|
||||
right-title="已选角色"
|
||||
:fetch-available-api="fetchAllRoles"
|
||||
:fetch-selected-api="fetchPermissionRoles"
|
||||
:filter-selected="filterRoles"
|
||||
:item-config="{ id: 'roleID', label: 'name', sublabel: 'description' }"
|
||||
unit-name="个"
|
||||
search-placeholder="搜索角色名称..."
|
||||
@@ -622,7 +628,11 @@ async function fetchPermissionMenus() {
|
||||
success: result.success,
|
||||
login: result.login ?? true,
|
||||
auth: result.auth ?? true,
|
||||
dataList: result.data?.menus || []
|
||||
dataList: result.dataList?.map(item => ({
|
||||
...item,
|
||||
name: item.menuName,
|
||||
url: item.menuUrl
|
||||
})) || []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -651,10 +661,115 @@ async function fetchPermissionRoles() {
|
||||
success: result.success,
|
||||
login: result.login ?? true,
|
||||
auth: result.auth ?? true,
|
||||
dataList: result.data?.roles || []
|
||||
dataList: result.dataList?.map(item => ({
|
||||
...item,
|
||||
name: item.roleName,
|
||||
description: item.roleDescription
|
||||
})) || []
|
||||
};
|
||||
}
|
||||
|
||||
// 过滤已选菜单项
|
||||
function filterMenus(available: any[], selected: any[]) {
|
||||
const selectedIds = new Set(selected.map(item => item.menuID));
|
||||
return available.filter(item => !selectedIds.has(item.menuID));
|
||||
}
|
||||
|
||||
// 过滤已选角色项
|
||||
function filterRoles(available: any[], selected: any[]) {
|
||||
const selectedIds = new Set(selected.map(item => item.roleID));
|
||||
return available.filter(item => !selectedIds.has(item.roleID));
|
||||
}
|
||||
|
||||
// 将菜单扁平数据转换为树形结构(根据parentID构建)
|
||||
function transformMenusToTree(flatData: any[]): any[] {
|
||||
if (!flatData || flatData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tree: any[] = [];
|
||||
const map = new Map<string, any>();
|
||||
const maxDepth = flatData.length; // 最多遍历len层
|
||||
|
||||
// 初始化所有节点
|
||||
flatData.forEach(item => {
|
||||
if (item.menuID) {
|
||||
map.set(item.menuID, { ...item, children: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 循环构建树结构,最多遍历maxDepth次
|
||||
for (let depth = 0; depth < maxDepth; depth++) {
|
||||
let hasChanges = false;
|
||||
|
||||
flatData.forEach(item => {
|
||||
if (!item.menuID) return;
|
||||
|
||||
const node = map.get(item.menuID);
|
||||
if (!node) return;
|
||||
|
||||
// 如果节点已经在树中,跳过
|
||||
if (isMenuNodeInTree(node, tree)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.parentID || item.parentID === '0' || item.parentID === '') {
|
||||
// 根节点
|
||||
if (!isMenuNodeInTree(node, tree)) {
|
||||
tree.push(node);
|
||||
hasChanges = true;
|
||||
}
|
||||
} else {
|
||||
// 查找父节点
|
||||
const parent = map.get(item.parentID);
|
||||
if (parent && isMenuNodeInTree(parent, tree)) {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
if (!parent.children.includes(node)) {
|
||||
parent.children.push(node);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有变化,说明树构建完成
|
||||
if (!hasChanges) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空的children数组
|
||||
function cleanEmptyChildren(nodes: any[]) {
|
||||
nodes.forEach(node => {
|
||||
if (node.children && node.children.length === 0) {
|
||||
delete node.children;
|
||||
} else if (node.children && node.children.length > 0) {
|
||||
cleanEmptyChildren(node.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cleanEmptyChildren(tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
// 检查菜单节点是否已经在树中
|
||||
function isMenuNodeInTree(node: any, tree: any[]): boolean {
|
||||
for (const treeNode of tree) {
|
||||
if (treeNode.menuID === node.menuID) {
|
||||
return true;
|
||||
}
|
||||
if (treeNode.children && treeNode.children.length > 0) {
|
||||
if (isMenuNodeInTree(node, treeNode.children)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 绑定菜单
|
||||
async function handleBindMenu(permission: SysPermission) {
|
||||
currentPermission.value = permission;
|
||||
@@ -708,6 +823,8 @@ async function handleMenuConfirm(items: SysMenu[]) {
|
||||
}
|
||||
|
||||
ElMessage.success('菜单绑定保存成功');
|
||||
bindMenuDialogVisible.value = false;
|
||||
resetBindList();
|
||||
} catch (error) {
|
||||
console.error('保存菜单绑定失败:', error);
|
||||
ElMessage.error('保存菜单绑定失败');
|
||||
@@ -757,6 +874,8 @@ async function handleRoleConfirm(items: SysRole[]) {
|
||||
}
|
||||
|
||||
ElMessage.success('角色绑定保存成功');
|
||||
bindRoleDialogVisible.value = false;
|
||||
resetBindList();
|
||||
} catch (error) {
|
||||
console.error('保存角色绑定失败:', error);
|
||||
ElMessage.error('保存角色绑定失败');
|
||||
|
||||
@@ -102,9 +102,14 @@
|
||||
:title="`绑定权限 - ${currentRole?.name || ''}`"
|
||||
left-title="可选权限"
|
||||
right-title="已选权限"
|
||||
:available-items="availablePermissions"
|
||||
:initial-target-items="initialBoundPermissions"
|
||||
:fetch-available-api="fetchAllPermissions"
|
||||
:fetch-selected-api="fetchRolePermissions"
|
||||
:filter-selected="filterPermissions"
|
||||
:item-config="{ id: 'permissionID', label: 'name', sublabel: 'code' }"
|
||||
:use-tree="true"
|
||||
:tree-transform="transformPermissionsToTree"
|
||||
:tree-props="{ children: 'children', label: 'displayName', id: 'permissionID' }"
|
||||
:only-leaf-selectable="true"
|
||||
unit-name="个"
|
||||
search-placeholder="搜索权限名称或编码..."
|
||||
@confirm="handlePermissionConfirm"
|
||||
@@ -124,7 +129,7 @@ import { GenericSelector } from '@/components/base';
|
||||
defineOptions({
|
||||
name: 'RoleManageView'
|
||||
});
|
||||
import { ref, onMounted, reactive, computed } from 'vue';
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
|
||||
@@ -134,9 +139,10 @@ const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
// 权限绑定相关数据
|
||||
const permissionList = ref<SysPermission[]>([]);
|
||||
// 移除:改为由 GenericSelector 通过 API 加载
|
||||
// const permissionList = ref<SysPermission[]>([]);
|
||||
// const initialBoundPermissions = ref<SysPermission[]>([]);
|
||||
const currentRole = ref<SysRole | null>(null);
|
||||
const initialBoundPermissions = ref<SysPermission[]>([]);
|
||||
|
||||
// 对话框状态
|
||||
const dialogVisible = ref(false);
|
||||
@@ -251,43 +257,105 @@ function resetForm() {
|
||||
});
|
||||
}
|
||||
|
||||
// 计算可选权限(过滤掉已绑定的)
|
||||
const availablePermissions = computed(() => {
|
||||
const boundIds = new Set(initialBoundPermissions.value.map(p => p.permissionID));
|
||||
return permissionList.value.filter(p => !boundIds.has(p.permissionID));
|
||||
});
|
||||
// 获取所有权限的API函数
|
||||
async function fetchAllPermissions() {
|
||||
const permission: SysPermission = {
|
||||
permissionID: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
return await permissionApi.getPermissionList(permission);
|
||||
}
|
||||
|
||||
// 获取角色已绑定权限的API函数
|
||||
async function fetchRolePermissions() {
|
||||
if (!currentRole.value || !currentRole.value.roleID) {
|
||||
return {
|
||||
success: true,
|
||||
dataList: [],
|
||||
code: 200,
|
||||
message: '',
|
||||
login: true,
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
return await roleApi.getRolePermission({
|
||||
roleID: currentRole.value.roleID
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤已选项
|
||||
function filterPermissions(available: any[], selected: any[]) {
|
||||
const selectedIds = new Set(selected.map(item => item.permissionID));
|
||||
return available.filter(item => !selectedIds.has(item.permissionID));
|
||||
}
|
||||
|
||||
// 将权限按模块ID构建树形结构
|
||||
function transformPermissionsToTree(flatData: any[]) {
|
||||
if (!flatData || flatData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 按模块分组
|
||||
const moduleMap = new Map<string, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
flatData.forEach(item => {
|
||||
const moduleID = item.moduleID;
|
||||
|
||||
if (!moduleID) {
|
||||
// 没有模块ID的权限作为根节点
|
||||
tree.push({
|
||||
...item,
|
||||
displayName: item.name,
|
||||
isModule: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!moduleMap.has(moduleID)) {
|
||||
// 创建模块节点
|
||||
moduleMap.set(moduleID, {
|
||||
permissionID: moduleID, // 使用moduleID作为ID,确保唯一性
|
||||
displayName: item.moduleName || moduleID,
|
||||
moduleID: moduleID,
|
||||
moduleName: item.moduleName,
|
||||
moduleCode: item.moduleCode,
|
||||
moduleDescription: item.moduleDescription,
|
||||
children: [],
|
||||
isModule: true // 标记这是模块节点
|
||||
});
|
||||
}
|
||||
|
||||
// 添加权限到模块的children中
|
||||
const moduleNode = moduleMap.get(moduleID);
|
||||
if (moduleNode) {
|
||||
moduleNode.children.push({
|
||||
...item,
|
||||
displayName: item.name,
|
||||
isModule: false // 标记这是权限节点
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 将所有模块添加到树中
|
||||
moduleMap.forEach(moduleNode => {
|
||||
tree.push(moduleNode);
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
// 查看绑定权限
|
||||
async function handleBindPermission(row: SysRole) {
|
||||
currentRole.value = row;
|
||||
|
||||
try {
|
||||
// 获取所有权限
|
||||
let permission: SysPermission = {
|
||||
permissionID: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
const permissionResult = await permissionApi.getPermissionList(permission);
|
||||
permissionList.value = permissionResult.dataList || [];
|
||||
|
||||
// 获取已绑定的权限
|
||||
const bindingResult = await roleApi.getRolePermission({
|
||||
roleID: row.roleID
|
||||
});
|
||||
initialBoundPermissions.value = bindingResult.dataList || [];
|
||||
|
||||
bindPermissionDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取权限绑定信息失败:', error);
|
||||
ElMessage.error('获取权限绑定信息失败');
|
||||
}
|
||||
bindPermissionDialogVisible.value = true;
|
||||
// 不再需要预加载数据,由 GenericSelector 在打开时自动调用 API 加载
|
||||
}
|
||||
|
||||
// 重置绑定列表
|
||||
function resetBindList() {
|
||||
initialBoundPermissions.value = [];
|
||||
currentRole.value = null;
|
||||
}
|
||||
|
||||
@@ -301,11 +369,20 @@ async function handlePermissionConfirm(items: SysPermission[]) {
|
||||
try {
|
||||
submitting.value = true;
|
||||
|
||||
// 获取当前已绑定的权限ID
|
||||
const currentBoundIds = initialBoundPermissions.value.map(p => p.permissionID).filter((id): id is string => !!id);
|
||||
// 重新获取当前已绑定的权限
|
||||
const bindingResult = await roleApi.getRolePermission({
|
||||
roleID: currentRole.value.roleID
|
||||
});
|
||||
const currentBoundPermissions = bindingResult.dataList || [];
|
||||
|
||||
// 新选择的权限ID
|
||||
const newSelectedIds = items.map(p => p.permissionID).filter((id): id is string => !!id);
|
||||
// 获取当前已绑定的权限ID
|
||||
const currentBoundIds = currentBoundPermissions.map(p => p.permissionID).filter((id): id is string => !!id);
|
||||
|
||||
// 新选择的权限ID(过滤掉模块节点,只保留权限节点)
|
||||
const newSelectedIds = items
|
||||
.filter((item: any) => !item.isModule) // 只保留权限节点,过滤模块节点
|
||||
.map(p => p.permissionID)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
// 找出需要绑定的权限(新增的)
|
||||
const permissionsToBind = newSelectedIds.filter(id => !currentBoundIds.includes(id));
|
||||
@@ -316,7 +393,7 @@ async function handlePermissionConfirm(items: SysPermission[]) {
|
||||
// 构建需要绑定的权限对象数组
|
||||
if (permissionsToBind.length > 0) {
|
||||
const permissionsToBindObjects = permissionsToBind.map(permissionID => {
|
||||
const permission = permissionList.value.find(p => p.permissionID === permissionID);
|
||||
const permission = items.find(p => p.permissionID === permissionID);
|
||||
return permission || { permissionID: permissionID };
|
||||
});
|
||||
|
||||
@@ -331,7 +408,7 @@ async function handlePermissionConfirm(items: SysPermission[]) {
|
||||
// 构建需要解绑的权限对象数组
|
||||
if (permissionsToUnbind.length > 0) {
|
||||
const permissionsToUnbindObjects = permissionsToUnbind.map(permissionID => {
|
||||
const permission = permissionList.value.find(p => p.permissionID === permissionID);
|
||||
const permission = currentBoundPermissions.find(p => p.permissionID === permissionID);
|
||||
return permission || { permissionID: permissionID };
|
||||
});
|
||||
|
||||
@@ -344,6 +421,8 @@ async function handlePermissionConfirm(items: SysPermission[]) {
|
||||
}
|
||||
|
||||
ElMessage.success('权限绑定保存成功');
|
||||
bindPermissionDialogVisible.value = false;
|
||||
resetBindList();
|
||||
|
||||
// 刷新角色列表
|
||||
await loadRoleList();
|
||||
|
||||
@@ -319,16 +319,17 @@
|
||||
:title="selectorMode === 'add' ? '添加人员' : '删除人员'"
|
||||
:left-title="selectorMode === 'add' ? '可添加人员' : '当前人员'"
|
||||
:right-title="selectorMode === 'add' ? '待添加人员' : '待删除人员'"
|
||||
:available-items="selectorMode === 'remove' ? availableUsers : []"
|
||||
:initial-target-items="[]"
|
||||
:fetch-available-api="selectorMode === 'add' ? fetchAllUsers : fetchCurrentUsers"
|
||||
:fetch-selected-api="selectorMode === 'add' ? fetchTaskUsers : undefined"
|
||||
:filter-selected="filterUsers"
|
||||
:loading="saving"
|
||||
:item-config="{ id: 'id', label: 'username', sublabel: 'deptName' }"
|
||||
:use-tree="true"
|
||||
:tree-transform="transformUsersToTree"
|
||||
:tree-props="{ children: 'children', label: 'displayName', id: 'id' }"
|
||||
:only-leaf-selectable="true"
|
||||
unit-name="人"
|
||||
search-placeholder="搜索人员..."
|
||||
:use-pagination="selectorMode === 'add'"
|
||||
:fetch-api="selectorMode === 'add' ? userApi.getUserPage : undefined"
|
||||
:filter-params="userFilterParams"
|
||||
:page-size="20"
|
||||
@confirm="handleUserSelectConfirm"
|
||||
@cancel="closeSelectorModal"
|
||||
/>
|
||||
@@ -388,9 +389,7 @@ const deleting = ref(false);
|
||||
const managingTask = ref<LearningTask | null>(null);
|
||||
const selectorMode = ref<'add' | 'remove'>('add');
|
||||
const currentUsers = ref<UserVO[]>([]);
|
||||
const availableUsers = ref<UserVO[]>([]);
|
||||
const saving = ref(false);
|
||||
const userFilterParams = ref<any>({});
|
||||
|
||||
// 计算显示的页码
|
||||
const displayPages = computed(() => {
|
||||
@@ -523,7 +522,9 @@ function handleEdit(task: LearningTask) {
|
||||
emit('edit', task);
|
||||
}
|
||||
|
||||
function handleStatistics(task: LearningTask) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function handleStatistics(_task: LearningTask) {
|
||||
// TODO: 实现统计功能
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -565,30 +566,171 @@ function closeUserList() {
|
||||
currentUsers.value = [];
|
||||
}
|
||||
|
||||
// 获取所有用户的API函数
|
||||
async function fetchAllUsers() {
|
||||
const filter: any = { username: undefined };
|
||||
return await userApi.getUserList(filter);
|
||||
}
|
||||
|
||||
// 获取当前任务用户的API函数(用于删除模式)
|
||||
async function fetchCurrentUsers() {
|
||||
// 删除模式时,左侧显示当前任务的用户列表
|
||||
return {
|
||||
success: true,
|
||||
dataList: currentUsers.value,
|
||||
code: 200,
|
||||
message: '',
|
||||
login: true,
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
|
||||
// 获取任务已分配用户的API函数
|
||||
async function fetchTaskUsers() {
|
||||
if (!managingTask.value || !managingTask.value.taskID) {
|
||||
return {
|
||||
success: true,
|
||||
dataList: [],
|
||||
code: 200,
|
||||
message: '',
|
||||
login: true,
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
|
||||
// 从任务详情中获取用户列表
|
||||
const result = await learningTaskApi.getTaskUsers(managingTask.value.taskID);
|
||||
if (result.success && result.dataList) {
|
||||
// 将返回的用户列表转换为UserVO格式
|
||||
const users = (result.dataList || []).map((item: any) => ({
|
||||
id: item.userID,
|
||||
username: item.username,
|
||||
deptID: item.deptID,
|
||||
deptName: item.deptName,
|
||||
parentDeptID: item.parentDeptID
|
||||
}));
|
||||
return {
|
||||
success: true,
|
||||
dataList: users,
|
||||
code: 200,
|
||||
message: '',
|
||||
login: true,
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
dataList: [],
|
||||
code: result.code || 500,
|
||||
message: result.message || '获取任务用户失败',
|
||||
login: true,
|
||||
auth: true
|
||||
};
|
||||
}
|
||||
|
||||
// 过滤已选用户
|
||||
function filterUsers(available: any[], selected: any[]) {
|
||||
const selectedIds = new Set(selected.map(item => item.id));
|
||||
return available.filter(item => !selectedIds.has(item.id));
|
||||
}
|
||||
|
||||
// 将用户扁平数据转换为树形结构(按部门分组)
|
||||
function transformUsersToTree(flatData: any[]): any[] {
|
||||
if (!flatData || flatData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 按部门分组
|
||||
const deptMap = new Map<string, any>();
|
||||
const tree: any[] = [];
|
||||
|
||||
flatData.forEach(item => {
|
||||
const deptID = item.deptID || 'unknown';
|
||||
const deptName = item.deptName || '未分配部门';
|
||||
|
||||
if (!deptMap.has(deptID)) {
|
||||
// 创建部门节点
|
||||
deptMap.set(deptID, {
|
||||
id: `dept_${deptID}`, // 使用特殊前缀避免与用户ID冲突
|
||||
displayName: deptName,
|
||||
deptID: deptID,
|
||||
deptName: deptName,
|
||||
parentDeptID: item.parentDeptID,
|
||||
children: [],
|
||||
isDept: true // 标记这是部门节点
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户到部门的children中
|
||||
const deptNode = deptMap.get(deptID);
|
||||
if (deptNode) {
|
||||
deptNode.children.push({
|
||||
...item,
|
||||
displayName: item.username || item.id,
|
||||
isDept: false // 标记这是用户节点
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 构建部门层级关系
|
||||
const deptNodes = Array.from(deptMap.values());
|
||||
const deptTreeMap = new Map<string, any>();
|
||||
|
||||
// 初始化所有部门节点
|
||||
deptNodes.forEach(dept => {
|
||||
deptTreeMap.set(dept.deptID, { ...dept });
|
||||
});
|
||||
|
||||
// 构建部门树
|
||||
deptNodes.forEach(dept => {
|
||||
const node = deptTreeMap.get(dept.deptID);
|
||||
if (!node) return;
|
||||
|
||||
if (!dept.parentDeptID || dept.parentDeptID === '0' || dept.parentDeptID === '') {
|
||||
// 根部门
|
||||
tree.push(node);
|
||||
} else {
|
||||
// 子部门
|
||||
const parent = deptTreeMap.get(dept.parentDeptID);
|
||||
if (parent) {
|
||||
// 将用户节点暂存
|
||||
const users = node.children || [];
|
||||
node.children = [];
|
||||
|
||||
// 添加部门到父部门
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(node);
|
||||
|
||||
// 将用户添加回来(放在部门子节点之后)
|
||||
node.children = users;
|
||||
} else {
|
||||
// 找不到父节点,作为根节点
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
// 显示添加人员选择器
|
||||
function showAddUserSelector() {
|
||||
selectorMode.value = 'add';
|
||||
|
||||
// 设置过滤参数(可以过滤掉已分配的用户,但在组件内部会自动过滤)
|
||||
userFilterParams.value = {};
|
||||
|
||||
showUserSelector.value = true;
|
||||
}
|
||||
|
||||
// 显示删除人员选择器
|
||||
function showRemoveUserSelector() {
|
||||
selectorMode.value = 'remove';
|
||||
|
||||
// 左侧显示当前用户
|
||||
availableUsers.value = [...currentUsers.value];
|
||||
|
||||
showUserSelector.value = true;
|
||||
}
|
||||
|
||||
// 关闭选择器弹窗
|
||||
function closeSelectorModal() {
|
||||
showUserSelector.value = false;
|
||||
availableUsers.value = [];
|
||||
}
|
||||
|
||||
// 处理用户选择器确认事件
|
||||
|
||||
Reference in New Issue
Block a user