修复菜单路由问题,完善导入错提示优化

This commit is contained in:
zjf committed 2026-08-28 15:44:23 +08:00
1 parent 19ad2a5d99
commit e9be7ab7c4
7 files changed
+148 -25

No files matched your search

+9
View File
@@ -55,6 +55,15 @@ export function putObj(obj?: Object) {
})
}
/** 批量赋值菜品封面图 */
export function batchUpdatePic(data: { ids: Array<string | number>; picUrl: string }) {
return request({
url: "/canteen/canteenSpu/batchPic",
method: "put",
data,
})
}
/** Excel 导入菜品:importMode=cover|skip|force;兼容 cover=true/false */
export function importSpu(data: FormData, params: Object) {
return request({
+2 -1
View File
@@ -4,7 +4,7 @@
<el-menu router :default-active="state.defaultActive" :ellipsis="false" background-color="transparent"
mode="horizontal">
<template v-for="val in menuLists">
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<el-sub-menu :index="toSubMenuIndex(val.path)" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon"/>
<span class="font-semibold">{{ $t(val.name) }}</span>
@@ -38,6 +38,7 @@ import {useRoutesList} from '/@/stores/routesList';
import {useThemeConfig} from '/@/stores/themeConfig';
import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
import { toSubMenuIndex } from '/@/layout/navMenu/menuIndex';
// 引入组件
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
+4
View File
@@ -0,0 +1,4 @@
/** 目录型菜单的 el-sub-menu index,避免与叶子路由 path 冲突 */
export function toSubMenuIndex(path: string) {
return `sub:${path || ""}`
}
+2 -1
View File
@@ -1,6 +1,6 @@
<template>
<template v-for="val in chils">
<el-sub-menu :index="val.path" :key="val.path" v-if="val.children && val.children.length > 0">
<el-sub-menu :index="toSubMenuIndex(val.path)" :key="val.path" v-if="val.children && val.children.length > 0">
<template #title>
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.name) }}</span>
@@ -28,6 +28,7 @@
import { computed } from 'vue';
import { RouteRecordRaw } from 'vue-router';
import other from '/@/utils/other';
import { toSubMenuIndex } from '/@/layout/navMenu/menuIndex';
// 定义父组件传过来的值
const props = defineProps({
+33 -21
View File
@@ -1,7 +1,7 @@
<template>
<el-menu
ref="menuRef"
router
:key="state.menuRenderKey"
:default-active="state.defaultActive"
:default-openeds="state.defaultOpeneds"
background-color="transparent"
@@ -10,8 +10,8 @@
:collapse-transition="false"
>
<template v-for="val in menuLists">
<!-- 三级菜单 -->
<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<!-- 三级菜单:index 必须与叶子 path 隔离,避免目录 path 与子路由相同导致自动折叠 -->
<el-sub-menu :index="toSubMenuIndex(val.path)" v-if="val.children && val.children.length > 0" :key="val.path">
<template #title>
<SvgIcon :name="val.meta.icon" />
<span>{{ $t(val.name) }}</span>
@@ -38,6 +38,7 @@
import { RouteRecordRaw } from "vue-router"
import { useThemeConfig } from "/@/stores/themeConfig"
import other from "/@/utils/other"
import { toSubMenuIndex } from "/@/layout/navMenu/menuIndex"
// 引入组件
const SubItem = defineAsyncComponent(() => import("/@/layout/navMenu/subItem.vue"))
@@ -55,10 +56,10 @@ const props = defineProps({
const storesThemeConfig = useThemeConfig()
const { themeConfig } = storeToRefs(storesThemeConfig)
const route = useRoute()
const menuRef = ref()
const state = reactive({
defaultActive: route.meta.isDynamic ? route.meta.isDynamicPath : route.path,
defaultOpeneds: [] as string[],
menuRenderKey: 0,
isCollapse: false,
})
@@ -78,42 +79,54 @@ const setParentHighlight = (currentRoute: RouteToFrom) => {
else return path
}
/** 找到当前激活项的所有父级 path,保持左侧分组展开 */
/**
* 找到当前激活项的所有父级 submenu index。
* 有 children 的节点只作为目录,不参与 path 相等匹配(避免目录 path = 子路由 path 时提前命中、漏掉展开)。
*/
const findParentPaths = (menus: RouteItems, active: string, trail: string[] = []): string[] | null => {
for (const m of menus || []) {
if (!m) continue
if (m.children && m.children.length > 0) {
const found = findParentPaths(m.children as RouteItems, active, [...trail, toSubMenuIndex(m.path)])
if (found) return found
continue
}
if (m.path === active) {
return trail
}
if (m.children && m.children.length > 0) {
const found = findParentPaths(m.children as RouteItems, active, [...trail, m.path])
if (found) return found
}
}
// 隐藏子路由 / 详情页:按前缀打开最近父级
for (const m of menus || []) {
if (!m?.children?.length) continue
if (active.startsWith(`${m.path}/`)) {
const found = findParentPaths(m.children as RouteItems, active, [...trail, m.path])
const found = findParentPaths(m.children as RouteItems, active, [...trail, toSubMenuIndex(m.path)])
if (found) return found
return [...trail, m.path]
return [...trail, toSubMenuIndex(m.path)]
}
}
return null
}
const ensureParentsOpen = (openeds: string[]) => {
nextTick(() => {
const menu = menuRef.value as { open?: (i: string) => void } | undefined
if (!menu?.open) return
openeds.forEach((path) => {
try {
menu.open!(path)
} catch {
// ignore
}
})
})
}
const refreshMenuState = (currentRoute: RouteToFrom = route) => {
const active = setParentHighlight(currentRoute) as string
const openeds = findParentPaths(menuLists.value, active) || []
const sameActive = state.defaultActive === active
const sameOpeneds =
state.defaultOpeneds.length === openeds.length && state.defaultOpeneds.every((p, i) => p === openeds[i])
state.defaultActive = active
state.defaultOpeneds = openeds
// 仅在激活项或父级展开变化时重渲染,避免无意义闪烁
if (!sameActive || !sameOpeneds) {
state.menuRenderKey += 1
}
ensureParentsOpen(openeds)
}
// 打开外部链接
@@ -130,13 +143,12 @@ onBeforeRouteUpdate((to) => {
const clientWidth = document.body.clientWidth
if (clientWidth < 1000) themeConfig.value.isCollapse = false
})
// 侧栏菜单数据被经典分栏/路由过滤重建后,重新带上父级展开
// 菜单列表整体替换时(经典分栏切换)再同步一次
watch(
menuLists,
() => menuLists.value.map((m) => m.path).join("|"),
() => {
refreshMenuState(route)
},
{ deep: true },
)
// 设置菜单的收起/展开
watch(
+23 -1
View File
@@ -643,7 +643,12 @@ const submitImport = async () => {
importUploadRef.value?.clearFiles()
refreshMenuData()
} catch (err: any) {
useMessage().error(err.msg || "导入失败")
const tip = String(err?.msg || "导入失败")
ElMessageBox.alert(tip, "导入失败", {
confirmButtonText: "知道了",
customClass: "daily-menu-import-error-box",
dangerouslyUseHTMLString: false,
})
} finally {
importLoading.value = false
}
@@ -828,3 +833,20 @@ onBeforeUnmount(() => {
border-left: 2px solid rgb(255, 187, 0);
}
</style>
<style lang="scss">
/* MessageBox 挂到 body,需非 scoped */
.daily-menu-import-error-box {
max-width: 640px;
.el-message-box__message {
max-height: 420px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.55;
font-size: 13px;
color: #303133;
}
}
</style>
+75 -1
View File
@@ -48,6 +48,17 @@
>
删 除
</el-button>
<el-button
plain
:disabled="multiple"
icon="Picture"
type="primary"
class="ml10"
v-auth="'canteen_canteenSpu_edit'"
@click="openBatchPic"
>
批量赋值图片
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'canteen_canteenSpu_export'"
@@ -173,13 +184,30 @@
<el-button type="primary" :loading="importLoading" @click="submitImport">开始导入</el-button>
</template>
</el-dialog>
<el-dialog v-model="batchPicVisible" title="批量赋值封面图" width="520px" :close-on-click-modal="false" destroy-on-close>
<el-alert
type="info"
:closable="false"
show-icon
class="mb-4"
:title="`已选 ${selectObjs.length} 道菜,将统一设置同一张封面(可上传或从图库选择,多道菜共用同一地址)。`"
/>
<div class="batch-pic-upload">
<upload-img v-model:imageUrl="batchPicUrl" enable-library />
</div>
<template #footer>
<el-button @click="batchPicVisible = false">取消</el-button>
<el-button type="primary" :loading="batchPicLoading" @click="submitBatchPic">确定赋值</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="systemCanteenSpu">
import type { UploadFile, UploadInstance } from "element-plus"
import { BasicTableProps, useTable } from "/@/hooks/table"
import { fetchList, delObjs, importSpu } from "/@/api/canteen/canteenSpu"
import { fetchList, delObjs, importSpu, batchUpdatePic } from "/@/api/canteen/canteenSpu"
import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
@@ -188,6 +216,7 @@ import { downBlobFile } from "/@/utils/other"
// 引入组件
const FormDialog = defineAsyncComponent(() => import("./form.vue"))
const UploadImg = defineAsyncComponent(() => import("/@/components/Upload/Image.vue"))
const { spu_status, is_show } = useDict("spu_status", "is_show")
// 定义变量内容
@@ -324,6 +353,45 @@ const selectionChangHandle = (objs: { id: string }[]) => {
multiple.value = !objs.length
}
const batchPicVisible = ref(false)
const batchPicLoading = ref(false)
const batchPicUrl = ref("")
const openBatchPic = () => {
if (!selectObjs.value?.length) {
useMessage().warning("请先勾选要赋值图片的菜品")
return
}
batchPicUrl.value = ""
batchPicVisible.value = true
}
const submitBatchPic = async () => {
if (!selectObjs.value?.length) {
useMessage().warning("请先勾选菜品")
return
}
if (!batchPicUrl.value) {
useMessage().warning("请上传或从图库选择封面图")
return
}
batchPicLoading.value = true
try {
const res: any = await batchUpdatePic({
ids: selectObjs.value.map((id: any) => String(id)),
picUrl: batchPicUrl.value,
})
useMessage().success(res.msg || "赋值成功")
batchPicVisible.value = false
batchPicUrl.value = ""
getDataList()
} catch (err: any) {
useMessage().error(err.msg || "赋值失败")
} finally {
batchPicLoading.value = false
}
}
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
@@ -349,4 +417,10 @@ const handleDelete = async (ids: string[]) => {
align-items: flex-start;
gap: 8px;
}
.batch-pic-upload {
display: flex;
justify-content: center;
padding: 8px 0 4px;
}
</style>