mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-13 12:28:54 +08:00
feat(seller): 调整商家菜单布局与商品列表展示
This commit is contained in:
47
seller/scripts/test-seller-navigation.mjs
Normal file
47
seller/scripts/test-seller-navigation.mjs
Normal file
@@ -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);
|
||||||
|
});
|
||||||
@@ -31,6 +31,6 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
mainColor: "#ff5c58", //主题色
|
mainColor: "#ff5c58", //主题色
|
||||||
setting: {
|
setting: {
|
||||||
isUseTabsRouter: true, //多标签Tab页模式
|
isUseTabsRouter: false, // 多标签页内嵌模式默认关闭,用户可在页面配置中开启。
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
55
seller/src/libs/seller-navigation.js
Normal file
55
seller/src/libs/seller-navigation.js
Normal file
@@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -450,47 +450,29 @@ util.initAllMenuData = function (constRoutes, data) {
|
|||||||
|
|
||||||
util.initMenuData = function (data) {
|
util.initMenuData = function (data) {
|
||||||
const appStore = useAppStore();
|
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];
|
let navList = [HOME_NAV_ITEM];
|
||||||
menuData.forEach((e) => {
|
appStore.navigationMenus.forEach((e) => {
|
||||||
navList.push({ name: e.name, title: e.title });
|
navList.push({ name: e.name, title: e.title });
|
||||||
});
|
});
|
||||||
if (navList.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appStore.setNavList(navList);
|
appStore.setNavList(navList);
|
||||||
let currNav = window.localStorage.getItem("currNav");
|
let currNav = window.localStorage.getItem("currNav");
|
||||||
if (!currNav) {
|
if (!navList.some((item) => item.name === currNav)) {
|
||||||
currNav = HOME_NAV_NAME;
|
currNav = HOME_NAV_NAME;
|
||||||
}
|
}
|
||||||
appStore.setCurrNav(currNav);
|
appStore.setCurrNav(currNav);
|
||||||
|
|
||||||
if (currNav === HOME_NAV_NAME) {
|
// 首页也必须准备全部标签入口;切换栏目只更新展示,不能依赖再次初始化路由补齐标签。
|
||||||
appStore.updateMenulist([]);
|
appStore.tagsList = appStore.routers.flatMap((item) => item.children || []);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let item of menuData) {
|
|
||||||
if (item.name == currNav) {
|
|
||||||
menuData = item.children;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
util.initRouterNode(menuRoutes, menuData);
|
|
||||||
appStore.updateMenulist(
|
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) {
|
util.initRouterNode = function (routers, data) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const useAppStore = defineStore("app", {
|
|||||||
loading: false,
|
loading: false,
|
||||||
added: false,
|
added: false,
|
||||||
navList: [],
|
navList: [],
|
||||||
|
navigationMenus: [],
|
||||||
currNav: "",
|
currNav: "",
|
||||||
lang: "",
|
lang: "",
|
||||||
openedSubmenuArr: [],
|
openedSubmenuArr: [],
|
||||||
@@ -28,6 +29,11 @@ export const useAppStore = defineStore("app", {
|
|||||||
refMenu: "",
|
refMenu: "",
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
/** 清理账号绑定的路由与标签目录,保留静态页面,防止切换账号后复用旧菜单。 */
|
||||||
|
resetNavigationRoutes() {
|
||||||
|
this.routers = [otherRouter];
|
||||||
|
this.tagsList = [...otherRouter.children];
|
||||||
|
},
|
||||||
childrenMenu(v) {
|
childrenMenu(v) {
|
||||||
this.refMenu = v;
|
this.refMenu = v;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { removeStore } from "@/libs/storage";
|
import { removeStore } from "@/libs/storage";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
// 退出登录时需要清除的会话/店铺数据
|
// 退出登录时需要清除的会话/店铺数据
|
||||||
// 保留 uuid(设备标识)、setting(主题)、currNav、category(缓存)、saveLogin(记住登录状态)等无关键
|
// 保留 uuid(设备标识)、setting(主题)、category(缓存)、saveLogin(记住登录状态)等无关键
|
||||||
const LOGOUT_KEYS = [
|
const LOGOUT_KEYS = [
|
||||||
"accessToken",
|
"accessToken",
|
||||||
"refreshToken",
|
"refreshToken",
|
||||||
"menuData",
|
"menuData",
|
||||||
|
"currNav",
|
||||||
|
"storeOpenedList",
|
||||||
"seller_expiration_time",
|
"seller_expiration_time",
|
||||||
"sellersiteName",
|
"sellersiteName",
|
||||||
"sellerlogoImg",
|
"sellerlogoImg",
|
||||||
@@ -20,6 +23,15 @@ export const useUserStore = defineStore("user", {
|
|||||||
Cookies.remove("userInfoSeller");
|
Cookies.remove("userInfoSeller");
|
||||||
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
|
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
|
||||||
LOGOUT_KEYS.forEach((key) => removeStore(key));
|
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" }];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,36 +3,44 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main" :class="{ 'menu-collapsed': !showSubMenu }">
|
<div class="main">
|
||||||
<div class="sidebar-menu-con menu-bar">
|
<div class="sidebar-menu-con menu-bar">
|
||||||
<div class="logo-con">
|
<div class="logo-con">
|
||||||
<img alt="店铺 Logo" :src="storeSideLogo" key="max-logo" />
|
<img alt="店铺 Logo" :src="storeSideLogo" key="max-logo" />
|
||||||
|
<span class="brand-caption">商家工作台</span>
|
||||||
</div>
|
</div>
|
||||||
<shrinkable-menu />
|
<shrinkable-menu />
|
||||||
</div>
|
</div>
|
||||||
<div class="main-header-con" :style="{ height: setting.isUseTabsRouter ? '106px' : '60px' }">
|
<div class="main-header-con" :style="{ height: setting.isUseTabsRouter ? '106px' : '60px' }">
|
||||||
<div class="main-header">
|
<div class="main-header">
|
||||||
<div class="header-avator-con">
|
<div class="header-avator-con">
|
||||||
<div></div>
|
<div class="header-page-info">
|
||||||
|
<span class="workspace-label">商家后台</span>
|
||||||
|
<span class="header-divider">/</span>
|
||||||
|
<strong>{{ currentPageTitle }}</strong>
|
||||||
|
</div>
|
||||||
<div class="user-dropdown-menu-con">
|
<div class="user-dropdown-menu-con">
|
||||||
<el-row
|
<el-row
|
||||||
justify="end"
|
justify="end"
|
||||||
align="middle"
|
align="middle"
|
||||||
class="user-dropdown-innercon"
|
class="user-dropdown-innercon"
|
||||||
>
|
>
|
||||||
|
<menu-search />
|
||||||
<store-qrcode />
|
<store-qrcode />
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li class="nav-item" @click="im">
|
<li class="nav-item">
|
||||||
<el-tooltip content="联系客服" placement="bottom">
|
<el-tooltip content="联系客服" placement="bottom">
|
||||||
<el-button type="warning" size="small" :loading="load">
|
<el-button type="warning" size="small" :loading="load" @click="im">
|
||||||
<el-icon><ChatDotRound /></el-icon>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
客服
|
客服
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" @click="handleClickSetting">
|
<li class="nav-item">
|
||||||
<el-tooltip content="设置" placement="bottom">
|
<el-tooltip content="设置" placement="bottom">
|
||||||
<el-icon :size="16"><Setting /></el-icon>
|
<button type="button" class="header-icon-button" aria-label="页面设置" @click="handleClickSetting">
|
||||||
|
<el-icon :size="17"><Setting /></el-icon>
|
||||||
|
</button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -45,6 +53,7 @@
|
|||||||
>
|
>
|
||||||
<el-icon><UserFilled /></el-icon>
|
<el-icon><UserFilled /></el-icon>
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
|
<span class="header-store-name">{{ userInfo.storeName || '我的店铺' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@@ -88,6 +97,7 @@ import tagsPageOpened from "./main-components/tags-page-opened.vue";
|
|||||||
import circleLoading from "@/views/my-components/lili/circle-loading.vue";
|
import circleLoading from "@/views/my-components/lili/circle-loading.vue";
|
||||||
import configDrawer from "@/views/main-components/config-drawer.vue";
|
import configDrawer from "@/views/main-components/config-drawer.vue";
|
||||||
import storeQrcode from "@/views/main-components/store-qrcode.vue";
|
import storeQrcode from "@/views/main-components/store-qrcode.vue";
|
||||||
|
import menuSearch from "@/views/main-components/menu-search.vue";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import util from "@/libs/util.js";
|
import util from "@/libs/util.js";
|
||||||
import { logout } from "@/api/index";
|
import { logout } from "@/api/index";
|
||||||
@@ -115,7 +125,7 @@ const load = ref(false);
|
|||||||
const setting = computed(() => settingStore.setting);
|
const setting = computed(() => settingStore.setting);
|
||||||
const loading = computed(() => appStore.loading);
|
const loading = computed(() => appStore.loading);
|
||||||
const pageTagsList = computed(() => appStore.storeOpenedList);
|
const pageTagsList = computed(() => appStore.storeOpenedList);
|
||||||
const showSubMenu = computed(() => appStore.menuList.length > 0);
|
const currentPageTitle = computed(() => route.meta.title || pageTagsList.value.find((item) => item.name === route.name)?.title || '工作台');
|
||||||
const lang = computed(() => appStore.lang);
|
const lang = computed(() => appStore.lang);
|
||||||
|
|
||||||
function handleClickSetting() {
|
function handleClickSetting() {
|
||||||
@@ -148,7 +158,7 @@ function init() {
|
|||||||
appStore.addOpenSubmenu(pathArr[1].name);
|
appStore.addOpenSubmenu(pathArr[1].name);
|
||||||
}
|
}
|
||||||
storeSideLogo.value = localStorage.getItem("sellerlogoImg");
|
storeSideLogo.value = localStorage.getItem("sellerlogoImg");
|
||||||
window.document.title = localStorage.getItem("sellersiteName");
|
util.title(currentPageTitle.value);
|
||||||
const link =
|
const link =
|
||||||
document.querySelector("link[rel*='icon']") ||
|
document.querySelector("link[rel*='icon']") ||
|
||||||
document.createElement("link");
|
document.createElement("link");
|
||||||
@@ -203,6 +213,7 @@ watch(
|
|||||||
appStore.addOpenSubmenu(pathArr[1].name);
|
appStore.addOpenSubmenu(pathArr[1].name);
|
||||||
}
|
}
|
||||||
checkTag(name);
|
checkTag(name);
|
||||||
|
util.title(currentPageTitle.value);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -210,6 +221,12 @@ watch(lang, () => {
|
|||||||
util.setCurrentPath(route.name);
|
util.setCurrentPath(route.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 刷新时权限菜单晚于布局挂载;目录到达后补齐当前标签和标题,无需再切换一次栏目。
|
||||||
|
watch(() => appStore.tagsList, () => {
|
||||||
|
checkTag(route.name);
|
||||||
|
util.title(currentPageTitle.value);
|
||||||
|
});
|
||||||
|
|
||||||
appStore.setOpenedList();
|
appStore.setOpenedList();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -150,9 +150,9 @@
|
|||||||
row-key="id"
|
row-key="id"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column v-if="!scheduledOnly" type="selection" width="100" align="center" />
|
<el-table-column v-if="!scheduledOnly" type="selection" width="48" align="center" />
|
||||||
<el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
|
<el-table-column prop="id" label="SPU编码" width="180" show-overflow-tooltip />
|
||||||
<el-table-column label="商品图片" width="100" align="center">
|
<el-table-column label="商品图片" width="80" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<img alt="商品图片"
|
<img alt="商品图片"
|
||||||
v-if="row && row.original"
|
v-if="row && row.original"
|
||||||
@@ -172,60 +172,57 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
|
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
|
||||||
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
|
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
|
||||||
<el-table-column label="价格" width="120">
|
<el-table-column label="价格" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
|
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="buyCount" label="销量" width="90" />
|
<el-table-column prop="buyCount" label="销量" width="70" />
|
||||||
<el-table-column prop="quantity" label="库存" width="90" />
|
<el-table-column prop="quantity" label="库存" width="70" />
|
||||||
<el-table-column label="商品类型" width="150">
|
<el-table-column label="商品类型" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品分类" min-width="200" show-overflow-tooltip>
|
<el-table-column label="商品分类" min-width="240" class-name="goods-category-column">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ platformCategoryText(row?.categoryPath) }}
|
{{ platformCategoryText(row?.categoryPath) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!scheduledOnly" label="状态" width="150">
|
<el-table-column v-if="!scheduledOnly" label="状态" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="审核状态" width="150">
|
<el-table-column label="审核状态" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
|
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!scheduledOnly" prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
|
<el-table-column v-if="!scheduledOnly" prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" :width="scheduledOnly ? 150 : 320" align="right" fixed="right">
|
<el-table-column label="操作" :width="scheduledOnly ? 100 : 190" align="right" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="goods-actions">
|
||||||
<template v-if="scheduledOnly">
|
<template v-if="scheduledOnly">
|
||||||
<a class="link-text" @click="emit('edit-schedule', row)">编辑</a>
|
<a class="link-text" @click="emit('edit-schedule', row)">编辑</a>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
|
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="row.marketEnable === 'DOWN'">
|
<template v-if="row.marketEnable === 'DOWN'">
|
||||||
<a class="link-text" @click="upper(row)">上架</a>
|
<a class="link-text" @click="upper(row)">上架</a>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="editGoods(row)">编辑</a>
|
<a class="link-text" @click="editGoods(row)">编辑</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a class="link-text" @click="edit(row)">下架</a>
|
<a class="link-text" @click="edit(row)">下架</a>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="editGoods(row)">编辑</a>
|
<a class="link-text" @click="editGoods(row)">编辑</a>
|
||||||
</template>
|
</template>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="copyGoods(row)">复制商品</a>
|
<a class="link-text" @click="copyGoods(row)">复制商品</a>
|
||||||
<!-- E_COUPON 专属:卡池管理入口(原型 P-03) -->
|
<!-- E_COUPON 专属:卡池管理入口(原型 P-03) -->
|
||||||
<template v-if="row.goodsType === 'E_COUPON'">
|
<template v-if="row.goodsType === 'E_COUPON'">
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
|
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -753,6 +750,19 @@ onMounted(() => {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
// 分类完整换行,不使用省略号;操作按钮保持可读,额外入口允许换行。
|
||||||
|
:deep(.goods-category-column .cell) {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.goods-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 12px;
|
||||||
|
.link-text { white-space: nowrap; }
|
||||||
|
}
|
||||||
.link-text {
|
.link-text {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
92
seller/src/views/main-components/menu-search.vue
Normal file
92
seller/src/views/main-components/menu-search.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<!--
|
||||||
|
商家功能搜索:仅检索当前账号已授权且已注册的菜单,支持键盘选择。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-08
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<button type="button" class="menu-search-trigger" aria-label="搜索功能菜单" @click="openSearch">
|
||||||
|
<el-icon :size="17"><Search /></el-icon><span>搜索功能菜单</span><kbd>/</kbd>
|
||||||
|
</button>
|
||||||
|
<el-dialog v-model="visible" class="seller-menu-search" title="搜索功能" width="560px" align-center
|
||||||
|
@opened="focusInput" @closed="keyword = ''">
|
||||||
|
<el-input ref="inputRef" v-model="keyword" placeholder="搜索菜单,例如:商品、订单、优惠券" size="large" clearable
|
||||||
|
aria-label="搜索菜单" @keydown.down.prevent="moveSelection(1)" @keydown.up.prevent="moveSelection(-1)"
|
||||||
|
@keydown.enter.prevent="navigate(results[selected])">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<div class="search-summary">{{ keyword.trim() ? `找到 ${results.length} 个功能` : '全部可用功能' }}</div>
|
||||||
|
<div ref="resultsRef" class="search-results">
|
||||||
|
<button v-for="(item, index) in results" :key="`${item.topName}-${item.name}`" type="button"
|
||||||
|
class="search-result" :class="{ selected: selected === index }" @click="navigate(item)" @mouseenter="selected = index">
|
||||||
|
<div><strong>{{ item.title }}</strong><span>{{ item.topTitle }} / {{ item.groupTitle }}</span></div>
|
||||||
|
<el-icon><ArrowRight /></el-icon>
|
||||||
|
</button>
|
||||||
|
<el-empty v-if="!results.length" :image-size="72" description="没有匹配的功能,试试其他关键词" />
|
||||||
|
</div>
|
||||||
|
<template #footer><div class="search-help"><span>↑ ↓ 选择</span><span>Enter 打开</span><span>Esc 关闭</span></div></template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { Search, ArrowRight } from "@element-plus/icons-vue";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
import { flattenNavigation, searchNavigation } from "@/libs/seller-navigation";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const visible = ref(false);
|
||||||
|
const keyword = ref("");
|
||||||
|
const selected = ref(0);
|
||||||
|
const inputRef = ref(null);
|
||||||
|
const resultsRef = ref(null);
|
||||||
|
const items = computed(() => flattenNavigation(appStore.navigationMenus).filter((item) => router.hasRoute(item.name)));
|
||||||
|
const results = computed(() => searchNavigation(items.value, keyword.value));
|
||||||
|
|
||||||
|
/** 每次打开清空上次关键词,避免残留搜索状态。 */
|
||||||
|
function openSearch() { keyword.value = ""; selected.value = 0; visible.value = true; }
|
||||||
|
/** 等弹层完成动画后聚焦输入框。 */
|
||||||
|
function focusInput() { inputRef.value?.focus(); }
|
||||||
|
/** 键盘移动候选项并滚动到可见区域;空列表不产生无效索引。 */
|
||||||
|
async function moveSelection(step) {
|
||||||
|
if (!results.value.length) return;
|
||||||
|
selected.value = (selected.value + step + results.value.length) % results.value.length;
|
||||||
|
await nextTick();
|
||||||
|
resultsRef.value?.children[selected.value]?.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
/** 只允许打开当前权限搜索结果;清空关闭的弹层避免遮挡业务页面。 */
|
||||||
|
function navigate(item) {
|
||||||
|
if (!item || !items.value.some((entry) => entry.name === item.name)) return;
|
||||||
|
visible.value = false;
|
||||||
|
router.push({ name: item.name });
|
||||||
|
}
|
||||||
|
/** 仅在非编辑区域按 / 唤起搜索,不截获输入框、富文本和其他弹窗内的按键。 */
|
||||||
|
function handleShortcut(event) {
|
||||||
|
if (event.key !== "/" || event.ctrlKey || event.metaKey || event.altKey || visible.value ||
|
||||||
|
event.target.closest?.('input, textarea, select, [contenteditable="true"], [role="dialog"]')) return;
|
||||||
|
event.preventDefault();
|
||||||
|
openSearch();
|
||||||
|
}
|
||||||
|
watch(results, () => { selected.value = 0; });
|
||||||
|
watch(() => appStore.navigationMenus, () => { visible.value = false; keyword.value = ""; });
|
||||||
|
onMounted(() => window.addEventListener("keydown", handleShortcut));
|
||||||
|
onBeforeUnmount(() => window.removeEventListener("keydown", handleShortcut));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.menu-search-trigger { width: 240px; height: 34px; display: flex; align-items: center; gap: 9px; padding: 0 12px; border: 1px solid #eceef3; border-radius: 7px; background: #f7f8fb; color: #969cab; font: inherit; font-size: 12px; cursor: pointer;
|
||||||
|
&:hover { border-color: #c5d2ff; color: #315efb; }
|
||||||
|
&:focus-visible { outline: 2px solid #315efb; }
|
||||||
|
kbd { margin-left: auto; padding: 0 5px; border: 1px solid #e0e4ed; border-radius: 3px; background: #fff; font-family: inherit; }
|
||||||
|
}
|
||||||
|
.search-summary { padding: 18px 2px 8px; font-size: 12px; color: #969cab; }
|
||||||
|
.search-results { max-height: 380px; overflow-y: auto; }
|
||||||
|
.search-result { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 12px; margin: 3px 0; border: 0; border-radius: 7px; background: #fff; text-align: left; cursor: pointer; color: #858c9c;
|
||||||
|
strong { display: block; font-size: 14px; font-weight: 500; color: #303749; }
|
||||||
|
span { display: block; margin-top: 5px; font-size: 12px; }
|
||||||
|
&.selected, &:focus-visible { background: #eef2ff; strong { color: #315efb; } }
|
||||||
|
}
|
||||||
|
.search-help { display: flex; gap: 20px; color: #969cab; font-size: 12px; }
|
||||||
|
@media (max-width: 1100px) { .menu-search-trigger { width: 180px; } }
|
||||||
|
</style>
|
||||||
@@ -1,166 +1,99 @@
|
|||||||
|
<!--
|
||||||
|
商家单侧栏导航:栏目纵向展开,功能入口按两列排列,沿用当前账号权限与路由定位。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-08
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="shrinkable-menu" :class="{ 'no-sub-menu': !menuList.length }">
|
<nav class="seller-navigation" aria-label="商家功能菜单">
|
||||||
<el-menu
|
<button type="button" class="nav-heading home-link" :class="{ 'is-active': route.name === 'home_index' }"
|
||||||
ref="sideMenu"
|
:aria-current="route.name === 'home_index' ? 'page' : undefined" @click="router.push({ name: 'home_index' })">
|
||||||
class="nav-menu-dark"
|
<el-icon :size="20"><House /></el-icon><span>首页</span>
|
||||||
:default-active="currNav"
|
</button>
|
||||||
@select="selectNav"
|
<section v-for="section in sections" :key="section.name" class="nav-section">
|
||||||
>
|
<button type="button" class="nav-heading" :class="{ 'is-current': activeItem?.topName === section.name }"
|
||||||
<el-menu-item v-for="(item, i) in navList" :key="i" :index="item.name">
|
:aria-label="section.title" :aria-expanded="!closedSections.includes(section.name)"
|
||||||
{{ item.title }}
|
:aria-controls="`seller-nav-${section.name}`" @click="toggleSection(section.name)">
|
||||||
</el-menu-item>
|
<el-icon :size="19"><component :is="navIcon(section)" /></el-icon>
|
||||||
</el-menu>
|
<span>{{ section.title }}</span>
|
||||||
<el-menu
|
<el-icon class="section-arrow" :class="{ 'is-open': !closedSections.includes(section.name) }" :size="14"><ArrowDown /></el-icon>
|
||||||
v-if="menuList.length"
|
</button>
|
||||||
ref="childrenMenu"
|
<div v-show="!closedSections.includes(section.name)" :id="`seller-nav-${section.name}`" class="nav-grid">
|
||||||
:key="currNav"
|
<button v-for="menu in section.items" :key="menu.name" type="button" class="nav-link"
|
||||||
class="sub-menu"
|
:class="{ 'is-active': activeItem?.name === menu.name }"
|
||||||
:default-active="$route.name"
|
:aria-current="activeItem?.name === menu.name ? 'page' : undefined"
|
||||||
@select="changeMenu"
|
:title="`${menu.groupTitle} / ${menu.title}`" @click="router.push({ name: menu.name })">
|
||||||
>
|
|
||||||
<template v-for="item in menuList" :key="item.id">
|
|
||||||
<el-menu-item-group :title="item.title">
|
|
||||||
<el-menu-item
|
|
||||||
v-for="menu in item.children"
|
|
||||||
:key="menu.name"
|
|
||||||
:index="menu.name"
|
|
||||||
>
|
|
||||||
{{ menu.title }}
|
{{ menu.title }}
|
||||||
</el-menu-item>
|
</button>
|
||||||
</el-menu-item-group>
|
|
||||||
</template>
|
|
||||||
</el-menu>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch, onMounted } from "vue";
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import util, { HOME_NAV_NAME } from "@/libs/util.js";
|
import { House, Goods, Tickets, Wallet, Present, TrendCharts, Shop, Bell, Grid, ArrowDown, User, Box } from "@element-plus/icons-vue";
|
||||||
|
import { HOME_NAV_NAME } from "@/libs/util.js";
|
||||||
|
import { flattenNavigation, findNavigationItem } from "@/libs/seller-navigation";
|
||||||
import { setStore } from "@/libs/storage";
|
import { setStore } from "@/libs/storage";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
defineOptions({ name: "shrinkableMenu" });
|
defineOptions({ name: "shrinkableMenu" });
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const closedSections = ref([]);
|
||||||
|
const allItems = computed(() => flattenNavigation(appStore.navigationMenus).filter((item) => router.hasRoute(item.name)));
|
||||||
|
const activeItem = computed(() => findNavigationItem(allItems.value, route.name));
|
||||||
|
const sections = computed(() => appStore.navigationMenus.map((top) => ({
|
||||||
|
...top, items: allItems.value.filter((item) => item.topName === top.name),
|
||||||
|
})).filter((top) => top.items.length));
|
||||||
|
|
||||||
const childrenMenu = ref(null);
|
/** 使用业务栏目图标,新增的未知栏目显示通用图标。 */
|
||||||
|
function navIcon(item) {
|
||||||
const menuList = computed(() => appStore.menuList);
|
return ({ 商品: Goods, 订单: Tickets, 财务: Wallet, 营销: Present, 统计: TrendCharts,
|
||||||
const navList = computed(() => appStore.navList);
|
数据: TrendCharts, 设置: Shop, 店铺: Shop, 消息: Bell, 会员: User, 采购: Box })[item.title] || Grid;
|
||||||
const currNav = computed(() => appStore.currNav);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.name,
|
|
||||||
() => {
|
|
||||||
syncRouteMenu(route);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function syncRouteMenu(route) {
|
|
||||||
if (!route?.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (route.name === "home_index") {
|
|
||||||
activateHomeNav();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstRouterName =
|
|
||||||
route.meta?.firstRouterName || util.getFirstRouterName(route.name);
|
|
||||||
if (firstRouterName && firstRouterName !== currNav.value) {
|
|
||||||
selectNav(firstRouterName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateHomeNav() {
|
/** 各栏目独立展开收起,可同时查看多个栏目的功能,不改变当前业务页面。 */
|
||||||
appStore.setCurrNav(HOME_NAV_NAME);
|
function toggleSection(name) {
|
||||||
setStore("currNav", HOME_NAV_NAME);
|
closedSections.value = closedSections.value.includes(name)
|
||||||
appStore.updateMenulist([]);
|
? closedSections.value.filter((item) => item !== name) : [...closedSections.value, name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeMenu(name) {
|
/** 刷新、搜索和详情跳转后展开所属栏目;仅定位授权入口,不改变菜单顺序和权限。 */
|
||||||
if (!name) return;
|
async function syncRouteMenu() {
|
||||||
router.push({ name });
|
const name = activeItem.value?.topName || HOME_NAV_NAME;
|
||||||
}
|
|
||||||
|
|
||||||
function selectNav(name) {
|
|
||||||
appStore.childrenMenu(childrenMenu.value);
|
|
||||||
appStore.setCurrNav(name);
|
appStore.setCurrNav(name);
|
||||||
|
appStore.updateMenulist(appStore.navigationMenus.find((top) => top.name === name)?.children || []);
|
||||||
setStore("currNav", name);
|
setStore("currNav", name);
|
||||||
if (name === HOME_NAV_NAME) {
|
closedSections.value = closedSections.value.filter((item) => item !== name);
|
||||||
appStore.updateMenulist([]);
|
await nextTick();
|
||||||
if (route.name !== "home_index") {
|
document.querySelector('.seller-navigation .nav-link.is-active')?.scrollIntoView({ block: "nearest" });
|
||||||
router.push({ name: "home_index" });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
util.initRouter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
watch(() => appStore.navigationMenus, () => { closedSections.value = []; });
|
||||||
syncRouteMenu(route);
|
watch([() => route.fullPath, () => appStore.navigationMenus], syncRouteMenu, { immediate: true });
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.shrinkable-menu {
|
// 隐藏滚动条但保留滚轮、触控和键盘滚动,确保下方栏目仍可访问。
|
||||||
height: calc(100% - 60px);
|
.seller-navigation { height: calc(100% - 64px); box-sizing: border-box; padding: 18px 14px 32px; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; background: #fff;
|
||||||
width: 180px;
|
&::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||||
display: flex;
|
|
||||||
transition: width 0.3s;
|
|
||||||
|
|
||||||
&.no-sub-menu {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.nav-heading { width: 100%; display: flex; align-items: center; gap: 13px; min-height: 46px; padding: 11px 14px; border: 0; border-radius: 6px; background: transparent; color: #242832; text-align: left; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
|
||||||
.nav-menu-dark {
|
&:hover { background: #f7f8fc; }
|
||||||
width: 80px;
|
&:focus-visible { outline: 2px solid #315efb; outline-offset: -2px; }
|
||||||
background-color: #191a23;
|
&.is-active { color: #315efb; background: #f0f4ff; }
|
||||||
border-right: none;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
:deep(.el-menu-item) {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 8px !important;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.3;
|
|
||||||
height: auto;
|
|
||||||
min-height: 56px;
|
|
||||||
white-space: normal;
|
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item:not(.is-active):hover),
|
|
||||||
:deep(.el-menu-item:not(.is-active):focus) {
|
|
||||||
background-color: #43444d !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active) {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active:hover),
|
|
||||||
:deep(.el-menu-item.is-active:focus) {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.home-link { margin-bottom: 16px; }
|
||||||
.sub-menu {
|
.nav-section { margin-bottom: 22px; }
|
||||||
width: 100px;
|
.section-arrow { margin-left: auto; color: #b8bdc7; transition: transform .15s; &.is-open { transform: rotate(180deg); } }
|
||||||
overflow-y: auto;
|
.nav-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 8px; padding: 2px 5px 0 43px; }
|
||||||
border-right: none;
|
.nav-link { min-width: 0; min-height: 36px; padding: 7px 5px; border: 0; border-radius: 5px; background: transparent; color: #62666f; text-align: left; font: inherit; font-size: 13px; line-height: 22px; overflow-wrap: anywhere; cursor: pointer;
|
||||||
|
&:hover { background: #f5f7fc; color: #315efb; }
|
||||||
:deep(.el-menu-item-group__title) {
|
&:focus-visible { outline: 2px solid #315efb; outline-offset: -2px; }
|
||||||
height: 40px;
|
&.is-active { color: #315efb; background: #eef3ff; font-weight: 600; }
|
||||||
line-height: 40px;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) { .section-arrow { transition: none; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,10 +29,11 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 21;
|
z-index: 21;
|
||||||
width: 180px;
|
width: 300px;
|
||||||
transition: width 0.3s; // background: rgb(73, 80, 96)
|
transition: width 0.3s; // background: rgb(73, 80, 96)
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: rgba(0, 21, 41, 0.35) 2px 0px 6px;
|
border-right: 1px solid #e9ecf2;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-text {
|
.layout-text {
|
||||||
@@ -53,11 +54,12 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: block;
|
display: block;
|
||||||
padding-left: 180px;
|
padding-left: 300px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 106px;
|
height: 106px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
box-shadow: 0 2px 1px 1px rgba(100, 100, 100, 0.1);
|
border-bottom: 1px solid #e9ecf2;
|
||||||
|
box-shadow: none;
|
||||||
transition: padding 0.3s;
|
transition: padding 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +86,7 @@
|
|||||||
.tags-con {
|
.tags-con {
|
||||||
height: 46px;
|
height: 46px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
background: #f0f0f0;
|
background: #f7f8fa;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@@ -123,10 +125,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
min-width: 740px;
|
min-width: 0;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 2px 1px 1px rgba(100, 100, 100, 0.1);
|
border-bottom: 1px solid #e9ecf2;
|
||||||
|
box-shadow: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
|
|
||||||
@@ -153,7 +156,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding: 0 30px;
|
padding: 0 24px;
|
||||||
|
gap: 20px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@@ -227,7 +231,7 @@
|
|||||||
|
|
||||||
.single-page-con {
|
.single-page-con {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 180px;
|
left: 300px;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 106px;
|
top: 106px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -235,7 +239,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #f0f0f0;
|
background-color: #f7f8fa;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: left 0.3s;
|
transition: left 0.3s;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -247,22 +251,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.menu-collapsed {
|
|
||||||
.sidebar-menu-con {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-header-con {
|
|
||||||
padding-left: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-page-con {
|
|
||||||
left: 80px;
|
|
||||||
right: 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-copy {
|
&-copy {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px 0 20px;
|
padding: 10px 0 20px;
|
||||||
@@ -276,19 +264,23 @@
|
|||||||
|
|
||||||
.logo-con {
|
.logo-con {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 64px;
|
||||||
padding: 8px;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
padding: 12px 16px;
|
||||||
border-bottom: 1px solid #eee;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid #e9ecf2;
|
||||||
img {
|
img {
|
||||||
height: 44px;
|
max-height: 36px;
|
||||||
width: auto;
|
max-width: 48px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-bar {
|
.menu-bar {
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-bar::-webkit-scrollbar {
|
.menu-bar::-webkit-scrollbar {
|
||||||
@@ -339,3 +331,32 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导航与顶部栏的样式局限于商家外层布局,不覆盖业务页面的组件主题。
|
||||||
|
.main {
|
||||||
|
.brand-caption { color: #252b3a; font-size: 15px; font-weight: 600; white-space: nowrap; }
|
||||||
|
.header-page-info { display: flex; align-items: center; gap: 12px; min-width: 0; font-size: 13px;
|
||||||
|
strong { color: #303749; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
}
|
||||||
|
.workspace-label { color: #969cab; white-space: nowrap; }
|
||||||
|
.header-divider { color: #d9dde6; }
|
||||||
|
.user-dropdown-innercon { flex-wrap: nowrap; gap: 16px; }
|
||||||
|
.header-store-name { margin-left: 9px; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: #525a6b; }
|
||||||
|
.header-avator-con .nav-item { margin: 0; width: auto; height: auto; border: 0; border-radius: 0; }
|
||||||
|
.header-icon-button { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 1px solid #eceef3; border-radius: 6px; background: #fff; color: #626879; cursor: pointer;
|
||||||
|
&:hover { background: #f5f7fb; color: #315efb; }
|
||||||
|
&:focus-visible { outline: 2px solid #315efb; }
|
||||||
|
}
|
||||||
|
.single-page-con .single-page { margin: 16px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main {
|
||||||
|
.header-avator-con { padding: 0 16px; gap: 12px; }
|
||||||
|
.user-dropdown-innercon { gap: 10px; }
|
||||||
|
.workspace-label, .header-divider, .header-store-name { display: none; }
|
||||||
|
.single-page-con .single-page { margin: 12px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.main .sidebar-menu-con, .main-header-con, .main .single-page-con { transition: none; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user