Files
KCGL/inventory-web/src/router/index.ts

418 lines
13 KiB
TypeScript
Raw Normal View History

import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'
2026-02-04 14:29:59 +08:00
import { useUserStore } from '@/stores/user'
import BomManage from '@/views/bom/BomManage.vue'
import { ElMessage } from 'element-plus'
// [新增] 扩展 RouteMeta 类型定义,防止 TS 报错
declare module 'vue-router' {
interface RouteMeta {
title?: string
icon?: string
hidden?: boolean
roles?: string[] // 允许的角色列表
}
}
const routes: Array<RouteRecordRaw> = [
2026-02-04 14:29:59 +08:00
// 1. 登录页
2026-02-04 13:30:07 +08:00
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
2026-02-04 14:29:59 +08:00
meta: { hidden: true }
2026-02-04 13:30:07 +08:00
},
2026-02-04 14:29:59 +08:00
// 2. 首页 Dashboard
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '首页', icon: 'HomeFilled' }
}
]
},
// 3. 物料管理
{
path: '/material',
component: Layout,
redirect: '/material/index',
meta: { title: '物料管理', icon: 'Box' },
children: [
{
path: 'index',
name: 'MaterialBase',
component: () => import('@/views/material/list.vue'),
meta: { title: '基础信息', icon: 'Box' }
},
{
path: 'buyOdoo',
name: 'BuyOdoo',
component: () => import('@/views/material/buyOdoo.vue'),
meta: { title: '基础信息(Odoo)', icon: 'Grid' }
}
]
},
// 4. 库存管理 (入库)
{
path: '/inventory',
component: Layout,
2026-02-06 10:16:37 +08:00
meta: { title: '入库管理', icon: 'Shop' },
redirect: '/inventory/buy',
children: [
{
path: 'buy',
name: 'InventoryBuy',
component: () => import('@/views/stock/inbound/buy.vue'),
meta: { title: '采购件' }
},
{
path: 'semi',
name: 'InventorySemi',
component: () => import('@/views/stock/inbound/semi.vue'),
meta: { title: '半成品' }
},
{
path: 'product',
name: 'InventoryProduct',
component: () => import('@/views/stock/inbound/product.vue'),
meta: { title: '成品' }
},
{
path: 'service',
name: 'InventoryService',
component: () => import('@/views/stock/inbound/service.vue'),
meta: { title: '服务权益' }
},
2026-02-06 10:16:37 +08:00
// [原有] 入库记录整合
{
path: 'summary',
name: 'InventorySummary',
component: () => import('@/views/stock/inbound/inbound_summary.vue'),
meta: { title: '入库记录' }
},
// 维修管理
{
path: 'repair',
name: 'RepairManagement',
component: () => import('@/views/stock/inbound/repair.vue'),
meta: { title: '维修管理', permission: 'inbound_repair' }
feat(return): 前端退回入口与不良品在管台账看板 出库记录页(views/outbound/index.vue): - 明细行新增「退回」列,returnable_quantity <= 0 时按钮置灰,行内显示「已退 N」 - 对话框展示 原出库数量 / 已退回数量 / 本次可退最大,默认带入可退最大值 - 退回类型用下拉单选:良品(加回库存)/ 不良品(转入异常待处理) - 退回原因必填,提交前三重校验;按钮 :loading + 函数内 submitting 双保险防抖 - 成功后刷新列表。错误提示不重复弹——request 拦截器对 HTTP 400 已取 data.msg 展示,对话框 catch 只收尾 配套后端(services/outbound_service.py): - get_grouped_list 的出库明细补 id / returned_quantity / returnable_quantity。 原先明细不带 id,退回接口无从指定 outbound_id 新增页面(views/stock/defective/index.vue,路由 /inventory/defective): - 展示 物料名称/规格/SKU/退回时间/操作人/退回总数/剩余待处理/状态 - 顶部 alert 提示在管总量,并明确「这批实物不在库存表中,盘点请以本台账 为准」——在管坏件对盘点不可见是本方案的固有盲区,必须在页面上主动提醒 - 仅对 remaining_qty > 0 的行提供「修复回库」「报废销毁」,终态行显示已结案 - 两个弹窗均带数量上限约束与 loading 防抖,成功后刷新 新增 api/inbound/return.ts 承载四个逆向物流接口。 侧边栏由 router/index.ts 驱动,加路由即入菜单;注意侧边栏只过滤 meta.hidden、不看 meta.permission,故菜单对所有角色可见,访问控制由后端 接口负责(与既有「维修管理」一致)。
2026-09-16 15:45:40 +08:00
},
// ★ 不良品在管台账(逆向物流):退回的坏件不入库存表,
// 实物在仓但不在库存行中,故单独成页承载其去向与处置。
{
path: 'defective',
name: 'DefectiveGoods',
component: () => import('@/views/stock/defective/index.vue'),
meta: { title: '不良品在管台账', permission: 'inventory_stocktake' }
}
]
},
// 4.1 盘点管理 (独立顶级菜单)
{
path: '/stocktake',
component: Layout,
redirect: '/stocktake/operation',
meta: { title: '盘点管理', icon: 'DataBoard' },
children: [
2026-02-06 10:16:37 +08:00
{
path: 'operation',
2026-02-06 10:16:37 +08:00
name: 'InventoryStocktake',
component: () => import('@/views/stock/stocktake/index.vue'),
meta: { title: '盲盘作业' }
},
{
path: 'adjustment',
name: 'StockAdjustment',
component: () => import('@/views/stock/adjustment/index.vue'),
meta: { title: '盈亏调整' }
}
]
},
2026-02-06 10:16:37 +08:00
// 5. 出库管理
{
2026-02-06 10:16:37 +08:00
path: '/outbound',
component: Layout,
2026-02-06 10:16:37 +08:00
meta: { title: '出库管理', icon: 'Van' },
redirect: '/outbound/index',
children: [
2026-02-06 10:16:37 +08:00
// ★ [新增] 出库选单打印页面
{
path: 'selection',
name: 'OutboundSelection',
component: () => import('@/views/outbound/Selection.vue'),
meta: { title: '出库选单' }
},
{
path: 'create',
name: 'OutboundCreate',
component: () => import('@/views/outbound/create.vue'),
meta: { title: '扫码出库' }
},
{
path: 'index',
name: 'OutboundList',
component: () => import('@/views/outbound/index.vue'),
meta: { title: '出库记录' }
},
{
path: 'approval',
name: 'OutboundApproval',
component: () => import('@/views/outbound/approval/index.vue'),
meta: {
title: '出库审批',
icon: 'Stamp'
}
}
]
},
// 5. BOM 管理
{
path: '/bom',
component: Layout,
meta: { title: 'BOM管理', icon: 'Document' },
redirect: '/bom/manage',
children: [
{
path: 'manage',
name: 'BomManage',
component: BomManage,
meta: { title: 'BOM配方管理', icon: 'list' }
}
]
},
// 5.1 采购管理
{
path: '/purchase',
component: Layout,
meta: { title: '采购管理', icon: 'ShoppingCart' },
children: [
{
path: '',
name: 'PurchaseList',
component: () => import('@/views/purchase/index.vue'),
meta: { title: '采购申请' }
}
]
},
feat(my-requests): 我的申请单独立模块(采购管理之后) 路由位置 -------- 放在「采购管理」之后,作为独立一级模块: 采购管理 我的申请单 ← 新增(icon: Tickets) 借库管理 ★ 独立模块而非挂在某个业务页下:申请人可能提交出库/借库/报废三类单据, 放在任一业务模块下都会让其它模块的人找不到。 页面功能 -------- · 类型筛选:全部 / 出库 / 借库 / 报废 · 状态筛选:全部 / 待审批 / 已通过 / 已驳回 / 已完成 / 已撤回 · 展开行显示物料明细(数量字段已由后端归一化) · 撤回按钮仅对 status 0/1 可见(已预占但未执行) 撤回不硬编码 URL —— 使用后端回传的 withdraw_endpoint 分发: const res = await request({ url: row.withdraw_endpoint, method: 'post' }) 这样将来模块端点调整(如借库从 close 换成正式 withdraw),前端无需改动。 权限说明 -------- 菜单不做权限过滤(Sidebar 只过滤 meta.hidden),所有角色都会看到本页。 这正是预期 —— 申请人本就不应需要任何权限码。数据由后端强制按 applicant_id 过滤,看不到他人单据。 顺带清理 -------- 移除先前加在 Selection.vue 的重复实现(约 110 行模板/脚本/样式), 改为一个跳转链接「查看我的申请单 >」;并移除随之失效的 3 个导入 (onMounted / Refresh / 两个 API 函数)。 验证:新页面 SFC 编译零告警;Selection.vue 清理后编译通过。
2026-09-10 15:37:11 +08:00
// 6.0 我的申请单(申请人视角,跨模块聚合)
//
// ★ 独立模块而非挂在某个业务页下:申请人可能提交出库/借库/报废三类单据,
// 放在任一业务模块下都会让其它模块的人找不到。
// 本页数据来自 /my-requests 聚合端点(仅需登录态,服务端硬编码
// applicant_id=当前用户),因此不需要也不应配置审批类权限码。
{
path: '/my-requests',
component: Layout,
children: [
{
path: '',
name: 'MyRequests',
component: () => import('@/views/my-requests/index.vue'),
meta: { title: '我的申请单', icon: 'Tickets' }
}
]
},
// 6. 借库管理
{
path: '/operation',
component: Layout,
2026-02-06 17:11:47 +08:00
meta: { title: '借库管理', icon: 'Operation' },
redirect: '/operation/borrow',
children: [
{
path: 'borrow_apply',
name: 'BorrowApply',
component: () => import('@/views/borrow/apply/index.vue'),
meta: { title: '借库选单' }
},
{
path: 'borrow',
name: 'OpBorrow',
component: () => import('@/views/transaction/borrow.vue'),
meta: { title: '扫码借库' }
},
{
path: 'repair',
name: 'OpRepair',
component: () => import('@/views/transaction/return.vue'),
2026-02-06 17:11:47 +08:00
meta: { title: '返还' }
},
{
2026-02-06 17:11:47 +08:00
path: 'records',
name: 'OpRecords',
component: () => import('@/views/transaction/records.vue'),
meta: { title: '借还记录' }
},
{
path: 'borrow_approval',
name: 'BorrowApproval',
component: () => import('@/views/borrow/approval/index.vue'),
meta: {
title: '借库审批',
icon: 'Stamp'
}
}
]
},
// 6.1 报废管理 (独立一级菜单)
{
path: '/scrap',
component: Layout,
meta: { title: '报废管理', icon: 'Delete' },
redirect: '/scrap/index',
children: [
{
path: 'apply',
name: 'ScrapApply',
component: () => import('@/views/operation/scrap/apply/index.vue'),
meta: { title: '报废申请' }
},
{
path: 'create',
name: 'ScrapCreate',
component: () => import('@/views/operation/scrap/create.vue'),
meta: { title: '按单报废' }
},
{
path: 'index',
name: 'ScrapList',
component: () => import('@/views/operation/scrap/index.vue'),
meta: { title: '报废记录' }
},
{
path: 'approval',
name: 'ScrapApproval',
component: () => import('@/views/operation/scrap/approval/index.vue'),
meta: { title: '报废审批' }
}
]
},
// 7. 系统管理
2026-02-04 13:30:07 +08:00
{
path: '/system',
component: Layout,
// [修复] 添加 redirect,点击父菜单时跳转到子页面
redirect: '/system/user-create',
2026-02-04 13:30:07 +08:00
meta: {
title: '系统管理',
icon: 'Setting'
2026-02-04 13:30:07 +08:00
},
children: [
{
path: 'user-create',
name: 'UserCreate',
component: () => import('@/views/system/UserCreate.vue'),
meta: {
title: '账号开通',
icon: 'User'
}
},
// [新增] 权限分配页面,只有超级管理员可进
{
path: 'permission',
name: 'PermissionConfig',
component: () => import('@/views/system/PermissionConfig.vue'),
meta: {
title: '权限分配',
icon: 'Lock'
}
},
{
path: 'audit',
name: 'AuditLog',
component: () => import('@/views/system/AuditLog.vue'),
meta: {
title: '审计日志',
icon: 'Document'
}
2026-02-04 14:29:59 +08:00
}
2026-02-04 13:30:07 +08:00
]
},
// 404 路由
{
path: '/:pathMatch(.*)*',
redirect: '/dashboard',
meta: { hidden: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
2026-02-04 13:30:07 +08:00
// ==========================================
// 全局路由守卫
2026-02-04 13:30:07 +08:00
// ==========================================
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
const token = userStore.token || localStorage.getItem('access_token') || localStorage.getItem('token')
// [修复] 优先从 user 对象获取,并统一转大写,防止大小写不一致导致权限失效
const rawRole = userStore.user?.role || userStore.role || localStorage.getItem('role') || 'user'
const userRole = String(rawRole).toUpperCase()
// ============================================================
// 安全兜底:检查 refresh_token 是否即将过期(30分钟)
// ============================================================
if (token && userStore.isRefreshTokenExpiringSoon()) {
// 仅在用户主动操作时提示,避免页面加载就弹窗
const isUserAction = to.path !== '/login' && to.path !== '/'
if (isUserAction) {
ElMessage.warning('您的登录状态即将失效,请及时保存数据并重新登录')
}
}
// 调试日志
if (to.path.includes('/system')) {
console.log(`路由守卫检查: Path=${to.path}, UserRole=${userRole}, Required=${to.meta.roles}`)
}
2026-02-04 14:29:59 +08:00
2026-02-04 13:30:07 +08:00
if (to.path === '/login') {
2026-02-04 14:29:59 +08:00
if (token) {
next('/')
} else {
next()
}
2026-02-04 13:30:07 +08:00
return
}
if (!token) {
2026-02-04 14:29:59 +08:00
next({ path: '/login', replace: true })
2026-02-04 13:30:07 +08:00
return
}
// 权限检查逻辑
2026-02-04 13:30:07 +08:00
if (to.meta.roles && Array.isArray(to.meta.roles)) {
if (to.meta.roles.includes(userRole)) {
next()
} else {
console.warn(`权限不足: 用户角色 ${userRole} 不在允许列表 ${to.meta.roles} 中`)
2026-02-04 13:30:07 +08:00
next('/dashboard')
}
} else {
next()
}
})
export default router