diff --git a/src/router/guards/beforeEach.ts b/src/router/guards/beforeEach.ts
index 891a316..18c60db 100644
--- a/src/router/guards/beforeEach.ts
+++ b/src/router/guards/beforeEach.ts
@@ -110,14 +110,22 @@ async function handleRouteGuard(
return
}
- // 尝试刷新路由重新注册
+ // 路由未匹配时,判断是 404 还是 403
if (userStore.isLogin) {
- isRouteRegistered.value = false
- await handleDynamicRoutes(to, router, next)
- return
+ // 检查路由是否存在于 asyncRoutes 中
+ const routeExistsInAsync = checkRouteExistsInAsyncRoutes(to.path, asyncRoutes)
+ console.log(`[路由检查] 目标路径: ${to.path}, 是否存在于asyncRoutes: ${routeExistsInAsync}`)
+
+ if (routeExistsInAsync) {
+ // 路由存在于 asyncRoutes 但未注册,说明用户没有权限
+ console.warn('路由存在但用户无权限访问:', to.path)
+ next(RoutesAlias.Exception403 || '/exception/403')
+ return
+ }
}
- // 如果以上都不匹配,跳转到404
+ // 路由不存在,跳转到 404
+ console.log(`[路由检查] 路径 ${to.path} 不存在于asyncRoutes,跳转404`)
next(RoutesAlias.Exception404)
}
@@ -374,7 +382,7 @@ function convertBackendMenuToRoute(
}
}
- // 递归处理所有层级的 children
+ // 递归处理后端返回的 children
if (menu.children && menu.children.length > 0) {
const children = menu.children
.map((child: any) => convertBackendMenuToRoute(child, routeMap, menuUrl))
@@ -453,6 +461,88 @@ function isValidMenuList(menuList: AppRouteRecord[]): boolean {
return Array.isArray(menuList) && menuList.length > 0
}
+/**
+ * 检查路由是否存在于 asyncRoutes 中
+ * 支持动态参数匹配,如 /account-management/enterprise-customer/customer-accounts/3000 匹配 enterprise-customer/customer-accounts/:id
+ * @param targetPath 目标路由路径,如 /account-management/enterprise-customer/customer-accounts/3000
+ * @param routes 路由配置数组
+ * @param parentPath 父路由路径
+ */
+function checkRouteExistsInAsyncRoutes(
+ targetPath: string,
+ routes: AppRouteRecord[],
+ parentPath = ''
+): boolean {
+ for (const route of routes) {
+ // 构建完整路径
+ const fullPath = route.path.startsWith('/')
+ ? route.path
+ : parentPath
+ ? `${parentPath}/${route.path}`.replace(/\/+/g, '/')
+ : `/${route.path}`
+
+ // 检查当前路由是否匹配(支持动态参数)
+ const isMatch = matchRoutePath(targetPath, fullPath)
+
+ if (isMatch) {
+ console.log(`[路由匹配成功] 目标: ${targetPath}, 匹配到: ${fullPath}`)
+ return true
+ }
+
+ // 递归检查子路由
+ if (route.children && route.children.length > 0) {
+ if (checkRouteExistsInAsyncRoutes(targetPath, route.children, fullPath)) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+/**
+ * 匹配路由路径,支持动态参数
+ * @param targetPath 实际路径,如 /account-management/enterprise-customer/customer-accounts/3000
+ * @param routePath 路由定义路径,如 /account-management/enterprise-customer/customer-accounts/:id
+ */
+function matchRoutePath(targetPath: string, routePath: string): boolean {
+ // 移除查询参数
+ const cleanTargetPath = targetPath.split('?')[0]
+ const cleanRoutePath = routePath.split('?')[0]
+
+ // 如果完全匹配,直接返回
+ if (cleanTargetPath === cleanRoutePath) {
+ return true
+ }
+
+ // 将路径分割成段
+ const targetSegments = cleanTargetPath.split('/').filter(Boolean)
+ const routeSegments = cleanRoutePath.split('/').filter(Boolean)
+
+ // 段数必须相同
+ if (targetSegments.length !== routeSegments.length) {
+ return false
+ }
+
+ // 逐段比较
+ for (let i = 0; i < routeSegments.length; i++) {
+ const routeSegment = routeSegments[i]
+ const targetSegment = targetSegments[i]
+
+ // 如果是动态参数(以 : 开头),跳过比较
+ if (routeSegment.startsWith(':')) {
+ continue
+ }
+
+ // 如果不是动态参数,必须完全匹配
+ if (routeSegment !== targetSegment) {
+ return false
+ }
+ }
+
+ return true
+}
+
/**
* 重置路由相关状态
*/
diff --git a/src/router/guards/permission.ts b/src/router/guards/permission.ts
index c02762c..46c4353 100644
--- a/src/router/guards/permission.ts
+++ b/src/router/guards/permission.ts
@@ -107,10 +107,12 @@ export const hasRoutePermission = (
*/
function checkMenuAccess(path: string, menus: any[]): boolean {
for (const menu of menus) {
- // 检查当前菜单的 URL 是否匹配
- if (menu.url && (path === menu.url || path.startsWith(menu.url + '/'))) {
+ // 检查当前菜单的 URL 是否匹配(支持动态参数)
+ if (menu.url && matchMenuPath(path, menu.url)) {
+ console.log(`[菜单权限检查] 路径 ${path} 匹配菜单 ${menu.url}`)
return true
}
+
// 递归检查子菜单
if (menu.children && menu.children.length > 0) {
if (checkMenuAccess(path, menu.children)) {
@@ -118,9 +120,53 @@ function checkMenuAccess(path: string, menus: any[]): boolean {
}
}
}
+ console.log(`[菜单权限检查] 路径 ${path} 未找到匹配的菜单`)
return false
}
+/**
+ * 匹配菜单路径,支持动态参数
+ * @param actualPath 实际访问路径,如 /account-management/enterprise-customer/customer-accounts/3000
+ * @param menuPath 菜单定义路径,如 /account-management/enterprise-customer/customer-accounts/:id
+ */
+function matchMenuPath(actualPath: string, menuPath: string): boolean {
+ // 移除查询参数
+ const cleanActualPath = actualPath.split('?')[0]
+ const cleanMenuPath = menuPath.split('?')[0]
+
+ // 如果完全匹配,直接返回
+ if (cleanActualPath === cleanMenuPath) {
+ return true
+ }
+
+ // 将路径分割成段
+ const actualSegments = cleanActualPath.split('/').filter(Boolean)
+ const menuSegments = cleanMenuPath.split('/').filter(Boolean)
+
+ // 段数必须相同
+ if (actualSegments.length !== menuSegments.length) {
+ return false
+ }
+
+ // 逐段比较
+ for (let i = 0; i < menuSegments.length; i++) {
+ const menuSegment = menuSegments[i]
+ const actualSegment = actualSegments[i]
+
+ // 如果是动态参数(以 : 开头),跳过比较
+ if (menuSegment.startsWith(':')) {
+ continue
+ }
+
+ // 如果不是动态参数,必须完全匹配
+ if (menuSegment !== actualSegment) {
+ return false
+ }
+ }
+
+ return true
+}
+
/**
* 检查 Token 是否有效
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口
diff --git a/src/views/account-management/account/index.vue b/src/views/account-management/account/index.vue
index f663a56..9e80a9a 100644
--- a/src/views/account-management/account/index.vue
+++ b/src/views/account-management/account/index.vue
@@ -95,18 +95,6 @@
分配角色
{{ currentAccountName }}
-
- 超级管理员不能分配角色
-
-
- 平台用户只能分配一个平台角色
-
-
- 代理账号只能分配一个客户角色
-
-
- 企业账号只能分配一个客户角色
-
diff --git a/src/views/account-management/enterprise-cards/index.vue b/src/views/account-management/enterprise-cards/index.vue
index 961c210..5f41f97 100644
--- a/src/views/account-management/enterprise-cards/index.vue
+++ b/src/views/account-management/enterprise-cards/index.vue
@@ -67,6 +67,7 @@
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
+ @row-contextmenu="handleRowContextMenu"
>
@@ -106,7 +107,7 @@
@selection-change="handleAvailableCardsSelectionChange"
>
-
+
+
+
+
@@ -224,6 +233,8 @@
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { useAuth } from '@/composables/useAuth'
import { formatDateTime } from '@/utils/business/format'
+ import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue'
+ import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { BgColorEnum } from '@/enums/appEnum'
import type {
@@ -264,6 +275,11 @@
const availableCardsLoading = ref(false)
const availableCardsList = ref([])
const selectedAvailableCards = ref([])
+ const allocatedCardIccids = ref>(new Set())
+
+ // 右键菜单相关
+ const cardOperationMenuRef = ref>()
+ const currentOperatingCard = ref(null)
// 卡搜索表单初始值
const initialCardSearchState = {
@@ -433,8 +449,7 @@
{ label: '状态', prop: 'status' },
{ label: '状态名称', prop: 'status_name' },
{ label: '网络状态', prop: 'network_status' },
- { label: '网络状态名称', prop: 'network_status_name' },
- { label: '操作', prop: 'operation' }
+ { label: '网络状态名称', prop: 'network_status_name' }
]
const cardList = ref([])
@@ -540,41 +555,6 @@
prop: 'network_status_name',
label: '网络状态名称',
width: 130
- },
- {
- prop: 'operation',
- label: '操作',
- width: 100,
- fixed: 'right',
- formatter: (row: EnterpriseCardItem) => {
- const buttons = []
-
- if (row.network_status === 0) {
- // 停机状态,显示复机按钮
- if (hasAuth('enterprise_cards:resume')) {
- buttons.push(
- h(ArtButtonTable, {
- text: '复机',
- iconClass: BgColorEnum.SUCCESS,
- onClick: () => handleResume(row)
- })
- )
- }
- } else {
- // 开机状态,显示停机按钮
- if (hasAuth('enterprise_cards:suspend')) {
- buttons.push(
- h(ArtButtonTable, {
- text: '停机',
- iconClass: BgColorEnum.ERROR,
- onClick: () => handleSuspend(row)
- })
- )
- }
- }
-
- return h('div', { style: 'display: flex; gap: 8px;' }, buttons)
- }
}
])
@@ -849,10 +829,17 @@
getAvailableCardsList()
}
+ // 检查卡是否可选(已授权的卡不可选)
+ const checkCardSelectable = (row: StandaloneIotCard) => {
+ return !allocatedCardIccids.value.has(row.iccid)
+ }
+
// 显示授权对话框
const showAllocateDialog = () => {
allocateDialogVisible.value = true
selectedAvailableCards.value = []
+ // 收集已授权的卡的ICCID
+ allocatedCardIccids.value = new Set(cardList.value.map((card) => card.iccid))
// 重置搜索条件
Object.assign(cardSearchForm, { ...initialCardSearchState })
cardPagination.page = 1
@@ -954,6 +941,56 @@
getTableData()
}
+ // 右键菜单项配置
+ const cardOperationMenuItems = computed((): MenuItemType[] => {
+ const items: MenuItemType[] = []
+
+ if (!currentOperatingCard.value) return items
+
+ if (currentOperatingCard.value.network_status === 0) {
+ // 停机状态 - 显示复机
+ if (hasAuth('enterprise_cards:resume')) {
+ items.push({
+ key: 'resume',
+ label: '复机'
+ })
+ }
+ } else {
+ // 开机状态 - 显示停机
+ if (hasAuth('enterprise_cards:suspend')) {
+ items.push({
+ key: 'suspend',
+ label: '停机'
+ })
+ }
+ }
+
+ return items
+ })
+
+ // 处理右键菜单
+ const handleRowContextMenu = (row: EnterpriseCardItem, column: any, event: MouseEvent) => {
+ event.preventDefault()
+ currentOperatingCard.value = row
+ nextTick(() => {
+ cardOperationMenuRef.value?.show(event)
+ })
+ }
+
+ // 处理菜单选择
+ const handleCardOperationMenuSelect = (item: MenuItemType) => {
+ if (!currentOperatingCard.value) return
+
+ switch (item.key) {
+ case 'suspend':
+ handleSuspend(currentOperatingCard.value)
+ break
+ case 'resume':
+ handleResume(currentOperatingCard.value)
+ break
+ }
+ }
+
// 停机卡
const handleSuspend = (row: EnterpriseCardItem) => {
ElMessageBox.confirm('确定要停机该卡吗?', '停机卡', {
diff --git a/src/views/account-management/enterprise-customer/index.vue b/src/views/account-management/enterprise-customer/index.vue
index 1d1a964..54ca484 100644
--- a/src/views/account-management/enterprise-customer/index.vue
+++ b/src/views/account-management/enterprise-customer/index.vue
@@ -747,8 +747,7 @@
// 查看客户账号
const viewCustomerAccounts = (row: EnterpriseItem) => {
router.push({
- name: 'EnterpriseCustomerAccounts',
- params: { id: row.id },
+ path: `/account-management/enterprise-customer/customer-accounts/${row.id}`,
query: { type: 'enterprise' }
})
}
diff --git a/src/views/asset-management/iot-card-management/index.vue b/src/views/asset-management/iot-card-management/index.vue
index 52a4973..4244626 100644
--- a/src/views/asset-management/iot-card-management/index.vue
+++ b/src/views/asset-management/iot-card-management/index.vue
@@ -342,7 +342,12 @@
-
+
{{ seriesBindingResult.success_count }}
@@ -1295,22 +1300,21 @@
try {
const res = await CardService.allocateStandaloneCards(params)
+ // code === 0 表示操作成功(接口调用成功),显示结果对话框
if (res.code === 0) {
allocationResult.value = res.data
resultTitle.value = '批量分配结果'
allocateDialogVisible.value = false
resultDialogVisible.value = true
+
// 清空选择
- if (tableRef.value) {
- tableRef.value.clearSelection()
- }
selectedCards.value = []
+
// 刷新列表
- getTableData()
+ await getTableData()
}
} catch (error) {
console.error(error)
- ElMessage.error('批量分配失败,请重试')
} finally {
allocateLoading.value = false
}
@@ -1348,18 +1352,21 @@
try {
const res = await CardService.recallStandaloneCards(params)
+ // code === 0 表示操作成功(接口调用成功),显示结果对话框
if (res.code === 0) {
allocationResult.value = res.data
resultTitle.value = '批量回收结果'
recallDialogVisible.value = false
resultDialogVisible.value = true
+
// 清空选择
- if (tableRef.value) {
- tableRef.value.clearSelection()
- }
selectedCards.value = []
+
// 刷新列表
- getTableData()
+ await getTableData()
+ } else {
+ // code !== 0 才是真正的失败(接口调用失败)
+ ElMessage.error(res.msg || '批量回收失败,请重试')
}
} catch (error) {
console.error(error)
@@ -1424,6 +1431,12 @@
}
}
+ // 关闭套餐系列绑定结果对话框
+ const handleSeriesBindingResultDialogClose = () => {
+ // 刷新列表
+ getTableData()
+ }
+
// 执行套餐系列绑定
const handleSeriesBinding = async () => {
if (!seriesBindingFormRef.value) return
@@ -1450,13 +1463,10 @@
seriesBindingResultDialogVisible.value = true
// 清空选择
- if (tableRef.value) {
- tableRef.value.clearSelection()
- }
selectedCards.value = []
- // 刷新列表
- getTableData()
+ // 立即刷新列表
+ await getTableData()
// 显示消息提示
if (res.data.fail_count === 0) {
@@ -1471,6 +1481,7 @@
}
} catch (error) {
console.error(error)
+ ElMessage.error('批量设置套餐系列失败,请重试')
} finally {
seriesBindingLoading.value = false
}
diff --git a/src/views/common/account-list.vue b/src/views/common/account-list.vue
index cc3a4a9..48ed9a6 100644
--- a/src/views/common/account-list.vue
+++ b/src/views/common/account-list.vue
@@ -99,14 +99,6 @@
}}
{{ currentAccountName }}
-
- 代理账号只能分配一个客户角色
-
@@ -776,18 +768,26 @@
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%;
+ display: flex;
+
+ .role-info {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ flex: 1;
+ width: 100%;
+
+ > span:first-child {
+ flex: 1;
+ min-width: 0;
+ }
+ }
}
}
}
@@ -800,6 +800,18 @@
.role-info {
flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+
+ > span:first-child {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
}
.el-button {
diff --git a/src/views/package-management/package-list/index.vue b/src/views/package-management/package-list/index.vue
index ae9b54a..68ef589 100644
--- a/src/views/package-management/package-list/index.vue
+++ b/src/views/package-management/package-list/index.vue
@@ -480,7 +480,7 @@
{
prop: 'real_data_mb',
label: '真流量',
- width: 100,
+ width: 120,
formatter: (row: PackageResponse) => `${row.real_data_mb}MB`
},
{