diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
index 9c308ba..d1eedc4 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -73,7 +73,7 @@
-
+
@@ -83,10 +83,11 @@
@@ -97,7 +98,7 @@
删除
-
+
@@ -130,7 +131,7 @@
:bordered="false"
:single-line="false"
children-key="children"
- size="large"
+ size="medium"
class="custom-table"
@update:checked-row-keys="handleRowSelectionChange"
@update:expanded-row-keys="handleExpandedKeysChange"
@@ -142,13 +143,14 @@
:type="getEmptyType()"
:title="getEmptyTitle()"
:description="getEmptyDescription()"
- :show-action="true"
+ :show-action="getShowEmptyAction()"
:action-text="getEmptyActionText()"
size="medium"
@action="handleEmptyAction"
>
-
+
@@ -376,11 +378,11 @@
/>
-
-
+
+
@@ -432,17 +434,29 @@
-
-
-
-
- 是
-
-
- 否
-
-
-
+
+
+
+
+
+
+ 是
+
+
+ 否
+
+
+
+
+
+
+
+
+
+
@@ -473,18 +487,34 @@
-
+
-
-
-
- 是
-
-
- 否
-
-
-
+
+
+
+
+
+ 是
+
+
+ 否
+
+
+
+
+
+
+
+
+ 是
+
+
+ 否
+
+
+
+
+
@@ -514,7 +544,8 @@ import CoiEmpty from '@/components/common/CoiEmpty.vue'
import CoiIcon from '@/components/common/CoiIcon.vue'
import IconSelect from '@/components/common/IconSelect.vue'
import { PERMISSIONS } from '@/constants/permissions'
-import { coiMsgBox, coiMsgError, coiMsgSuccess } from '@/utils/coi'
+import { usePermission } from '@/hooks'
+import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
import {
addMenu,
batchDeleteMenu,
@@ -531,6 +562,9 @@ import { colord } from 'colord'
// 应用状态
const appStore = useAppStore()
+// 权限检查
+const { hasButton } = usePermission()
+
// 主题色计算属性
const themeColors = computed(() => {
const primary = appStore.primaryColor
@@ -598,13 +632,13 @@ const rules: FormRules = {
],
parentId: [
{
- required: false, // 改为非必填,因为'0'是有效的顶级菜单值
- type: 'string',
+ required: true,
message: '请选择上级菜单',
trigger: ['change', 'blur'],
validator: (rule: any, value: any, callback: any) => {
- // parentId 应该是一个有效的字符串
- if (typeof value === 'string' && value.length > 0) {
+ // 允许字符串或数字类型,并确保有效值
+ const stringValue = String(value)
+ if (stringValue && stringValue.length > 0 && stringValue !== 'undefined' && stringValue !== 'null') {
callback()
}
else {
@@ -728,7 +762,7 @@ const columns: DataTableColumns = [
render: row => row.auth || '-',
},
{
- title: '页面路径',
+ title: '选择路由',
key: 'component',
width: 200,
align: 'center',
@@ -798,16 +832,16 @@ const columns: DataTableColumns = [
const buttons = []
// 新增子菜单按钮
- if (row.menuType !== '3') {
+ if (row.menuType !== '3' && hasButton(PERMISSIONS.MENU.ADD)) {
buttons.push(h(NTooltip, {
trigger: 'hover',
}, {
default: () => '新增子菜单',
trigger: () => h(NButton, {
- type: 'info',
+ type: 'success',
size: 'medium',
circle: true,
- class: 'action-btn action-btn-info',
+ class: 'action-btn action-btn-success',
onClick: () => handleAddChild(row),
}, {
icon: () => h(NIcon, { size: 18 }, { default: () => h(IconParkOutlinePlus) }),
@@ -816,42 +850,46 @@ const columns: DataTableColumns = [
}
// 编辑按钮
- buttons.push(h(NTooltip, {
- trigger: 'hover',
- }, {
- default: () => '编辑',
- trigger: () => h(NButton, {
- type: 'primary',
- size: 'medium',
- circle: true,
- class: 'action-btn action-btn-edit',
- onClick: () => handleEdit(row),
- }, {
- icon: () => h(NIcon, { size: 18 }, { default: () => h(IconParkOutlineEdit) }),
- }),
- }))
-
- // 删除按钮
- buttons.push(h(NPopconfirm, {
- onPositiveClick: () => handleDelete(row.menuId),
- negativeText: '取消',
- positiveText: '确定',
- }, {
- default: () => '确定删除此菜单吗?',
- trigger: () => h(NTooltip, {
+ if (hasButton(PERMISSIONS.MENU.UPDATE)) {
+ buttons.push(h(NTooltip, {
trigger: 'hover',
}, {
- default: () => '删除',
+ default: () => '编辑',
trigger: () => h(NButton, {
- type: 'error',
+ type: 'primary',
size: 'medium',
circle: true,
- class: 'action-btn action-btn-delete',
+ class: 'action-btn action-btn-edit',
+ onClick: () => handleEdit(row),
}, {
- icon: () => h(NIcon, { size: 18 }, { default: () => h(IconParkOutlineDelete) }),
+ icon: () => h(NIcon, { size: 18 }, { default: () => h(IconParkOutlineEdit) }),
}),
- }),
- }))
+ }))
+ }
+
+ // 删除按钮
+ if (hasButton(PERMISSIONS.MENU.DELETE)) {
+ buttons.push(h(NPopconfirm, {
+ onPositiveClick: () => handleDelete(row.menuId),
+ negativeText: '取消',
+ positiveText: '确定',
+ }, {
+ default: () => '确定删除此菜单吗?',
+ trigger: () => h(NTooltip, {
+ trigger: 'hover',
+ }, {
+ default: () => '删除',
+ trigger: () => h(NButton, {
+ type: 'error',
+ size: 'medium',
+ circle: true,
+ class: 'action-btn action-btn-delete',
+ }, {
+ icon: () => h(NIcon, { size: 18 }, { default: () => h(IconParkOutlineDelete) }),
+ }),
+ }),
+ }))
+ }
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
},
@@ -908,22 +946,31 @@ function addTreeIndexToMenus(menus: MenuVo[]) {
// 构建菜单树
function buildMenuTree(menus: MenuVo[]): MenuVo[] {
- const menuMap = new Map()
+ const menuMap = new Map()
const result: MenuVo[] = []
- // 先将所有菜单放入map
+ // 先将所有菜单放入map,确保ID为字符串类型
menus.forEach((menu) => {
- menuMap.set(menu.menuId, { ...menu, children: [] })
+ const normalizedMenu = {
+ ...menu,
+ menuId: String(menu.menuId),
+ parentId: String(menu.parentId),
+ children: [],
+ }
+ menuMap.set(normalizedMenu.menuId, normalizedMenu)
})
// 构建树形结构
menus.forEach((menu) => {
- const currentMenu = menuMap.get(menu.menuId)!
- if (menu.parentId === 0) {
+ const menuId = String(menu.menuId)
+ const parentId = String(menu.parentId)
+ const currentMenu = menuMap.get(menuId)!
+
+ if (parentId === '0') {
result.push(currentMenu)
}
else {
- const parent = menuMap.get(menu.parentId)
+ const parent = menuMap.get(parentId)
if (parent) {
parent.children = parent.children || []
parent.children.push(currentMenu)
@@ -1028,7 +1075,7 @@ async function getMenuCascaderData() {
// 转换菜单数据为级联选择器格式
function convertToMenuCascader(menuTree: MenuVo[]): MenuCascaderBo[] {
return menuTree.map(menu => ({
- value: menu.menuId,
+ value: String(menu.menuId), // 确保value为字符串类型
label: menu.menuName,
children: menu.children && menu.children.length > 0
? convertToMenuCascader(menu.children)
@@ -1036,14 +1083,14 @@ function convertToMenuCascader(menuTree: MenuVo[]): MenuCascaderBo[] {
}))
}
-// 获取选中菜单的显示文本
-function getSelectedMenuText() {
- if (formData.value.parentId === '0') {
+// 构建菜单完整路径
+function buildMenuPath(menuId: string): string {
+ if (menuId === '0') {
return '最顶级菜单'
}
// 先在顶级菜单中查找
- const topMenu = topLevelMenus.value.find(menu => menu.value === formData.value.parentId)
+ const topMenu = topLevelMenus.value.find(menu => menu.value === menuId)
if (topMenu) {
return topMenu.label
}
@@ -1051,9 +1098,9 @@ function getSelectedMenuText() {
// 再在子菜单中查找
for (const menu of topLevelMenus.value) {
if (menu.children?.length) {
- const subMenu = menu.children.find(child => child.value === formData.value.parentId)
+ const subMenu = menu.children.find(child => child.value === menuId)
if (subMenu) {
- return subMenu.label
+ return `${menu.label} / ${subMenu.label}`
}
}
}
@@ -1061,10 +1108,15 @@ function getSelectedMenuText() {
return selectedMenuText.value
}
+// 获取选中菜单的显示文本
+function getSelectedMenuText() {
+ return buildMenuPath(formData.value.parentId)
+}
+
// 处理选择父菜单
-function handleSelectParent(value: string, label: string) {
+function handleSelectParent(value: string, _label: string) {
formData.value.parentId = value
- selectedMenuText.value = label
+ selectedMenuText.value = buildMenuPath(value) // 使用完整路径
// 关闭弹窗
parentSelectorRef.value?.setShow(false)
@@ -1163,28 +1215,7 @@ function handleAdd() {
// 设置父菜单显示文本的辅助函数
function setParentMenuText(parentId: string) {
- if (parentId === '0') {
- selectedMenuText.value = '最顶级菜单'
- return
- }
-
- // 在级联数据中查找父菜单名称
- function findMenuNameById(menus: MenuCascaderBo[], targetId: string): string | null {
- for (const menu of menus) {
- if (menu.value === targetId) {
- return menu.label
- }
- if (menu.children?.length) {
- const found = findMenuNameById(menu.children, targetId)
- if (found)
- return found
- }
- }
- return null
- }
-
- const parentMenuName = findMenuNameById(topLevelMenus.value, parentId)
- selectedMenuText.value = parentMenuName || `菜单ID: ${parentId}`
+ selectedMenuText.value = buildMenuPath(parentId)
}
// 新增子菜单
@@ -1194,8 +1225,8 @@ function handleAddChild(parentMenu: MenuVo) {
resetFormData()
// 设置父菜单信息
- formData.value.parentId = parentMenu.menuId // 直接使用字符串,无需转换
- selectedMenuText.value = parentMenu.menuName // 设置显示文本
+ formData.value.parentId = String(parentMenu.menuId) // 确保转换为字符串类型
+ selectedMenuText.value = buildMenuPath(String(parentMenu.menuId)) // 设置完整路径显示文本
// 根据父菜单类型智能设置子菜单默认类型
if (parentMenu.menuType === '1') {
@@ -1219,10 +1250,11 @@ function handleEdit(menu: MenuVo) {
formData.value = {
...menu,
sorted: Number(menu.sorted), // 确保 sorted 是数字类型
+ parentId: String(menu.parentId), // 确保 parentId 是字符串类型
}
// 设置父菜单显示文本
- setParentMenuText(menu.parentId)
+ setParentMenuText(String(menu.parentId))
menuDialogRef.value?.coiOpen()
}
@@ -1339,9 +1371,21 @@ function handleMenuTypeChange(value: string) {
// 提交表单
async function handleSubmit() {
- try {
- await formRef.value?.validate()
+ if (!formRef.value)
+ return
+ try {
+ // 先进行表单验证
+ await formRef.value.validate()
+ }
+ catch {
+ // 表单验证失败,提示用户检查填写内容
+ coiMsgWarning('验证失败,请检查填写内容')
+ return
+ }
+
+ // 表单验证通过,执行API调用
+ try {
// 保存当前的展开状态
const currentExpandedKeys = [...expandedKeys.value]
const currentIsAllExpanded = isAllExpanded.value
@@ -1367,7 +1411,10 @@ async function handleSubmit() {
}
catch (error) {
if (error instanceof Error) {
- coiMsgError(error.message || '操作失败')
+ coiMsgError(error.message || (isEdit.value ? '修改失败,请检查网络连接' : '新增失败,请检查网络连接'))
+ }
+ else {
+ coiMsgError(isEdit.value ? '修改失败,请检查网络连接' : '新增失败,请检查网络连接')
}
}
}
@@ -1427,6 +1474,15 @@ function getEmptyActionText() {
return hasSearchConditions() ? '重置搜索' : '新增菜单'
}
+function getShowEmptyAction() {
+ // 有搜索条件时总是显示重置按钮
+ if (hasSearchConditions()) {
+ return true
+ }
+ // 无数据时只有有新增权限才显示新增按钮
+ return hasButton(PERMISSIONS.MENU.ADD)
+}
+
function handleEmptyAction() {
if (hasSearchConditions()) {
handleReset()
@@ -1449,18 +1505,35 @@ onMounted(() => {
}
.custom-table {
- font-size: 14px;
+ font-size: 13px;
+}
+
+.custom-table :deep(.n-data-table-td) {
+ padding: 8px 12px !important;
+}
+
+.custom-table :deep(.n-data-table-th) {
+ padding: 8px 12px !important;
+ font-size: 13px !important;
}
.custom-table :deep(.n-data-table-thead .n-data-table-th) {
background-color: #f8f9fa;
- font-weight: 600;
+ font-weight: normal;
+}
+
+.custom-table :deep(.n-data-table-tbody .n-data-table-tr) {
+ line-height: 1.4 !important;
}
.custom-table :deep(.n-data-table-tbody .n-data-table-tr:hover) {
background-color: #f5f7fa;
}
+.custom-table :deep(.n-data-table-tbody .n-data-table-td) {
+ font-size: 13px !important;
+}
+
.table-wrapper {
background-color: #fff;
}
@@ -1551,6 +1624,17 @@ onMounted(() => {
box-shadow: 0 4px 16px rgba(6, 182, 212, 0.4) !important;
}
+/* 成功按钮 */
+.action-btn-success {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
+ color: white !important;
+}
+
+.action-btn-success:hover {
+ background: linear-gradient(135deg, #059669 0%, #047857 100%) !important;
+ box-shadow: 0 4px 16px rgba(16, 185, 129, 0.4) !important;
+}
+
/* 紧凑表单样式 */
.compact-form :deep(.n-form-item) {
margin-bottom: 8px !important;
@@ -1574,13 +1658,13 @@ onMounted(() => {
.compact-form :deep(.n-input .n-input__input-el),
.compact-form :deep(.n-input-number .n-input__input-el) {
- padding: 8px 12px !important;
+ padding: 2px 1px !important;
min-height: 32px !important;
}
.compact-form :deep(.n-cascader .n-cascader-trigger) {
min-height: 32px !important;
- padding: 8px 12px !important;
+ padding: 2px 1px !important;
}
/* 自定义菜单选择器样式 */
@@ -1602,7 +1686,7 @@ onMounted(() => {
.column-header {
background: #f5f5f5;
- padding: 8px 12px;
+ padding: 8px 10px;
font-size: 12px;
font-weight: 500;
color: #666;
@@ -1617,7 +1701,7 @@ onMounted(() => {
.menu-option {
display: flex;
align-items: center;
- padding: 8px 12px;
+ padding: 8px 10px;
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
position: relative;