fetch(modify):修改账号列表中的分配角色弹窗
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 2m33s

This commit is contained in:
sexygoat
2026-02-09 10:22:23 +08:00
parent b94c043a56
commit e8700c2585
5 changed files with 602 additions and 61 deletions

View File

@@ -422,7 +422,7 @@
"agent": "代理商管理", "agent": "代理商管理",
"customerAccount": "客户账号", "customerAccount": "客户账号",
"enterpriseCustomer": "企业客户", "enterpriseCustomer": "企业客户",
"enterpriseCustomerAccounts": "企业客户账号列表", "enterpriseCustomerAccounts": "客户账号列表",
"enterpriseCards": "企业卡管理", "enterpriseCards": "企业卡管理",
"customerCommission": "客户账号佣金" "customerCommission": "客户账号佣金"
}, },

View File

@@ -82,27 +82,107 @@
</ElDialog> </ElDialog>
<!-- 分配角色对话框 --> <!-- 分配角色对话框 -->
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px"> <ElDialog v-model="roleDialogVisible" width="900px">
<ElCheckboxGroup v-model="selectedRoles"> <template #header>
<div v-for="role in allRoles" :key="role.id" style="margin-bottom: 12px"> <div class="dialog-header">
<ElCheckbox :value="role.id"> <span class="dialog-title">分配角色</span>
{{ role.role_name }} <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 <ElTag
:type="role.role_type === 1 ? 'primary' : 'success'" :type="role.role_type === 1 ? 'primary' : 'success'"
size="small" size="small"
style="margin-left: 8px"
> >
{{ role.role_type === 1 ? '平台角色' : '客户角色' }} {{ role.role_type === 1 ? '平台角色' : '客户角色' }}
</ElTag> </ElTag>
</span>
</ElCheckbox> </ElCheckbox>
</div> </div>
</ElCheckboxGroup> </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> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<ElButton @click="roleDialogVisible = false">取消</ElButton> <ElButton @click="roleDialogVisible = false">关闭</ElButton>
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
>提交</ElButton
>
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
@@ -122,7 +202,7 @@
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue' import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { AccountService } from '@/api/modules/account' import { AccountService } from '@/api/modules/account'
import { RoleService } from '@/api/modules/role' import { RoleService } from '@/api/modules/role'
import { ShopService } from '@/api/modules' import { ShopService, EnterpriseService } from '@/api/modules'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
import type { PlatformRole } from '@/types/api' import type { PlatformRole } from '@/types/api'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
@@ -139,8 +219,12 @@
const loading = ref(false) const loading = ref(false)
const roleSubmitLoading = ref(false) const roleSubmitLoading = ref(false)
const currentAccountId = ref<number>(0) const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('')
const selectedRoles = ref<number[]>([]) const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([]) const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([])
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 定义表单搜索初始值 // 定义表单搜索初始值
const initialSearchState = { const initialSearchState = {
@@ -239,6 +323,22 @@
placeholder: '请输入店铺名称搜索' 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: '状态', label: '状态',
prop: 'status', prop: 'status',
@@ -436,6 +536,7 @@
getAccountList() getAccountList()
loadAllRoles() loadAllRoles()
loadShopList() loadShopList()
loadEnterpriseList()
}) })
// 加载所有角色列表 // 加载所有角色列表
@@ -450,10 +551,29 @@
} }
} }
// 计算属性:过滤后的可分配角色
const filteredAvailableRoles = computed(() => {
if (!leftRoleFilter.value) return allRoles.value
const keyword = leftRoleFilter.value.toLowerCase()
return allRoles.value.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) => { const showRoleDialog = async (row: any) => {
currentAccountId.value = row.id currentAccountId.value = row.id
currentAccountName.value = row.username
selectedRoles.value = [] selectedRoles.value = []
rolesToAdd.value = []
leftRoleFilter.value = ''
rightRoleFilter.value = ''
try { try {
// 每次打开对话框时重新加载最新的角色列表 // 每次打开对话框时重新加载最新的角色列表
@@ -464,7 +584,8 @@
if (res.code === 0) { if (res.code === 0) {
// 提取角色ID数组 // 提取角色ID数组
const roles = res.data || [] const roles = res.data || []
selectedRoles.value = roles.map((role: any) => role.id) // 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
// 数据加载完成后再打开对话框 // 数据加载完成后再打开对话框
roleDialogVisible.value = true roleDialogVisible.value = true
} }
@@ -473,19 +594,41 @@
} }
} }
// 提交分配角色 // 批量添加角色
const handleAssignRoles = async () => { const addRoles = async () => {
roleSubmitLoading.value = true if (rolesToAdd.value.length === 0) return
try { try {
await AccountService.assignRolesToAccount(currentAccountId.value, selectedRoles.value) // 将选中的角色添加到已分配列表
ElMessage.success('分配角色成功') const newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
roleDialogVisible.value = false await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
ElMessage.success('角色添加成功')
// 刷新列表以更新角色显示 // 刷新列表以更新角色显示
await getAccountList() await getAccountList()
} catch (error) { } catch (error) {
console.error(error) console.error('添加角色失败:', error)
} finally { }
roleSubmitLoading.value = false }
// 移除单个角色
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('角色移除失败')
} }
} }
@@ -620,10 +763,156 @@
const handleShopSearch = (query: string) => { const handleShopSearch = (query: string) => {
loadShopList(query) 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)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.account-page { .account-page {
// 账号管理页面样式 // 账号管理页面样式
} }
.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> </style>

View File

@@ -6,7 +6,7 @@
v-model:filter="searchForm" v-model:filter="searchForm"
:items="searchFormItems" :items="searchFormItems"
:show-expand="false" :show-expand="false"
:label-width="90" label-width="90"
@reset="handleReset" @reset="handleReset"
@search="handleSearch" @search="handleSearch"
></ArtSearchBar> ></ArtSearchBar>
@@ -210,7 +210,7 @@
import { h } from 'vue' import { h } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { EnterpriseService, ShopService } from '@/api/modules' import { EnterpriseService, ShopService } from '@/api/modules'
import { ElMessage, ElMessageBox, ElTag, ElSwitch } from 'element-plus' import { ElMessage, ElSwitch } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { EnterpriseItem, ShopResponse } from '@/types/api' import type { EnterpriseItem, ShopResponse } from '@/types/api'
import type { SearchFormItem } from '@/types' import type { SearchFormItem } from '@/types'
@@ -220,8 +220,6 @@
import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { BgColorEnum } from '@/enums/appEnum'
import { RoutesAlias } from '@/router/routesAlias'
defineOptions({ name: 'EnterpriseCustomer' }) defineOptions({ name: 'EnterpriseCustomer' })
@@ -807,9 +805,3 @@
} }
} }
</script> </script>
<style lang="scss" scoped>
.enterprise-customer-page {
// 可以在这里添加企业客户页面特定样式
}
</style>

View File

@@ -114,18 +114,107 @@
</ElDialog> </ElDialog>
<!-- 分配角色对话框 --> <!-- 分配角色对话框 -->
<ElDialog v-model="roleDialogVisible" title="分配角色" width="500px"> <ElDialog v-model="roleDialogVisible" width="900px">
<ElCheckboxGroup v-model="selectedRoles"> <template #header>
<div v-for="role in allRoles" :key="role.ID" style="margin-bottom: 12px"> <div class="dialog-header">
<ElCheckbox :label="role.ID">{{ role.role_name }}</ElCheckbox> <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> </div>
</ElCheckboxGroup> </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> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<ElButton @click="roleDialogVisible = false">取消</ElButton> <ElButton @click="roleDialogVisible = false">关闭</ElButton>
<ElButton type="primary" @click="handleAssignRoles" :loading="roleSubmitLoading"
>提交</ElButton
>
</div> </div>
</template> </template>
</ElDialog> </ElDialog>
@@ -185,8 +274,12 @@
const roleSubmitLoading = ref(false) const roleSubmitLoading = ref(false)
const passwordSubmitLoading = ref(false) const passwordSubmitLoading = ref(false)
const currentAccountId = ref<number>(0) const currentAccountId = ref<number>(0)
const currentAccountName = ref<string>('')
const selectedRoles = ref<number[]>([]) const selectedRoles = ref<number[]>([])
const allRoles = ref<PlatformRole[]>([]) const allRoles = ref<PlatformRole[]>([])
const rolesToAdd = ref<number[]>([])
const leftRoleFilter = ref('')
const rightRoleFilter = ref('')
// 定义表单搜索初始值 // 定义表单搜索初始值
const initialSearchState = { const initialSearchState = {
@@ -528,10 +621,29 @@
} }
} }
// 计算属性:过滤后的可分配角色
const filteredAvailableRoles = computed(() => {
if (!leftRoleFilter.value) return allRoles.value
const keyword = leftRoleFilter.value.toLowerCase()
return allRoles.value.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: PlatformAccount) => { const showRoleDialog = async (row: PlatformAccount) => {
currentAccountId.value = row.id currentAccountId.value = row.id
currentAccountName.value = row.username
selectedRoles.value = [] selectedRoles.value = []
rolesToAdd.value = []
leftRoleFilter.value = ''
rightRoleFilter.value = ''
try { try {
// 每次打开对话框时重新加载最新的角色列表 // 每次打开对话框时重新加载最新的角色列表
@@ -542,7 +654,8 @@
if (res.code === 0) { if (res.code === 0) {
// 提取角色ID数组 // 提取角色ID数组
const roles = res.data || [] const roles = res.data || []
selectedRoles.value = roles.map((role: any) => role.id) // 兼容 ID 和 id 两种字段名
selectedRoles.value = roles.map((role: any) => role.ID || role.id)
// 数据加载完成后再打开对话框 // 数据加载完成后再打开对话框
roleDialogVisible.value = true roleDialogVisible.value = true
} }
@@ -551,19 +664,42 @@
} }
} }
// 提交分配角色 // 批量添加角色
const handleAssignRoles = async () => { const addRoles = async () => {
roleSubmitLoading.value = true if (rolesToAdd.value.length === 0) return
try { try {
await AccountService.assignRolesToAccount(currentAccountId.value, selectedRoles.value) // 将选中的角色添加到已分配列表
ElMessage.success('分配角色成功') const newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
roleDialogVisible.value = false await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles
rolesToAdd.value = []
ElMessage.success('角色添加成功')
// 刷新列表以更新角色显示 // 刷新列表以更新角色显示
await getAccountList() await getAccountList()
} catch (error) { } catch (error) {
console.error(error) console.error('添加角色失败:', error)
} finally { ElMessage.error('角色添加失败')
roleSubmitLoading.value = false }
}
// 移除单个角色
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('角色移除失败')
} }
} }
@@ -768,4 +904,128 @@
.platform-account-page { .platform-account-page {
// 平台账号管理页面样式 // 平台账号管理页面样式
} }
.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> </style>

View File

@@ -28,7 +28,7 @@
:currentPage="pagination.page" :currentPage="pagination.page"
:pageSize="pagination.pageSize" :pageSize="pagination.pageSize"
:total="pagination.total" :total="pagination.total"
:marginTop="10" :marginTop="10"v
height="60vh" height="60vh"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@@ -43,7 +43,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed } from 'vue' import { h, computed, ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElTag, ElIcon, ElButton, ElCard } from 'element-plus' import { ElMessage, ElTag, ElIcon, ElButton, ElCard } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue' import { ArrowLeft } from '@element-plus/icons-vue'