增加了tabs

This commit is contained in:
李志强 2025-10-30 17:34:32 +08:00
parent ddf90424ba
commit 8430eb509c
12 changed files with 522 additions and 271 deletions

View File

@ -46,15 +46,14 @@
</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';
const emit = defineEmits(['menu-click']);
export default {
name: "CommonAside",
setup() {
const router = useRouter();
const route = useRoute();
const list = ref([]);
@ -63,9 +62,6 @@ export default {
const store = useAllDataStore();
const isCollapse = computed(() => store.state.isCollapse);
const width = computed(() => store.state.isCollapse ? '64px' : '180px');
// Element Plus
//
const asideBgColor = ref('#0081ff');
const asideTextColor = ref('#ffffff');
@ -259,11 +255,11 @@ export default {
return sorted;
});
//
// emit
const handleMenuSelect = (index) => {
const menuItem = findMenuItemByPath(list.value, index);
if (menuItem && menuItem.route) {
router.push(menuItem.route);
emit('menu-click', menuItem);
}
};
@ -280,21 +276,6 @@ export default {
}
return null;
};
return {
list,
sortedMenuList,
store,
isCollapse,
width,
loading,
handleMenuSelect,
route,
asideBgColor,
asideTextColor
};
}
};
</script>
<style scoped lang="less">

View File

@ -1,7 +1,7 @@
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,
@ -19,6 +19,78 @@ export const useAllDataStore = defineStore('allData', () => {
state,
count,
doubleCount,
increment
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);
}
})
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,
};
});

View File

@ -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);
};
// tabtab
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>

View File

@ -0,0 +1,13 @@
<template>
<div>
123
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>

View File

@ -67,10 +67,20 @@
<div class="knowledge-repos">
<div class="repos-header">
<h2>知识库列表</h2>
<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">
@ -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: "确定",
@ -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;
}
}

View File

View 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("更新成功");

View File

@ -167,7 +167,7 @@ func (c *KnowledgeController) Update() {
c.Data["json"] = map[string]interface{}{
"code": 0,
"message": "更新成功",
"data": nil,
// "data": nil,
}
c.ServeJSON()
}

View File

@ -105,7 +105,7 @@ func (c *MenuController) UpdateMenu() {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "更新菜单成功",
"data": menu,
// "data": menu,
}
}

View File

@ -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