diff --git a/src/api/products.js b/src/api/products.js index 9d462de..1a2f3a9 100644 --- a/src/api/products.js +++ b/src/api/products.js @@ -51,3 +51,55 @@ export function deleteProducts(id) { method: 'delete' }) } + +/** + * 获取产品分类列表 + * @param {Object} params - 查询参数 + * @returns {Promise} + */ +export function getProductsTypesList(params) { + return request({ + url: '/admin/productsTypesList', + method: 'get', + params + }) +} + +/** + * 添加产品分类 + * @param {Object} data - 分类数据 + * @returns {Promise} + */ +export function addProductsTypes(data) { + return request({ + url: '/admin/addProductsTypes', + method: 'post', + data + }) +} + +/** + * 更新产品分类 + * @param {number} id - 分类ID + * @param {Object} data - 分类数据 + * @returns {Promise} + */ +export function updateProductsTypes(id, data) { + return request({ + url: `/admin/editProductsTypes/${id}`, + method: 'put', + data + }) +} + +/** + * 删除产品分类 + * @param {number} id - 分类ID + * @returns {Promise} + */ +export function deleteProductsTypes(id) { + return request({ + url: `/admin/deleteProductsTypes/${id}`, + method: 'delete' + }) +} diff --git a/src/components/CommonAside.vue b/src/components/CommonAside.vue index d0d09b6..e0b2a70 100644 --- a/src/components/CommonAside.vue +++ b/src/components/CommonAside.vue @@ -27,7 +27,7 @@ :default-active="route.path" > -

{{ isCollapse ? "管理" : currentModule?.title || "子菜单" }}

+

{{ isCollapse ? "管理" : asideTitle }}

@@ -248,12 +248,30 @@ const displayMenus = computed(() => { if (!currentModule.value) { return []; } + + const currentPath = route.path; + // 访问“父级 index 页面”时不展开子菜单;只有进入“子页面”后才展示 children。 + // 例如:/apps/cms/products(父级)时不展示其 children(如 /apps/cms/products/types)。 + if (currentPath === currentModule.value.path) { + return []; + } + return currentModule.value.children || []; }); +const asideTitle = computed(() => { + if (isCollapse.value) return "管理"; + if (!currentModule.value) return "子菜单"; + return displayMenus.value.length > 0 ? (currentModule.value.title || "子菜单") : "子菜单"; +}); + const processMenus = (menus) => { return menus .filter((menu) => { + // is_visible 控制“侧边栏是否展示”,不参与动态路由与缓存层面的过滤 + if (menu.is_visible !== undefined && Number(menu.is_visible) === 0) { + return false; + } if (menu.path && menu.path.trim() !== "") return true; if (menu.children && menu.children.length > 0) return true; return false; diff --git a/src/router/dynamicRoutes.js b/src/router/dynamicRoutes.js index 84e6060..c325149 100644 --- a/src/router/dynamicRoutes.js +++ b/src/router/dynamicRoutes.js @@ -1,54 +1,174 @@ import { createComponentLoader } from '@/utils/pathResolver'; +function computeFullPath(menuPath, parentPath) { + if (!menuPath) return parentPath || ''; + if (menuPath.startsWith('/')) { + return menuPath.replace(/\/+/g, '/'); + } + const base = (parentPath || '').replace(/\/$/, ''); + return `${base}/${menuPath}`.replace(/\/+/g, '/'); +} + +/** 将子路由的绝对路径转为相对父布局的路径,供 Vue Router 嵌套使用 */ +function toRelativeChildPath(parentAbs, childAbs) { + const base = (parentAbs || '').replace(/\/$/, ''); + const target = (childAbs || '').replace(/\/$/, ''); + if (!target) return ''; + if (target === base) return ''; + const prefix = `${base}/`; + if (target.startsWith(prefix)) { + return target.slice(prefix.length); + } + // 兜底:取最后一段(菜单 path 配置异常时) + const parts = target.split('/').filter(Boolean); + return parts.length ? parts[parts.length - 1] : ''; +} + +function hasPageComponent(menu) { + return menu.type === 4 || (menu.component_path && String(menu.component_path).trim() !== ''); +} + +function resolvePageComponent(menu) { + if (menu.type === 4) { + return () => import('@/views/onepage/index.vue'); + } + if (menu.component_path && String(menu.component_path).trim() !== '') { + return createComponentLoader(menu.component_path); + } + return () => import('@/views/404/404.vue'); +} + +/** + * 菜单子节点 -> 嵌套路由(path 相对 layoutAbsPath) + */ +function convertNestedMenuChildren(children, layoutAbsPath) { + if (!children || children.length === 0) return []; + return children.map((child) => nestedMenuToRoute(child, layoutAbsPath)); +} + +function nestedMenuToRoute(menu, layoutAbsPath) { + const childAbs = computeFullPath(menu.path, layoutAbsPath); + const relPath = toRelativeChildPath(layoutAbsPath, childAbs); + const hasChildren = menu.children && menu.children.length > 0; + const ownPage = hasPageComponent(menu); + + const meta = { + title: menu.title, + icon: menu.icon, + id: menu.id, + componentPath: menu.component_path, + }; + + // 既有自己的页面又有子菜单:套一层 EmptyLayout,避免父页面组件里没有 导致子路由无法渲染 + if (hasChildren && ownPage) { + return { + path: relPath, + name: `menu_${menu.id}`, + meta, + component: () => import('@/views/layouts/EmptyLayout.vue'), + children: [ + { + path: '', + name: `menu_${menu.id}_index`, + meta: { ...meta }, + component: resolvePageComponent(menu), + }, + ...convertNestedMenuChildren(menu.children, childAbs), + ], + }; + } + + // 纯目录 + 子节点 + if (hasChildren && !ownPage) { + const route = { + path: relPath, + name: `menu_${menu.id}`, + meta, + component: () => import('@/views/layouts/EmptyLayout.vue'), + children: convertNestedMenuChildren(menu.children, childAbs), + }; + const firstChild = menu.children[0]; + if (firstChild && firstChild.path) { + const firstAbs = computeFullPath(firstChild.path, childAbs); + const firstRel = toRelativeChildPath(childAbs, firstAbs); + if (firstRel) { + route.redirect = firstRel; + } + } + return route; + } + + // 叶子页面 + return { + path: relPath, + name: `menu_${menu.id}`, + meta, + component: resolvePageComponent(menu), + }; +} + // 递归转换嵌套菜单为嵌套路由 export function convertMenusToRoutes(menus, parentPath = '') { if (!menus || menus.length === 0) return []; - return menus.map(menu => { - // 拼接完整的路由路径(处理相对路径) - const fullPath = menu.path ? - (menu.path.startsWith('/') ? menu.path : `${parentPath}/${menu.path}`) + return menus.map((menu) => { + const fullPath = menu.path + ? menu.path.startsWith('/') + ? menu.path.replace(/\/+/g, '/') + : `${(parentPath || '').replace(/\/$/, '')}/${menu.path}`.replace(/\/+/g, '/') : ''; - const route = { - path: fullPath || menu.path || '', - name: `menu_${menu.id}`, - meta: { - title: menu.title, - icon: menu.icon, - id: menu.id, - componentPath: menu.component_path - } + const hasChildren = menu.children && menu.children.length > 0; + const ownPage = hasPageComponent(menu); + + const meta = { + title: menu.title, + icon: menu.icon, + id: menu.id, + componentPath: menu.component_path, }; - // 1. 处理组件加载 - if (menu.type === 4) { - // 单页类型 - route.component = () => import('@/views/onepage/index.vue'); - } else if (menu.component_path && menu.component_path.trim() !== '') { - // 正常页面 - route.component = createComponentLoader(menu.component_path); - } else if (menu.children && menu.children.length > 0) { - // 目录节点:必须给组件,否则父级无法渲染子级 - route.component = () => import('@/views/layouts/EmptyLayout.vue'); - } else { - // 异常:既没路径也没子菜单 - route.component = () => import('@/views/404/404.vue'); + // 顶层:有页面 + 有子菜单 -> EmptyLayout + 默认子路由 + 相对 path 子路由 + if (hasChildren && ownPage) { + return { + path: fullPath || menu.path || '', + name: `menu_${menu.id}`, + meta, + component: () => import('@/views/layouts/EmptyLayout.vue'), + children: [ + { + path: '', + name: `menu_${menu.id}_index`, + meta: { ...meta }, + component: resolvePageComponent(menu), + }, + ...convertNestedMenuChildren(menu.children, fullPath), + ], + }; } - // 2. 递归子路由(传递当前完整路径作为父路径) - if (menu.children && menu.children.length > 0) { + const route = { + path: fullPath || menu.path || '', + name: `menu_${menu.id}`, + meta, + }; + + if (menu.type === 4) { + route.component = () => import('@/views/onepage/index.vue'); + } else if (menu.component_path && menu.component_path.trim() !== '') { + route.component = createComponentLoader(menu.component_path); + } else if (hasChildren) { + route.component = () => import('@/views/layouts/EmptyLayout.vue'); route.children = convertMenusToRoutes(menu.children, fullPath); - - // 目录节点添加重定向,防止点击父菜单页面空白 const firstChild = menu.children[0]; if (firstChild && firstChild.path) { - // 计算第一个子路由的完整路径 - const childFullPath = firstChild.path.startsWith('/') ? - firstChild.path : - `${fullPath}/${firstChild.path}`; + const childFullPath = firstChild.path.startsWith('/') + ? firstChild.path + : `${fullPath}/${firstChild.path}`; route.redirect = childFullPath; } + } else { + route.component = () => import('@/views/404/404.vue'); } return route; diff --git a/src/views/apps/cms/products/index.vue b/src/views/apps/cms/products/index.vue index beaf2f3..215cbff 100644 --- a/src/views/apps/cms/products/index.vue +++ b/src/views/apps/cms/products/index.vue @@ -3,16 +3,16 @@

企业产品管理

+ + + 分类管理 + - - - + 添加产品 - - - + 刷新
@@ -96,6 +96,7 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue' +import { useRouter } from 'vue-router' import { getProductsList, updateProducts, @@ -106,6 +107,8 @@ import EditDialog from './components/edit.vue' // @ts-ignore const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +const router = useRouter() + // 获取图片完整URL // 接口可能返回: // 1) 绝对地址:http(s)://... @@ -244,28 +247,16 @@ const handleDelete = async (row) => { } } -// 状态变化 -const handleStatusChange = async (row, val) => { - try { - const res = await updateProducts(row.id, { status: val }) - if (res.code === 200) { - ElMessage.success('状态更新成功') - } else { - ElMessage.error(res.msg || '状态更新失败') - row.status = val === 1 ? 0 : 1 - } - } catch (error) { - console.error('更新企业产品状态失败:', error) - ElMessage.error('更新企业产品状态失败') - row.status = val === 1 ? 0 : 1 - } -} - // 编辑成功回调 const handleSuccess = () => { fetchList() } +// 跳转到产品分类管理页 +const handleTypes = () => { + router.push('/apps/cms/products/types') +} + // 初始化 onMounted(() => { fetchList() diff --git a/src/views/apps/cms/products/types/components/edit.vue b/src/views/apps/cms/products/types/components/edit.vue new file mode 100644 index 0000000..bc50820 --- /dev/null +++ b/src/views/apps/cms/products/types/components/edit.vue @@ -0,0 +1,211 @@ + + + + + + diff --git a/src/views/apps/cms/products/types/index.vue b/src/views/apps/cms/products/types/index.vue new file mode 100644 index 0000000..e3bc056 --- /dev/null +++ b/src/views/apps/cms/products/types/index.vue @@ -0,0 +1,240 @@ + + + + + + \ No newline at end of file diff --git a/src/views/system/menus/components/edit.vue b/src/views/system/menus/components/edit.vue index 6ca532f..a0e865b 100644 --- a/src/views/system/menus/components/edit.vue +++ b/src/views/system/menus/components/edit.vue @@ -78,6 +78,14 @@ /> + + + + >({ icon: '', sort: 0, status: 1, + is_visible: 1, type: 1, permission: '', }); @@ -220,6 +230,7 @@ watch(() => props.menu, (newMenu) => { icon: '', sort: 0, status: 1, + is_visible: 1, type: 1, permission: '', }; @@ -256,6 +267,7 @@ watch(() => props.visible, (newVisible) => { icon: '', sort: 0, status: 1, + is_visible: 1, type: 1, permission: '', }; diff --git a/src/views/system/menus/manager.vue b/src/views/system/menus/manager.vue index 0d0c8b4..85b65e3 100644 --- a/src/views/system/menus/manager.vue +++ b/src/views/system/menus/manager.vue @@ -89,6 +89,12 @@ align="center" > + + + +