Files
ApiServer-Web-admin_dashboa…/src/views/user/UserGroup.vue
T
lin 084aeebf13
Build and Deploy Vue3 / build (push) Successful in 4m33s
Build and Deploy Vue3 / deploy (push) Successful in 1m19s
fix:用户组列表成员弹窗限制高度
2026-01-21 10:57:06 +08:00

806 lines
23 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-group-container">
<!-- 主容器 -->
<el-card class="main-container" shadow="never">
<!-- 操作栏 -->
<div class="filter-section">
<div class="filter-content">
<div class="action-bar">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增用户组
</el-button>
<el-button type="success" @click="fetchGroupList">
<el-icon><Refresh /></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-id"></div>
<div class="skeleton-cell skeleton-name"></div>
<div class="skeleton-cell skeleton-auth"></div>
<div class="skeleton-cell skeleton-price"></div>
<div class="skeleton-cell skeleton-level"></div>
<div class="skeleton-cell skeleton-type"></div>
<div class="skeleton-cell skeleton-count"></div>
<div class="skeleton-cell skeleton-time"></div>
<div class="skeleton-cell skeleton-action"></div>
</div>
</div>
<el-table
v-else
v-loading="loading"
:data="groupList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column label="组ID" width="100">
<template #default="{ row }">
{{ row.group_id || row.GroupId || row.id || row.Id }}
</template>
</el-table-column>
<el-table-column label="组名称" min-width="200">
<template #default="{ row }">
<span class="group-name">{{ row.group_name || row.name || row.Name }}</span>
</template>
</el-table-column>
<el-table-column label="权限" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<el-tag type="info" effect="plain">{{ row.auth || row.Auth || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="升级金额" width="120">
<template #default="{ row }">
<span v-if="row.floor_price || row.FloorPrice" class="price-text">¥{{ row.floor_price || row.FloorPrice }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="下一级组ID" width="120">
<template #default="{ row }">
{{ row.higher_level_id || row.HigherLevelId || '-' }}
</template>
</el-table-column>
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="(row.fixed || row.Fixed) ? 'warning' : 'success'" size="small">
{{ (row.fixed || row.Fixed) ? '固定' : '可升级' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="成员数量" width="100" align="center">
<template #default="{ row }">
<el-tag type="info" size="small" effect="plain">
{{ row.member_count || row.MemberCount || 0 }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ row.create_time || row.CreateTime || row.CreatedAt || '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<el-button type="primary" link @click="handleEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button type="success" link @click="handleViewMembers(row)">
<el-icon><User /></el-icon>成员
</el-button>
<el-button type="danger" link @click="handleDelete(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
</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="650px"
destroy-on-close
append-to-body
class="custom-dialog"
>
<el-scrollbar max-height="60vh">
<el-form
ref="groupFormRef"
:model="groupForm"
:rules="groupRules"
label-width="140px"
>
<el-form-item v-if="dialogType === 'edit'" label="组ID">
<el-input v-model="groupForm.group_id" disabled />
</el-form-item>
<el-form-item label="组名称" prop="name">
<el-input v-model="groupForm.name" placeholder="请输入组名称" />
</el-form-item>
<el-form-item label="权限" prop="auth">
<el-input v-model="groupForm.auth" type="textarea" :rows="4" placeholder="请输入权限配置(JSON格式)" />
</el-form-item>
<el-form-item label="下一级用户组ID">
<el-input
v-if="selectedHigherGroupInfo"
:model-value="`${selectedHigherGroupInfo.Name || selectedHigherGroupInfo.name} (ID: ${groupForm.higher_level_id})`"
readonly
style="width: 100%"
>
<template #suffix>
<el-icon class="clear-icon" @click="clearHigherGroup"><Close /></el-icon>
</template>
<template #append>
<el-button @click="groupSelectorVisible = true">
<el-icon><Connection /></el-icon>
</el-button>
</template>
</el-input>
<el-input
v-else
placeholder="请选择下一级用户组(可选)"
readonly
style="width: 100%"
@click="groupSelectorVisible = true"
>
<template #append>
<el-button @click="groupSelectorVisible = true">
<el-icon><Connection /></el-icon>
</el-button>
</template>
</el-input>
</el-form-item>
<!-- 用户组选择器 -->
<UserGroupSelector
v-model="groupSelectorVisible"
:current-group-id="groupForm.higher_level_id"
:exclude-group-id="groupForm.group_id"
@confirm="handleHigherGroupSelect"
/>
<el-form-item label="升级所需消费金额">
<el-input-number
v-model="groupForm.floor_price"
:min="0"
:precision="2"
placeholder="请输入升级所需消费金额"
style="width: 100%"
>
<template #prepend>¥</template>
</el-input-number>
</el-form-item>
<el-form-item label="是否为固定用户组">
<el-switch
v-model="groupForm.fixed"
active-text="固定不可升级"
inactive-text="可升级"
/>
<div style="color: #909399; font-size: 12px; margin-top: 4px;">
固定用户组不能通过消费金额自动升级
</div>
</el-form-item>
</el-form>
</el-scrollbar>
<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="memberDialogVisible"
title="用户组成员"
width="800px"
append-to-body
class="custom-dialog"
>
<el-scrollbar max-height="60vh">
<el-table
v-loading="memberLoading"
:data="memberList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
>
<el-table-column label="用户ID" width="100">
<template #default="{ row }">
{{ row.user_id || row.UserId || row.id || row.Id }}
</template>
</el-table-column>
<el-table-column label="用户名" min-width="150">
<template #default="{ row }">
<div class="user-info">
<span class="username">{{ row.username || row.Username || row.UserName || row.name || row.Name }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="邮箱" min-width="200">
<template #default="{ row }">
{{ row.email || row.Email || '-' }}
</template>
</el-table-column>
<el-table-column label="加入时间" width="180">
<template #default="{ row }">
{{ row.join_time || row.JoinTime || row.CreatedAt || '-' }}
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="memberParams.page"
v-model:page-size="memberParams.count"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
:total="memberTotal"
@size-change="handleMemberSizeChange"
@current-change="handleMemberCurrentChange"
background
class="pagination"
/>
</el-scrollbar>
</el-dialog>
<!-- 添加成员对话框 -->
<el-dialog
v-model="addMemberDialogVisible"
title="添加用户组成员"
width="500px"
append-to-body
>
<el-form
ref="memberFormRef"
:model="memberForm"
:rules="memberRules"
label-width="100px"
>
<el-form-item label="用户ID" prop="user_ids">
<el-input v-model="memberForm.user_ids" placeholder="请输入用户ID,多个用逗号分隔" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="addMemberDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitAddMember">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Refresh, Edit, User, Delete, Connection, Close } from '@element-plus/icons-vue'
import {
getUserGroupList,
getUserGroupMemberList,
createUserGroup,
updateUserGroupInfo,
deleteUserGroup,
addUserGroupMember
} from '@/api/admin/user'
import { formatTime } from '@/utils/tool'
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
// 查询参数
const queryParams = reactive({
page: 1,
count: 10
})
// 成员查询参数
const memberParams = reactive({
group_id: '',
page: 1,
count: 10
})
// 用户组表单
const groupForm = reactive({
group_id: undefined,
name: '',
auth: '',
higher_level_id: undefined,
floor_price: undefined,
fixed: false
})
const groupRules = {
name: [
{ required: true, message: '请输入组名称', trigger: 'blur' }
],
auth: [
{ required: true, message: '请输入权限', trigger: 'blur' }
]
}
// 成员表单
const memberForm = reactive({
group_id: '',
user_ids: ''
})
const memberRules = {
user_ids: [
{ required: true, message: '请输入用户ID', trigger: 'blur' }
]
}
// 状态数据
const loading = ref(false)
const memberLoading = ref(false)
const groupList = ref([])
const memberList = ref([])
const total = ref(0)
const memberTotal = ref(0)
const dialogVisible = ref(false)
const memberDialogVisible = ref(false)
const addMemberDialogVisible = ref(false)
const dialogType = ref('add')
const groupFormRef = ref(null)
const memberFormRef = ref(null)
// 用户组选择器相关
const groupSelectorVisible = ref(false)
const selectedHigherGroupInfo = ref(null)
// 获取用户组列表
const fetchGroupList = async () => {
loading.value = true
try {
const res = await getUserGroupList(queryParams)
const code = res.data?.code || res.code
if (code === 200) {
let responseData = res.data?.data || res.data
responseData.data.forEach(item => {
item.CreatedAt = formatTime(item.CreatedAt)
})
if (Array.isArray(responseData)) {
groupList.value = responseData
total.value = responseData.length
} else if (responseData.list) {
groupList.value = responseData.list || []
total.value = responseData.total || responseData.all_count || 0
} else if (responseData.data && Array.isArray(responseData.data)) {
groupList.value = responseData.data
total.value = responseData.all_count || responseData.data.length
} else {
groupList.value = []
total.value = 0
}
} else {
ElMessage.error(res.data?.message || '获取用户组列表失败')
}
} catch (error) {
ElMessage.error('获取用户组列表失败')
} finally {
loading.value = false
}
}
// 获取成员列表
const fetchMemberList = async () => {
memberLoading.value = true
try {
const res = await getUserGroupMemberList(memberParams)
const code = res.data?.code || res.code
if (code === 200) {
const responseData = res.data?.data || res.data
if (Array.isArray(responseData)) {
memberList.value = responseData
memberTotal.value = responseData.length
} else if (responseData.list) {
memberList.value = responseData.list || []
memberTotal.value = responseData.total || responseData.all_count || 0
} else if (responseData.data && Array.isArray(responseData.data)) {
memberList.value = responseData.data
memberTotal.value = responseData.all_count || responseData.data.length
} else {
memberList.value = []
memberTotal.value = 0
}
} else {
ElMessage.error(res.data?.message || '获取成员列表失败')
}
} catch (error) {
ElMessage.error('获取成员列表失败')
} finally {
memberLoading.value = false
}
}
// 分页
const handleSizeChange = (size) => {
queryParams.count = size
fetchGroupList()
}
const handleCurrentChange = (page) => {
queryParams.page = page
fetchGroupList()
}
const handleMemberSizeChange = (size) => {
memberParams.count = size
fetchMemberList()
}
const handleMemberCurrentChange = (page) => {
memberParams.page = page
fetchMemberList()
}
// 新增用户组
const handleAdd = () => {
dialogType.value = 'add'
dialogVisible.value = true
selectedHigherGroupInfo.value = null
Object.assign(groupForm, {
group_id: undefined,
name: '',
auth: '',
higher_level_id: undefined,
floor_price: undefined,
fixed: false
})
groupFormRef.value?.resetFields()
}
// 编辑用户组
const handleEdit = (row) => {
dialogType.value = 'edit'
dialogVisible.value = true
selectedHigherGroupInfo.value = null
const groupId = row.group_id || row.GroupId || row.id || row.Id
const groupName = row.group_name || row.name || row.Name
const groupAuth = row.auth || row.Auth || ''
const higherLevelId = row.higher_level_id || row.HigherLevelId
const floorPrice = row.floor_price || row.FloorPrice
const fixed = row.fixed || row.Fixed || false
Object.assign(groupForm, {
group_id: groupId,
name: groupName,
auth: typeof groupAuth === 'object' ? JSON.stringify(groupAuth, null, 2) : groupAuth,
higher_level_id: higherLevelId,
floor_price: floorPrice,
fixed: fixed
})
// 查找下一级用户组信息用于显示
if (higherLevelId) {
const higherGroup = groupList.value.find(item => {
const itemId = item.group_id || item.GroupId || item.id || item.Id
return itemId === higherLevelId
})
if (higherGroup) {
selectedHigherGroupInfo.value = higherGroup
} else {
// 如果在列表中找不到,创建一个简单的对象用于显示
selectedHigherGroupInfo.value = { group_id: higherLevelId, name: `用户组${higherLevelId}` }
}
}
}
// 查看成员
const handleViewMembers = (row) => {
const groupId = row.group_id || row.GroupId || row.id || row.Id
memberParams.group_id = groupId
memberParams.page = 1
memberDialogVisible.value = true
fetchMemberList()
}
// 添加成员
const handleAddMember = (row) => {
const groupId = row.group_id || row.GroupId || row.id || row.Id
memberForm.group_id = groupId
memberForm.user_ids = ''
addMemberDialogVisible.value = true
}
// 删除用户组
const handleDelete = (row) => {
const groupId = row.group_id || row.GroupId || row.id || row.Id
const groupName = row.group_name || row.name || row.Name
ElMessageBox.confirm(`确认删除用户组 ${groupName} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await deleteUserGroup({ group_id: groupId })
const code = res.data?.code || res.code
if (code === 200) {
ElMessage.success('删除成功')
fetchGroupList()
} else {
ElMessage.error(res.data?.message || '删除失败')
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
// 提交用户组表单
const submitForm = () => {
groupFormRef.value?.validate(async (valid) => {
if (valid) {
try {
let res
const submitData = {
name: groupForm.name,
auth: groupForm.auth
}
if (groupForm.higher_level_id !== undefined && groupForm.higher_level_id !== null) {
submitData.higher_level_id = groupForm.higher_level_id
}
if (groupForm.floor_price !== undefined && groupForm.floor_price !== null) {
submitData.floor_price = groupForm.floor_price
}
submitData.fixed = groupForm.fixed
if (dialogType.value === 'add') {
res = await createUserGroup(submitData)
} else {
submitData.group_id = groupForm.group_id
res = await updateUserGroupInfo(submitData)
}
const code = res.data?.code || res.code
if (code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchGroupList()
} else {
ElMessage.error(res.data?.message || '操作失败')
}
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
// 提交添加成员
const submitAddMember = () => {
memberFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const res = await addUserGroupMember(memberForm)
const code = res.data?.code || res.code
if (code === 200) {
ElMessage.success('添加成功')
addMemberDialogVisible.value = false
fetchGroupList()
} else {
ElMessage.error(res.data?.message || '添加失败')
}
} catch (error) {
ElMessage.error('添加失败')
}
}
})
}
// 下一级用户组选择处理
const handleHigherGroupSelect = (group) => {
const groupId = group.group_id || group.GroupId || group.id || group.Id
groupForm.higher_level_id = groupId
selectedHigherGroupInfo.value = group
}
const clearHigherGroup = () => {
groupForm.higher_level_id = undefined
selectedHigherGroupInfo.value = null
}
// 初始化
onMounted(() => {
fetchGroupList()
})
</script>
<style scoped>
.user-group-container {
padding: 0;
}
.main-container {
border: 1px solid #e1e8ed;
background: #ffffff;
}
.filter-section {
padding: 0;
border-bottom: 1px solid #e1e8ed;
background: #fafbfc;
}
.filter-content {
display: flex;
justify-content: flex-end;
align-items: center;
padding: 16px 20px;
gap: 20px;
flex-wrap: wrap;
}
.action-bar {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.table-section {
padding: 0;
}
.group-name {
font-weight: 500;
color: #2c3e50;
}
.price-text {
color: #f56c6c;
font-weight: 500;
}
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.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;
}
/* 表格样式优化 */
: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;
}
/* 骨架屏样式 */
.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 {
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;
border-radius: 4px;
}
.skeleton-id { width: 100px; }
.skeleton-name { width: 200px; }
.skeleton-auth { flex: 1; min-width: 200px; }
.skeleton-price { width: 120px; }
.skeleton-level { width: 120px; }
.skeleton-type { width: 100px; }
.skeleton-count { width: 100px; }
.skeleton-time { width: 180px; }
.skeleton-action { width: 280px; height: 32px; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 选择器清除图标样式 */
.clear-icon {
cursor: pointer;
color: #909399;
transition: color 0.2s;
}
.clear-icon:hover {
color: #f56c6c;
}
/* 弹窗样式 */
:deep(.custom-dialog) {
max-height: 80vh;
display: flex;
flex-direction: column;
}
:deep(.custom-dialog .el-dialog__body) {
flex: 1;
overflow: hidden;
padding: 20px;
}
:deep(.custom-dialog .el-scrollbar__view) {
padding-right: 10px;
}
/* 成员弹窗内的分页样式 */
:deep(.custom-dialog .pagination) {
margin-top: 16px;
padding: 0;
border-top: none;
background: transparent;
}
</style>