feat(web): 引入 Vite 前端应用并扩展仓库忽略规则
将整套 web 源码纳入仓库,并为 web/node_modules、构建产物及本地环境文件配置 .gitignore,同时移除占位用的 assets/.gitkeep。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
534
web/src/components/Sidebar.vue
Normal file
534
web/src/components/Sidebar.vue
Normal file
@@ -0,0 +1,534 @@
|
||||
<template>
|
||||
<aside class="sidebar" :class="{ collapsed: isCollapsed }">
|
||||
<div class="sider_top">
|
||||
<!-- <div class="img_box">
|
||||
<img :src="isCollapsed ? foldLogo : siderTopLogo" alt="">
|
||||
</div> -->
|
||||
<!-- GENUINE OPTICS (THAILAND) -->
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="(item, index) in allMenuItems" :key="item.groupIndex" class="menu-group">
|
||||
<!-- 父级菜单项 -->
|
||||
<div class="menu-parent" :class="{ expanded: item.expanded, 'has-active-child': hasActiveChild(item), 'parent-active': isParentActive(item, item.groupIndex) }"
|
||||
@click="toggleMenu(item.groupIndex)">
|
||||
<div class="menu-parent-inner">
|
||||
<img v-if="item.iconImg" :src="item.iconImg" class="menu-icon-img" :alt="item.label" />
|
||||
<i v-else class="pi" :class="item.icon"></i>
|
||||
<span v-if="!isCollapsed">{{ item.label }}</span>
|
||||
</div>
|
||||
<i v-if="!isCollapsed" class="pi pi-chevron-down caret" :class="{ expanded: item.expanded }"></i>
|
||||
</div>
|
||||
<!-- 子级菜单项 -->
|
||||
<ul v-if="!isCollapsed && item.expanded && item.children" class="sub-menu">
|
||||
<li v-for="(child, childIndex) in item.children" :key="childIndex" @click="goPage(child.name)"
|
||||
:class="{ active: currentName === child.name }" class="sub-menu-item">
|
||||
<div class="sub-menu-inner">
|
||||
<img v-if="child.iconImg" :src="child.iconImg" class="sub-menu-icon-img" alt="" />
|
||||
<i v-else class="dot"></i>
|
||||
<span>{{ child.label }}</span>
|
||||
</div>
|
||||
<div v-if="currentName === child.name" class="active-indicator"></div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<div class="watermark">
|
||||
<img :src="isCollapsed ? foldCompanyLogo : companyLogo" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getTimesheetRouteName } from "../configs/common";
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useUiStore } from "../store/ui";
|
||||
import systemIcon from "../images/sider/system.png";
|
||||
import assetsIcon from "../images/sider/assets.png";
|
||||
import meteringIcon from "../images/sider/metering.png";
|
||||
import operationLogIcon from "../images/sider/log.svg";
|
||||
import overviewDashboardIcon from "../images/sider/overview.svg";
|
||||
import siderTopLogo from "../images/sider/newLogo.svg";
|
||||
import foldLogo from "../images/sider/newLogo.svg";
|
||||
import companyLogo from "../images/logo.png";
|
||||
import foldCompanyLogo from "../images/sider/foldCompanyLoago.svg";
|
||||
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const uiStore = useUiStore();
|
||||
const { isSidebarCollapsed: isCollapsed } = storeToRefs(uiStore);
|
||||
|
||||
// 菜单展开状态 - 与 fullMenuConfig 下标一致:0 概览看板、1 资产管理、2 计量管理、3 系统管理、4 操作日志
|
||||
const menuExpandedStates = ref({
|
||||
0: true, // 概览看板默认展开(与默认首页一致)
|
||||
1: false,
|
||||
2: false,
|
||||
3: false,
|
||||
4: false,
|
||||
});
|
||||
|
||||
// 当前“选中”的父级索引(仅此父级显示蓝色,其他保持灰色)
|
||||
const lastClickedParentIndex = ref(0);
|
||||
|
||||
// 所有菜单项配置(路由 name 与 Vue Router 一致)
|
||||
const fullMenuConfig = [
|
||||
{
|
||||
// 概览看板
|
||||
labelKey: "menu.overviewDashboard",
|
||||
icon: "pi-chart-line",
|
||||
iconImg: overviewDashboardIcon,
|
||||
isAsset: false,
|
||||
children: [
|
||||
{ labelKey: "menu.overviewDashboard", name: "OverviewDashboard", icon: "pi-chart-line" },
|
||||
// { labelKey: "role.pagePermission.equipmentRepairReport", name: "EquipmentRepairReport", icon: "pi-file" },
|
||||
// { labelKey: "role.pagePermission.equipmentAcceptanceReport", name: "EquipmentAcceptanceReport", icon: "pi-file" },
|
||||
{ labelKey: "role.pagePermission.equipmentMaintenanceReport", name: "EquipmentMaintenanceReport", icon: "pi-file" },
|
||||
{ labelKey: "role.pagePermission.equipmentMeteringReport", name: "EquipmentMeteringReport", icon: "pi-file" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// 资产管理
|
||||
labelKey: "menu.assetManagement",
|
||||
icon: "pi-database",
|
||||
iconImg: assetsIcon,
|
||||
isAsset: true,
|
||||
children: [
|
||||
{ labelKey: "menu.assetList", name: "AssetList", icon: "pi-box" },
|
||||
{ labelKey: "menu.assetInventory", name: "AssetInventory", icon: "pi-list" },
|
||||
{ labelKey: "menu.deviceMaintain", name: "DeviceMaintain", icon: "pi-wrench" },
|
||||
{ labelKey: "menu.deviceRepair", name: "DeviceRepair", icon: "pi-box" },
|
||||
{ labelKey: "menu.assetAlarmRecord", name: "AssetAlarmRecord", icon: "pi-bell" },
|
||||
{ labelKey: "menu.accessControlManage", name: "AccessControlManage", icon: "pi-lock" },
|
||||
{ labelKey: "menu.attachmentManage", name: "AttachmentManage", icon: "pi-paperclip" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// 计量管理
|
||||
labelKey: "menu.measurementManagement",
|
||||
icon: "pi-database",
|
||||
iconImg: meteringIcon,
|
||||
isAsset: true,
|
||||
children: [
|
||||
{ labelKey: "menu.deviceMetering", name: "DeviceMetering", icon: "pi-paperclip" },
|
||||
{ labelKey: "menu.measureMaterialManage", name: "MeasureMaterialManage", icon: "pi-box" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// 系统管理
|
||||
labelKey: "menu.systemManagement",
|
||||
icon: "pi-cog",
|
||||
iconImg: systemIcon,
|
||||
isAsset: false,
|
||||
children: [
|
||||
{ labelKey: "menu.userManagement", name: "UserManage", icon: "pi-users" },
|
||||
{ labelKey: "menu.roleManagement", name: "RoleManage", icon: "pi-shield" },
|
||||
{ labelKey: "menu.employeeManagement", name: "EmployeeManage", icon: "pi-user" },
|
||||
{ labelKey: "menu.factoryManagement", name: "FactoryManage", icon: "pi-building" },
|
||||
{ labelKey: "menu.sensorManagement", name: "SensorManage", icon: "pi-microchip" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// 操作日志(子项:操作日志页)
|
||||
labelKey: "menu.operationLogParent",
|
||||
icon: "pi-history",
|
||||
iconImg: operationLogIcon,
|
||||
isAsset: false,
|
||||
children: [
|
||||
{ labelKey: "menu.operationLog", name: "OperationLog", icon: "pi-history" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 左侧菜单始终展示全部配置项(不再按接口 permission_codes 过滤)
|
||||
const allMenuItems = computed(() => {
|
||||
const expanded = menuExpandedStates.value;
|
||||
return fullMenuConfig.map((group, groupIndex) => {
|
||||
const visibleChildren = group.children || [];
|
||||
return {
|
||||
groupIndex,
|
||||
label: t(group.labelKey),
|
||||
icon: group.icon,
|
||||
iconImg: group.iconImg,
|
||||
expanded: expanded[groupIndex],
|
||||
isAsset: group.isAsset,
|
||||
children: visibleChildren.map((child) => ({
|
||||
label: t(child.labelKey),
|
||||
name: getTimesheetRouteName(child.name),
|
||||
icon: child.icon,
|
||||
iconImg: child.iconImg,
|
||||
})),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 获取当前可见菜单的所有路由名称
|
||||
const getAllMenuRouteNames = () => {
|
||||
const routeNames = [];
|
||||
allMenuItems.value.forEach((item) => {
|
||||
if (item.children) {
|
||||
item.children.forEach((child) => {
|
||||
if (child.name) routeNames.push(child.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
return routeNames;
|
||||
};
|
||||
|
||||
// 默认路由名称(概览看板);当前路由不在菜单内时的回退路由
|
||||
const defaultRouteName = getTimesheetRouteName("OverviewDashboard");
|
||||
|
||||
// 计算当前选中的路由名称
|
||||
const currentName = computed(() => {
|
||||
const routeName = route.name;
|
||||
const menuRouteNames = getAllMenuRouteNames();
|
||||
|
||||
// 如果当前路由不在菜单项中,或者路由为空/未定义,则默认选中概览看板
|
||||
if (!routeName || !menuRouteNames.includes(routeName)) {
|
||||
return defaultRouteName;
|
||||
}
|
||||
|
||||
return routeName;
|
||||
});
|
||||
|
||||
// 判断该父菜单下是否有当前激活的子项(用于父级标题变色)
|
||||
const hasActiveChild = (item) => {
|
||||
if (!item.children) return false;
|
||||
return item.children.some((child) => child.name === currentName.value);
|
||||
};
|
||||
|
||||
// 当前父级是否为“选中”状态(仅一个父级为 true,用于蓝色高亮)
|
||||
const isParentActive = (item, groupIndex) => {
|
||||
return groupIndex === lastClickedParentIndex.value && (hasActiveChild(item) || item.expanded);
|
||||
};
|
||||
|
||||
const toggleMenu = (index) => {
|
||||
if (isCollapsed.value) {
|
||||
const targetGroup = allMenuItems.value.find((item) => item.groupIndex === index);
|
||||
if (targetGroup?.children?.length) {
|
||||
goPage(targetGroup.children[0].name);
|
||||
}
|
||||
lastClickedParentIndex.value = index;
|
||||
return;
|
||||
}
|
||||
menuExpandedStates.value[index] = !menuExpandedStates.value[index];
|
||||
lastClickedParentIndex.value = index;
|
||||
};
|
||||
|
||||
const goPage = (name) => {
|
||||
if (name) {
|
||||
router.push({ name });
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
// 清除本地存储的 token 和 uid
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("uid");
|
||||
// 跳转到登录页
|
||||
router.push({ name: getTimesheetRouteName("Login") });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
(newName) => {
|
||||
const menuRouteNames = getAllMenuRouteNames();
|
||||
if (newName && !menuRouteNames.includes(newName)) {
|
||||
router.push({ name: defaultRouteName });
|
||||
}
|
||||
// 手风琴:只展开当前路由所在分组,其余分组折叠
|
||||
const newExpanded = { 0: false, 1: false, 2: false, 3: false, 4: false };
|
||||
allMenuItems.value.forEach((item) => {
|
||||
if (item.children && item.children.some((child) => child.name === newName)) {
|
||||
newExpanded[item.groupIndex] = true;
|
||||
lastClickedParentIndex.value = item.groupIndex;
|
||||
}
|
||||
});
|
||||
menuExpandedStates.value = newExpanded;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 2px 0px 15px 0px rgba(0, 69, 143, 0.05);
|
||||
border-radius: 0px 0px 0px 0px;
|
||||
padding: 1rem 0;
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
padding-bottom: 4rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
.sider_top {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 25px;
|
||||
|
||||
.img_box {
|
||||
width: 140px;
|
||||
height: 26px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 72px;
|
||||
min-width: 72px;
|
||||
max-width: 72px;
|
||||
padding: 0 8px;
|
||||
|
||||
.sider_top .img_box {
|
||||
width: 44px;
|
||||
height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar>ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 父级菜单样式 */
|
||||
.menu-parent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.7rem 0.6rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .menu-parent {
|
||||
justify-content: center;
|
||||
padding: 0.7rem 0;
|
||||
}
|
||||
|
||||
.menu-parent-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
/* flex: 1; */
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .menu-parent-inner {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu-parent .pi {
|
||||
font-size: 1rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.menu-parent .menu-icon-img {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 父级菜单默认正常色(灰) */
|
||||
.menu-parent .pi-cog,
|
||||
.menu-parent .pi-database,
|
||||
.menu-parent .pi-ruler,
|
||||
.menu-parent .pi-chart-line,
|
||||
.menu-parent .pi-history {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.menu-parent span {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
/* 仅当前“选中”的父级(parent-active)显示主题色,其他父级保持灰色 */
|
||||
.menu-parent.parent-active .pi,
|
||||
.menu-parent.parent-active span {
|
||||
color: #3067E5;
|
||||
}
|
||||
|
||||
.caret {
|
||||
margin-top: 3px;
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.menu-parent .caret {
|
||||
color: #6b7280;
|
||||
scale: .75;
|
||||
}
|
||||
|
||||
.menu-parent.parent-active .caret {
|
||||
color: #3067E5;
|
||||
}
|
||||
|
||||
.caret.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 子菜单容器 */
|
||||
.sub-menu {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 子菜单项样式 */
|
||||
.sub-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.sub-menu-item:hover {
|
||||
background: #EFF4FF;
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
}
|
||||
|
||||
/* .sub-menu-item.active {
|
||||
background: #fff5f0;
|
||||
} */
|
||||
|
||||
.sub-menu-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem 0.75rem 3rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sub-menu-item .pi {
|
||||
font-size: 0.875rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.sub-menu-item:not(.active) .pi {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.sub-menu-item:not(.active) span {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* flex: 1; */
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sub-menu-item.active .pi {
|
||||
color: #3067E5;
|
||||
}
|
||||
|
||||
.sub-menu-item.active span {
|
||||
color: #3067E5;
|
||||
font-size: 0.875rem;
|
||||
/* white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; */
|
||||
}
|
||||
|
||||
/* 激活指示器(右侧橙色竖条) */
|
||||
.active-indicators {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: #3067E5;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
color: #3067E5;
|
||||
background-color: #fff5f0;
|
||||
}
|
||||
|
||||
.watermark {
|
||||
text-align: center;
|
||||
width: 110px;
|
||||
height: 20px;
|
||||
img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar.collapsed .watermark {
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background: #3067E5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.sub-menu-icon-img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user