feat(web): 引入 Vite 前端应用并扩展仓库忽略规则

将整套 web 源码纳入仓库,并为 web/node_modules、构建产物及本地环境文件配置 .gitignore,同时移除占位用的 assets/.gitkeep。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
caopeng
2026-05-17 15:22:29 +08:00
parent 759637dba2
commit a254aae503
161 changed files with 69862 additions and 11 deletions

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