Files
one-pipe-system/src/views/account-management/account/index.vue
sexygoat d43de4cd06
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 8m39s
修改bug
2026-03-11 17:09:35 +08:00

1013 lines
29 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>
<ArtTableFullScreen>
<div class="account-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader
:columnList="columnOptions"
v-model:columns="columnChecks"
@refresh="handleRefresh"
>
<template #left>
<ElButton @click="showDialog('add')" v-permission="'account:add'">新增账号</ElButton>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
:marginTop="10"
:row-class-name="getRowClassName"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 鼠标悬浮提示 -->
<TableContextMenuHint :visible="showContextMenuHint" :position="hintPosition" />
<!-- 右键菜单 -->
<ArtMenuRight
ref="contextMenuRef"
:menu-items="contextMenuItems"
:menu-width="120"
@select="handleContextMenuSelect"
/>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '添加账号' : '编辑账号'"
width="30%"
>
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
<ElFormItem label="账号名称" prop="username">
<ElInput v-model="formData.username" placeholder="请输入账号名称" />
</ElFormItem>
<ElFormItem v-if="dialogType === 'add'" label="密码" prop="password">
<ElInput
v-model="formData.password"
type="password"
placeholder="请输入密码"
show-password
/>
</ElFormItem>
<ElFormItem label="手机号" prop="phone">
<ElInput v-model="formData.phone" placeholder="请输入手机号" maxlength="11" />
</ElFormItem>
<ElFormItem v-if="dialogType === 'add'" label="账号类型" prop="user_type">
<ElSelect
v-model="formData.user_type"
placeholder="请选择账号类型"
style="width: 100%"
>
<ElOption label="超级管理员" :value="1" />
<ElOption label="平台用户" :value="2" />
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 分配角色对话框 -->
<ElDialog v-model="roleDialogVisible" width="900px">
<template #header>
<div class="dialog-header">
<span class="dialog-title">分配角色</span>
<div class="account-info">
<span class="account-name">{{ currentAccountName }}</span>
</div>
</div>
</template>
<div class="role-transfer-container">
<!-- 左侧可分配角色列表 -->
<div class="transfer-panel">
<div class="panel-header">
<span class="panel-title">可分配角色</span>
<ElInput
v-model="leftRoleFilter"
placeholder="搜索角色"
clearable
size="small"
style="width: 180px"
/>
</div>
<div class="panel-body">
<ElCheckboxGroup v-model="rolesToAdd" class="role-list">
<div v-for="role in filteredAvailableRoles" :key="role.ID" class="role-item">
<ElCheckbox :label="role.ID" :disabled="selectedRoles.includes(role.ID)">
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
</ElCheckbox>
</div>
</ElCheckboxGroup>
</div>
</div>
<!-- 中间操作按钮 -->
<div class="transfer-buttons">
<ElButton
type="primary"
:icon="'ArrowRight'"
@click="addRoles"
:disabled="rolesToAdd.length === 0"
>
添加
</ElButton>
</div>
<!-- 右侧已分配角色列表 -->
<div class="transfer-panel">
<div class="panel-header">
<span class="panel-title">已分配角色</span>
<ElInput
v-model="rightRoleFilter"
placeholder="搜索角色"
clearable
size="small"
style="width: 180px"
/>
</div>
<div class="panel-body">
<div class="role-list">
<div
v-for="role in filteredAssignedRoles"
:key="role.ID"
class="role-item assigned-role-item"
>
<span class="role-info">
<span>{{ role.role_name }}</span>
<ElTag :type="role.role_type === 1 ? 'primary' : 'success'" size="small">
{{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag>
</span>
<ElButton type="danger" size="small" link @click="removeSingleRole(role.ID)">
移除
</ElButton>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="roleDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import { h } from 'vue'
import { useRoute } from 'vue-router'
import { FormInstance, ElSwitch, ElCheckbox, ElCheckboxGroup, ElTag } from 'element-plus'
import { ElMessageBox, ElMessage } from 'element-plus'
import type { FormRules } from 'element-plus'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { useTableContextMenu } from '@/composables/useTableContextMenu'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { AccountService } from '@/api/modules/account'
import { RoleService } from '@/api/modules/role'
import { ShopService, EnterpriseService } from '@/api/modules'
import type { SearchFormItem } from '@/types'
import type { PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format'
import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants'
defineOptions({ name: 'Account' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { hasAuth } = useAuth()
const route = useRoute()
// 使用表格右键菜单功能
const {
showContextMenuHint,
hintPosition,
getRowClassName,
handleCellMouseEnter,
handleCellMouseLeave
} = useTableContextMenu()
const dialogType = ref('add')
const dialogVisible = ref(false)
const roleDialogVisible = ref(false)
const loading = ref(false)
const roleSubmitLoading = ref(false)
const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('')
const currentAccountType = ref<number>(0)
const contextMenuRef = ref<InstanceType<typeof ArtMenuRight>>()
const currentRow = ref<any | null>(null)
const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([])
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 定义表单搜索初始值
const initialSearchState = {
name: '',
phone: '',
user_type: undefined as number | undefined,
shop_id: undefined as number | undefined,
enterprise_id: undefined as number | undefined,
status: undefined as number | undefined
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
// 店铺和企业列表
const shopList = ref<any[]>([])
const enterpriseList = ref<any[]>([])
const pagination = reactive({
currentPage: 1,
pageSize: 20,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
// 选中的行数据
const selectedRows = ref<any[]>([])
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
pagination.currentPage = 1 // 重置到第一页
getAccountList()
}
// 搜索处理
const handleSearch = () => {
console.log('搜索参数:', formFilters)
pagination.currentPage = 1 // 搜索时重置到第一页
getAccountList()
}
// 表单配置项
const formItems = computed<SearchFormItem[]>(() => [
{
label: '账号名称',
prop: 'name',
type: 'input',
config: {
clearable: true,
placeholder: '请输入账号名称'
}
},
{
label: '手机号',
prop: 'phone',
type: 'input',
config: {
clearable: true,
placeholder: '请输入手机号'
}
},
{
label: '账号类型',
prop: 'user_type',
type: 'select',
options: [
{ label: '超级管理员', value: 1 },
{ label: '平台用户', value: 2 },
{ label: '代理账号', value: 3 },
{ label: '企业账号', value: 4 }
],
config: {
clearable: true,
placeholder: '请选择账号类型'
}
},
{
label: '关联店铺',
prop: 'shop_id',
type: 'select',
options: shopList.value.map((shop) => ({
label: shop.shop_name,
value: shop.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleShopSearch,
placeholder: '请输入店铺名称搜索'
}
},
{
label: '关联企业',
prop: 'enterprise_id',
type: 'select',
options: enterpriseList.value.map((enterprise) => ({
label: enterprise.enterprise_name,
value: enterprise.id
})),
config: {
clearable: true,
filterable: true,
remote: true,
remoteMethod: handleEnterpriseSearch,
placeholder: '请输入企业名称搜索'
}
},
{
label: '状态',
prop: 'status',
type: 'select',
options: STATUS_SELECT_OPTIONS,
config: {
clearable: true,
placeholder: '请选择状态'
}
}
])
// 列配置
const columnOptions = [
{ label: '账号名称', prop: 'username' },
{ label: '手机号', prop: 'phone' },
{ label: '账号类型', prop: 'user_type' },
{ label: '店铺名称', prop: 'shop_name' },
{ label: '企业名称', prop: 'enterprise_name' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' }
]
// 显示对话框
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
// 重置表单验证状态
if (formRef.value) {
formRef.value.resetFields()
}
if (type === 'edit' && row) {
formData.id = row.id
formData.username = row.username
formData.phone = row.phone
formData.user_type = row.user_type
formData.shop_id = row.shop_id || null
formData.enterprise_id = row.enterprise_id || null
formData.password = ''
} else {
formData.id = ''
formData.username = ''
formData.phone = ''
formData.user_type = 2
formData.shop_id = null
formData.enterprise_id = null
formData.password = ''
}
}
// 删除账号
const deleteAccount = (row: any) => {
ElMessageBox.confirm(`确定要删除账号 ${row.username} 吗?`, '删除账号', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
})
.then(async () => {
try {
await AccountService.deleteAccount(row.id)
ElMessage.success('删除成功')
getAccountList()
} catch (error) {
console.error(error)
}
})
.catch(() => {
// 账号取消删除
})
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'username',
label: '账号名称',
minWidth: 120
},
{
prop: 'phone',
label: '手机号',
width: 130
},
{
prop: 'user_type',
label: '账号类型',
width: 120,
formatter: (row: any) => {
const typeMap: Record<number, string> = {
1: '超级管理员',
2: '平台用户',
3: '代理账号',
4: '企业账号'
}
return typeMap[row.user_type] || '-'
}
},
{
prop: 'shop_name',
label: '店铺名称',
minWidth: 150,
formatter: (row: any) => {
return row.shop_name || '-'
}
},
{
prop: 'enterprise_name',
label: '企业名称',
minWidth: 150,
formatter: (row: any) => {
return row.enterprise_name || '-'
}
},
{
prop: 'status',
label: '状态',
width: 100,
formatter: (row: any) => {
return h(ElSwitch, {
modelValue: row.status,
activeValue: CommonStatus.ENABLED,
inactiveValue: CommonStatus.DISABLED,
activeText: getStatusText(CommonStatus.ENABLED),
inactiveText: getStatusText(CommonStatus.DISABLED),
inlinePrompt: true,
disabled: !hasAuth('account:modify_status'),
'onUpdate:modelValue': (val: string | number | boolean) =>
handleStatusChange(row, val as number)
})
}
},
{
prop: 'created_at',
label: '创建时间',
width: 180,
formatter: (row: any) => formatDateTime(row.created_at)
}
])
// 表单实例
const formRef = ref<FormInstance>()
// 表单数据
const formData = reactive({
id: '',
username: '',
password: '',
phone: '',
user_type: 2,
shop_id: null as number | null,
enterprise_id: null as number | null
})
onMounted(() => {
// 从 URL 查询参数中读取 shop_id
const shopIdParam = route.query.shop_id
if (shopIdParam) {
formFilters.shop_id = Number(shopIdParam)
}
getAccountList()
loadAllRoles()
loadShopList()
loadEnterpriseList()
})
// 加载所有角色列表
const loadAllRoles = async () => {
try {
const res = await RoleService.getRoles({ page: 1, pageSize: 100 })
if (res.code === 0) {
allRoles.value = res.data.items || []
}
} catch (error) {
console.error('获取角色列表失败:', error)
}
}
// 计算属性:过滤后的可分配角色(根据账号类型过滤)
const filteredAvailableRoles = computed(() => {
let roles = allRoles.value
// 根据账号类型过滤角色
if (currentAccountType.value === 1) {
// 超级管理员:不能分配任何角色
return []
} else if (currentAccountType.value === 3) {
// 代理账号:只显示客户角色
roles = roles.filter((role) => role.role_type === 2)
} else if (currentAccountType.value === 4) {
// 企业账号:只显示客户角色
roles = roles.filter((role) => role.role_type === 2)
} else if (currentAccountType.value === 2) {
// 平台用户:只显示平台角色
roles = roles.filter((role) => role.role_type === 1)
}
// 根据搜索关键词过滤
if (!leftRoleFilter.value) return roles
const keyword = leftRoleFilter.value.toLowerCase()
return roles.filter((role) => role.role_name.toLowerCase().includes(keyword))
})
// 计算属性:过滤后的已分配角色
const filteredAssignedRoles = computed(() => {
const assignedRolesList = allRoles.value.filter((role) => selectedRoles.value.includes(role.ID))
if (!rightRoleFilter.value) return assignedRolesList
const keyword = rightRoleFilter.value.toLowerCase()
return assignedRolesList.filter((role) => role.role_name.toLowerCase().includes(keyword))
})
// 显示分配角色对话框
const showRoleDialog = async (row: any) => {
currentAccountId.value = row.id
currentAccountName.value = row.username
currentAccountType.value = row.user_type
selectedRoles.value = []
rolesToAdd.value = []
leftRoleFilter.value = ''
rightRoleFilter.value = ''
try {
// 每次打开对话框时重新加载最新的角色列表
await loadAllRoles()
// 先加载当前账号的角色,再打开对话框
const res = await AccountService.getAccountRoles(row.id)
if (res.code === 0) {
// 提取角色ID数组
const roles = res.data || []
// 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
// 数据加载完成后再打开对话框
roleDialogVisible.value = true
}
} catch (error) {
console.error('获取账号角色失败:', error)
}
}
// 批量添加角色
const addRoles = async () => {
if (rolesToAdd.value.length === 0) return
try {
// 所有账号只能分配一个角色
if (rolesToAdd.value.length > 1) {
ElMessage.warning('只能分配一个角色')
return
}
// 新角色会替换之前的角色
const newRoles = rolesToAdd.value
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
ElMessage.success('角色分配成功')
// 刷新列表以更新角色显示
await getAccountList()
} catch (error) {
console.error('分配角色失败:', error)
}
}
// 移除单个角色
const removeSingleRole = async (roleId: number) => {
try {
// 从已分配列表中移除该角色
const newRoles = selectedRoles.value.filter((id) => id !== roleId)
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
ElMessage.success('角色移除成功')
// 刷新列表以更新角色显示
await getAccountList()
} catch (error) {
console.error('移除角色失败:', error)
ElMessage.error('角色移除失败')
}
}
// 获取账号列表
const getAccountList = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
username: formFilters.name || undefined,
phone: formFilters.phone || undefined,
user_type: formFilters.user_type,
shop_id: formFilters.shop_id,
enterprise_id: formFilters.enterprise_id,
status: formFilters.status
}
const res = await AccountService.getAccounts(params)
if (res.code === 0) {
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
}
} catch (error) {
console.error('获取账号列表失败:', error)
} finally {
loading.value = false
}
}
const handleRefresh = () => {
getAccountList()
}
// 处理表格行选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 表单验证规则
const rules = reactive<FormRules>({
username: [
{ required: true, message: '请输入账号名称', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 8, max: 32, message: '密码长度为 8-32 个字符', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
user_type: [{ required: true, message: '请选择账号类型', trigger: 'change' }],
shop_id: [
{
validator: (rule: any, value: any, callback: any) => {
if (formData.user_type === 3 && !value) {
callback(new Error('代理账号必须关联店铺'))
} else {
callback()
}
},
trigger: 'change'
}
],
enterprise_id: [
{
validator: (rule: any, value: any, callback: any) => {
if (formData.user_type === 4 && !value) {
callback(new Error('企业账号必须关联企业'))
} else {
callback()
}
},
trigger: 'change'
}
]
})
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
if (dialogType.value === 'add') {
// 创建账号
const data: any = {
username: formData.username,
phone: formData.phone,
password: formData.password,
user_type: formData.user_type
}
// 根据账号类型添加相应的字段
if (formData.user_type === 3) {
data.shop_id = formData.shop_id
} else if (formData.user_type === 4) {
data.enterprise_id = formData.enterprise_id
}
await AccountService.createAccount(data)
ElMessage.success('添加成功')
} else {
// 编辑账号 - 只提交username和phone
const data: any = {
username: formData.username,
phone: formData.phone
}
await AccountService.updateAccount(Number(formData.id), data)
ElMessage.success('更新成功')
}
dialogVisible.value = false
getAccountList()
} catch (error) {
console.error(error)
}
}
})
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getAccountList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getAccountList()
}
// 状态切换
const handleStatusChange = async (row: any, newStatus: number) => {
const oldStatus = row.status
// 先更新UI
row.status = newStatus
try {
await AccountService.updateAccountStatus(row.id, newStatus as 0 | 1)
ElMessage.success('状态切换成功')
} catch (error) {
// 切换失败,恢复原状态
row.status = oldStatus
console.error(error)
}
}
// 加载店铺列表
const loadShopList = async (keyword: string = '') => {
try {
const res = await ShopService.getShops({
page: 1,
page_size: 20, // 默认加载20条
status: 1, // 只加载启用的店铺
shop_name: keyword || undefined // 根据店铺名称搜索
})
if (res.code === 0) {
shopList.value = res.data.items || []
}
} catch (error) {
console.error('获取店铺列表失败:', error)
}
}
// 店铺搜索处理
const handleShopSearch = (query: string) => {
loadShopList(query)
}
// 加载企业列表
const loadEnterpriseList = async (keyword: string = '') => {
try {
const res = await EnterpriseService.getEnterprises({
page: 1,
page_size: 20, // 默认加载20条
status: 1, // 只加载启用的企业
enterprise_name: keyword || undefined // 根据企业名称搜索
})
if (res.code === 0) {
enterpriseList.value = res.data.items || []
}
} catch (error) {
console.error('获取企业列表失败:', error)
}
}
// 企业搜索处理
const handleEnterpriseSearch = (query: string) => {
loadEnterpriseList(query)
}
// 右键菜单项配置
const contextMenuItems = computed((): MenuItemType[] => {
const items: MenuItemType[] = []
if (hasAuth('account:patch_role')) {
items.push({ key: 'assignRole', label: '分配角色' })
}
if (hasAuth('account:edit')) {
items.push({ key: 'edit', label: '编辑' })
}
if (hasAuth('account:delete')) {
items.push({ key: 'delete', label: '删除' })
}
return items
})
// 处理表格行右键菜单
const handleRowContextMenu = (row: any, column: any, event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
currentRow.value = row
contextMenuRef.value?.show(event)
}
// 处理右键菜单选择
const handleContextMenuSelect = (item: MenuItemType) => {
if (!currentRow.value) return
switch (item.key) {
case 'assignRole':
showRoleDialog(currentRow.value)
break
case 'edit':
showDialog('edit', currentRow.value)
break
case 'delete':
deleteAccount(currentRow.value)
break
}
}
</script>
<style lang="scss" scoped>
.account-page {
// 账号管理页面样式
}
:deep(.el-table__row.table-row-with-context-menu) {
cursor: pointer;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.dialog-title {
font-size: 18px;
font-weight: 600;
}
.account-info {
display: flex;
align-items: center;
gap: 8px;
.account-name {
font-size: 14px;
color: var(--el-text-color-regular);
}
}
}
.role-transfer-container {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 20px;
padding: 20px 0;
min-height: 500px;
.transfer-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
max-width: 380px;
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color);
.panel-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 12px;
.role-list {
display: flex;
flex-direction: column;
gap: 8px;
.role-item {
padding: 10px 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
transition: all 0.2s;
&:hover {
background: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
.role-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
:deep(.el-checkbox) {
width: 100%;
.el-checkbox__label {
width: 100%;
}
}
}
.assigned-role-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
.role-info {
flex: 1;
}
.el-button {
flex-shrink: 0;
}
}
}
}
}
.transfer-buttons {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
padding: 0 10px;
.el-button {
width: 100px;
}
}
}
</style>