Files
KCGL/inventory-web/src/router/index.ts
yueli fd99d33a0d 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

418 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import Layout from '@/layout/index.vue'
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> = [
// 1. 登录页
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true }
},
// 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,
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: '服务权益' }
},
// [原有] 入库记录整合
{
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' }
},
// ★ 不良品在管台账(逆向物流):退回的坏件不入库存表,
// 实物在仓但不在库存行中,故单独成页承载其去向与处置。
{
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: [
{
path: 'operation',
name: 'InventoryStocktake',
component: () => import('@/views/stock/stocktake/index.vue'),
meta: { title: '盲盘作业' }
},
{
path: 'adjustment',
name: 'StockAdjustment',
component: () => import('@/views/stock/adjustment/index.vue'),
meta: { title: '盈亏调整' }
}
]
},
// 5. 出库管理
{
path: '/outbound',
component: Layout,
meta: { title: '出库管理', icon: 'Van' },
redirect: '/outbound/index',
children: [
// ★ [新增] 出库选单打印页面
{
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: '采购申请' }
}
]
},
// 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,
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'),
meta: { title: '返还' }
},
{
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. 系统管理
{
path: '/system',
component: Layout,
// [修复] 添加 redirect点击父菜单时跳转到子页面
redirect: '/system/user-create',
meta: {
title: '系统管理',
icon: 'Setting'
},
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'
}
}
]
},
// 404 路由
{
path: '/:pathMatch(.*)*',
redirect: '/dashboard',
meta: { hidden: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// ==========================================
// 全局路由守卫
// ==========================================
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}`)
}
if (to.path === '/login') {
if (token) {
next('/')
} else {
next()
}
return
}
if (!token) {
next({ path: '/login', replace: true })
return
}
// 权限检查逻辑
if (to.meta.roles && Array.isArray(to.meta.roles)) {
if (to.meta.roles.includes(userRole)) {
next()
} else {
console.warn(`权限不足: 用户角色 ${userRole} 不在允许列表 ${to.meta.roles}`)
next('/dashboard')
}
} else {
next()
}
})
export default router