From f9db706769c2b3662355d98666a2f1e6667faf49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E5=BF=97=E5=BC=BA?= <357099073@qq.com>
Date: Sat, 21 Mar 2026 14:12:44 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=A7=E5=93=81=E5=88=86?=
=?UTF-8?q?=E7=B1=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/products.js | 52 ++++
src/components/CommonAside.vue | 20 +-
src/router/dynamicRoutes.js | 188 +++++++++++---
src/views/apps/cms/products/index.vue | 37 +--
.../cms/products/types/components/edit.vue | 211 +++++++++++++++
src/views/apps/cms/products/types/index.vue | 240 ++++++++++++++++++
src/views/system/menus/components/edit.vue | 12 +
src/views/system/menus/manager.vue | 6 +
8 files changed, 708 insertions(+), 58 deletions(-)
create mode 100644 src/views/apps/cms/products/types/components/edit.vue
create mode 100644 src/views/apps/cms/products/types/index.vue
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 @@