feat(ui): 菜单权限过滤与退回记录页

侧边栏支持按权限隐藏菜单(Sidebar/index.vue):
- 新增 canSee():读取 meta.permissions(数组,满足任一即可)
- ★ 同时修了一个既有缺陷:原实现只过滤顶层路由,**子路由的权限判定不生效**,
  未授权用户仍能在菜单里看到入口。现改为递归过滤,父菜单的子项被过滤光后
  父菜单本身也不再显示。
- ★ 刻意只认 meta.permissions(新键),历史遗留的 meta.permission(单数,
  如「维修管理」的 inbound_repair)保持不生效——一旦开始消费它,会让若干
  菜单对部分角色突然消失,属于超出本次范围的静默行为变更。两者若要统一,
  建议单独排期逐个核对影响面。

路由守卫(router/index.ts):
- 新增 meta.permissions 校验,未授权时提示并跳首页。侧边栏负责「菜单看不见」,
  守卫负责「直接敲 URL 也进不去」,后端负责「绕过前端直接调接口也不行」,
  三层任一失效都不至于越权。
- 顺带修了原守卫的分支结构:roles 通过时直接 next(),会跳过后续检查;
  改为不通过才中断,落到末尾统一 next()。

新增「退回记录」页(views/outbound/returns/index.vue):
- 纯只读台账,无任何操作按钮,供库管核对历史退回
- 展示 退回时间 / 原出库单号 / 物料名称 / 规格 / SKU / 退回数量 /
  退回类型 / 退回原因 / 操作人
- 退回类型用标签区分(不良品=红,良品=绿)
- 源库存行或原单被删除时显示占位文案而非空白,台账不留哑行

api/inbound/return.ts 补 getReturnList()。
This commit is contained in:
yueli
2026-09-16 16:46:02 +08:00
parent 9eb4792d4a
commit 27436f5efe
4 changed files with 285 additions and 8 deletions

View File

@ -0,0 +1,193 @@
<template>
<div class="app-container">
<!-- 筛选区 -->
<el-form :inline="true" class="filter-form" @submit.prevent>
<el-form-item label="退回类型">
<el-select v-model="query.return_type" placeholder="全部" style="width: 140px">
<el-option label="全部" value="全部" />
<el-option label="良品(加回库存)" value="良品" />
<el-option label="不良品(转在管台账)" value="不良品" />
</el-select>
</el-form-item>
<el-form-item label="搜索">
<el-input
v-model="query.keyword"
placeholder="出库单号 / SKU / 操作人"
style="width: 240px;"
clearable
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
</el-form-item>
<el-form-item label="退回时间">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 260px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetFilter">重置</el-button>
</el-form-item>
</el-form>
<el-table
:data="list"
v-loading="loading"
border
style="width: 100%; margin-top: 16px;"
:header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
>
<el-table-column prop="return_time" label="退回时间" width="170" align="center">
<template #default="{ row }">{{ row.return_time || '-' }}</template>
</el-table-column>
<el-table-column prop="outbound_no" label="原出库单号" min-width="190" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.outbound_no">{{ row.outbound_no }}</span>
<span v-else class="muted">(原单已删除)</span>
</template>
</el-table-column>
<el-table-column prop="material_name" label="物料名称" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.material_name">{{ row.material_name }}</span>
<span v-else class="muted">(原库存行已删除)</span>
</template>
</el-table-column>
<el-table-column prop="spec_model" label="规格型号" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.spec_model">{{ row.spec_model }}</span>
<span v-else class="muted">-</span>
</template>
</el-table-column>
<el-table-column prop="sku" label="SKU" width="150" show-overflow-tooltip />
<el-table-column label="退回数量" width="100" align="right">
<template #default="{ row }">{{ formatQty(row.return_qty) }}</template>
</el-table-column>
<el-table-column label="退回类型" width="150" align="center">
<template #default="{ row }">
<el-tag :type="row.return_type === '不良品' ? 'danger' : 'success'">
{{ row.return_type === '不良品' ? '不良品(转在管)' : '良品(已回库)' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="reason" label="退回原因" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.reason">{{ row.reason }}</span>
<span v-else class="muted">-</span>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="110" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.operator">{{ row.operator }}</span>
<span v-else class="muted">-</span>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 20px; text-align: right;">
<el-pagination
background
layout="total, prev, pager, next"
:total="total"
:page-size="query.page_size"
:current-page="query.page"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getReturnList } from '@/api/inbound/return'
import { formatQty } from '@/utils/format'
// ★ 只读台账:无任何写操作按钮,供库管核对历史退回记录。
// 权限由 /outbound/returns 接口的 outbound_return_list 校验,
// 菜单可见性与直达 URL 由路由 meta.permissions 控制。
const list = ref<any[]>([])
const total = ref(0)
const loading = ref(false)
const dateRange = ref<string[]>([])
const query = reactive({
page: 1,
page_size: 20,
keyword: '',
return_type: '全部',
})
const fetchData = async () => {
loading.value = true
try {
const res = await getReturnList({
page: query.page,
page_size: query.page_size,
keyword: query.keyword || undefined,
return_type: query.return_type,
start_date: dateRange.value?.[0] || undefined,
end_date: dateRange.value?.[1] || undefined,
})
list.value = res.data.list || []
total.value = res.data.total || 0
} catch (e) {
console.error('[退回记录] 查询失败', e)
} finally {
loading.value = false
}
}
const handleSearch = () => {
query.page = 1
fetchData()
}
const handlePageChange = (val: number) => {
query.page = val
fetchData()
}
const resetFilter = () => {
query.page = 1
query.keyword = ''
query.return_type = '全部'
dateRange.value = []
fetchData()
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.filter-form {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
margin-right: 12px;
}
.muted {
color: #c0c4cc;
}
</style>