Files
ApiServer-Web-admin_dashboa…/src/views/user/UserList.vue
T
2026-01-20 17:54:45 +08:00

1591 lines
40 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">用户ID</span>
<el-input
v-model="jumpUserId"
placeholder="输入ID跳转"
clearable
class="search-input-small"
@keyup.enter="handleJumpToUser"
/>
</div>
<div class="search-buttons">
<el-button type="primary" @click="handleQuery">
<el-icon><Search /></el-icon><span class="btn-text">查询</span>
</el-button>
<el-button @click="resetQuery">重置</el-button>
<el-button type="success" @click="fetchUserList">
<el-icon><Refresh /></el-icon><span class="btn-text">刷新</span>
</el-button>
<el-button type="warning" @click="handleJumpToUser">
<el-icon><Position /></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>新增用户
</el-button>
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>批量删除
</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" />
<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" />
<el-table-column prop="UserId" label="用户ID" width="100" />
<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 || ''" />
</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>
<!-- 分页 -->
<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"
/>
</div>
</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"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="dialogType === 'add'">
<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 v-if="dialogType === 'edit'">
<el-form-item label="邮箱" prop="Email">
<el-input v-model="userForm.Email" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="用户组" prop="UserGroupId">
<el-select v-model="userForm.UserGroupId" placeholder="请选择用户组" style="width: 100%">
<el-option label="默认用户组" :value="1" />
<el-option label="VIP用户组" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="实名姓名" prop="RealName.Name">
<el-input v-model="userForm.RealName.Name" placeholder="请输入实名姓名" />
</el-form-item>
<el-form-item label="身份证号" prop="RealName.IdCard">
<el-input v-model="userForm.RealName.IdCard" placeholder="请输入身份证号" />
</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>
<!-- 修改头像对话框 -->
<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="400px"
append-to-body
>
<el-form :model="groupForm" label-width="80px">
<el-form-item label="用户ID">
<el-input v-model="groupForm.user_id" disabled />
</el-form-item>
<el-form-item label="用户组ID">
<el-input v-model="groupForm.user_group_id" placeholder="请输入用户组ID" />
</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>
<!-- 实名信息对话框 -->
<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-input-number v-model="realnameForm.type" :min="0" :max="2" />
</el-form-item>
<el-form-item label="状态">
<el-input-number v-model="realnameForm.status" :min="0" :max="1" />
</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="800px"
append-to-body
>
<el-table :data="loginHistory" style="width: 100%">
<el-table-column prop="ip" label="IP地址" />
<el-table-column prop="user_agent" label="用户代理" />
<el-table-column prop="created_at" label="登录时间" />
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="loginHistoryDialogVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
<!-- 操作记录对话框 -->
<el-dialog
v-model="operationHistoryDialogVisible"
title="用户操作记录"
width="800px"
append-to-body
>
<el-table :data="operationHistory" style="width: 100%">
<el-table-column prop="action" label="操作" />
<el-table-column prop="description" label="描述" />
<el-table-column prop="ip" label="IP地址" />
<el-table-column prop="created_at" label="操作时间" />
</el-table>
<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"
/>
</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 } from '@element-plus/icons-vue'
import AvatarSelector from '@/components/admin/AvatarSelector.vue'
import {
getUserList,
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 queryParams = reactive({
key: '',
page: 1,
count: 10
})
// 用户表单
const userForm = reactive({
UserId: undefined,
username: '',
Email: '',
Phone: '',
password: '',
UserGroupId: undefined,
RealName: {
Name: '',
IdCard: ''
}
})
const showAvatarSelector = ref(false)
const currentEditUser = ref({})
// 表单规则
const userRules = computed(() => {
const baseRules = {
UserName: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
]
}
// 新建用户时添加密码验证
if (dialogType.value === 'add') {
baseRules.Password = [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
]
}
// 编辑用户时添加其他字段验证
if (dialogType.value === 'edit') {
baseRules.Email = [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
}
return baseRules
})
// 状态数据
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: ''
})
// 实名信息管理相关
const realnameDialogVisible = ref(false)
const realnameForm = reactive({
user_id: '',
name: '',
id_card: '',
type: 0,
status: 0
})
// 登录记录相关
const loginHistoryDialogVisible = ref(false)
const loginHistory = ref([])
// 操作记录相关
const operationHistoryDialogVisible = ref(false)
const operationHistory = ref([])
// 获取用户列表
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.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: '',
Email: '',
Phone: '',
UserGroupId: undefined,
RealName: {
Name: '',
IdCard: ''
}
})
userFormRef.value?.resetFields()
}
// 编辑用户
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
Object.assign(userForm, {
UserId: row.UserId,
username: row.username,
Email: row.Email,
Phone: row.Phone,
UserGroupId: row.UserGroupId,
RealName: {
Name: row.RealName?.Name || '',
IdCard: row.RealName?.IdCard || ''
}
})
}
// 删除用户
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.code === 200) {
ElMessage.success('模拟登录成功')
// 可以跳转到用户视角页面
}
} catch (error) {
ElMessage.error('模拟登录失败')
}
}
// 提交表单
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 {
// 编辑用户传递完整信息
res = await updateUserInfo(userForm)
}
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
// 获取当前头像URL
if (row.CoverID) {
try {
const res = await getFileDetail({ file_id: row.CoverID })
console.log("获取头像1234", res)
if (res.data.code === 200) {
currentAvatarUrl.value = res.data.data.url
}
} catch (error) {
console.error('获取头像失败:', error)
currentAvatarUrl.value = ''
}
} else {
currentAvatarUrl.value = ''
}
avatarDialogVisible.value = true
}
const handleAvatarConfirm = async (data) => {
selectedAvatarId.value = data.cover_id
avatarForm.cover_id = data.cover_id
currentAvatarUrl.value = data.url
ElMessage.success('头像选择成功,请点击确定按钮保存')
}
// 修改密码
const handlePasswordModify = (row) => {
passwordForm.user_id = row.UserId
passwordForm.password = ''
passwordDialogVisible.value = true
}
// 修改用户组
const handleGroupModify = (row) => {
groupForm.user_id = row.UserId
groupForm.user_group_id = row.UserGroupId || ''
groupDialogVisible.value = true
}
// 修改实名信息
const handleRealnameModify = (row) => {
realnameForm.user_id = row.UserId
realnameForm.name = row.RealName?.Name || ''
realnameForm.id_card = row.RealName?.IdCard || ''
realnameForm.type = row.RealName?.Type || 0
realnameForm.status = row.RealName?.Status || 0
realnameDialogVisible.value = true
}
// 获取登录记录
const handleLoginHistory = async (row) => {
try {
const res = await getUserLoginRecord({
user_id: row.UserId,
page: 1,
count: 20
})
if (res.code === 200) {
loginHistory.value = res.data.data || []
loginHistoryDialogVisible.value = true
}
} catch (error) {
ElMessage.error('获取登录记录失败')
}
}
// 获取操作记录
const handleOperationHistory = async (row) => {
try {
const res = await getUserOperationRecord({
user_id: row.UserId,
page: 1,
count: 20
})
if (res.code === 200) {
operationHistory.value = res.data.data || []
operationHistoryDialogVisible.value = true
}
} catch (error) {
ElMessage.error('获取操作记录失败')
}
}
// 提交头像修改
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.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 {
display: 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%;
}
.btn-text {
display: none;
}
.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;
}
/* 骨架屏样式 */
.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;
}
.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-table) {
border: none;
color: #2c3e50;
}
:deep(.el-table__header) {
background: #f8f9fa;
}
:deep(.el-table th) {
background: #f8f9fa !important;
border-bottom: 2px solid #e1e8ed;
color: #2c3e50;
font-weight: 600;
font-size: 13px;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f2f5;
color: #34495e;
}
:deep(.el-table tr:hover > td) {
background-color: #f8f9fa !important;
}
:deep(.el-card__body) {
padding: 0;
}
</style>