增加悬浮窗功能

This commit is contained in:
2025-12-31 11:47:15 +08:00
parent c4066a3b45
commit cc44bdc9b1
8 changed files with 351 additions and 78 deletions

View File

@@ -3,7 +3,7 @@ import { Account_BaseUrl } from "@/api/config";
const baseURL = Account_BaseUrl + "/admin/quick";
const API = {
getList(data: any) {
return request<any>({
return request<any, QuickMenu[]>({
url: `${baseURL}`,
method: "get",
params: data
@@ -35,3 +35,34 @@ export default API;
/**
* 悬浮窗配置 实体类。
*
* QuickMenu
*/
export interface QuickMenu {
createTime?: string;
id?: number;
/**
* 菜单Id
*/
menuId: number;
/**
* 店铺Id
*/
shopId?: number;
/**
* 排序
*/
sort?: number;
/**
* 状态 1-启用 0-禁用
*/
status?: number;
updateTime?: string;
/**
* 菜单图标
*/
url?: string;
[property: string]: any;
}

View File

@@ -8,6 +8,8 @@
:data="menus"
:render-after-expand="false"
style="width: 240px"
node-key="menuId"
:disabled-key="disabled"
></el-tree-select>
</el-form-item>
@@ -40,7 +42,9 @@
</template>
<script setup>
import { ref, onMounted } from "vue";
// 补充缺失的导入
import { ref, onMounted, watch } from "vue";
import { ElNotification, ElMessage } from "element-plus"; // 导入提示组件
import { filterNumberInput } from "@/utils";
import { roleTemplateAdd } from "@/api/account/roleTemplate";
import MenuAPI from "@/api/account/menu";
@@ -50,80 +54,147 @@ const props = defineProps({
isGetMenu: {
type: Boolean,
required: false,
default: false, // 补充默认值
},
parMenus: {
type: Array,
required: false,
default: () => [], // 补充默认值
},
item: {
type: Object,
required: false,
default: () => null,
},
});
watch(
() => props.item,
(newval) => {
if (newval) {
form.value = newval;
}
},
{ deep: true }
);
const menus = ref([]);
onMounted(async () => {
if (!props.isGetMenu) {
return;
}
const res = await MenuAPI.getRoutes();
console.log("getRoutes", res);
menus.value = returnMenu(res);
console.log(menus.value);
});
watch(
() => props.parMenus.length,
(newval) => {
menus.value = returnMenu(props.parMenus);
}
);
/**
* 格式化菜单数据:核心逻辑 - 禁用有子菜单的父节点,仅允许选叶子节点
* @param arr 原始菜单数组
* @returns 格式化后带禁用标记的菜单数组
*/
function returnMenu(arr) {
let result = [];
for (let menu of arr) {
menu.label = menu.title;
menu.value = menu.menuId;
result.push({
// 过滤掉"默认接口目录"
if (menu.title === "默认接口目录") continue;
// 筛选有效子菜单:非隐藏 + 类型为0
const children = menu.children ? menu.children.filter((v) => !v.hidden && v.type === 0) : [];
// 递归处理子菜单
const formattedChildren = returnMenu(children);
// 核心:判断是否为叶子节点(无有效子菜单)
const isLeaf = formattedChildren.length === 0;
// 组装节点有子菜单则禁用disabled: true无则允许选择
const menuNode = {
...menu,
label: menu.title,
value: menu.menuId,
children: menu.children ? returnMenu(menu.children) : [],
});
menuId: menu.menuId, // 保持node-key一致
disabled: !isLeaf, // 有子菜单 → 禁用,无 → 启用
children: formattedChildren,
};
// 特殊处理如果子菜单只有1个直接扁平化保持原有逻辑
if (formattedChildren.length === 1) {
result.push({
...formattedChildren[0],
disabled: formattedChildren[0].children.length > 0, // 子节点仍判断是否有后代
});
} else {
result.push(menuNode);
}
}
return result;
}
onMounted(async () => {
if (!props.isGetMenu) return;
try {
const res = await MenuAPI.getRoutes();
menus.value = returnMenu(res);
} catch (error) {
ElMessage.error("菜单数据加载失败");
console.error("getRoutes error:", error);
}
});
// 监听父组件传入的菜单变化
watch(
() => props.parMenus,
(newVal) => {
if (newVal.length) {
menus.value = returnMenu(newVal);
}
},
{ deep: true } // 监听数组内部变化
);
// 对话框显隐绑定
const visible = defineModel({
type: Boolean,
required: false,
default: false,
});
const formRef = ref(null);
const loading = ref(false);
// 表单初始化
const form = ref({
menuId: "",
sort: 0,
url: "",
status: 1,
});
/**
* 自定义校验规则:确保选中的是叶子节点(兜底验证)
* @param rule 校验规则
* @param value 选中的menuId
* @param callback 回调函数
*/
function validateLeafMenu(rule, value, callback) {
if (!value) {
return callback(new Error("请选择菜单"));
}
// 递归查找节点,判断是否为叶子节点
const findNode = (nodes, menuId) => {
for (let node of nodes) {
if (node.menuId === menuId) return node;
if (node.children) {
const res = findNode(node.children, menuId);
if (res) return res;
}
}
return null;
};
const selectedNode = findNode(menus.value, value);
if (!selectedNode) {
callback(new Error("选择的菜单不存在"));
} else if (selectedNode.disabled) {
callback(new Error("有子菜单的父节点不可选,请选择子菜单"));
} else {
callback(); // 校验通过
}
}
// 表单校验规则:修复拼写错误 + 新增自定义叶子节点校验
const rules = ref({
menuId: [
{
required: true,
message: "请选择菜单",
triiger: "blur",
validator: validateLeafMenu, // 替换原有规则为自定义校验
trigger: "change", // 选择变化时校验原triiger拼写错误
},
],
});
// 重置表单
function resetForm() {
form.value = {
menuId: "",
@@ -131,48 +202,65 @@ function resetForm() {
url: "",
status: 1,
};
formRef.value.resetFields();
if (formRef.value) {
formRef.value.resetFields(); // 避免null调用
}
}
// 提交
// 提交事件
const emits = defineEmits(["success"]);
function submitHandle() {
formRef.value.validate(async (vaild) => {
try {
if (vaild) {
loading.value = true;
if (form.value.sort === "") {
form.value.sort = 0;
}
await quickApi.add(form.value);
async function submitHandle() {
try {
// 修复vaild拼写错误
const valid = await formRef.value.validate();
if (!valid) return;
ElNotification({
title: "注意",
message: "添加成功",
type: "success",
});
loading.value = true;
// 排序值兜底
form.value.sort = form.value.sort === "" ? 0 : form.value.sort;
emits("success");
visible.value = false;
}
} catch (error) {
console.log(error);
// 区分新增/编辑
if (form.value.id) {
await quickApi.edit(form.value);
ElNotification({ title: "成功", message: "编辑成功", type: "success" });
} else {
await quickApi.add(form.value);
ElNotification({ title: "成功", message: "添加成功", type: "success" });
}
setTimeout(() => {
loading.value = false;
}, 500);
});
emits("success");
visible.value = false;
} catch (error) {
ElMessage.error(form.value.id ? "编辑失败" : "添加失败");
console.error("submit error:", error);
} finally {
loading.value = false; // 无论成败都关闭loading
}
}
// 监听props.item初始化表单
watch(
() => props.item,
(newval) => {
if (newval) {
form.value = { ...newval }; // 深拷贝,避免修改原对象
}
},
{ deep: true, immediate: true } // 初始化触发
);
</script>
<style scoped lang="scss">
.dialog_footer {
display: flex;
gap: 14px;
.btn {
flex: 1;
}
}
// 可选:美化禁用节点的样式
.el-select-dropdown__item.is-disabled {
color: var(--el-text-color-regular);
cursor: pointer;
}
</style>

View File

@@ -55,7 +55,10 @@
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { useQuickStore, usePermissionStore } from "@/store";
const quickStore = useQuickStore();
import { ref, reactive, onMounted, toRaw } from "vue";
import MenuAPI from "@/api/account/menu";
import quickApi from "@/api/account/quick";
@@ -64,9 +67,9 @@ import dialogAdd from "./dialog-add.vue";
const showAdd = ref(false);
const nowEditMenu = ref({});
const nowEditMenu = ref(null);
function editMenu(item) {
nowEditMenu.value = item;
nowEditMenu.value = { ...item };
showAdd.value = true;
}
const tableData = reactive({
@@ -78,6 +81,7 @@ const tableData = reactive({
});
function Refresh() {
quickStore.getQuickMenus();
getList();
}
function getList() {
@@ -86,7 +90,7 @@ function getList() {
.getList({
page: tableData.page,
size: tableData.size,
isEdit: true,
isEdit: 1,
})
.then((res) => {
tableData.list = res;
@@ -101,25 +105,45 @@ const menus = ref([]);
async function getMenus() {
const res = await MenuAPI.getRoutes();
menus.value = res;
console.log("menus", res);
}
const menusIdMap = computed(() => {
const map = getMenuMao();
const map = getMenuMap();
console.log("map", map);
return map;
});
function returnMenuName(item) {
const menu = menusIdMap.value.get(`${item.menuId}`);
return menu.title;
console.log("menuId", item.menuId);
console.log("menu", menu);
return menu ? menu.title : "";
}
function getMenuMao() {
function getMenuMap() {
// 初始化Map
const map = new Map();
for (const menu of menus.value) {
map.set(menu.menuId, menu);
if (menu.children && menu.children.length > 0) {
for (const child of menu.children) {
map.set(child.menuId, child);
}
// 定义递归函数处理菜单节点
function processMenuNode(menuNode) {
// 将当前节点存入Map
map.set(menuNode.menuId, menuNode);
// 如果有子节点且子节点数组不为空,则递归处理每个子节点
if (menuNode.children && Array.isArray(menuNode.children) && menuNode.children.length > 0) {
menuNode.children.forEach((childNode) => {
processMenuNode(childNode);
});
}
}
// 遍历根级菜单,逐个处理
if (menus.value && Array.isArray(menus.value)) {
menus.value.forEach((rootMenu) => {
processMenuNode(rootMenu);
});
}
return map;
}
function isEnableChange(value, item) {

View File

@@ -2,6 +2,7 @@
<div class="fast-menu">
<el-dropdown
trigger="click"
@command="menuClick"
placement="top-end"
@visible-change="handleVisibleChange"
append-to-body="true"
@@ -13,8 +14,18 @@
</el-icon> -->
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(item, index) in list" :key="index">
Action {{ index + 1 }}
<el-dropdown-item
v-for="(item, index) in quickStore.quickMenus"
:key="index"
:command="item.menuId"
>
{{ returnMenuName(item.menuId) }}
</el-dropdown-item>
<el-dropdown-item :command="-1">
<el-icon>
<Edit color="#4080FF" />
</el-icon>
<span style="color: #4080ff">编辑</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -22,8 +33,30 @@
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
const list = ref(new Array(10).fill(1));
import { ref, computed } from "vue";
import { useQuickStore, usePermissionStore, useUserStore } from "@/store";
import router from "@/router";
const userStore = useUserStore();
const quickStore = useQuickStore();
const permissionStore = usePermissionStore();
console.log("permissionStore", permissionStore.menus);
function menuClick(menuId: number) {
console.log("menuId", menuId);
if (menuId == -1) {
if (userStore.userInfo.id == 1) {
return router.push("/system/commonlyUsedMenu");
} else {
return router.push("/commonlyUsedMenu");
}
}
permissionStore.menuJump(menuId);
}
function returnMenuName(menuId: number) {
return permissionStore.returnMenuName(menuId);
}
const imgsrc = computed(() => {
return showMenu.value

View File

@@ -14,5 +14,6 @@ export * from "./modules/settings";
export * from "./modules/tags-view";
export * from "./modules/user";
export * from "./modules/dict";
export * from "./modules/quick";
export { store };

View File

@@ -16,6 +16,7 @@ export const usePermissionStore = defineStore("permission", () => {
// 路由是否加载完成
const isRoutesLoaded = ref(false);
const menus = ref<RouteVO[]>([])
/**
@@ -31,6 +32,7 @@ export const usePermissionStore = defineStore("permission", () => {
}
MenuAPI.getRoutes()
.then((data) => {
menus.value = data
if (!isTest) {
const dynamicRoutes = parseDynamicRoutes(data.filter(v => v.type == 0));
dynamicRoutes.forEach((route) => {
@@ -93,8 +95,60 @@ export const usePermissionStore = defineStore("permission", () => {
});
isRoutesLoaded.value = false;
};
function getMenuMap() {
// 初始化Map
const map = new Map();
// 定义递归函数处理菜单节点
function processMenuNode(menuNode) {
// 将当前节点存入Map
map.set(menuNode.menuId, menuNode);
// 如果有子节点且子节点数组不为空,则递归处理每个子节点
if (menuNode.children && Array.isArray(menuNode.children) && menuNode.children.length > 0) {
menuNode.children.forEach((childNode) => {
processMenuNode(childNode);
});
}
}
// 遍历根级菜单,逐个处理
if (menus.value && Array.isArray(menus.value)) {
menus.value.forEach((rootMenu) => {
processMenuNode(rootMenu);
});
}
return map;
}
function returnMenuName(menuId: number | string) {
const menu = menusIdMap.value.get(`${menuId}`);
return menu ? menu.title : '';
}
const menusIdMap = computed(() => {
const map = getMenuMap();
return map;
});
function menuJump(menuId: number | string) {
const menu = menusIdMap.value.get(`${menuId}`);
console.log('menu', menu);
if (menu) {
if (menu.name) {
router.push({ name: menu.name as string })
} else {
router.push({ path: menu.path as string })
}
}
}
return {
menuJump,
returnMenuName,
menus,
routes,
mixedLayoutLeftRoutes,
isRoutesLoaded,

View File

@@ -0,0 +1,37 @@
import { store } from "@/store";
import quickApi, { type QuickMenu } from "@/api/account/quick";
import { usePermissionStoreHook } from "@/store/modules/permission";
export const useQuickStore = defineStore("quick", () => {
const quickMenus = ref<QuickMenu[]>([]);
async function getQuickMenus() {
const res = await quickApi.getList({ isEdit: 0 });
const obj: any = {}
let arr = []
for (let menu of res) {
if (obj.hasOwnProperty(menu.menuId) || menu.status != 1 || !usePermissionStoreHook().returnMenuName(menu.menuId)) {
continue;
} else {
obj[menu.menuId] = true;
arr.push(menu)
}
}
quickMenus.value = arr;
}
getQuickMenus()
return {
quickMenus, getQuickMenus
};
});
/**
* 用于在组件外部如在Pinia Store 中)使用 Pinia 提供的 store 实例。
* 官方文档解释了如何在组件外部使用 Pinia Store
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
*/
export function useQuickStoreHook() {
return useQuickStore(store);
}

View File

@@ -0,0 +1,5 @@
<template>
<div>
<FastMenuConfig></FastMenuConfig>
</div>
</template>