增加了tabs
This commit is contained in:
parent
ddf90424ba
commit
8430eb509c
@ -46,194 +46,202 @@
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useAllDataStore } from "@/stores";
|
||||
import { getAllMenus } from "@/api/menu";
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, defineEmits } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAllDataStore } from '@/stores';
|
||||
import { getAllMenus } from '@/api/menu';
|
||||
|
||||
export default {
|
||||
name: "CommonAside",
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
const emit = defineEmits(['menu-click']);
|
||||
|
||||
const store = useAllDataStore();
|
||||
const isCollapse = computed(() => store.state.isCollapse);
|
||||
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const list = ref([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const store = useAllDataStore();
|
||||
const isCollapse = computed(() => store.state.isCollapse);
|
||||
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
|
||||
const asideBgColor = ref('#0081ff');
|
||||
const asideTextColor = ref('#ffffff');
|
||||
|
||||
// 更新主题颜色
|
||||
const updateThemeColors = () => {
|
||||
try {
|
||||
const root = document.documentElement;
|
||||
const bgColor = getComputedStyle(root).getPropertyValue('--aside-bg-color').trim();
|
||||
const textColor = getComputedStyle(root).getPropertyValue('--aside-text-color').trim();
|
||||
|
||||
// 主题颜色(用于 Element Plus 组件,需要响应式)
|
||||
// 初始值设为可见的默认值,避免初始化时不可见
|
||||
const asideBgColor = ref('#0081ff');
|
||||
const asideTextColor = ref('#ffffff');
|
||||
// 只有当获取到有效值时才更新
|
||||
if (bgColor) {
|
||||
asideBgColor.value = bgColor;
|
||||
}
|
||||
if (textColor) {
|
||||
asideTextColor.value = textColor;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('更新主题颜色失败:', e);
|
||||
// 出错时保持默认值
|
||||
}
|
||||
};
|
||||
|
||||
// 将后端数据转换为前端需要的格式
|
||||
const transformMenuData = (menus) => {
|
||||
// 首先映射字段格式
|
||||
const mappedMenus = menus.map(menu => ({
|
||||
id: menu.id,
|
||||
path: menu.path,
|
||||
icon: menu.icon || 'fa-circle',
|
||||
label: menu.name,
|
||||
route: menu.path,
|
||||
parentId: menu.parentId || 0,
|
||||
order: menu.order || 0,
|
||||
children: []
|
||||
}));
|
||||
|
||||
// 构建树形结构
|
||||
const menuMap = new Map();
|
||||
const rootMenus = [];
|
||||
|
||||
// 先创建所有菜单的映射
|
||||
mappedMenus.forEach(menu => {
|
||||
menuMap.set(menu.id, menu);
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
mappedMenus.forEach(menu => {
|
||||
if (menu.parentId === 0) {
|
||||
rootMenus.push(menu);
|
||||
} else {
|
||||
const parent = menuMap.get(menu.parentId);
|
||||
if (parent) {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(menu);
|
||||
} else {
|
||||
// 如果找不到父节点,作为根节点处理
|
||||
rootMenus.push(menu);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 按 order 排序(确保排序正确)
|
||||
const sortMenus = (menus) => {
|
||||
if (!menus || menus.length === 0) return;
|
||||
|
||||
// 更新主题颜色
|
||||
const updateThemeColors = () => {
|
||||
try {
|
||||
const root = document.documentElement;
|
||||
const bgColor = getComputedStyle(root).getPropertyValue('--aside-bg-color').trim();
|
||||
const textColor = getComputedStyle(root).getPropertyValue('--aside-text-color').trim();
|
||||
|
||||
// 只有当获取到有效值时才更新
|
||||
if (bgColor) {
|
||||
asideBgColor.value = bgColor;
|
||||
}
|
||||
if (textColor) {
|
||||
asideTextColor.value = textColor;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('更新主题颜色失败:', e);
|
||||
// 出错时保持默认值
|
||||
// 对当前层级的菜单进行排序
|
||||
menus.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999; // 没有order的排在最后
|
||||
const orderB = Number(b.order) ?? 999999;
|
||||
const diff = orderA - orderB;
|
||||
|
||||
// 如果 order 相同,按 id 排序(保证稳定性)
|
||||
if (diff === 0) {
|
||||
return (a.id || 0) - (b.id || 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 将后端数据转换为前端需要的格式
|
||||
const transformMenuData = (menus) => {
|
||||
// 首先映射字段格式
|
||||
const mappedMenus = menus.map(menu => ({
|
||||
id: menu.id,
|
||||
path: menu.path,
|
||||
icon: menu.icon || 'fa-circle',
|
||||
label: menu.name,
|
||||
route: menu.path,
|
||||
parentId: menu.parentId || 0,
|
||||
order: menu.order || 0,
|
||||
children: []
|
||||
}));
|
||||
|
||||
// 构建树形结构
|
||||
const menuMap = new Map();
|
||||
const rootMenus = [];
|
||||
|
||||
// 先创建所有菜单的映射
|
||||
mappedMenus.forEach(menu => {
|
||||
menuMap.set(menu.id, menu);
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
mappedMenus.forEach(menu => {
|
||||
if (menu.parentId === 0) {
|
||||
rootMenus.push(menu);
|
||||
} else {
|
||||
const parent = menuMap.get(menu.parentId);
|
||||
if (parent) {
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(menu);
|
||||
} else {
|
||||
// 如果找不到父节点,作为根节点处理
|
||||
rootMenus.push(menu);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 按 order 排序(确保排序正确)
|
||||
const sortMenus = (menus) => {
|
||||
if (!menus || menus.length === 0) return;
|
||||
|
||||
// 对当前层级的菜单进行排序
|
||||
menus.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999; // 没有order的排在最后
|
||||
const orderB = Number(b.order) ?? 999999;
|
||||
const diff = orderA - orderB;
|
||||
|
||||
// 如果 order 相同,按 id 排序(保证稳定性)
|
||||
if (diff === 0) {
|
||||
return (a.id || 0) - (b.id || 0);
|
||||
}
|
||||
|
||||
return diff;
|
||||
});
|
||||
|
||||
// 递归排序子菜单
|
||||
menus.forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
sortMenus(menu.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 先排序根菜单
|
||||
sortMenus(rootMenus);
|
||||
|
||||
// console.log('排序后的根菜单:', rootMenus.map(m => ({ name: m.label, order: m.order })));
|
||||
|
||||
return rootMenus;
|
||||
};
|
||||
|
||||
// 获取菜单数据
|
||||
const fetchMenus = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 优先从 localStorage 读取
|
||||
const cachedMenus = localStorage.getItem('menuData');
|
||||
let menuData = null;
|
||||
|
||||
if (cachedMenus) {
|
||||
try {
|
||||
menuData = JSON.parse(cachedMenus);
|
||||
// console.log('从缓存读取菜单数据');
|
||||
} catch (e) {
|
||||
console.warn('缓存菜单数据解析失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果缓存中没有或解析失败,从接口获取
|
||||
if (!menuData) {
|
||||
const res = await getAllMenus();
|
||||
if (res && res.success && res.data) {
|
||||
menuData = res.data;
|
||||
// 保存到缓存
|
||||
localStorage.setItem('menuData', JSON.stringify(menuData));
|
||||
} else {
|
||||
console.error('获取菜单失败:', res?.message || '未知错误');
|
||||
list.value = [];
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 转换并排序菜单数据
|
||||
const transformedMenus = transformMenuData(menuData);
|
||||
// console.log('转换后的菜单数据:', transformedMenus);
|
||||
// console.log('菜单顺序(按 order 排序):', transformedMenus.map(m => ({ name: m.label, order: m.order, id: m.id })));
|
||||
list.value = transformedMenus;
|
||||
} catch (error) {
|
||||
console.error('获取菜单异常:', error);
|
||||
list.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时获取菜单
|
||||
onMounted(() => {
|
||||
// 先更新主题颜色,确保组件可见
|
||||
updateThemeColors();
|
||||
|
||||
// 延迟一点获取菜单,确保主题已初始化
|
||||
setTimeout(() => {
|
||||
fetchMenus();
|
||||
}, 100);
|
||||
|
||||
// 监听主题变化事件
|
||||
window.addEventListener('theme-change', updateThemeColors);
|
||||
|
||||
// 监听 CSS 变量变化(MutationObserver)
|
||||
const observer = new MutationObserver(updateThemeColors);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
});
|
||||
return diff;
|
||||
});
|
||||
|
||||
// 递归排序子菜单
|
||||
menus.forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
sortMenus(menu.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 计算属性:统一排序所有菜单项(不再区分有无子菜单)
|
||||
const sortedMenuList = computed(() => {
|
||||
// 创建副本并排序,确保按 order 排序
|
||||
const sorted = [...list.value].sort((a, b) => {
|
||||
// 先排序根菜单
|
||||
sortMenus(rootMenus);
|
||||
|
||||
// console.log('排序后的根菜单:', rootMenus.map(m => ({ name: m.label, order: m.order })));
|
||||
|
||||
return rootMenus;
|
||||
};
|
||||
|
||||
// 获取菜单数据
|
||||
const fetchMenus = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 优先从 localStorage 读取
|
||||
const cachedMenus = localStorage.getItem('menuData');
|
||||
let menuData = null;
|
||||
|
||||
if (cachedMenus) {
|
||||
try {
|
||||
menuData = JSON.parse(cachedMenus);
|
||||
// console.log('从缓存读取菜单数据');
|
||||
} catch (e) {
|
||||
console.warn('缓存菜单数据解析失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果缓存中没有或解析失败,从接口获取
|
||||
if (!menuData) {
|
||||
const res = await getAllMenus();
|
||||
if (res && res.success && res.data) {
|
||||
menuData = res.data;
|
||||
// 保存到缓存
|
||||
localStorage.setItem('menuData', JSON.stringify(menuData));
|
||||
} else {
|
||||
console.error('获取菜单失败:', res?.message || '未知错误');
|
||||
list.value = [];
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 转换并排序菜单数据
|
||||
const transformedMenus = transformMenuData(menuData);
|
||||
// console.log('转换后的菜单数据:', transformedMenus);
|
||||
// console.log('菜单顺序(按 order 排序):', transformedMenus.map(m => ({ name: m.label, order: m.order, id: m.id })));
|
||||
list.value = transformedMenus;
|
||||
} catch (error) {
|
||||
console.error('获取菜单异常:', error);
|
||||
list.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时获取菜单
|
||||
onMounted(() => {
|
||||
// 先更新主题颜色,确保组件可见
|
||||
updateThemeColors();
|
||||
|
||||
// 延迟一点获取菜单,确保主题已初始化
|
||||
setTimeout(() => {
|
||||
fetchMenus();
|
||||
}, 100);
|
||||
|
||||
// 监听主题变化事件
|
||||
window.addEventListener('theme-change', updateThemeColors);
|
||||
|
||||
// 监听 CSS 变量变化(MutationObserver)
|
||||
const observer = new MutationObserver(updateThemeColors);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
});
|
||||
});
|
||||
|
||||
// 计算属性:统一排序所有菜单项(不再区分有无子菜单)
|
||||
const sortedMenuList = computed(() => {
|
||||
// 创建副本并排序,确保按 order 排序
|
||||
const sorted = [...list.value].sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999;
|
||||
const orderB = Number(b.order) ?? 999999;
|
||||
if (orderA === orderB) {
|
||||
return (a.id || 0) - (b.id || 0);
|
||||
}
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
// 确保子菜单也排序
|
||||
sorted.forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999;
|
||||
const orderB = Number(b.order) ?? 999999;
|
||||
if (orderA === orderB) {
|
||||
@ -241,60 +249,33 @@ export default {
|
||||
}
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
// 确保子菜单也排序
|
||||
sorted.forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.sort((a, b) => {
|
||||
const orderA = Number(a.order) ?? 999999;
|
||||
const orderB = Number(b.order) ?? 999999;
|
||||
if (orderA === orderB) {
|
||||
return (a.id || 0) - (b.id || 0);
|
||||
}
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return sorted;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return sorted;
|
||||
});
|
||||
|
||||
// 菜单点击事件处理
|
||||
const handleMenuSelect = (index) => {
|
||||
const menuItem = findMenuItemByPath(list.value, index);
|
||||
if (menuItem && menuItem.route) {
|
||||
router.push(menuItem.route);
|
||||
}
|
||||
};
|
||||
|
||||
// 递归查找菜单项
|
||||
const findMenuItemByPath = (menus, path) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu;
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findMenuItemByPath(menu.children, path);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
list,
|
||||
sortedMenuList,
|
||||
store,
|
||||
isCollapse,
|
||||
width,
|
||||
loading,
|
||||
handleMenuSelect,
|
||||
route,
|
||||
asideBgColor,
|
||||
asideTextColor
|
||||
};
|
||||
// 菜单点击事件:emit 通知父组件
|
||||
const handleMenuSelect = (index) => {
|
||||
const menuItem = findMenuItemByPath(list.value, index);
|
||||
if (menuItem && menuItem.route) {
|
||||
emit('menu-click', menuItem);
|
||||
}
|
||||
};
|
||||
|
||||
// 递归查找菜单项
|
||||
const findMenuItemByPath = (menus, path) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu;
|
||||
}
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const found = findMenuItemByPath(menu.children, path);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
@ -1,24 +1,96 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
|
||||
// 初始化state数据
|
||||
// ========== 全局状态 Store ==========
|
||||
function initState() {
|
||||
return {
|
||||
isCollapse: false,
|
||||
};
|
||||
return {
|
||||
isCollapse: false,
|
||||
};
|
||||
}
|
||||
|
||||
export const useAllDataStore = defineStore('allData', () => {
|
||||
const state = reactive(initState());
|
||||
const count = ref(0);
|
||||
const doubleCount = computed(() => count.value * 2);
|
||||
function increment() {
|
||||
count.value++;
|
||||
const state = reactive(initState());
|
||||
const count = ref(0);
|
||||
const doubleCount = computed(() => count.value * 2);
|
||||
function increment() {
|
||||
count.value++;
|
||||
}
|
||||
return {
|
||||
state,
|
||||
count,
|
||||
doubleCount,
|
||||
increment,
|
||||
};
|
||||
});
|
||||
|
||||
// ========== 多标签页 Tabs Store ==========
|
||||
import { defineStore as defineTabsStore } from 'pinia';
|
||||
import { ref as vueRef } from 'vue';
|
||||
|
||||
/**
|
||||
* 多标签页Tabs状态管理
|
||||
* tabList每个tab结构: {
|
||||
* title: 标签显示名,
|
||||
* fullPath: 路由路径(唯一key),
|
||||
* name: 路由name,
|
||||
* icon: 图标(可选)
|
||||
* }
|
||||
*/
|
||||
export const useTabsStore = defineTabsStore('tabs', () => {
|
||||
// 固定首页tab
|
||||
const defaultDashboardPath = '/dashboard';
|
||||
const tabList = vueRef([
|
||||
{ title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' },
|
||||
]);
|
||||
const activeTab = vueRef(defaultDashboardPath);
|
||||
|
||||
// 添加tab,若已存在则激活
|
||||
function addTab(tab) {
|
||||
const exist = tabList.value.find((t) => t.fullPath === tab.fullPath);
|
||||
if (!exist) {
|
||||
tabList.value.push(tab);
|
||||
}
|
||||
return {
|
||||
state,
|
||||
count,
|
||||
doubleCount,
|
||||
increment
|
||||
activeTab.value = tab.fullPath;
|
||||
}
|
||||
|
||||
// 删除指定tab并切换激活tab
|
||||
function removeTab(fullPath) {
|
||||
const idx = tabList.value.findIndex((t) => t.fullPath === fullPath);
|
||||
if (idx > -1) {
|
||||
tabList.value.splice(idx, 1);
|
||||
// 只在关闭当前激活tab时切换激活tab
|
||||
if (activeTab.value === fullPath) {
|
||||
if (tabList.value.length > 0) {
|
||||
// 优先激活右侧(如无则激活左侧)
|
||||
const newIdx = idx >= tabList.value.length ? tabList.value.length - 1 : idx;
|
||||
activeTab.value = tabList.value[newIdx].fullPath;
|
||||
} else {
|
||||
// 全部关闭,兜底首页
|
||||
activeTab.value = defaultDashboardPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭其他,只留首页和当前激活tab
|
||||
function closeOthers() {
|
||||
tabList.value = tabList.value.filter(
|
||||
(t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value
|
||||
);
|
||||
}
|
||||
|
||||
// 关闭全部,只留首页
|
||||
function closeAll() {
|
||||
tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath);
|
||||
activeTab.value = defaultDashboardPath;
|
||||
}
|
||||
|
||||
return {
|
||||
tabList,
|
||||
activeTab,
|
||||
addTab,
|
||||
removeTab,
|
||||
closeOthers,
|
||||
closeAll,
|
||||
};
|
||||
});
|
||||
@ -1,18 +1,148 @@
|
||||
<script setup>
|
||||
import CommonAside from "@/components/CommonAside.vue";
|
||||
import CommonHeader from "@/components/CommonHeader.vue";
|
||||
import CommonAside from '@/components/CommonAside.vue';
|
||||
import CommonHeader from '@/components/CommonHeader.vue';
|
||||
import { useTabsStore } from '@/stores';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, watch, reactive, nextTick } from 'vue';
|
||||
import { More, DArrowRight } from '@element-plus/icons-vue'
|
||||
|
||||
const tabsStore = useTabsStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const defaultDashboardPath = '/dashboard';
|
||||
|
||||
// 1. 侧栏菜单点击:加入/激活Tab并切换路由
|
||||
const handleAsideMenuClick = (menuItem) => {
|
||||
tabsStore.addTab({
|
||||
title: menuItem.label,
|
||||
fullPath: menuItem.path,
|
||||
name: menuItem.label,
|
||||
icon: menuItem.icon
|
||||
});
|
||||
if (route.fullPath !== menuItem.path) {
|
||||
router.push(menuItem.path);
|
||||
}
|
||||
};
|
||||
const tabsCloseTab = (targetKey) => {
|
||||
tabsStore.removeTab(targetKey);
|
||||
if (route.fullPath !== tabsStore.activeTab) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
};
|
||||
const closeOthers = () => {
|
||||
tabsStore.closeOthers();
|
||||
if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) {
|
||||
router.push(tabsStore.activeTab);
|
||||
}
|
||||
};
|
||||
const closeAll = () => {
|
||||
tabsStore.closeAll();
|
||||
router.push(defaultDashboardPath);
|
||||
};
|
||||
// 主动监听tab激活,保证切tab时内容区切换
|
||||
watch(
|
||||
() => tabsStore.activeTab,
|
||||
(newVal) => {
|
||||
if (newVal && router.currentRoute.value.fullPath !== newVal) {
|
||||
router.push(newVal);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ========== 右键菜单逻辑 ========== //
|
||||
const contextMenu = reactive({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
tab: null,
|
||||
});
|
||||
const contextDropdownRef = ref(null);
|
||||
|
||||
const onTabContextMenu = (event, tab) => {
|
||||
event.preventDefault();
|
||||
contextMenu.visible = true;
|
||||
contextMenu.x = event.clientX;
|
||||
contextMenu.y = event.clientY;
|
||||
contextMenu.tab = tab;
|
||||
nextTick(() => {
|
||||
// 溢出优化可扩展
|
||||
});
|
||||
document.body.addEventListener('click', hideContextMenu, { once: true });
|
||||
};
|
||||
function hideContextMenu() {
|
||||
contextMenu.visible = false;
|
||||
contextMenu.tab = null;
|
||||
}
|
||||
function closeTabContextTab() {
|
||||
if (contextMenu.tab && contextMenu.tab.fullPath !== defaultDashboardPath) {
|
||||
tabsStore.removeTab(contextMenu.tab.fullPath);
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
function closeOthersContextTab() {
|
||||
if (contextMenu.tab) {
|
||||
tabsStore.activeTab = contextMenu.tab.fullPath;
|
||||
tabsStore.closeOthers();
|
||||
}
|
||||
hideContextMenu();
|
||||
}
|
||||
function closeAllTabs() {
|
||||
tabsStore.closeAll();
|
||||
hideContextMenu();
|
||||
router.push(defaultDashboardPath);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container class="main-container">
|
||||
<common-aside />
|
||||
<common-aside @menu-click="handleAsideMenuClick" />
|
||||
<el-container>
|
||||
<el-header class="main-header">
|
||||
<common-header />
|
||||
</el-header>
|
||||
<el-main class="right-main">
|
||||
<router-view />
|
||||
<div class="multi-tabs-wrapper">
|
||||
<el-tabs
|
||||
v-model="tabsStore.activeTab"
|
||||
type="card"
|
||||
class="multi-tabs"
|
||||
closable
|
||||
@tab-remove="tabsCloseTab"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="tab in tabsStore.tabList"
|
||||
:key="tab.fullPath"
|
||||
:label="tab.title"
|
||||
:name="tab.fullPath"
|
||||
:closable="tab.fullPath !== defaultDashboardPath"
|
||||
@contextmenu="onTabContextMenu($event, tab)"
|
||||
/>
|
||||
</el-tabs>
|
||||
<!-- 跟随浮动到 tabs 最右侧的批量按钮 -->
|
||||
<div class="floated-tabs-extra-btn">
|
||||
<el-dropdown>
|
||||
<span class="extra-action-btn">
|
||||
<el-icon><DArrowRight /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="closeOthers">关闭其他</el-dropdown-item>
|
||||
<el-dropdown-item @click="closeAll">关闭全部</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容毛玻璃卡片容器 -->
|
||||
<div class="main-panel glass-card">
|
||||
<keep-alive :max="10">
|
||||
<router-view v-slot="{ Component }">
|
||||
<component :is="Component" />
|
||||
</router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
@ -20,33 +150,61 @@ import CommonHeader from "@/components/CommonHeader.vue";
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.common-layout,
|
||||
.main-container {
|
||||
.common-layout, .main-container {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
background-color: var(--bg-color-page);
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
:deep(.el-aside) {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
background-color: var(--header-bg-color, #0081ff);
|
||||
transition: background-color 0.3s ease;
|
||||
height: 60px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.right-main {
|
||||
background-color: var(--bg-color-page);
|
||||
color: var(--text-color-primary);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
.multi-tabs-wrapper {
|
||||
position: relative;
|
||||
zoom: 1;
|
||||
min-height: 45px;
|
||||
}
|
||||
.floated-tabs-extra-btn {
|
||||
float: right;
|
||||
margin-top: -40px;
|
||||
margin-right: 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
.extra-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.2s;
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
.more-menu {
|
||||
font-size: 13px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
0
pc/src/views/apps/knowledge/category/edit.vue
Normal file
0
pc/src/views/apps/knowledge/category/edit.vue
Normal file
13
pc/src/views/apps/knowledge/category/index.vue
Normal file
13
pc/src/views/apps/knowledge/category/index.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
123
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
@ -67,10 +67,20 @@
|
||||
<div class="knowledge-repos">
|
||||
<div class="repos-header">
|
||||
<h2>知识库列表</h2>
|
||||
<el-button type="primary" @click="handleCreate" class="create-btn">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
新建知识库
|
||||
</el-button>
|
||||
<div>
|
||||
<el-button type="primary" @click="handleCreate" class="create-btn">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
新建知识库
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleCategory" class="create-btn">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
分类管理
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleTags" class="create-btn">
|
||||
<i class="fas fa-tags"></i>
|
||||
标签管理
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="repos-grid">
|
||||
@ -286,7 +296,7 @@ async function fetchRepoList() {
|
||||
repoList.value = result.list || result.data?.list || [];
|
||||
total.value = result.total || result.data?.total || 0;
|
||||
}
|
||||
|
||||
|
||||
// 更新统计数据(避免重复调用 API)
|
||||
updateStats();
|
||||
} catch (error: any) {
|
||||
@ -314,6 +324,16 @@ function handleEdit(repo: Knowledge) {
|
||||
router.push(`/apps/knowledge/edit/${repo.id}`);
|
||||
}
|
||||
|
||||
function handleCategory() {
|
||||
// 跳转到分类管理页面,使用路径跳转
|
||||
router.push(`/apps/knowledge/category`);
|
||||
}
|
||||
|
||||
function handleTags() {
|
||||
// 跳转到标签管理页面,使用路径跳转
|
||||
router.push(`/apps/knowledge/tags`);
|
||||
}
|
||||
|
||||
function handleDelete(repo: Knowledge) {
|
||||
ElMessageBox.confirm(`确认删除知识「${repo.title}」?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@ -457,7 +477,7 @@ onMounted(() => {
|
||||
.search-input {
|
||||
flex: 1;
|
||||
|
||||
:deep(.el-input__wrapper){
|
||||
:deep(.el-input__wrapper) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
@ -706,7 +726,8 @@ onMounted(() => {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--border-color);
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
transition: background-color 0.3s ease, color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.repo-owner {
|
||||
@ -744,7 +765,8 @@ onMounted(() => {
|
||||
border-color: var(--border-color-lighter);
|
||||
font-size: 12px;
|
||||
padding: 4px 12px;
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
transition: background-color 0.3s ease, color 0.3s ease,
|
||||
border-color 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
0
pc/src/views/apps/knowledge/tag/edit.vue
Normal file
0
pc/src/views/apps/knowledge/tag/edit.vue
Normal file
0
pc/src/views/apps/knowledge/tag/index.vue
Normal file
0
pc/src/views/apps/knowledge/tag/index.vue
Normal file
@ -145,7 +145,7 @@
|
||||
>
|
||||
<el-input
|
||||
v-model="currentMenu.ComponentPath"
|
||||
placeholder="例如:@/views/settings/index.vue"
|
||||
placeholder="例如:/apps/knowledge/index.vue"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
@ -440,10 +440,15 @@ const saveMenu = async () => {
|
||||
const valid = await menuFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
// 解决后端时间字段问题:过滤掉 CreateTime/UpdateTime
|
||||
const payload = { ...currentMenu.value };
|
||||
delete payload.CreateTime;
|
||||
delete payload.UpdateTime;
|
||||
|
||||
try {
|
||||
if (currentMenu.value.Id === 0) {
|
||||
// 新增菜单
|
||||
const result = await createMenu(currentMenu.value as Menu);
|
||||
const result = await createMenu(payload as Menu);
|
||||
if (result.success) {
|
||||
ElMessage.success("菜单添加成功");
|
||||
dialogVisible.value = false;
|
||||
@ -455,7 +460,7 @@ const saveMenu = async () => {
|
||||
// 编辑菜单
|
||||
const result = await updateMenu(
|
||||
currentMenu.value.Id!,
|
||||
currentMenu.value as Menu
|
||||
payload as Menu
|
||||
);
|
||||
if (result.success) {
|
||||
ElMessage.success("更新成功");
|
||||
|
||||
@ -167,7 +167,7 @@ func (c *KnowledgeController) Update() {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 0,
|
||||
"message": "更新成功",
|
||||
"data": nil,
|
||||
// "data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ func (c *MenuController) UpdateMenu() {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "更新菜单成功",
|
||||
"data": menu,
|
||||
// "data": menu,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -248,7 +248,7 @@ func UpdateKnowledge(id int, k *Knowledge) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新字段(不包含category_name,因为它是从联查获取的)
|
||||
// 更新字段
|
||||
knowledge.Title = k.Title
|
||||
knowledge.CategoryId = k.CategoryId
|
||||
knowledge.Tags = k.Tags
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user