Skip to content

Commit 113e473

Browse files
committed
前端增加设备标签栏,方便设备之间切换
1 parent 3cb4355 commit 113e473

15 files changed

Lines changed: 276 additions & 70 deletions

front/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ declare module 'vue' {
7777
SingleRegister: typeof import('./src/components/register/SingleRegister.vue')['default']
7878
Slave: typeof import('./src/components/device/Slave.vue')['default']
7979
Table: typeof import('./src/components/device/Table.vue')['default']
80+
TagsView: typeof import('./src/components/layout/TagsView.vue')['default']
8081
TextNode: typeof import('./src/components/common/TextNode.vue')['default']
8182
WritePointDialog: typeof import('./src/components/device/WritePointDialog.vue')['default']
8283
}

front/src/App.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup>
22
import Sidebar from "./views/SideBar.vue";
33
import AppHeader from "@/components/header/AppHeader.vue";
4+
import TagsView from "@/components/layout/TagsView.vue";
45
import { currentTheme } from "@/utils/theme";
56
</script>
67

@@ -10,10 +11,16 @@ import { currentTheme } from "@/utils/theme";
1011
<Sidebar />
1112
<el-container direction="vertical">
1213
<AppHeader />
14+
<!-- 标签页 -->
15+
<TagsView />
1316
<el-main class="main-content">
1417
<el-scrollbar view-class="app-scrollbar-view">
1518
<div class="app-view-container">
16-
<router-view />
19+
<router-view v-slot="{ Component, route }">
20+
<keep-alive>
21+
<component :is="Component" :key="route.fullPath" />
22+
</keep-alive>
23+
</router-view>
1724
</div>
1825
<!-- 全局底部版权 -->
1926
<footer class="app-footer">
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<template>
2+
<div class="tags-view-container">
3+
<el-scrollbar wrap-class="tags-view-wrapper">
4+
<router-link
5+
v-for="tag in visitedViews"
6+
:key="tag.path"
7+
:to="{ path: tag.path, query: tag.query }"
8+
class="tags-view-item"
9+
:class="isActive(tag) ? 'active' : ''"
10+
@contextmenu.prevent="openMenu(tag, $event)"
11+
>
12+
{{ tag.title }}
13+
<el-icon
14+
v-if="visitedViews.length > 1"
15+
class="el-icon-close"
16+
@click.prevent.stop="closeSelectedTag(tag)"
17+
>
18+
<Close />
19+
</el-icon>
20+
</router-link>
21+
</el-scrollbar>
22+
</div>
23+
</template>
24+
25+
<script setup lang="ts">
26+
import { computed } from 'vue';
27+
import { useRoute, useRouter } from 'vue-router';
28+
import { Close } from '@element-plus/icons-vue';
29+
import { visitedViews, delView, type TagView } from '@/store/tagsView';
30+
31+
const route = useRoute();
32+
const router = useRouter();
33+
34+
const isActive = (tag: TagView) => {
35+
return tag.path === route.path;
36+
};
37+
38+
const closeSelectedTag = async (view: TagView) => {
39+
const views = await delView(view);
40+
if (isActive(view)) {
41+
toLastView(views, view);
42+
}
43+
};
44+
45+
const toLastView = (views: TagView[], view: TagView) => {
46+
const latestView = views.slice(-1)[0];
47+
if (latestView) {
48+
router.push((latestView.fullPath || latestView.path) as string);
49+
} else {
50+
// default redirect to home or somewhere safe if no views
51+
router.push('/');
52+
}
53+
};
54+
55+
const openMenu = (tag: TagView, e: MouseEvent) => {
56+
// context menu logic can be added here if needed
57+
};
58+
</script>
59+
60+
<style lang="scss" scoped>
61+
.tags-view-container {
62+
height: 34px;
63+
width: 100%;
64+
flex-shrink: 0; // prevent being squished by main content
65+
background: var(--bg-main);
66+
border-bottom: 1px solid var(--sidebar-border);
67+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
68+
z-index: 10; // ensure it is above the main scrollbar content if any shadows exist
69+
overflow: hidden;
70+
71+
.tags-view-wrapper {
72+
.tags-view-item {
73+
display: inline-block;
74+
position: relative;
75+
cursor: pointer;
76+
height: 26px;
77+
line-height: 26px;
78+
border: 1px solid var(--sidebar-border);
79+
color: var(--text-primary);
80+
background: var(--panel-bg);
81+
padding: 0 8px;
82+
font-size: 13px;
83+
margin-left: 5px;
84+
margin-top: 4px;
85+
border-radius: 4px;
86+
text-decoration: none;
87+
88+
&:first-of-type {
89+
margin-left: 15px;
90+
}
91+
92+
&.active {
93+
background-color: var(--color-primary);
94+
color: #fff;
95+
border-color: var(--color-primary);
96+
97+
&::before {
98+
content: '';
99+
background: #fff;
100+
display: inline-block;
101+
width: 8px;
102+
height: 8px;
103+
border-radius: 50%;
104+
position: relative;
105+
margin-right: 2px;
106+
}
107+
}
108+
109+
.el-icon-close {
110+
width: 16px;
111+
height: 16px;
112+
vertical-align: middle;
113+
border-radius: 50%;
114+
text-align: center;
115+
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
116+
transform-origin: 100% 50%;
117+
margin-left: 2px;
118+
119+
&:before {
120+
transform: scale(0.6);
121+
display: inline-block;
122+
}
123+
124+
&:hover {
125+
background-color: #b4bccc;
126+
color: #fff;
127+
}
128+
}
129+
}
130+
}
131+
}
132+
</style>

front/src/router/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// src/router/index.js
22
import { createRouter, createWebHashHistory } from 'vue-router';
3+
import { addView } from '@/store/tagsView';
34

45
// 创建路由器实例
56
const menuRouter = createRouter({
@@ -9,13 +10,21 @@ const menuRouter = createRouter({
910
path: '/device/:deviceName',
1011
name: 'device-detail', // Use a fixed name for the route config
1112
component: () => import('../views/Device.vue'),
12-
props: true // Allow params to be passed as props if needed
13+
props: true, // Allow params to be passed as props if needed
1314
},
1415
// Optional: Add a default redirect or home route if needed
1516
// { path: '/', redirect: '/device/some-default' }
1617
],
1718
});
1819

20+
// 全局后置钩子,用于收集访问过的页面作为标签页
21+
menuRouter.afterEach((to) => {
22+
// 我们只收集设备页面或者其他需要标签页的页面
23+
if (to.name || to.path.startsWith('/device')) {
24+
addView(to);
25+
}
26+
});
27+
1928
export async function setUpRoutes() {
2029
// Deprecated: No longer needed as we use dynamic params
2130
console.log('setUpRoutes is deprecated and no longer needed.');

front/src/store/tagsView.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ref } from 'vue';
2+
import type { RouteLocationNormalized } from 'vue-router';
3+
4+
export interface TagView extends Partial<RouteLocationNormalized> {
5+
title?: string;
6+
}
7+
8+
export const visitedViews = ref<TagView[]>([]);
9+
10+
export const addView = (view: RouteLocationNormalized) => {
11+
if (visitedViews.value.some(v => v.path === view.path)) return;
12+
visitedViews.value.push(
13+
Object.assign({}, view, {
14+
title: (view.meta.title as string) || (view.params.deviceName as string) || (view.name as string) || '标签页'
15+
})
16+
);
17+
};
18+
19+
export const delView = (view: TagView): Promise<TagView[]> => {
20+
return new Promise(resolve => {
21+
const index = visitedViews.value.findIndex(v => v.path === view.path);
22+
if (index > -1) {
23+
visitedViews.value.splice(index, 1);
24+
}
25+
resolve([...visitedViews.value]);
26+
});
27+
};
28+
29+
export const delOthersViews = (view: TagView): Promise<TagView[]> => {
30+
return new Promise(resolve => {
31+
visitedViews.value = visitedViews.value.filter(v => v.path === view.path);
32+
resolve([...visitedViews.value]);
33+
});
34+
};
35+
36+
export const delAllViews = (): Promise<void> => {
37+
return new Promise(resolve => {
38+
visitedViews.value = [];
39+
resolve();
40+
});
41+
};

front/src/views/SideBar.vue

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import AddDeviceGroupDialog from "@/components/device/AddDeviceGroupDialog.vue";
7272
import { currentTheme } from "@/utils/theme";
7373
import { isCollapse } from "@/components/header/isCollapse";
7474
import menuRouter from "@/router/index";
75+
import { delView, visitedViews } from "@/store/tagsView";
7576
import { deleteChannel, getChannelList } from "@/api/channelApi";
7677
import {
7778
getDeviceGroupTree,
@@ -234,10 +235,11 @@ const navigateToDevice = (deviceName: string, forceRefresh = false) => {
234235
localStorage.setItem("activeRoute", path);
235236
236237
if (forceRefresh) {
237-
router.push(`${path}?t=${Date.now()}`);
238-
} else {
239-
router.push(path);
238+
// For tabs, we actually probably don't want to ever force refresh
239+
// using query params as it breaks keep-alive matching easily by path.
240+
// If needed, can use another mechanism. For now, just navigate.
240241
}
242+
router.push(path);
241243
};
242244
243245
const showAddDeviceDialog = () => {
@@ -293,11 +295,25 @@ const handleDeleteDeviceByName = async (deviceName: string) => {
293295
await deleteChannel(channel.id);
294296
ElMessage.success('删除成功');
295297
298+
const path = `/device/${deviceName}`;
299+
// 如果存在这个标签,需要关闭它
300+
const targetView = visitedViews.value.find(v => v.path === path);
301+
if (targetView) {
302+
await delView(targetView);
303+
}
304+
296305
if (currentDeviceName.value === deviceName) {
297306
currentDeviceName.value = '';
298307
currentNodeKey.value = '';
299308
localStorage.removeItem("activeRoute");
300-
router.push('/');
309+
310+
// Navigate to another view if available
311+
const latestView = visitedViews.value.slice(-1)[0];
312+
if (latestView) {
313+
router.push((latestView.fullPath || latestView.path) as string);
314+
} else {
315+
router.push('/');
316+
}
301317
}
302318
303319
if (menuRouter.hasRoute(deviceName)) {

www/assets/Device-DaRq7Iua.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

www/assets/Device-DqApM5Js.js

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)