增加了tabs
This commit is contained in:
parent
ddf90424ba
commit
8430eb509c
@ -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">
|
||||
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
@ -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>
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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…
Reference in New Issue
Block a user