Files
ApiServer-Web-admin_dashboa…/src/views/user/UserList.vue
T
shiran 84769954c4
Build and Deploy Vue3 / build (push) Successful in 1m23s
Build and Deploy Vue3 / deploy (push) Successful in 36s
feat(system): 管理员权限页重构与用户选择器升级
- 重构 PermissionAdmin.vue:卡片式权限类型选择、拥有者名称解析、过期标识

- getUserList API 改用 params 对象,支持 is_admin 筛选

- UserList 新增管理员/普通用户身份筛选

- UserListSelector 重构为卡片网格布局,选中角标、动画提示条

- UserSelector 搜索栏加入身份筛选

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 17:59:24 +08:00

2007 lines
55 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="user-list-container">
<!-- 搜索和用户列表 -->
<el-card class="main-container" shadow="never">
<!-- 搜索和操作栏 -->
<div class="filter-section">
<!-- 第一行搜索区域 -->
<div class="filter-row search-row">
<div class="search-group">
<span class="search-label">关键字</span>
<el-input
v-model="queryParams.key"
placeholder="请输入用户名/邮箱"
clearable
class="search-input"
/>
</div>
<div class="search-group">
<span class="search-label">身份</span>
<el-select v-model="queryParams.is_admin" placeholder="全部" clearable class="search-input-small" style="width: 110px">
<el-option label="管理员" :value="true" />
<el-option label="普通用户" :value="false" />
</el-select>
</div>
<div class="search-group">
<span class="search-label">用户ID</span>
<el-input
:model-value="jumpUserName || (jumpUserId ? jumpUserId : '')"
placeholder="输入ID跳转"
readonly
clearable
class="search-input-small"
style="cursor:pointer"
@click="showJumpUserSelector = true"
@clear="jumpUserId = ''; jumpUserName = ''"
/>
</div>
<div class="search-buttons">
<el-button type="warning" @click="handleJumpToUser">
<el-icon><Position /></el-icon><span class="btn-text">跳转</span>
</el-button>
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon><span class="btn-text">查询</span>
</el-button>
<el-button @click="resetQuery">
<el-icon><RefreshLeft /></el-icon><span class="btn-text">重置</span>
</el-button>
<el-button type="success" @click="fetchUserList">
<el-icon><Refresh /></el-icon><span class="btn-text">刷新</span>
</el-button>
</div>
</div>
<!-- 第二行操作栏 -->
<div class="filter-row action-row">
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon><span class="btn-text">新增用户</span>
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon><span class="btn-text">批量删除</span>
</el-button>
</div>
</div>
</div>
<!-- 用户列表 -->
<div class="table-section">
<!-- 骨架屏 -->
<div v-if="loading" class="skeleton-container">
<div v-for="i in 5" :key="i" class="skeleton-row">
<div class="skeleton-cell skeleton-checkbox"></div>
<div class="skeleton-cell skeleton-id"></div>
<div class="skeleton-cell skeleton-user">
<div class="skeleton-avatar"></div>
<div class="skeleton-text-group">
<div class="skeleton-text skeleton-text-primary"></div>
<div class="skeleton-text skeleton-text-secondary"></div>
</div>
</div>
<div class="skeleton-cell skeleton-avatar"></div>
<div class="skeleton-cell skeleton-phone"></div>
<div class="skeleton-cell skeleton-realname">
<div class="skeleton-text skeleton-text-primary"></div>
<div class="skeleton-text skeleton-text-secondary"></div>
</div>
<div class="skeleton-cell skeleton-group"></div>
<div class="skeleton-cell skeleton-status"></div>
<div class="skeleton-cell skeleton-time"></div>
<div class="skeleton-cell skeleton-action"></div>
</div>
</div>
<!-- 移动端卡片列表 -->
<div v-else class="mobile-card-list">
<div v-for="row in userList" :key="row.UserId" class="user-card">
<div class="card-header">
<el-checkbox v-model="row.selected" @change="handleCardSelect(row)" />
<el-avatar :size="48" :src="row.avatarUrl" class="card-avatar">
<el-icon :size="24"><User /></el-icon>
</el-avatar>
<div class="card-user-info">
<div class="card-username">{{ row.UserName }}</div>
<div class="card-email">{{ row.Email || '未设置邮箱' }}</div>
</div>
<el-tag :type="row.IsDeleted ? 'danger' : 'success'" size="small">
{{ row.IsDeleted ? '已删除' : '正常' }}
</el-tag>
</div>
<div class="card-body">
<div class="card-info-row">
<span class="card-label">用户ID:</span>
<span class="card-value">{{ row.UserId }}</span>
</div>
<div class="card-info-row">
<span class="card-label">手机号:</span>
<span class="card-value">{{ row.Phone || '未设置' }}</span>
</div>
<div class="card-info-row">
<span class="card-label">用户组:</span>
<span class="card-value">{{ row.UserGroup?.Name || '默认用户组' }}</span>
</div>
<div class="card-info-row">
<span class="card-label">实名:</span>
<span class="card-value">{{ row.RealName?.Name || '未实名' }}</span>
</div>
<div class="card-info-row">
<span class="card-label">注册时间:</span>
<span class="card-value">{{ formatDate(row.CreatedAt) }}</span>
</div>
</div>
<div class="card-footer">
<el-button type="primary" size="small" @click="handleUserDetail(row)">详情</el-button>
<el-dropdown trigger="click" @command="(command) => handleCommand(command, row)">
<el-button size="small">更多<el-icon><ArrowDown /></el-icon></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="edit">编辑用户</el-dropdown-item>
<el-dropdown-item command="avatar">修改头像</el-dropdown-item>
<el-dropdown-item command="password">修改密码</el-dropdown-item>
<el-dropdown-item command="group">修改用户组</el-dropdown-item>
<el-dropdown-item command="realname">实名信息</el-dropdown-item>
<el-dropdown-item command="balance">余额管理</el-dropdown-item>
<el-dropdown-item command="delete" divided>删除用户</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- 数据表格PC端 -->
<el-table
v-if="!loading"
:data="userList"
@selection-change="handleSelectionChange"
class="desktop-table"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column type="selection" width="55" fixed="left" />
<el-table-column prop="UserId" label="用户ID" width="100" fixed="left" />
<el-table-column label="用户信息" min-width="100">
<template #default="{ row }">
<div class="user-info">
<div class="user-detail">
<div class="username">{{ row.UserName }}</div>
<div class="email">{{ row.Email || '未设置' }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="CoverID" label="头像" width="100">
<template #default="{ row }">
<el-avatar :size="40" :src="row.avatarUrl">
<el-icon :size="20"><User /></el-icon>
</el-avatar>
</template>
</el-table-column>
<el-table-column prop="Phone" label="手机号码" width="130">
<template #default="{ row }">
{{ row.Phone || '未设置' }}
</template>
</el-table-column>
<el-table-column label="实名信息" width="150">
<template #default="{ row }">
<div v-if="row.RealName && row.RealName.Name">
<div class="real-name">{{ row.RealName.Name }}</div>
<div class="id-card">{{ row.RealName.IdCard || '未设置' }}</div>
</div>
<span v-else class="text-gray">未实名</span>
</template>
</el-table-column>
<el-table-column label="用户组" width="120">
<template #default="{ row }">
<div class="group_name">{{ row.UserGroup?.Name || '默认用户组' }}</div>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.IsDeleted ? 'danger' : 'success'">
{{ row.IsDeleted ? '已删除' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="CreatedAt" label="注册时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleUserDetail(row)">
<el-icon><View /></el-icon>详情
</el-button>
<el-dropdown trigger="click" @command="(command) => handleCommand(command, row)">
<el-button type="primary" link>
更多<el-icon><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="edit">编辑用户</el-dropdown-item>
<el-dropdown-item command="avatar">修改头像</el-dropdown-item>
<el-dropdown-item command="password">修改密码</el-dropdown-item>
<el-dropdown-item command="group">修改用户组</el-dropdown-item>
<el-dropdown-item command="realname">实名信息</el-dropdown-item>
<el-dropdown-item command="balance">余额管理</el-dropdown-item>
<el-dropdown-item command="loginHistory">登录记录</el-dropdown-item>
<el-dropdown-item command="operationHistory">操作记录</el-dropdown-item>
<el-dropdown-item command="simulateLogin">模拟登录</el-dropdown-item>
<el-dropdown-item command="delete" divided>删除用户</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.count"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
background
class="pagination"
/>
</el-card>
<!-- 用户编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增用户' : '编辑用户'"
width="600px"
append-to-body
>
<el-form
ref="userFormRef"
:model="userForm"
:rules="userRules"
label-width="100px"
>
<!-- 新增用户表单 -->
<template v-if="dialogType === 'add'">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="userForm.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item label="手机号" prop="Phone">
<el-input v-model="userForm.Phone" placeholder="请输入手机号" />
</el-form-item>
</template>
<!-- 编辑用户表单 -->
<template v-else>
<el-form-item label="用户ID">
<el-input v-model="userForm.user_id" disabled />
</el-form-item>
<el-form-item label="用户名" prop="user_name">
<el-input v-model="userForm.user_name" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="userForm.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="userForm.sex" placeholder="请选择性别" style="width: 100%">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="userForm.age" :min="0" :max="150" placeholder="请输入年龄" style="width: 100%" />
</el-form-item>
<el-form-item label="推介人" prop="recommend_id">
<div class="recommend-user-selector">
<el-input
v-model="selectedRecommendUserName"
placeholder="点击选择推介用户"
readonly
@click="showUserSelector = true"
>
<template #append>
<el-button @click="showUserSelector = true">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<el-button
v-if="userForm.recommend_id"
type="danger"
link
@click="clearRecommendUser"
class="clear-btn"
>
清除
</el-button>
</div>
</el-form-item>
</template>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 推介用户选择器 -->
<UserListSelector
v-model="showUserSelector"
:current-user-id="userForm.recommend_id"
@confirm="handleRecommendUserConfirm"
/>
<!-- 筛选用户ID选择器 -->
<UserListSelector
v-model="showJumpUserSelector"
@confirm="u => { jumpUserId = String(u.user_id); jumpUserName = u.user_name || `用户 #${u.user_id}` }"
/>
<!-- 修改头像对话框 -->
<el-dialog
v-model="avatarDialogVisible"
title="修改用户头像"
width="400px"
append-to-body
>
<el-form :model="avatarForm" label-width="80px">
<el-form-item label="用户ID">
<el-input v-model="avatarForm.user_id" disabled />
</el-form-item>
<el-form-item label="当前头像">
<div class="avatar-preview-container">
<div class="avatar-preview" @click="showAvatarSelector = true">
<el-avatar
:size="80"
:src="currentAvatarUrl"
fit="cover"
>
<el-icon :size="40"><User /></el-icon>
</el-avatar>
<div class="avatar-overlay">
<el-icon :size="24"><Edit /></el-icon>
<span>点击选择</span>
</div>
</div>
<div class="avatar-info">
<p class="avatar-id">头像ID: {{ avatarForm.cover_id || '未设置' }}</p>
<el-button
type="primary"
size="small"
@click="showAvatarSelector = true"
>
选择头像
</el-button>
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="avatarDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAvatarModify">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 修改密码对话框 -->
<el-dialog
v-model="passwordDialogVisible"
title="修改用户密码"
width="400px"
append-to-body
>
<el-form :model="passwordForm" label-width="80px">
<el-form-item label="用户ID">
<el-input v-model="passwordForm.user_id" disabled />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="passwordForm.password" type="password" placeholder="请输入新密码" show-password />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="passwordDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPasswordModify">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 修改用户组对话框 -->
<el-dialog
v-model="groupDialogVisible"
title="修改用户组"
width="500px"
append-to-body
>
<el-form :model="groupForm" label-width="100px">
<el-form-item label="用户ID">
<el-input v-model="groupForm.user_id" disabled />
</el-form-item>
<el-form-item label="当前用户组">
<el-input v-model="groupForm.current_group_name" disabled placeholder="未分配用户组" />
</el-form-item>
<el-form-item label="选择用户组">
<div class="group-selector-wrapper">
<el-input
v-model="selectedGroupName"
placeholder="点击选择用户组"
readonly
@click="showGroupSelector = true"
>
<template #append>
<el-button @click="showGroupSelector = true">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<el-button
v-if="groupForm.user_group_id"
type="danger"
link
@click="clearSelectedGroup"
class="clear-btn"
>
清除
</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="groupDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitGroupModify">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 用户组选择器 -->
<UserGroupSelector
v-model="showGroupSelector"
:current-group-id="groupForm.user_group_id"
@confirm="handleGroupSelectorConfirm"
/>
<!-- 实名信息对话框 -->
<el-dialog
v-model="realnameDialogVisible"
title="修改实名信息"
width="500px"
append-to-body
>
<el-form :model="realnameForm" label-width="100px">
<el-form-item label="用户ID">
<el-input v-model="realnameForm.user_id" disabled />
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="realnameForm.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="身份证">
<el-input v-model="realnameForm.id_card" placeholder="请输入身份证号" />
</el-form-item>
<el-form-item label="实名类型">
<el-select v-model="realnameForm.type" placeholder="请选择实名类型" style="width: 100%">
<el-option label="个人认证" :value="0" />
<el-option label="企业认证" :value="1" />
</el-select>
</el-form-item>
<el-form-item label="认证状态">
<el-select v-model="realnameForm.status" placeholder="请选择认证状态" style="width: 100%">
<el-option label="未认证" :value="0" />
<el-option label="已认证" :value="1" />
<el-option label="认证中" :value="2" />
<el-option label="认证失败" :value="3" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="realnameDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitRealnameModify">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 登录记录对话框 -->
<el-dialog
v-model="loginHistoryDialogVisible"
title="用户登录记录"
width="900px"
append-to-body
>
<el-table :data="loginHistory" style="width: 100%" v-loading="loginHistoryLoading">
<el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="Host" label="IP地址" width="150" />
<el-table-column prop="Location" label="登录地点" width="120" />
<el-table-column prop="Type" label="登录方式" width="100">
<template #default="{ row }">
<el-tag :type="row.Type === 'password' ? 'primary' : 'success'" size="small">
{{ row.Type === 'password' ? '密码登录' : row.Type }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="Origin" label="来源" show-overflow-tooltip />
<el-table-column prop="CreatedAt" label="登录时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
</el-table>
<div class="dialog-pagination">
<el-pagination
v-model:current-page="loginHistoryParams.page"
v-model:page-size="loginHistoryParams.count"
:page-sizes="[10, 20, 50]"
:total="loginHistoryTotal"
layout="total, sizes, prev, pager, next"
background
@size-change="handleLoginHistorySizeChange"
@current-change="handleLoginHistoryPageChange"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="loginHistoryDialogVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 操作记录对话框 -->
<el-dialog
v-model="operationHistoryDialogVisible"
title="用户操作记录"
width="900px"
append-to-body
>
<el-table :data="operationHistory" style="width: 100%" v-loading="operationHistoryLoading">
<el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="Type" label="操作类型" width="120">
<template #default="{ row }">
<el-tag type="info" size="small">{{ row.Type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="Note" label="操作说明" show-overflow-tooltip />
<el-table-column prop="Route" label="接口路由" width="200" show-overflow-tooltip />
<el-table-column prop="CreatedAt" label="操作时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
</el-table>
<div class="dialog-pagination">
<el-pagination
v-model:current-page="operationHistoryParams.page"
v-model:page-size="operationHistoryParams.count"
:page-sizes="[10, 20, 50]"
:total="operationHistoryTotal"
layout="total, sizes, prev, pager, next"
background
@size-change="handleOperationHistorySizeChange"
@current-change="handleOperationHistoryPageChange"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="operationHistoryDialogVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<AvatarSelector
v-model="showAvatarSelector"
:userId="currentEditUser.UserId"
:current-cover-id="currentEditUser.CoverID"
@confirm="handleAvatarConfirm"
/>
<!-- 模拟登录对话框 -->
<el-dialog v-model="tokenDialogVisible" title="模拟登录" width="450px" append-to-body>
<div class="token-container">
<el-form label-width="100px">
<el-form-item label="选择环境">
<el-select v-model="selectedEnvironment" placeholder="请选择登录环境" size="large" style="width: 100%">
<el-option label="正式环境" value="production">
<div class="env-option">
<span>正式环境</span>
<span class="env-url">www.007yjs.com</span>
</div>
</el-option>
<el-option label="测试环境" value="test">
<div class="env-option">
<span>测试环境</span>
<span class="env-url">apiserver.s1f.ren</span>
</div>
</el-option>
<el-option label="本地环境" value="local">
<div class="env-option">
<span>本地环境</span>
<span class="env-url">localhost:5173</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="tokenDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmJump" :disabled="!selectedEnvironment">确认跳转</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Delete, Search, ArrowDown, View, User, Edit, Refresh, Position, RefreshLeft } from '@element-plus/icons-vue'
import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import UserListSelector from '@/components/admin/UserListSelector.vue'
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
import {
getUserList,
getUserInfo,
deleteUser,
createTask,
updateUserInfo,
mockUserLogin,
updateUserAvatar,
updateUserPassword,
updateUserGroup,
updateUserRealName,
getUserLoginRecord,
getUserOperationRecord
} from '@/api/admin/user'
import { getFileDetail } from '@/api/admin/file'
import { closeAllMessage } from '../../utils/message'
const router = useRouter()
// 跳转用户ID
const jumpUserId = ref('')
const jumpUserName = ref('')
const showJumpUserSelector = ref(false)
// 查询参数
const queryParams = reactive({
key: '',
is_admin: undefined,
page: 1,
count: 10
})
// 用户表单
const userForm = reactive({
// 新增用户字段
username: '',
password: '',
Phone: '',
// 编辑用户字段
user_id: '',
user_name: '',
email: '',
phone: '',
sex: '',
age: 0,
cover_id: '',
recommend_id: ''
})
// 推介用户选择相关
const showUserSelector = ref(false)
const selectedRecommendUserName = ref('')
// 推介用户选择确认
const handleRecommendUserConfirm = (user) => {
if (user) {
userForm.recommend_id = String(user.user_id)
selectedRecommendUserName.value = `${user.user_name} (ID: ${user.user_id})`
}
showUserSelector.value = false
}
// 清除推介用户
const clearRecommendUser = () => {
userForm.recommend_id = ''
selectedRecommendUserName.value = ''
}
const showAvatarSelector = ref(false)
const currentEditUser = ref({})
// 表单规则
const userRules = computed(() => {
if (dialogType.value === 'add') {
return {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
]
}
} else {
return {
user_name: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
]
}
}
})
// 状态数据
const loading = ref(false)
const userList = ref([])
const total = ref(0)
const selectedRows = ref([])
const dialogVisible = ref(false)
const dialogType = ref('add')
const userFormRef = ref(null)
// 当前操作用户
const currentUser = ref({})
// 头像管理相关
const avatarDialogVisible = ref(false)
const avatarForm = reactive({
user_id: '',
cover_id: ''
})
const currentAvatarUrl = ref('')
const selectedAvatarId = ref('')
// 密码管理相关
const passwordDialogVisible = ref(false)
const passwordForm = reactive({
user_id: '',
password: ''
})
// 用户组管理相关
const groupDialogVisible = ref(false)
const groupForm = reactive({
user_id: '',
user_group_id: '',
current_group_name: ''
})
const showGroupSelector = ref(false)
const selectedGroupName = ref('')
// 用户组选择确认
const handleGroupSelectorConfirm = (group) => {
if (group) {
const groupId = group.group_id || group.GroupId || group.id || group.Id
const groupName = group.group_name || group.name || group.Name
groupForm.user_group_id = groupId
selectedGroupName.value = `${groupName} (ID: ${groupId})`
}
showGroupSelector.value = false
}
// 清除选择的用户组
const clearSelectedGroup = () => {
groupForm.user_group_id = ''
selectedGroupName.value = ''
}
// 实名信息管理相关
const realnameDialogVisible = ref(false)
const realnameForm = reactive({
user_id: '',
name: '',
id_card: '',
type: 0,
status: 0
})
// 登录记录相关
const loginHistoryDialogVisible = ref(false)
const loginHistory = ref([])
const loginHistoryLoading = ref(false)
const loginHistoryTotal = ref(0)
const loginHistoryParams = reactive({
user_id: '',
page: 1,
count: 10
})
// 操作记录相关
const operationHistoryDialogVisible = ref(false)
const operationHistory = ref([])
const operationHistoryLoading = ref(false)
const operationHistoryTotal = ref(0)
const operationHistoryParams = reactive({
user_id: '',
page: 1,
count: 10
})
// 模拟登录相关
const tokenDialogVisible = ref(false)
const loginToken = ref('')
const loginExpire = ref('')
const selectedEnvironment = ref('')
const environments = {
production: 'https://www.007yjs.com',
test: 'https://apiserver.s1f.ren',
local: 'http://localhost:5173'
}
// 获取用户列表
const fetchUserList = async () => {
loading.value = true
try {
const res = await getUserList(queryParams)
console.log("获取用户列表:", res)
if (res.data.code === 200) {
// 映射 API 返回的字段到组件使用的字段格式
userList.value = (res.data.data.data || []).map(user => ({
UserId: user.user_id,
UserName: user.user_name,
Phone: user.phone,
Email: user.email,
Sex: user.sex,
Age: user.age,
IsAdmin: user.is_admin,
CoverID: user.cover_id,
avatarUrl: user.cover || '', // 直接使用 cover 字段作为头像 URL
UserGroup: user.user_group,
RealName: user.real_name,
IsDeleted: user.is_deleted,
CreatedAt: user.created_at
}))
console.log("用户列表:", userList.value)
total.value = res.data.data.all_count || 0
}
} catch (error) {
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
// 查询用户列表
const handleQuery = () => {
queryParams.page = 1
fetchUserList()
}
// 重置查询
const resetQuery = () => {
queryParams.key = ''
queryParams.is_admin = undefined
queryParams.page = 1
fetchUserList()
}
// 跳转到指定用户详情
const handleJumpToUser = () => {
if (!jumpUserId.value || !jumpUserId.value.trim()) {
ElMessage.warning('请输入用户ID')
return
}
router.push({
path: '/user/detail',
query: { user_id: jumpUserId.value.trim() }
})
}
// 选择项变化
const handleSelectionChange = (selection) => {
selectedRows.value = selection
}
// 分页大小变化
const handleSizeChange = (size) => {
queryParams.count = size
fetchUserList()
}
// 分页页码变化
const handleCurrentChange = (page) => {
queryParams.page = page
fetchUserList()
}
// 新增用户
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
Object.assign(userForm, {
// 新增用户字段
username: '',
password: '',
Phone: '',
// 编辑用户字段
user_id: '',
user_name: '',
email: '',
phone: '',
sex: '',
age: 0,
cover_id: '',
recommend_id: ''
})
selectedRecommendUserName.value = ''
userFormRef.value?.resetFields()
}
// 编辑用户 - 通过getUserInfo获取用户详情
const handleEdit = async (row) => {
const userId = row.user_id || row.UserId
try {
const res = await getUserInfo({ user_id: userId })
if (res.data.code === 200) {
const user = res.data.data
dialogType.value = 'edit'
dialogVisible.value = true
// 根据用户详情接口返回的数据映射字段
// Sex: true=男, false=女
const sexValue = user.Sex === true ? '男' : (user.Sex === false ? '女' : '')
Object.assign(userForm, {
user_id: String(user.UserId),
user_name: user.UserName || '',
email: user.Email || '',
phone: user.Phone || '',
sex: sexValue,
age: user.Age || 0,
cover_id: user.CoverID ? String(user.CoverID) : '',
recommend_id: user.RecommendUserId ? String(user.RecommendUserId) : ''
})
// 设置推介用户显示
if (user.RecommendUserId) {
selectedRecommendUserName.value = `推介人ID: ${user.RecommendUserId}`
} else {
selectedRecommendUserName.value = ''
}
} else {
ElMessage.error('获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
ElMessage.error('获取用户信息失败')
}
}
// 删除用户
const handleDelete = (row) => {
ElMessageBox.confirm(`确认删除用户 ${row.UserName} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
console.log("用户信息:",row)
try {
const res = await deleteUser({ user_id: String(row.UserId) })
console.log("删除用户",res)
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchUserList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条记录')
return
}
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
ElMessage.success('批量删除成功')
fetchUserList()
}).catch(() => {})
}
// 用户详情跳转
const handleUserDetail = (row) => {
console.log("用户详情跳转:", row.UserId)
router.push({
path: '/user/detail',
query: { user_id: row.UserId }
})
}
// 命令处理函数
const handleCommand = (command, row) => {
currentUser.value = row
switch (command) {
case 'edit':
handleEdit(row)
break
case 'avatar':
handleAvatarModify(row)
break
case 'password':
handlePasswordModify(row)
break
case 'group':
handleGroupModify(row)
break
case 'realname':
handleRealnameModify(row)
break
case 'balance':
handleBalanceManage(row)
break
case 'loginHistory':
handleLoginHistory(row)
break
case 'operationHistory':
handleOperationHistory(row)
break
case 'simulateLogin':
handleSimulateLogin(row)
break
case 'delete':
handleDelete(row)
break
}
}
// 余额管理
const handleBalanceManage = (row) => {
router.push({
path: '/user/balance',
query: { user_id: row.UserId }
})
}
// 模拟登录
const handleSimulateLogin = async (row) => {
try {
const res = await mockUserLogin({ user_id: row.UserId })
if (res.data?.code === 200) {
loginToken.value = res.data.data.token || ''
loginExpire.value = res.data.data.expire_time || ''
selectedEnvironment.value = ''
tokenDialogVisible.value = true
} else {
ElMessage.error(res.data?.message || '模拟登录失败')
}
} catch (error) {
ElMessage.error('模拟登录失败')
}
}
// 确认跳转
const confirmJump = () => {
if (!selectedEnvironment.value) {
ElMessage.warning('请选择登录环境')
return
}
const baseUrl = environments[selectedEnvironment.value]
const url = `${baseUrl}/token-login?token=${loginToken.value}&expire=${loginExpire.value}`
window.open(url, '_blank')
const envName = selectedEnvironment.value === 'production' ? '正式' : selectedEnvironment.value === 'test' ? '测试' : '本地'
ElMessage.success(`正在跳转到${envName}环境`)
tokenDialogVisible.value = false
}
// 提交表单
const submitForm = () => {
userFormRef.value?.validate(async (valid) => {
if (valid) {
try {
let res
if (dialogType.value === 'add') {
// 新建用户只传递用户名和密码
const createData = {
username: userForm.username,
password: userForm.password,
phone: userForm.Phone
}
res = await createTask(createData)
} else {
// 编辑用户 - 按照接口文档传递参数
// sex: 接口需要 "true" 表示男,"false" 表示女
const sexValue = userForm.sex === '男' ? 'true' : (userForm.sex === '女' ? 'false' : '')
const updateData = {
user_id: userForm.user_id,
user_name: userForm.user_name,
email: userForm.email || '',
phone: userForm.phone || '',
sex: sexValue,
age: String(userForm.age),
cover_id: userForm.cover_id || '',
recommend_id: userForm.recommend_id || ''
}
res = await updateUserInfo(updateData)
}
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchUserList()
}
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
// 日期格式化
const formatDate = (dateString) => {
if (!dateString) return '未设置'
const date = new Date(dateString)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 修改头像
const handleAvatarModify = async (row) => {
avatarForm.user_id = row.UserId
avatarForm.cover_id = row.CoverID || ''
currentEditUser.value = row
currentAvatarUrl.value = ''
if (row.avatarUrl) {
currentAvatarUrl.value = row.avatarUrl
} else if (row.CoverID) {
try {
const res = await getFileDetail({ file_id: row.CoverID })
if (res.data.code === 200 && res.data.data?.url) {
currentAvatarUrl.value = res.data.data.url
}
} catch (error) {
console.error('获取头像失败:', error)
}
}
avatarDialogVisible.value = true
}
const handleAvatarConfirm = async (data) => {
selectedAvatarId.value = data.cover_id
avatarForm.cover_id = data.cover_id
currentAvatarUrl.value = data.url
ElMessage.success('头像选择成功,请点击确定按钮保存')
}
// 修改密码 - 通过getUserInfo获取用户详情
const handlePasswordModify = async (row) => {
const userId = row.user_id || row.UserId
try {
const res = await getUserInfo({ user_id: userId })
if (res.data.code === 200) {
const user = res.data.data
passwordForm.user_id = user.UserId
passwordForm.password = ''
passwordDialogVisible.value = true
} else {
ElMessage.error('获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
ElMessage.error('获取用户信息失败')
}
}
// 修改用户组 - 通过getUserInfo获取用户详情
const handleGroupModify = async (row) => {
const userId = row.user_id || row.UserId
try {
const res = await getUserInfo({ user_id: userId })
if (res.data.code === 200) {
const user = res.data.data
groupForm.user_id = user.UserId
groupForm.user_group_id = user.UserGroupId || ''
groupForm.current_group_name = user.UserGroup?.Name || '未分配用户组'
// 设置当前选择的用户组显示
if (user.UserGroupId && user.UserGroup?.Name) {
selectedGroupName.value = `${user.UserGroup.Name} (ID: ${user.UserGroupId})`
} else {
selectedGroupName.value = ''
}
groupDialogVisible.value = true
} else {
ElMessage.error('获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
ElMessage.error('获取用户信息失败')
}
}
// 修改实名信息 - 通过getUserInfo获取用户详情
const handleRealnameModify = async (row) => {
const userId = row.user_id || row.UserId
try {
const res = await getUserInfo({ user_id: userId })
if (res.data.code === 200) {
const user = res.data.data
realnameForm.user_id = user.UserId
realnameForm.name = user.RealName?.Name || ''
realnameForm.id_card = user.RealName?.IdCard || ''
realnameForm.type = user.RealName?.Type || 0
realnameForm.status = user.RealName?.Status || 0
realnameDialogVisible.value = true
} else {
ElMessage.error('获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息失败:', error)
ElMessage.error('获取用户信息失败')
}
}
// 获取登录记录
const handleLoginHistory = async (row) => {
try {
loginHistoryParams.user_id = row.UserId
loginHistoryParams.page = 1
loginHistoryDialogVisible.value = true
await fetchLoginHistory()
} catch (error) {
ElMessage.error('获取登录记录失败')
}
}
// 获取登录记录数据
const fetchLoginHistory = async () => {
loginHistoryLoading.value = true
try {
const res = await getUserLoginRecord({
user_id: loginHistoryParams.user_id,
page: loginHistoryParams.page,
count: loginHistoryParams.count
})
if (res.data?.code === 200) {
loginHistory.value = res.data.data?.data || []
loginHistoryTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取登录记录失败:', error)
} finally {
loginHistoryLoading.value = false
}
}
// 登录记录分页处理
const handleLoginHistorySizeChange = (size) => {
loginHistoryParams.count = size
loginHistoryParams.page = 1
fetchLoginHistory()
}
const handleLoginHistoryPageChange = (page) => {
loginHistoryParams.page = page
fetchLoginHistory()
}
// 获取操作记录
const handleOperationHistory = async (row) => {
try {
operationHistoryParams.user_id = row.UserId
operationHistoryParams.page = 1
operationHistoryDialogVisible.value = true
await fetchOperationHistory()
} catch (error) {
ElMessage.error('获取操作记录失败')
}
}
// 获取操作记录数据
const fetchOperationHistory = async () => {
operationHistoryLoading.value = true
try {
const res = await getUserOperationRecord({
user_id: operationHistoryParams.user_id,
page: operationHistoryParams.page,
count: operationHistoryParams.count
})
if (res.data?.code === 200) {
operationHistory.value = res.data.data?.data || []
operationHistoryTotal.value = res.data.data?.all_count || 0
}
} catch (error) {
console.error('获取操作记录失败:', error)
} finally {
operationHistoryLoading.value = false
}
}
// 操作记录分页处理
const handleOperationHistorySizeChange = (size) => {
operationHistoryParams.count = size
operationHistoryParams.page = 1
fetchOperationHistory()
}
const handleOperationHistoryPageChange = (page) => {
operationHistoryParams.page = page
fetchOperationHistory()
}
// 提交头像修改
const submitAvatarModify = async () => {
try {
const res = await updateUserAvatar({
user_id: avatarForm.user_id,
cover_id: avatarForm.cover_id
})
console.log("1234",res)
console.log("提交头像修改:",res)
if (res.data.code == 200) {
ElMessage.success('头像修改成功')
avatarDialogVisible.value = false
fetchUserList()
}
} catch (error) {
ElMessage.error('头像修改失败')
}
}
// 提交密码修改
const submitPasswordModify = async () => {
try {
const res = await updateUserPassword({
user_id: passwordForm.user_id,
password: passwordForm.password
})
if (res.code === 200) {
ElMessage.success('密码修改成功')
passwordDialogVisible.value = false
}
} catch (error) {
ElMessage.error('密码修改失败')
}
}
// 提交用户组修改
const submitGroupModify = async () => {
try {
const res = await updateUserGroup({
user_id: groupForm.user_id,
user_group_id: groupForm.user_group_id
})
if (res.data.code === 200) {
ElMessage.success('用户组修改成功')
groupDialogVisible.value = false
fetchUserList()
}
} catch (error) {
ElMessage.error('用户组修改失败')
}
}
// 提交实名信息修改
const submitRealnameModify = async () => {
try {
const res = await updateUserRealName({
user_id: realnameForm.user_id,
name: realnameForm.name,
id_card: realnameForm.id_card,
type: realnameForm.type,
status: realnameForm.status
})
if (res.code === 200) {
ElMessage.success('实名信息修改成功')
realnameDialogVisible.value = false
fetchUserList()
}
} catch (error) {
ElMessage.error('实名信息修改失败')
}
}
// 初始化
onMounted(() => {
fetchUserList()
})
</script>
<style scoped>
.user-list-container {
padding: 0;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-row {
display: flex;
align-items: center;
padding: 12px 20px;
gap: 16px;
flex-wrap: wrap;
}
.filter-row:not(:last-child) {
border-bottom: 1px solid #ebeef5;
}
.search-row {
padding: 16px 20px;
}
.action-row {
padding: 12px 20px;
background: #fff;
}
.search-group {
display: flex;
align-items: center;
gap: 8px;
}
.search-label {
font-size: 14px;
color: #606266;
white-space: nowrap;
flex-shrink: 0;
}
.search-input {
width: 200px;
}
.search-input-small {
width: 140px;
}
.search-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.btn-text {
margin-left: 4px;
}
.action-bar {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
/* 移动端卡片列表样式 */
.mobile-card-list {
display: none;
padding: 16px;
gap: 16px;
}
.user-card {
background: #fff;
border: 1px solid #e1e8ed;
padding: 16px;
margin-bottom: 12px;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f2f5;
}
.card-avatar {
flex-shrink: 0;
}
.card-user-info {
flex: 1;
min-width: 0;
}
.card-username {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-email {
font-size: 12px;
color: #909399;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-body {
padding: 12px 0;
}
.card-info-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
}
.card-label {
color: #909399;
flex-shrink: 0;
}
.card-value {
color: #2c3e50;
text-align: right;
word-break: break-all;
}
.card-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #f0f2f5;
}
/* PC端表格显示 */
.desktop-table {
width: 100%;
}
/* 平板适配 */
@media (max-width: 1024px) {
.search-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.search-group {
width: 100%;
}
.search-input,
.search-input-small {
flex: 1;
width: auto !important;
}
.search-buttons {
width: 100%;
justify-content: flex-start;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.filter-row {
padding: 12px 16px;
}
.search-row {
padding: 12px 16px;
gap: 10px;
}
.action-row {
padding: 10px 16px;
}
.search-group {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
.search-label {
font-size: 13px;
}
.search-input,
.search-input-small {
width: 100% !important;
}
.search-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
width: 100%;
}
.search-buttons .el-button {
margin: 0;
width: 100%;
}
.action-bar .btn-text {
}
.action-bar {
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.action-bar .el-button {
margin: 0;
width: 100%;
}
/* 移动端显示卡片,隐藏表格 */
.mobile-card-list {
display: block;
}
.desktop-table {
display: none !important;
}
/* 分页移动端样式 */
.pagination {
flex-wrap: wrap;
justify-content: center;
gap: 8px;
padding: 12px 16px;
}
.pagination :deep(.el-pagination__sizes),
.pagination :deep(.el-pagination__jump) {
display: none;
}
}
/* 超小屏幕适配 */
@media (max-width: 480px) {
.filter-row {
padding: 10px 12px;
}
.search-buttons {
grid-template-columns: repeat(4, 1fr);
}
.search-buttons .el-button {
padding: 8px 0;
font-size: 12px;
}
.action-bar {
grid-template-columns: 1fr 1fr;
}
.action-bar .el-button {
font-size: 13px;
padding: 8px 12px;
}
}
.table-section {
padding: 0;
overflow: hidden;
}
/* 骨架屏样式 */
.skeleton-container {
padding: 20px;
}
.skeleton-row {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
gap: 16px;
}
.skeleton-row:last-child {
border-bottom: none;
}
.skeleton-cell {
display: flex;
align-items: center;
}
.skeleton-checkbox {
width: 55px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-id {
width: 100px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-user {
flex: 1;
min-width: 150px;
gap: 12px;
}
.skeleton-avatar {
width: 40px;
height: 40px;
flex-shrink: 0;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-text-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-text {
height: 14px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-text-primary {
width: 80px;
}
.skeleton-text-secondary {
width: 120px;
}
.skeleton-phone {
width: 130px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-realname {
width: 150px;
flex-direction: column;
gap: 8px;
}
.skeleton-group {
width: 120px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-status {
width: 100px;
height: 24px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-time {
width: 180px;
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-action {
width: 200px;
height: 32px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.user-info {
display: flex;
align-items: center;
padding: 4px 0;
}
.user-detail {
margin-left: 12px;
}
.username {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
color: #2c3e50;
}
.email {
font-size: 12px;
color: #7f8c8d;
}
.real-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 2px;
color: #2c3e50;
}
.id-card {
font-size: 12px;
color: #7f8c8d;
}
.text-gray {
color: #95a5a6;
font-size: 12px;
}
.pagination {
margin-top: 20px;
padding: 16px 20px;
border-top: 1px solid #e1e8ed;
background: #fafbfc;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 0;
}
.recommend-user-selector,
.group-selector-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.recommend-user-selector .el-input,
.group-selector-wrapper .el-input {
flex: 1;
}
.recommend-user-selector .clear-btn,
.group-selector-wrapper .clear-btn {
flex-shrink: 0;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.avatar-preview-container {
display: flex;
align-items: center;
gap: 20px;
}
.avatar-preview {
position: relative;
cursor: pointer;
overflow: hidden;
transition: opacity 0.2s ease;
}
.avatar-preview:hover {
opacity: 0.8;
}
.avatar-preview:hover .avatar-overlay {
opacity: 1;
}
.avatar-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
opacity: 0;
transition: opacity 0.2s ease;
color: white;
}
.avatar-overlay span {
font-size: 12px;
font-weight: 500;
}
.avatar-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.avatar-id {
margin: 0;
font-size: 14px;
color: #606266;
}
:deep(.el-card__body) {
padding: 0;
}
/* 弹窗分页样式 */
.dialog-pagination {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
/* 模拟登录样式 */
.token-container {
padding: 10px 0;
}
.env-option {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.env-url {
font-size: 12px;
color: #909399;
}
</style>