修改: 权限重复
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 4m27s

This commit is contained in:
sexygoat
2026-02-26 17:34:11 +08:00
parent 3570b062a1
commit f1cb1e53c8
7 changed files with 140 additions and 127 deletions

View File

@@ -86,9 +86,18 @@
<span class="dialog-title">分配角色</span>
<div class="account-info">
<span class="account-name">{{ currentAccountName }}</span>
<ElTag v-if="currentAccountType === 1" type="danger" size="small" style="margin-left: 8px">
超级管理员不能分配角色
</ElTag>
<ElTag v-if="currentAccountType === 2" type="info" size="small" style="margin-left: 8px">
平台用户只能分配一个平台角色
</ElTag>
<ElTag v-if="currentAccountType === 3" type="warning" size="small" style="margin-left: 8px">
代理账号只能分配一个客户角色
</ElTag>
<ElTag v-if="currentAccountType === 4" type="warning" size="small" style="margin-left: 8px">
企业账号只能分配一个客户角色
</ElTag>
</div>
</div>
</template>
@@ -549,9 +558,15 @@
let roles = allRoles.value
// 根据账号类型过滤角色
if (currentAccountType.value === 3) {
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)
@@ -605,21 +620,15 @@
if (rolesToAdd.value.length === 0) return
try {
let newRoles: number[]
// 代理账号只能分配一个角色,会覆盖之前的角色
if (currentAccountType.value === 3) {
if (rolesToAdd.value.length > 1) {
ElMessage.warning('代理账号只能分配一个角色')
return
}
// 只保留新选择的一个角色
newRoles = rolesToAdd.value
} else {
// 其他账号类型可以分配多个角色
newRoles = [...new Set([...selectedRoles.value, ...rolesToAdd.value])]
// 所有账号只能分配一个角色
if (rolesToAdd.value.length > 1) {
ElMessage.warning('只能分配一个角色')
return
}
// 新角色会替换之前的角色
const newRoles = rolesToAdd.value
await AccountService.assignRolesToAccount(currentAccountId.value, newRoles)
selectedRoles.value = newRoles

View File

@@ -458,7 +458,7 @@
{
prop: 'operation',
label: '操作',
width: 280,
width: 190,
fixed: 'right',
formatter: (row: EnterpriseItem) => {
const buttons = []
@@ -481,16 +481,6 @@
)
}
// 只要有编辑、修改密码权限之一,就显示更多操作按钮
if (hasAuth('enterprise_customer:edit') || hasAuth('enterprise_customer:update_pwd')) {
buttons.push(
h(ArtButtonTable, {
text: '更多操作',
onContextmenu: (e: MouseEvent) => showEnterpriseOperationMenu(e, row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}

View File

@@ -358,7 +358,10 @@
</template>
</ElTableColumn>
</ElTable>
<div v-if="deviceCards.length === 0" style="text-align: center; padding: 20px; color: #909399">
<div
v-if="deviceCards.length === 0"
style="text-align: center; padding: 20px; color: #909399"
>
暂无绑定的卡片
</div>
</div>
@@ -374,7 +377,12 @@
<!-- 绑定卡弹窗 -->
<ElDialog v-model="bindCardDialogVisible" title="绑定卡到设备" width="500px">
<ElForm ref="bindCardFormRef" :model="bindCardForm" :rules="bindCardRules" label-width="100px">
<ElForm
ref="bindCardFormRef"
:model="bindCardForm"
:rules="bindCardRules"
label-width="100px"
>
<ElFormItem label="IoT卡" prop="iot_card_id">
<ElSelect
v-model="bindCardForm.iot_card_id"
@@ -396,7 +404,11 @@
</ElSelect>
</ElFormItem>
<ElFormItem label="插槽位置" prop="slot_position">
<ElSelect v-model="bindCardForm.slot_position" placeholder="请选择插槽位置" style="width: 100%">
<ElSelect
v-model="bindCardForm.slot_position"
placeholder="请选择插槽位置"
style="width: 100%"
>
<ElOption
v-for="i in currentDeviceDetail?.max_sim_slots || 4"
:key="i"
@@ -903,7 +915,9 @@
// 重新加载卡列表
await loadDeviceCards(currentDeviceDetail.value.id)
// 刷新设备详情以更新绑定卡数量
const detailRes = await DeviceService.getDeviceByImei(currentDeviceDetail.value.device_no)
const detailRes = await DeviceService.getDeviceByImei(
currentDeviceDetail.value.device_no
)
if (detailRes.code === 0 && detailRes.data) {
currentDeviceDetail.value = detailRes.data
}
@@ -929,10 +943,7 @@
})
.then(async () => {
try {
const res = await DeviceService.unbindCard(
currentDeviceDetail.value.id,
row.iot_card_id
)
const res = await DeviceService.unbindCard(currentDeviceDetail.value.id, row.iot_card_id)
if (res.code === 0) {
ElMessage.success('解绑成功')
// 重新加载卡列表
@@ -1070,23 +1081,6 @@
label: '创建时间',
width: 180,
formatter: (row: Device) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 120,
fixed: 'right',
formatter: (row: Device) => {
// Show "更多操作" only if user has at least one operation permission
const hasAnyOperationPermission = hasAuth('devices:delete') || hasAuth('devices:look_binding')
if (hasAnyOperationPermission) {
return h(ArtButtonTable, {
text: '更多操作',
onContextmenu: (e: MouseEvent) => showDeviceOperationMenu(e, row.device_no)
})
}
return null
}
}
])
@@ -1105,8 +1099,8 @@
try {
const res = await ShopService.getShops({
page: 1,
page_size: 9999, // 获取所有数据用于构建树形结构
status: 1 // 只获取启用的店铺
page_size: 9999, // 获取所有数据用于构建树形结构
status: 1 // 只获取启用的店铺
})
if (res.code === 0) {
shopTreeData.value = buildShopTree(res.data.items || [])
@@ -1349,7 +1343,7 @@
const params: any = {
page: 1,
page_size: 20,
status: 1 // 只获取启用的
status: 1 // 只获取启用的
}
if (seriesName) {
params.series_name = seriesName
@@ -1445,7 +1439,7 @@
// 通过设备号查看卡片
const handleViewCardsByDeviceNo = (deviceNo: string) => {
const device = deviceList.value.find(d => d.device_no === deviceNo)
const device = deviceList.value.find((d) => d.device_no === deviceNo)
if (device) {
handleViewCards(device)
} else {
@@ -1456,7 +1450,7 @@
// 通过设备号删除设备
const handleDeleteDeviceByNo = async (deviceNo: string) => {
// 先根据设备号找到设备对象
const device = deviceList.value.find(d => d.device_no === deviceNo)
const device = deviceList.value.find((d) => d.device_no === deviceNo)
if (device) {
deleteDevice(device)
} else {

View File

@@ -1096,18 +1096,6 @@
label: '创建时间',
width: 180,
formatter: (row: StandaloneIotCard) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 120,
fixed: 'right',
formatter: (row: StandaloneIotCard) => {
return h(ArtButtonTable, {
text: '更多操作',
onContextmenu: (e: MouseEvent) => showCardOperationMenu(e, row.iccid)
})
}
}
])

View File

@@ -37,12 +37,36 @@
:marginTop="10"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@row-contextmenu="handleRowContextMenu"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<!-- 右键菜单 -->
<div
v-if="contextMenuVisible"
:style="{
position: 'fixed',
left: contextMenuPosition.x + 'px',
top: contextMenuPosition.y + 'px',
zIndex: 9999
}"
>
<ElCard shadow="always" class="context-menu">
<div class="context-menu-item" @click="handleViewDetail(contextMenuRow)" v-if="hasAuth('package_series:detail')">
<span>详情</span>
</div>
<div class="context-menu-item" @click="showDialog('edit', contextMenuRow)" v-if="hasAuth('package_series:edit')">
<span>编辑</span>
</div>
<div class="context-menu-item danger" @click="deleteSeries(contextMenuRow)" v-if="hasAuth('package_series:delete')">
<span>删除</span>
</div>
</ElCard>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog
v-model="dialogVisible"
@@ -386,6 +410,11 @@
const tableRef = ref()
const formRef = ref<FormInstance>()
// 右键菜单状态
const contextMenuVisible = ref(false)
const contextMenuPosition = reactive({ x: 0, y: 0 })
const contextMenuRow = ref<PackageSeriesResponse | null>(null)
// 搜索表单初始值
const initialSearchState = {
series_name: '',
@@ -456,8 +485,7 @@
{ label: '强充计算类型', prop: 'force_calc_type' },
{ label: '时效类型', prop: 'validity_type' },
{ label: '状态', prop: 'status' },
{ label: '创建时间', prop: 'created_at' },
{ label: '操作', prop: 'operation' }
{ label: '创建时间', prop: 'created_at' }
]
// 表单验证规则
@@ -675,50 +703,34 @@
label: '创建时间',
width: 180,
formatter: (row: PackageSeriesResponse) => formatDateTime(row.created_at)
},
{
prop: 'operation',
label: '操作',
width: 220,
fixed: 'right',
formatter: (row: PackageSeriesResponse) => {
const buttons = []
// 详情按钮
buttons.push(
h(ArtButtonTable, {
text: '详情',
onClick: () => handleViewDetail(row)
})
)
if (hasAuth('package_series:edit')) {
buttons.push(
h(ArtButtonTable, {
text: '编辑',
onClick: () => showDialog('edit', row)
})
)
}
if (hasAuth('package_series:delete')) {
buttons.push(
h(ArtButtonTable, {
text: '删除',
onClick: () => deleteSeries(row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
])
onMounted(() => {
getTableData()
// 添加全局点击事件监听,关闭右键菜单
document.addEventListener('click', closeContextMenu)
})
onBeforeUnmount(() => {
// 移除全局点击事件监听
document.removeEventListener('click', closeContextMenu)
})
// 处理右键菜单
const handleRowContextMenu = (row: PackageSeriesResponse, column: any, event: MouseEvent) => {
event.preventDefault()
contextMenuRow.value = row
contextMenuPosition.x = event.clientX
contextMenuPosition.y = event.clientY
contextMenuVisible.value = true
}
// 关闭右键菜单
const closeContextMenu = () => {
contextMenuVisible.value = false
}
// 获取套餐系列列表
const getTableData = async () => {
loading.value = true
@@ -1046,4 +1058,35 @@
.dialog-footer {
text-align: right;
}
.context-menu {
min-width: 120px;
padding: 4px 0;
background: white;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
:deep(.el-card__body) {
padding: 0;
}
.context-menu-item {
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 14px;
color: var(--el-text-color-primary);
&:hover {
background-color: var(--el-fill-color-light);
}
&.danger {
color: var(--el-color-danger);
&:hover {
background-color: var(--el-color-danger-light-9);
}
}
}
}
</style>

View File

@@ -181,7 +181,9 @@
:label="role.role_name"
:value="role.ID"
>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div
style="display: flex; justify-content: space-between; align-items: center"
>
<span>{{ role.role_name }}</span>
<ElTag type="success" size="small">客户角色</ElTag>
</div>
@@ -623,7 +625,7 @@
{
prop: 'operation',
label: '操作',
width: 200,
width: 110,
fixed: 'right',
formatter: (row: ShopResponse) => {
const buttons = []
@@ -637,16 +639,6 @@
)
}
// 只要有编辑或删除权限之一,就显示更多操作按钮
if (hasAuth('shop:edit') || hasAuth('shop:delete')) {
buttons.push(
h(ArtButtonTable, {
text: '更多操作',
onContextmenu: (e: MouseEvent) => showShopOperationMenu(e, row)
})
)
}
return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
}
}
@@ -855,9 +847,7 @@
{ len: 11, message: '手机号必须为 11 位', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
],
default_role_id: [
{ required: true, message: '请选择默认角色', trigger: 'blur' }
]
default_role_id: [{ required: true, message: '请选择默认角色', trigger: 'blur' }]
})
// 提交表单
@@ -1048,9 +1038,8 @@
const showAddRoleDialog = async () => {
addRoleDialogVisible.value = true
// 如果已有默认角色,预选第一个
selectedRoleId.value = currentDefaultRoles.value.length > 0
? currentDefaultRoles.value[0].role_id
: undefined
selectedRoleId.value =
currentDefaultRoles.value.length > 0 ? currentDefaultRoles.value[0].role_id : undefined
await loadAvailableRoles()
}

View File

@@ -734,15 +734,15 @@
const getLeftCheckedKeysWithHalf = (): number[] => {
if (!leftTreeRef.value) return []
const checkedKeys = leftTreeRef.value.getCheckedKeys(false)
const parentIds = new Set<number>()
const allIds = new Set<number>(checkedKeys)
// 为每个勾选的节点找到所有父节点
checkedKeys.forEach((key: number) => {
const parents = getParentNodeIds(key)
parents.forEach(parentId => parentIds.add(parentId))
parents.forEach(parentId => allIds.add(parentId))
})
return [...checkedKeys, ...Array.from(parentIds)]
return Array.from(allIds)
}
// 添加权限