diff --git a/seller/scripts/test-seller-navigation.mjs b/seller/scripts/test-seller-navigation.mjs new file mode 100644 index 000000000..52f6b0ec8 --- /dev/null +++ b/seller/scripts/test-seller-navigation.mjs @@ -0,0 +1,47 @@ +/** + * 商家导航回归:覆盖权限裁剪、详情归属和跨分组检索,使用 Node 内置测试运行器。 + * @author Bulbasaur + * @since 2026-11-08 + */ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { flattenNavigation, findNavigationItem, searchNavigation } from '../src/libs/seller-navigation.js'; + +const component = () => {}; +const menus = [ + { name: 'goods', title: '商品', children: [{ title: '商品管理', children: [ + { name: 'goods', title: '商品列表', component }, + { name: 'disabled', title: '不可用页面', component: null }, + ] }] }, + { name: 'order', title: '订单', children: [{ title: '订单管理', children: [ + { name: 'orderList', title: '商品订单', component }, + ] }] }, +]; +const items = flattenNavigation(menus); + +test('搜索不补出未授权入口,也不展示无法加载的菜单', () => { + assert.equal(searchNavigation(items, '优惠券').length, 0); + assert.equal(searchNavigation(items, '不可用页面').length, 0); +}); +test('详情页刷新可定位已授权业务入口', () => { + assert.equal(findNavigationItem(items, 'goods-operation-edit')?.topName, 'goods'); + assert.equal(findNavigationItem(items, 'order-detail')?.name, 'orderList'); +}); +test('详情页对应入口被撤权后不能沿用旧账号菜单', () => { + const restricted = flattenNavigation(menus.slice(0, 1)); + assert.equal(findNavigationItem(restricted, 'order-detail'), null); + assert.equal(findNavigationItem([], 'goods-operation-edit'), null); +}); +test('详情本身是授权菜单时优先使用精确入口', () => { + const exact = { name: 'order-detail', topName: 'custom' }; + assert.equal(findNavigationItem([...items, exact], 'order-detail'), exact); +}); +test('关键词同时匹配栏目及名称,避免同名商品功能混淆', () => { + assert.deepEqual(searchNavigation(items, ' 订单 商品 ').map(item => item.name), ['orderList']); + assert.deepEqual(searchNavigation(items, '商品 列表').map(item => item.name), ['goods']); +}); +test('搜索清空与空权限账号正确处理', () => { + assert.equal(searchNavigation(items, ' ').length, 2); + assert.deepEqual(searchNavigation(flattenNavigation([]), ''), []); + assert.equal(findNavigationItem(items, 'unknown-detail'), null); +}); diff --git a/seller/src/config/index.js b/seller/src/config/index.js index 4420979c6..963e31883 100644 --- a/seller/src/config/index.js +++ b/seller/src/config/index.js @@ -31,6 +31,6 @@ module.exports = { */ mainColor: "#ff5c58", //主题色 setting: { - isUseTabsRouter: true, //多标签Tab页模式 + isUseTabsRouter: false, // 多标签页内嵌模式默认关闭,用户可在页面配置中开启。 }, }; diff --git a/seller/src/libs/seller-navigation.js b/seller/src/libs/seller-navigation.js new file mode 100644 index 000000000..bd229792b --- /dev/null +++ b/seller/src/libs/seller-navigation.js @@ -0,0 +1,55 @@ +/** + * 商家导航的权限菜单检索及详情页归属;仅在已授权菜单中定位,不增加访问权限。 + * @author Bulbasaur + * @since 2026-11-08 + */ + +// 非菜单页面回到对应业务入口;多个候选项按实际已授权入口匹配。 +const DETAIL_MENUS = { + "goods-operation-edit": ["goods"], + "goods-template-operation-edit": ["template-goods"], + "goods-draft-operation-edit": ["draftGoods", "draft-goods", "goods"], + "card-key-pool": ["goods"], + "add-coupon": ["coupon"], + "coupon-receive": ["coupon"], + "add-live": ["live"], + "bill-detail": ["storeBill", "accountStatementBill"], + "order-detail": ["orderList", "virtualOrderList"], + "export-order-deliver": ["orderList"], + "order-complaint-detail": ["orderComplaint"], + "return-goods-order-detail": ["returnGoodsOrder", "returnMoneyOrder"], + "full-discount-detail": ["full-discount", "full-cut"], + "seckill-goods": ["seckill"], + "pintuan-goods": ["pintuan"], + "pintuan-edit": ["pintuan"], + main: ["wapList"], + renovation: ["pcList"], + "appointment-system-form-designer": ["appointment-system-forms"], + "procurement-purchase-order-operation": ["procurementPurchaseOrderList"], + "procurement-inbound-operation": ["procurementInboundList"], + "procurement-inventory-count-detail": ["procurementInventoryCountList"], + "procurement-damage-report-operation": ["procurementDamageReportList"], +}; + +/** 将权限树展开为带栏目与分组信息的可跳转菜单,用于搜索和路由定位。 */ +export function flattenNavigation(menus) { + return menus.flatMap((top) => (top.children || []).flatMap((group) => + (group.children || []).filter((leaf) => leaf.name && typeof leaf.component === "function") + .map((leaf) => ({ ...leaf, topName: top.name, topTitle: top.title, groupTitle: group.title })) + )); +} + +/** 优先精确匹配菜单,再匹配详情页业务入口;无授权入口时返回空,不沿用旧账号栏目。 */ +export function findNavigationItem(items, routeName) { + return items.find((item) => item.name === routeName) || + (DETAIL_MENUS[routeName] || []).map((name) => items.find((item) => item.name === name)).find(Boolean) || null; +} + +/** 按菜单、分组和栏目检索,空格分隔的关键词须全部匹配。 */ +export function searchNavigation(items, keyword) { + const terms = keyword.trim().toLowerCase().split(/\s+/).filter(Boolean); + return items.filter((item) => { + const text = `${item.topTitle} ${item.groupTitle} ${item.title}`.toLowerCase(); + return terms.every((term) => text.includes(term)); + }); +} diff --git a/seller/src/libs/util.js b/seller/src/libs/util.js index 1e6be7590..0575d0602 100644 --- a/seller/src/libs/util.js +++ b/seller/src/libs/util.js @@ -450,47 +450,29 @@ util.initAllMenuData = function (constRoutes, data) { util.initMenuData = function (data) { const appStore = useAppStore(); - const menuRoutes = []; - let menuData = data; + // 全量导航与搜索只使用当前账号权限接口返回的菜单,沿用已下线菜单过滤规则。 + appStore.navigationMenus = data.map((top) => { + const children = []; + util.initRouterNode(children, top.children || []); + return { ...top, children: children.filter((group) => group.children?.length) }; + }).filter((top) => top.children.length); let navList = [HOME_NAV_ITEM]; - menuData.forEach((e) => { + appStore.navigationMenus.forEach((e) => { navList.push({ name: e.name, title: e.title }); }); - if (navList.length < 1) { - return; - } appStore.setNavList(navList); let currNav = window.localStorage.getItem("currNav"); - if (!currNav) { + if (!navList.some((item) => item.name === currNav)) { currNav = HOME_NAV_NAME; } appStore.setCurrNav(currNav); - if (currNav === HOME_NAV_NAME) { - appStore.updateMenulist([]); - return; - } + // 首页也必须准备全部标签入口;切换栏目只更新展示,不能依赖再次初始化路由补齐标签。 + appStore.tagsList = appStore.routers.flatMap((item) => item.children || []); - for (let item of menuData) { - if (item.name == currNav) { - menuData = item.children; - break; - } - } - util.initRouterNode(menuRoutes, menuData); appStore.updateMenulist( - menuRoutes.filter((item) => item.children.length > 0) + appStore.navigationMenus.find((item) => item.name === currNav)?.children || [] ); - - let tagsList = []; - appStore.routers.map((item) => { - if (item.children.length <= 1) { - tagsList.push(item.children[0]); - } else { - tagsList.push(...item.children); - } - }); - appStore.setTagsList(tagsList); }; util.initRouterNode = function (routers, data) { diff --git a/seller/src/stores/app.js b/seller/src/stores/app.js index 054785eaa..72ee75176 100644 --- a/seller/src/stores/app.js +++ b/seller/src/stores/app.js @@ -12,6 +12,7 @@ export const useAppStore = defineStore("app", { loading: false, added: false, navList: [], + navigationMenus: [], currNav: "", lang: "", openedSubmenuArr: [], @@ -28,6 +29,11 @@ export const useAppStore = defineStore("app", { refMenu: "", }), actions: { + /** 清理账号绑定的路由与标签目录,保留静态页面,防止切换账号后复用旧菜单。 */ + resetNavigationRoutes() { + this.routers = [otherRouter]; + this.tagsList = [...otherRouter.children]; + }, childrenMenu(v) { this.refMenu = v; }, diff --git a/seller/src/stores/user.js b/seller/src/stores/user.js index 81880d819..71b00506b 100644 --- a/seller/src/stores/user.js +++ b/seller/src/stores/user.js @@ -1,13 +1,16 @@ import { defineStore } from "pinia"; import Cookies from "js-cookie"; import { removeStore } from "@/libs/storage"; +import { useAppStore } from "@/stores/app"; // 退出登录时需要清除的会话/店铺数据 -// 保留 uuid(设备标识)、setting(主题)、currNav、category(缓存)、saveLogin(记住登录状态)等无关键 +// 保留 uuid(设备标识)、setting(主题)、category(缓存)、saveLogin(记住登录状态)等无关键 const LOGOUT_KEYS = [ "accessToken", "refreshToken", "menuData", + "currNav", + "storeOpenedList", "seller_expiration_time", "sellersiteName", "sellerlogoImg", @@ -20,6 +23,15 @@ export const useUserStore = defineStore("user", { Cookies.remove("userInfoSeller"); // 精确清除会话相关键,不再 localStorage.clear() 误删无关数据 LOGOUT_KEYS.forEach((key) => removeStore(key)); + // 导航和已打开标签属于当前账号,退出时同步清理内存,避免下一账号复用旧权限菜单。 + const appStore = useAppStore(); + appStore.navigationMenus = []; + appStore.setNavList([]); + appStore.updateMenulist([]); + appStore.setCurrNav(""); + appStore.setAdded(false); + appStore.resetNavigationRoutes(); + appStore.storeOpenedList = [{ title: "首页", path: "", name: "home_index" }]; }, }, }); diff --git a/seller/src/views/Main.vue b/seller/src/views/Main.vue index cf303c3f3..28667b0d1 100644 --- a/seller/src/views/Main.vue +++ b/seller/src/views/Main.vue @@ -3,36 +3,44 @@