修复菜单路由问题,完善导入错提示优化
This commit is contained in:
1 parent
19ad2a5d99
commit
e9be7ab7c4
7 files changed
+148
-25
No files matched your search
@@ -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({
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/** 目录型菜单的 el-sub-menu index,避免与叶子路由 path 冲突 */
|
||||
export function toSubMenuIndex(path: string) {
|
||||
return `sub:${path || ""}`
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user